算法学习-剑指 Offer(专项突击版),虚空剑指冠军刷题家(持续更新中)

文章目录

本文参考:

剑指 Offer(专项突击版)

本文与我的另一篇刷题笔记算法学习-剑指 Offer(第 2 版)同步更新,同样是程序员的经典刷题题库,需要尽快将其掌握住。

1.整数除法

二分法和快速乘法。

class Solution {
    public int divide(int a, int b) {
        boolean flag=(a<0&&b>0)||(a>0&&b<0)?false:true;
        //取long是为了防止2^31取反溢出
        long x=a;
        long y=b;
        if(a<0){
            x=-x;
        }
        if(b<0){
            y=-y;
        }
        long left=0;
        //a一定是最终结果可能的最大值
        long right=x;
        while(left<right){
            long mid=(left+right+1)/2;
            if(mul(mid,y)>x){
                right=mid-1;
            }else{
                left=mid;
            }
        }
        long res=flag?left:-left;
        return res<Integer.MIN_VALUE||res>Integer.MAX_VALUE?Integer.MAX_VALUE:(int)res;
    }
    //快速乘法
    public long mul(long x,long y){
        long res=0;
        while(y!=0){
            if((y&1)==1){
                res+=x;
            }
            y>>>=1;
            //增大乘数x
            x+=x;
        }
        return res;
    }
}
2.二进制加法

有两种做法,其一是参考lilyunoke大神的加法模板,这是我比较喜欢的做法,题解如下:

class Solution {
    public String addBinary(String a, String b) {
        int i=a.length()-1;
        int j=b.length()-1;
        int carry=0;
        StringBuilder sb=new StringBuilder();
        while(i>=0||j>=0){
            int digitA=i>=0?a.charAt(i)-'0':0;
            int digitB=j>=0?b.charAt(j)-'0':0;
            int sum=digitA+digitB+carry;
            carry=sum/2;
            int digit=sum%2;
            sb.append(digit);
            i--;
            j--;
        }
        if(carry!=0) sb.append(carry);
        return sb.reverse().toString();
    }
}

其中可以总结出来的加法模板是:

while ( A 没完 || B 没完){
	取到A 的当前位
    取到B 的当前位

    和 = A 的当前位 + B 的当前位 + 进位carry

    当前位 =% 10;
    进位 =/ 10;
    
    A左移调整
    B左移调整
}
判断进位是否为0,不为0额外加上
将结果反转

或者参考负雪明烛大佬的题解,不同的是循环条件,需要注意的是while循环结束条件,注意遍历完两个「加数」,以及进位不为0。

class Solution {
    public String addBinary(String a, String b) {
        int i=a.length()-1;
        int j=b.length()-1;
        int carry=0;
        StringBuilder sb=new StringBuilder();
        while(i>=0||j>=0||carry!=0){
            int digitA=i>=0?a.charAt(i)-'0':0;
            int digitB=j>=0?b.charAt(j)-'0':0;
            int sum=digitA+digitB+carry;
            carry=sum>=2?1:0;
            int digit=sum>=2?sum-2:sum;
            sb.append(digit);
            i--;
            j--;
        }
        return sb.reverse().toString();
    }
}
3.前n个数字二进制中1的个数

这题考虑到1的个数从0开始到n是有规律的就容易了,奇偶性按照动态规划做是非常巧妙的。

class Solution {
    public int[] countBits(int n) {
        int[]dp=new int[n+1];
        for(int i=1;i<=n;i++){
            if(i%2==1){
                dp[i]=dp[i-1]+1;
            }else{
                dp[i]=dp[i/2];
            }
        }
        return dp;
    }
}
4.只出现一次的数字

数位统计+按位计算二进制数

class Solution {
    public int singleNumber(int[] nums) {
        int[]bit=new int[32];
        for(int i=0;i<32;i++){
            for(int n:nums){
                if((n>>i&1)==1) bit[i]++;
            }
        }

        int ans=0;
        for(int i=0;i<32;i++){
            if(bit[i]%3!=0){
                ans|=1<<i;
            }
        }
        return ans;
    }
}
5.单词长度的最大乘积

如果遍历每一对单词,然后每次判断是否有重复元素,复杂度为O(N^2*L),约为O(10^9),TLE。因此尝试是否能将判断公共字母的时间复杂度降为O(1),所以我们需要进行一步预处理。针对每个单词words[i],采用位掩码记录该单词中出现过的字母,即如果那个字母出现,对应位置1, masks[i] |= 1 << (word.charAt(j) - 'a');,时间复杂度O(N*L),总时间复杂度O(N*L+N^2),约为O(10^6)

class Solution {
    public int maxProduct(String[] words) {
        int len=words.length;
        int[]states=new int[len];
        for(int i=0;i<len;i++){
            String w=words[i];
            int wlen=w.length();
            for(int j=0;j<wlen;j++){
                //掩码记录出现字母
                states[i]|=1<<w.charAt(j)-'a';                
            }
        }
        int res=0;
        for(int i=0;i<len;i++){
            for(int j=i+1;j<len;j++){
                if((states[i]&states[j])==0){
                    res=Math.max(res,words[i].length()*words[j].length());
                }
            }
        }
        return res;
    }
}
6.排序数组中两个数字之和

这题用双指针解答,主要是要理解双指针的单向移动不会错过答案。
同时这也是我开始用cpp刷的第一题,cpp刷题的入门知识同步更新于我的另一篇文章算法学习-以刷题为导向需要掌握的C++知识

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int i=0;
        int j=numbers.size()-1;
        while(i<j){
            int sum=numbers[i]+numbers[j];
            if(sum==target){
                return {i,j};
            }else if(sum>target){
                j--;
            }else{
                i++;
            }
        }
        //前面一定会返回答案
        return {0,0};
    }
};
7.数组中和为0的三个数

常规暴力解法为O(N^3)超时,固定两个数通过二分查找第三个数O(N^2logN)可以过,最优的解法是固定一个数,内部两个数的确定采用双指针,类似思路同上 6.排序数组中两个数字之和,整体时间复杂度为O(N^2),但是去重处理需要注意,通常是将数组「排序」后方便处理。

#include<iostream>
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        vector<vector<int>> res;
        int len=nums.size();
        for(int i=0;i<len;i++){
            int a=nums[i];
            //这里进行去重,因为针对第一个重复的元素来说,我们已经将包含重复的组合以及不重复的组合都枚举过了
            if(i>0&&nums[i]==nums[i-1]) continue;
            int left=i+1;
            int right=len-1;
            while(left<right){
                if(nums[i]+nums[left]+nums[right]>0) right--;
                else if(nums[i]+nums[left]+nums[right]<0) left++;
                else{
                    res.push_back({nums[i],nums[left],nums[right]});
                    //内部也要进行去重
                    while(left<right&&nums[left]==nums[left+1]) left++;
                    while(left<right&&nums[right]==nums[right-1]) right--;
                    //再进一位进行下一步判断
                    left++;
                    right--;
                }
            }
        }
        return res;
    }
};
8.和大于等于target的最短子数组

经典滑动窗口,学会cpp解法。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int len=nums.size();
        int sum=0;
        int left=0;
        int right=0;
        int ans=INT_MAX;
        while(right<len){
            sum+=nums[right];
            while(sum>=target){
                ans=min(ans,right-left+1);
                sum-=nums[left];
                left++;    
            }
            right++;
        }
        return ans==INT_MAX?0:ans;
    }
};
9.乘积小于K的子数组

参考题解,应用了滑动窗口思想,但是是通过每次left或者right的改变求满足条件的连续子数组数量。

class Solution {
public:
    int numSubarrayProductLessThanK(vector<int>& nums, int k) {
        int ans = 0;
        int left = 0;
        int right=0;
        int len = nums.size();
        int mul = 1;
        while (right < len) {
            mul *= nums[right];
            // 不满足条件的时候
            while(left<=right&&mul >= k){
                mul /= nums[left];
                left++;
            }
            ans += right - left + 1;
            right++;
        }
        return ans;
    }
};
10.和为K的子数组
class Solution {
    public int subarraySum(int[] nums, int k) {
        //key:前缀和 value:该前缀和的个数
        HashMap<Integer,Integer> map=new HashMap<>();
        //前缀和为0的时候初始化为1,方便后面preSum-k的get
        //细节,这里需要预存前缀和为 0 的情况,否则会漏掉前几位就满足的情况
        //例如输入[1,1,0],k = 2 如果没有这行代码,则会返回0,漏掉了1+1=2,和1+1+0=2的情况
        map.put(0,1);

        int preSum = 0;
        int ans = 0;
        for(int i:nums){
            preSum += i;
            //当前前缀和已知,判断是否含有 presum - k的前缀和,那么我们就知道某一区间的和为 k 了。
            if (map.containsKey(preSum-k)) {
                ans += map.get(preSum-k); //获取次数
            }
            //不断维护(preSum,value)的哈希表
            map.put(preSum,map.getOrDefault(preSum,0)+1);
        }
        return ans;
    }
}
11.01个数相同的子数组
class Solution {
    public int findMaxLength(int[] nums) {
        int cur=0;
        HashMap<Integer,Integer> map=new HashMap<>();
        map.put(0,-1);
        //可能有多种符合题目的情况,我们取最大值
        int ans=0;
        for(int i=0;i<nums.length;i++){
            cur += nums[i]==0?-1:1;
            if(map.containsKey(cur)){
                ans=Math.max(ans,i-map.get(cur));
                //else是一种贪心,当哈希表中存在相同的cur时,仍然放之前最小的那个
            }else{
                map.put(cur,i);
            }
        }
        return ans;
    }
}
12.左右两边子数组的和相等
class Solution {
    public int pivotIndex(int[] nums) {
        int sum=0;
        for(int i:nums){
            sum+=i;
        }
        int temp=0;
        for(int i=0;i<nums.length;i++){
            temp+=nums[i];
            int left=temp-nums[i];
            int right=sum-temp;
            if(left==right) return i;
        }
        return -1;
    }
}
13.二维子矩阵的和

这题直接按题意模拟也能做,但考虑到会调用 104sumRegion 方法,每次都要O(M*N)的时间复杂度,我们可以进行前缀和优化,用一次O(M*N)的时间算出前缀子矩阵和,然后每次调用sumRegion都只需要O(1)的复杂度。参考题解,前缀子矩阵和按列算出。

左上角 (row1, col1) 、右下角 (row2, col2)这个下标按找到的方块理解,不然会陷入索引里去。

class NumMatrix {
    //preSum[i][j]表示以(i,j)块为右下边界的子矩阵前缀和
    int [][]preSum;
    int row;
    int col;
    public NumMatrix(int[][] matrix) {
        row=matrix.length;
        col=matrix[0].length;
        preSum=new int[row+1][col+1];
        //按列计算
        for(int j=0;j<col;j++){
            int colsum=0;
            for(int i=0;i<row;i++){
                colsum+=matrix[i][j];
                preSum[i+1][j+1]=preSum[i+1][j]+colsum;
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return preSum[row2+1][col2+1]-preSum[row2+1][col1]-preSum[row1][col2+1]+preSum[row1][col1];
    }
}

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix obj = new NumMatrix(matrix);
 * int param_1 = obj.sumRegion(row1,col1,row2,col2);
 */
14.字符串中的变位词

固定大小的滑动窗口题

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int[] target=new int[26];
        for(char c:s1.toCharArray()){
            target[c-'a']++;
        }
        int left=0;
        int[] temp=new int[26];
        for(int right=0;right<s2.length();right++){
            temp[s2.charAt(right)-'a']++;
            if(right-left+1==s1.length()){
                if(Arrays.equals(temp,target)){
                    return true;
                }
                temp[s2.charAt(left)-'a']--;
                left++;
            }
        }
        return false;
    }
}
15.字符串中的所有变位词

固定大小的滑动窗口

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        int[]target=new int[26];
        for(char c:p.toCharArray()){
            target[c-'a']++;
        }
        ArrayList<Integer>ans=new ArrayList<>();
        int len=s.length();
        int left=0;
        int[]temp=new int[26];
        for(int right=0;right<len;right++){
            temp[s.charAt(right)-'a']++;
            if(right-left+1==p.length()){
                if(Arrays.equals(target,temp)){
                    ans.add(left);
                }
                temp[s.charAt(left)-'a']--;
                left++;
            }
        }
        return ans;
    }
}
16.不含重复字符的最长子字符串

窗口长度可变求最大值

class Solution {
    public int lengthOfLongestSubstring(String s) {
        HashSet<Character> window=new HashSet<>();
        int len=s.length();
        int left=0;
        int ans=0;
        for(int right=0;right<len;right++){
            while(window.contains(s.charAt(right))){
                window.remove(s.charAt(left++));
            }
            window.add(s.charAt(right));
            ans=Math.max(ans,right-left+1);
        }
        return ans;
    }
}
17.含有所有字符的最短字符

滑动窗口最小值
方法类似于 438.找到字符串中所有字母异位词,用target数组记录要匹配的值,大小写加上中间的字母总共58的大小。用check()检查是否包含,包含则调整左边界,缩小字符串长度。

class Solution {
    public String minWindow(String s, String t) {
        int[]target=new int[58];
        for(char c:t.toCharArray()){
            target[c-'A']++;
        }
        String res="";
        int left=0;
        int len=s.length();
        int[]temp=new int[58];
        int ans=0x3f3f3f3f;
        for(int right=0;right<len;right++){
            temp[s.charAt(right)-'A']++;
            while(check(temp,target)){
                int value=right-left+1;
                if(value<ans){
                    ans=value;
                    res=s.substring(left,right+1);
                }
                temp[s.charAt(left)-'A']--;
                left++;
            }
        }
        return res;
    }

    public boolean check(int[]temp,int[]target){
        for(int i=0;i<temp.length;i++){
            if(temp[i]<target[i]) return false;
        }
        return true;
    }
}
18.有效的回文串
class Solution {
    public boolean isPalindrome(String s) {
        int i=0;
        int j=s.length()-1;
        while(i<=j){
            if(!Character.isLetterOrDigit(s.charAt(i))){
                i++;
                continue;
            }
            if(!Character.isLetterOrDigit(s.charAt(j))){
                j--;
                continue;
            }
            char a=Character.toLowerCase(s.charAt(i));
            char b=Character.toLowerCase(s.charAt(j));
            if(a!=b) return false;
            i++;
            j--;
        }
        return true;
    }
}
19.最多删除一个字符得到回文

如果双指针不匹配,则尝试删除左指针元素或者右指针元素,再分别判断回文串。

class Solution {
    //如果双指针不匹配,则尝试删除左指针元素或者右指针元素,再分别判断回文串
    public boolean validPalindrome(String s) {
        int i=0;
        int j=s.length()-1;
        while(i<=j){
            if(s.charAt(i)==s.charAt(j)){
                i++;
                j--;
            }else{
                return isValid(s,i+1,j)||isValid(s,i,j-1);
            }
        }
        return true;
    }

    public boolean isValid(String s,int i,int j){
        while(i<=j){
            if(s.charAt(i)==s.charAt(j)){
                i++;
                j--;
            }else{
                return false;
            }
        }
        return true;
    }
}
20.回文字符串的个数

可以采用两种做法,一种是双指针暴力枚举O(N^2),但是枚举的过程中不断记录正向和反向的字符串,比较字符串是否相等O(1),而不需要额外来一次回文比较O(N),空间换时间。

class Solution {
    public int countSubstrings(String s) {
        int len=s.length();
        int cnt=0;
        for(int i=0;i<len;i++){
            String s1="";
            String s2="";
            for(int j=i;j<len;j++){
                s1+=s.charAt(j);
                s2=s.charAt(j)+s2;
                if(s1.equals(s2)) cnt++;
            }
        }
        return cnt;
    }
}

还有一种是中心拓展法,不用担心会不会记录重复的串,由于左右边界如果要展开都是双向同时展开的,串的一部分有重复不会影响最后整个串重复。考虑奇偶的展开是不一样的情况。

class Solution {
    public int countSubstrings(String s) {
        int cnt=0;
        int len=s.length();
        for(int k=0;k<=len-1;k++){
            cnt += countValid(s,k,k);
            cnt += countValid(s,k,k+1);
        }
        return cnt;
    }

    public int countValid(String s,int i,int j){
        int cnt=0;
        while(i>=0&&j<=s.length()-1){
            if(s.charAt(i--)==s.charAt(j++)){
                cnt++;
            }else break;
        }
        return cnt;
    }
}
21.删除链表的倒数第n个结点

快慢指针法,快指针先提前走n步,然后快慢指针同步走,快指针走到最后一个节点,慢指针刚好到删除节点的前一个节点。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyHead=new ListNode(0);
        dummyHead.next=head;
        ListNode fast=dummyHead;
        ListNode slow=dummyHead;
        while(n--!=0){
            fast=fast.next;
        }
        while(fast.next!=null){
            slow=slow.next;
            fast=fast.next;
        }
        slow.next=slow.next.next;
        return dummyHead.next;
    }
}
22.链表中环的入口节点

快慢指针的运用,快指针每次走两步,慢指针每次走一步,能相遇则一定有环否则无环。设链表共有 a+b 个节点,其中 链表头部到链表入口 有 a 个节点(不计链表入口节点), 链表环 有 b 个节点,快指针步数fast=2*slowfast=slow+n*b,如果两者在任何时刻相遇,都能得到此时slow=n*b。记下相遇时慢指针走了nb步,由于所有指针走到环的入口节点时的步数是k=a+nb,因此慢指针再走a步就能到环入口,这正是链表头开始走,可以和慢指针slow碰头的步数。

总而言之,并没有用到链表中行走长度的记录,而是用了两次指针的碰撞。第一次快慢指针碰撞记下slow的位置;然后重新定义个cur指针从head出发,继续尝试和slow碰撞,从而找到环的入口。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast=head;
        ListNode slow=head;
        // 如果出现null就说明无环
        while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if(fast==slow){
                ListNode cur=head;
                while(slow!=cur){
                    slow=slow.next;
                    cur=cur.next;
                }
                // 有环并找到为cur
                return cur;
            }
        }
        // 无环返回null
        return null;
    }
}
23.两个链表的第一个重合节点

参考题解,本质上就是要一直找到两个链表「地址相同的两个节点」,但两链表节点数不一样,没法对应。可以尝试拼接的做法,让p1遍历完链表A之后开始遍历链表B,让p2遍历完链表B之后开始遍历链表A,这样相当于「逻辑上」两条链表接在了一起,这样子就让相交节点前面长度补齐了。即使最后两链表没有相交,则p1=p2=null退出循环。两条拼接链表都只遍历一次。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode p1=headA;
        ListNode p2=headB;
        while(p1!=p2){
            p1=p1==null?headB:p1.next;
            p2=p2==null?headA:p2.next;
        }
        return p1;
    }
}
24.反转链表
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre=null;
        ListNode cur=head;
        while(cur!=null){
            ListNode temp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=temp;
        }
        return pre;
    }
}
25.链表中的两数相加

先用栈将两个链表的数字反转存储,然后双对象双指针的加法模板+头插法构建链表

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        Stack<Integer> st1=new Stack<>();
        Stack<Integer> st2=new Stack<>();
        while(l1!=null){
            st1.push(l1.val);
            l1=l1.next;
        }
        while(l2!=null){
            st2.push(l2.val);
            l2=l2.next;
        }
        int carry=0;
        ListNode anshead=null;
        while(!st1.isEmpty()||!st2.isEmpty()){
            int digitA=st1.isEmpty()?0:st1.peek();
            int digitB=st2.isEmpty()?0:st2.peek();
            int sum=digitA+digitB+carry;
            int res=sum%10;
            carry=sum/10;

            ListNode newNode=new ListNode(res);
            newNode.next=anshead;
            anshead=newNode;

            if(!st1.isEmpty()) st1.pop();
            if(!st2.isEmpty()) st2.pop();
        }
        if(carry!=0){
            ListNode newNode=new ListNode(carry);
            newNode.next=anshead;
            anshead=newNode;
        }
        return anshead;
    }
}
26.重排链表

这题涉及到的知识点非常多,参考题解,总体思路是中间分割,后半部分反转链表,然后合并链表。先中间分割链表,while(fast.next!=null&&fast.next.next!=null)在偶数节点情况下找到的是前面的中间节点,奇数情况下是正中间节点,我们都通过mid.next取到后小半部分,然后反转后半部分链表,合并链表是原地操作的,没有新建链表,将反转链表合并到前半部分上去。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public void reorderList(ListNode head) {
        ListNode mid= split(head);
        ListNode headb= reverse(mid.next);
        //这一步很关键,要将两个链表分开来
        mid.next=null;
        merge(head,headb);
    }
    public ListNode split(ListNode head){
        ListNode slow=head;
        ListNode fast=head;
        while(fast.next!=null&&fast.next.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        return slow;
    }
    public ListNode reverse(ListNode head){
        ListNode pre=null;
        ListNode cur=head;
        while(cur!=null){
            ListNode temp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=temp;
        }
        return pre;
    }
    public void merge(ListNode heada,ListNode headb){
        ListNode cura=heada;
        ListNode curb=headb;
        while(curb!=null){
            ListNode nexta=cura.next;
            ListNode nextb=curb.next;
            curb.next=nexta;
            cura.next=curb;
            cura=nexta;
            curb=nextb;
        }
    }
}
27.回文链表

从后向前找到链表中心点,偶数找到左边中心点,然后将后半部分链表反转,最后比较后小半部分是否和从head开始的前半部分相同。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode fast=head;
        ListNode slow=head;
        //找到中心点
        while(fast.next!=null&&fast.next.next!=null){
            slow=slow.next;
            fast=fast.next.next;
        }

        //后半部分反转
        ListNode pre=null;
        ListNode cur=slow.next;
        while(cur!=null){
            ListNode temp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=temp;
        }
        
        //后半部分偏小,比较反转的后半部分和前半部分是否回文
        while(pre!=null){
            if(pre.val!=head.val) return false;
            pre=pre.next;
            head=head.next;
        }
        return true;
    }
}
28.展平多级双向链表

遍历第一级节点,将后面的节点逐层拼接到第一级上,做法是将head.child!=null的孩子孩子节点头尾改变指向,放到第一级上去。

/*
// Definition for a Node.
class Node {
    public int val;
    public Node prev;
    public Node next;
    public Node child;
};
*/

class Solution {
    public Node flatten(Node head) {
        Node cur=head;
        while(cur!=null){
            //可能后面级插入上来的child也不为null
            if(cur.child!=null){
                //保存cur的下一个节点,便于插入后的拼接
                Node temp=cur.next;
                //改变cur的指向,双向改变
                Node childnode=cur.child;
                cur.next=childnode;
                childnode.prev=cur;
                cur.child=null;
                //找到child的最后一个节点
                Node last=cur;
                while(last.next!=null){
                    last=last.next;
                }
                //将child最后一个节点连接到第一级上
                last.next=temp;
                if(temp!=null) temp.prev=last;
            }
            //同时可以查看后面拼接上来的节点的child
            cur=cur.next;
        }
        return head;
    }
}
29.排序的循环链表

参考题解,考虑到五种情况,while(ptr->next != head)是循环链表循环查找的方式。从头开始查找插入的位置ptr,当查到1.val在当前节点ptr下一个点中间 2.最大值最小值 3.循环到头结束(1个元素或者都是相同元素),就在ptr后面插入元素,实际上就是找到链表插入基本操作里的pre节点了。

/*
// Definition for a Node.
class Node {
    public int val;
    public Node next;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val, Node _next) {
        val = _val;
        next = _next;
    }
};
*/

class Solution {
    public Node insert(Node head, int insertVal) {
        //列表为空
        if(head==null){
            Node newNode=new Node(insertVal);
            newNode.next=newNode;
            return newNode;
        }
        Node ptr=head;
        //循环到头结束
        while(ptr.next!=head){
            //val在当前节点ptr和下一个点中间
            if(insertVal>=ptr.val&&insertVal<=ptr.next.val) break;
            //在链表首尾相连处找到最大值最小值
            if(ptr.val>ptr.next.val&&(insertVal>=ptr.val||insertVal<=ptr.next.val)) break;
            ptr=ptr.next;
        }

        Node newNode=new Node(insertVal);
        newNode.next=ptr.next;
        ptr.next=newNode;
        return head;
    }
}
30.插入删除和随机访问都是O(1)的容器

HashMap+数组,HashMap在插入和删除时都为 O ( 1 ) O(1) O(1),但是删除的时候没法保证,因此本题将两者的优势结合起来,同时更新一个元素的HashMap和数组,它们的桥梁就是元素在数组中的下标,因此HashMap的value存的也是下标。

class RandomizedSet {
    int[]array=new int[(int)2e5];
    int idx=-1;
    HashMap<Integer,Integer>map=new HashMap<>();
    Random random=new Random();
    public RandomizedSet() {

    }
    
    public boolean insert(int val) {
        if(map.containsKey(val)) return false;
        else{
            // array和map同时记录
            array[++idx]=val;
            map.put(val,idx);
            return true;
        }
    }
    
    public boolean remove(int val) {
        if (!map.containsKey(val)) return false;
        else{
            int loc=map.remove(val);
            // 需要进行元素位置交换,填补删去的loc
            if (loc==idx){ // 删去的元素本身就在最后
                idx--;
            }else{ // array用最后一个元素填补删去的loc位置,map放上最后一个元素
                map.put(array[idx],loc);
                array[loc]=array[idx];
                idx--;
            }
            return true;
        }
    }
    
    public int getRandom() {
        return array[(int)random.nextInt(idx+1)];
    }
}

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet obj = new RandomizedSet();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */
31.最近最少使用缓存

HashMap+造轮子实现双向链表

hashmap查找一个节点的性能为O(1)-->get;链表对一个节点增删操作的时间为O(1),在已经得到要删除的节点的引用时,双向链表对一个节点的增删最方便,直接改变该节点的两个指向,性能为O(1)-->put、delete。两个数据结构增删改查的时候同步更新,联系就是hashmap为HashMap<Integer,Node>,value是key在双向链表中代表的节点引用Node。

get()和put()都是需要进行使用更新的操作,本质上get()就是将原来的key-value再重新put()一次,因此get()可以复用put()进行更新。put()在更新时,始终要将最近使用的节点通过addFirst()放在第一个节点上,如果有相同的key节点存在则需要先删除原先的节点,如果没有相同的key的存在,则判断是否已经满了,满了的话就先删除最后一个节点,然后将新节点放在双向链表头上。

双向链表的delete()和addFirst()方法传入参数都是Node引用,便于双向链表直接操作。其中delete()可以返回删除节点的key,便于紧接着根据key删除其在map里对应的node。

class LRUCache {
    HashMap<Integer,Node> map;
    DoubleLinkedList cache;
    int cap;
    public LRUCache(int capacity) {
        map=new HashMap<>();
        cache=new DoubleLinkedList();
        cap=capacity;
    }

    public class Node{
        int k,v;
        Node pre,next;
        public Node(int _k, int _v){
            k=_k;
            v=_v;
        }
    }

    public class DoubleLinkedList{
        Node head,tail;
        public DoubleLinkedList(){
            head=new Node(-1,-1);
            tail=new Node(-1,-1);
            head.next=tail;
            tail.pre=head;
        }
        public int delete(Node n){
            if(head.next==tail) return -1;
            n.next.pre=n.pre;
            n.pre.next=n.next;
            return n.k;
        }
        public int deleteLast(){
            return delete(tail.pre);
        }
        public void addFirst(Node newnode){
            newnode.next=head.next;
            newnode.pre=head;
            head.next.pre=newnode;
            head.next=newnode;
        }
    }
    
    public int get(int key) {
        if(!map.containsKey(key))return -1;
        else{
            int value=map.get(key).v;
            //往双向链表put相同的value
            put(key,value);
            return value;
        }
    }
    
    public void put(int key, int value) {
        Node newnode=new Node(key,value);
        //有相同key的节点进行更新替换
        if(map.containsKey(key)){
            //删除原先双向链表中的该节点,进行更新
            cache.delete(map.get(key));
        }else{ // 没有相同key的节点
            //已经存满了,删除最后未使用的节点
            if(map.size()==cap){
                int k=cache.deleteLast();  
                map.remove(k);
            }
        }
        //一份newnode关联了两个数据结构
        map.put(key,newnode);
        cache.addFirst(newnode);
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */
32.有效的变位词

直接哈希表记录记录两字符串各个字符是否相等,当仅包含小写字母的时候,所有字符是已知的,字典大小初始化为小写字母个数26。当为Unicode字符的时候,则字符很难全部表示,可以建立HashMap。其中题目要求不完全相同,因此需要把相同的字符排除。

class Solution {
    public boolean isAnagram(String s, String t) {
        HashMap<Character,Integer> map=new HashMap<>();
        int len1=s.length();
        int len2=t.length();
        //不完全相同,因此需要把相同的字符排除
        if(len1!=len2||s.equals(t)) return false;
        for(int i=0;i<len1;i++){
            char chs=s.charAt(i);
            map.put(chs,map.getOrDefault(chs,0)+1);
        }
        for(int i=0;i<len2;i++){
            char cht=t.charAt(i);
            map.put(cht,map.getOrDefault(cht,0)-1);
            if(map.get(cht)<0) return false;
        }
        return true;
    }
}
33.变位词组

采用HashMap key记录相同的变位词,每个字符出现次数相同而不要求顺序,那么我们可以对每个词组内部排序,然后去查找是否存在对应的key,有则在此key对应的value内增加,否则新增一条记录。最终将map的value全部返回。

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        HashMap<String,List<String>> map=new HashMap<>();
        for(String s:strs){
            char[] str=s.toCharArray();
            //返回void
            Arrays.sort(str);
            String key=new String(str);
            List<String> list=map.getOrDefault(key,new ArrayList<String>());
            list.add(s);
            map.put(key,list);
        }
        return new ArrayList<List<String>>(map.values());
    }
}
34.外星语言是否排序

用HashMap记录字典顺序的方法很精妙,然后采用双对象双指针的加法模板对两个单词进行逐位比较,从前到后按顺序一一比较。

class Solution {
    public boolean isAlienSorted(String[] words, String order) {
        HashMap<Character,Integer> map=new HashMap<>();
        for(int i=0;i<order.length();i++){
            map.put(order.charAt(i),i);
        }
        int len=words.length;
        for(int i=0;i<len-1;i++){
            String word1=words[i];
            String word2=words[i+1];
            int len1=word1.length();
            int len2=word2.length();
            int m=0;
            int n=0;
            while(m<len1||n<len2){
                int cm=m<len1?map.get(word1.charAt(m)):-1;
                int cn=n<len2?map.get(word2.charAt(n)):-1;
                //只要前面的小了,就可以不看后面的了
                if(cm<cn) break;
                //大于才需要return false,特殊情况是一直等于就一直往后看
                if(cm>cn) return false;
                m++;
                n++;
            }
        }
        return true;
    }
}
35.最小的时间差

将时间转换为分钟数,方便排序和做差,同时将最小时间加上24*60,以便循环做差。

class Solution {
    public int findMinDifference(List<String> timePoints) {
        int len=timePoints.size();
        ArrayList<Integer> time=new ArrayList<>();
        for(int i=0;i<len;i++){
            String[] hm=timePoints.get(i).split(":");
            time.add(Integer.parseInt(hm[0])*60+Integer.parseInt(hm[1]));
        }
        Collections.sort(time);

        //最小值+24*60可以计算循环的相差时间
        time.add(time.get(0)+24*60);
        int ans=0x3f3f3f3f;
        for(int i=0;i<len;i++){
            int res=time.get(i+1)-time.get(i);
            ans=Math.min(ans,res);
        }
        return ans;
    }
}
36.后缀表达式

经典栈

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer>st=new Stack<>();
        for(String t:tokens){
            if(t.equals("+")){
                st.push(st.pop()+st.pop());
            }else if(t.equals("-")){
                st.push(-st.pop()+st.pop());
            }else if(t.equals("*")){
                st.push(st.pop()*st.pop());
            }else if(t.equals("/")){
                int a=st.pop();
                int b=st.pop();
                st.push(b/a);
            }else{
                st.push(Integer.valueOf(t));
            }
        }
        return st.pop();
    }
}
37.小行星碰撞

栈+标志位,控制元素的进出。

class Solution {
    public int[] asteroidCollision(int[] asteroids) {
        Stack<Integer> st=new Stack<>();
        for(int a:asteroids){
            boolean flag=true;
            while(flag&&!st.isEmpty()&&st.peek()>0&&a<0){
                int mover=st.peek();
                int movel=-a;
                if(mover<=movel) st.pop();
                //a不加入,查看下一个a
                if(mover>=movel) flag=false;
            }
            if(flag) st.push(a);
        }
        int[]res=st.stream().mapToInt(x->x).toArray();
        return res;
    }
}
38.每日温度

单调递减栈应用,存储的是数组下标。

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int len=temperatures.length;
        int[]res=new int[len];
        Stack<Integer> st=new Stack<>();
        for(int i=0;i<len;i++){
            while(!st.isEmpty()&&temperatures[i]>temperatures[st.peek()]){
                int top=st.pop();
                res[top]=i-top;
            }
            st.push(i);
        }
        return res;
    }
}
39.直方图最大矩形面积

单调递增栈

class Solution {
    public int largestRectangleArea(int[] heights) {
        int len=heights.length;
        int[] newHeight=new int[len+2];
        for(int i=0;i<len;i++){
            newHeight[i+1]=heights[i];
        }
        long res=0;
        Stack<Integer> st=new Stack<>();
        for(int i=0;i<len+2;i++){
            while(!st.isEmpty()&&newHeight[i]<newHeight[st.peek()]){
                int top=st.pop();
                int h=newHeight[top];
                int w=i-st.peek()-1;
                res=Math.max(res,1L*h*w);
            }
            st.push(i);
        }
        return (int)res;
    }
}
40.矩阵中的最大矩形

不同于用DFS求最大的1的面积,这里需要保证它是矩形。转换为,针对每一行,计算以该行为底部向上的柱状图中连续的最大的矩形面积,整体复杂度为O(mn)

class Solution {
    public int maximalRectangle(String[] matrix) {
        int m=matrix.length;
        if(m==0) return 0;
        int n=matrix[0].length();
        int res=0;
        int[]heights=new int[n];
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(matrix[i].charAt(j)=='0') heights[j]=0;
                else heights[j]+=1;
            }
            res=Math.max(res,largestRectangleArea(heights));
        }
        return res;
    }


    //柱状图中的最大面积
    public int largestRectangleArea(int[] heights) {
        int len=heights.length;
        int[] newHeight=new int[len+2];
        for(int i=0;i<len;i++){
            newHeight[i+1]=heights[i];
        }
        long res=0;
        Stack<Integer> st=new Stack<>();
        for(int i=0;i<len+2;i++){
            while(!st.isEmpty()&&newHeight[i]<newHeight[st.peek()]){
                int top=st.pop();
                int h=newHeight[top];
                int w=i-st.peek()-1;
                res=Math.max(res,1L*h*w);
            }
            st.push(i);
        }
        return (int)res;
    }
}
41.滑动窗口的平均值

双端队列,用数组模拟。

class MovingAverage {
    int[]que=new int[10010];
    int start;
    int end;
    int n;
    int sum=0;
    /** Initialize your data structure here. */
    public MovingAverage(int size) {
        n=size;
    }
    
    public double next(int val) {
        que[end++]=val;
        sum+=val;
        if(end-start>n) sum-=que[start++];
        return 1.0*sum/(end-start);
    }
}

/**
 * Your MovingAverage object will be instantiated and called as such:
 * MovingAverage obj = new MovingAverage(size);
 * double param_1 = obj.next(val);
 */
42.最近请求次数

注意到调用时间严格递增,因此可以用双端队列先进先出的性质,将时间小于t-3000的出队列,最后返回队列长度。

class RecentCounter {
    int slot;
    Deque<Integer> que;
    public RecentCounter() {
        slot=3000;
        que=new ArrayDeque<>();
    }
    
    public int ping(int t) {
        que.offer(t);
        int start=t-3000;
        while(!que.isEmpty()&&que.peek()<start) que.poll();
        return que.size();
    }
}

/**
 * Your RecentCounter object will be instantiated and called as such:
 * RecentCounter obj = new RecentCounter();
 * int param_1 = obj.ping(t);
 */
43.往完全二叉树添加节点

采用和搜索层数无关的广度优先搜索,找到某个节点的左节点或者右节点为空,则将新节点插入到其左节点或者右节点上。后面的查询可以从该点开始查,不需要每次重新入队,将前面已经出队列的点再入队一次,因此可以将该点放入队头。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class CBTInserter {
    TreeNode root;
    ArrayDeque<TreeNode> que;
    public CBTInserter(TreeNode _root) {
        root=_root;
        que=new ArrayDeque<>();
    }
    
    public int insert(int v) {
        TreeNode newNode=new TreeNode(v);
        que.offer(root);
        while(!que.isEmpty()){
            TreeNode top=que.poll();
            if(top.left==null){
                top.left=newNode;
                que.offerFirst(top);
            }else if(top.right==null){
                top.right=newNode;
                que.offerFirst(top);
            }else{
                que.offer(top.left);
                que.offer(top.right);
                continue;
            }
            return top.val;
        }
        return -1;
    }
    
    public TreeNode get_root() {
        return root;
    }
}

/**
 * Your CBTInserter object will be instantiated and called as such:
 * CBTInserter obj = new CBTInserter(root);
 * int param_1 = obj.insert(v);
 * TreeNode param_2 = obj.get_root();
 */
44.二叉树每层的最大值

经典层序遍历

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> largestValues(TreeNode root) {
        ArrayList<Integer> ans=new ArrayList<>();
        if(root==null) return ans;
        Deque<TreeNode> que=new LinkedList<>();
        que.offer(root);
        while(!que.isEmpty()){
            int size=que.size();
            int maxV=Integer.MIN_VALUE;
            for(int i=0;i<size;i++){
                TreeNode top=que.poll();
                maxV=Math.max(maxV,top.val);
                if(top.left!=null) que.offer(top.left);
                if(top.right!=null) que.offer(top.right);
            }
            ans.add(maxV);
        }
        return ans;
    }
}
45.二叉树最底层最左边的值

同样是层次遍历,记录最后一层最左边的数字,只需要用一个局部变量ans每次都更新为最左边的数字,最后就是最后一层最左边的数字。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int findBottomLeftValue(TreeNode root) {
        Deque<TreeNode> que=new ArrayDeque<>();
        que.offer(root);
        int ans=-1;
        while(!que.isEmpty()){
            int size=que.size();
            for(int i=0;i<size;i++){
                TreeNode top=que.poll();
                if(i==0) ans=top.val;
                if(top.left!=null) que.offer(top.left);
                if(top.right!=null) que.offer(top.right);
            }
        }
        return ans;
    }
}
46.二叉树的右侧视图

经典层次遍历

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> rightSideView(TreeNode root) {
        Deque<TreeNode> que=new ArrayDeque<>();
        ArrayList<Integer> ans=new ArrayList<>();
        if(root==null) return ans;
        que.offer(root);
        while(!que.isEmpty()){
            int size=que.size();
            for(int i=0;i<size;i++){
                TreeNode top=que.poll();
                if(i==size-1) ans.add(top.val);
                if(top.left!=null) que.offer(top.left);
                if(top.right!=null) que.offer(top.right);
            }
        } 
        return ans;
    }
}
47.二叉树剪枝

后序遍历,从底向上进行二叉树剪枝。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public TreeNode pruneTree(TreeNode root) {
        if(root==null) return null;
        root.left=pruneTree(root.left);
        root.right=pruneTree(root.right);
        if(root.val==0&&root.left==null&&root.right==null) return null;
        return root;
    }
}
48.二叉树的序列化和反序列化

树的先序遍历+递归,序列化按照"root,A,B,null,…"的形式存入,其中将null也存入,这样子可以唯一确定一棵树。反序列化的时候也是根据前面序列化的结果,先根据,进行分割成数组,然后递归建树。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if(root==null){
            return "null,";
        }
        // 递归构建
        StringBuilder sb=new StringBuilder();
        sb.append(root.val+",");
        sb.append(serialize(root.left));
        sb.append(serialize(root.right));
        return sb.toString();
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        LinkedList<String> revertList= new LinkedList<>(Arrays.asList(data.split(",")));
        return buildTree(revertList);
    }

    // LinkedList<String> l传入的是引用,所有递归函数共用一个
    public TreeNode buildTree(LinkedList<String> l){
        // base情况
        if (Objects.equals(l.getFirst(),"null")){
            l.removeFirst();
            return null;
        }
        String s=l.removeFirst();
        TreeNode root=new TreeNode(Integer.parseInt(s));
        root.left=buildTree(l);
        root.right=buildTree(l);
        return root;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// TreeNode ans = deser.deserialize(ser.serialize(root));
49.从根节点到叶节点的路径数字之和

采用有返回值的dfs,该题还有无返回值dfs以及回溯的做法。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int sumNumbers(TreeNode root) {
        return dfs(root,0);
    }
    
    // 代表root节点的树的所有到叶子节点的路径值
    public int dfs(TreeNode root,int num){
        if(root==null) return 0;
        // 叶子结点返回路径数字num
        if (root.left==null&&root.right==null){
            return num*10+root.val;
        }
        // 非叶子结点返回它以下能达到的路径的数字的和
        return dfs(root.left,num*10+root.val)+dfs(root.right,num*10+root.val);
    }
}
50.向下的路径节点之和

双重dfs,将寻找路径之和的dfs2放入先序遍历的dfs1中,从每个节点开始找符合条件的值。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int cnt=0;
    public int pathSum(TreeNode root, int targetSum) {
        dfs1(root,targetSum);
        return cnt;
    }

    public void dfs1(TreeNode root, int target){
        if (root==null) return;
        dfs2(root,target);
        dfs1(root.left,target);
        dfs1(root.right,target);
    }

    public void dfs2(TreeNode root,long target){
        long res=target-root.val;
        if(res==0){
            cnt++;
        }
        if (root.left!=null) dfs2(root.left,res);
        if (root.right!=null) dfs2(root.right,res);
    }
}
51.节点之和最大的路径

后序遍历,全局变量res记录结果,递归函数返回一侧最大值。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int res=-0x3f3f3f3f;
    public int maxPathSum(TreeNode root) {
        dfs(root);
        return res;
    }

    public int dfs(TreeNode root){
        if(root==null) return 0;
        // 只保留左右递归贡献为正的部分
        int l=dfs(root.left);
        l=Math.max(0,l);
        int r=dfs(root.right);
        r=Math.max(0,r);
        // 记录答案
        res=Math.max(res,l+r+root.val);
        // 返回左右两侧的较大结果,别忘了加上根节点
        return Math.max(l,r)+root.val;
    }
}
52.展平二叉搜索树

中序遍历,通过dummyhead构建树免去了头节点的特殊判断

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    TreeNode temp;
    public TreeNode increasingBST(TreeNode root) {
        // dummyHead一直不改变,让temp一直向下改变指向
        TreeNode dummyHead=new TreeNode(-1);
        temp=dummyHead;
        dfs(root);
        return dummyHead.right;
    }
    public void dfs(TreeNode root){
        if(root==null){
            return;
        }
        dfs(root.left);
        // 这里开辟了新空间,所以下面left=null可以删掉,如果直接=root
        temp.right=new TreeNode(root.val);
        //System.out.println(temp.left);
        //temp.left=null; // =root的话这里需要清理root.left
        temp=temp.right;
        dfs(root.right);
    }
}
53.二叉搜索树中的中序后继
  1. 从根节点 root 开始,比较当前 cur 节点的值和节点 p 的值的大小关系
  2. 如果当前 cur 节点的值小于或等于节点 p 的值,那么比节点 p 的值大的节点应该在它的右子树
  3. 如果当前 cur 节点的值大于节点 p 的值,那么当前节点有可能是 p 的下一个节点,还需要判断当前 cur 节点的左节点是否满足以上条件
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
        TreeNode res=null;
        TreeNode cur=root;
        while(cur!=null){
            System.out.println(cur.val);
            if (cur.val<=p.val){
                cur=cur.right;
            }else{ // cur.val>p.val cur就有可能是要找的「下一个节点」
                res=cur;
                cur=cur.left; // 继续往下看是否有满足条件的更小值
            }
        }
        return res;
    }
}
54.所有大于等于节点的值之和

巧妙地利用了二叉搜索树的性质,对之前的中序遍历次序进行了调换,先dfs(right)再dfs(left)可以降序输出值,只需要用一个sum累加结果就可以。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    int sum;
    public TreeNode convertBST(TreeNode root) {
        dfs(root);
        return root;
    }


    public void dfs(TreeNode root){
        if (root==null) return;
        dfs(root.right);
        sum+=root.val;
        root.val=sum;
        dfs(root.left);
    }
}
55.二叉搜索树迭代器

迭代法的二叉树中序遍历。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class BSTIterator {
    Stack<TreeNode> st;
    TreeNode cur;
    public BSTIterator(TreeNode root) {
        cur=root;
        st=new Stack<>();
    }
    
    public int next() {
        while(cur!=null) {
            st.push(cur);
            cur=cur.left;
        }
        // 类比中序遍历,这里相当于既做了cur!=null入栈也做了cur==null弹栈的工作
        TreeNode top=st.pop();
        cur=top.right;
        return top.val;
    }
    
    public boolean hasNext() {
        return !(cur==null&&st.isEmpty());
    }
}

/**
 * Your BSTIterator object will be instantiated and called as such:
 * BSTIterator obj = new BSTIterator(root);
 * int param_1 = obj.next();
 * boolean param_2 = obj.hasNext();
 */
56.二叉搜索树中两个节点之和

二叉树的先序递归遍历+Set,在遍历的过程中用类似两数之和的方法存下root.val的补数,如果刚好是补数就return true,否则存入补数并继续往下遍历。

这点很奇怪的点是,没用上二叉搜索树性质直接遍历+Set复杂度是 O ( N ) O(N) O(N),而用上搜索树性质中序遍历建list+双指针反倒是 2 ∗ O ( N ) 2*O(N) 2O(N)

class Solution {
    // 两数之和,存某个数的「补数」
    HashSet<Integer> s=new HashSet<>();
    public boolean findTarget(TreeNode root, int k) {
        if(root==null) return false;
        if(s.contains(root.val)) return true;
        s.add(k-root.val);
        return findTarget(root.left,k)||findTarget(root.right,k);
    }
}
57.值和下标之差都在给定的范围内

桶排序+滑动窗口,整体还是滑动窗口,不过这个窗口维护了一个用HashMap实现的桶,能够在O(1)的时间内找到是否存在abs(nums[i] - nums[j]) <= t的值。

class Solution {
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        int n=nums.length;
        // 桶中最大间隔为t,那么能容纳w个元素
        int w=t+1;
        // 用哈希表记录桶,每个桶内只需要暂存一个元素,因为一旦出现两个元素就返回true了
        HashMap<Long,Long> mp=new HashMap<>();
        for(int i=0;i<n;i++){
            long id=getId(nums[i],w);
            if(mp.containsKey(id)&&Math.abs(nums[i]-mp.get(id))<=t) return true;
            if(mp.containsKey(id-1)&&Math.abs(nums[i]-mp.get(id-1))<=t) return true;
            if(mp.containsKey(id+1)&&Math.abs(nums[i]-mp.get(id+1))<=t) return true;
            mp.put(getId(nums[i],w),(long)nums[i]);
            // 滑动窗口,只考虑下标差在k之间的数字所放置的桶
            if(i>=k) mp.remove(getId(nums[i-k],w));
        }
        return false;
    }
	// 获取桶的key
    public long getId(long x,long w){
        if(x>=0) return x/w;
        else return (x+1)/w-1;
    }
}
58.日程表

通过构建二叉搜索树可以很巧妙地解决这题,将第一个book作为根节点,每次查询的时候进行建立。

class TreeNode{
    int start;
    int end;
    TreeNode left;
    TreeNode right;
    public TreeNode(int start,int end){
        this.start=start;
        this.end=end;
    }
}

public class MyCalendar {
    TreeNode root;

    public MyCalendar() {
        root=null;
    }
    
    public boolean book(int start, int end) {
        if(root==null){
            root=new TreeNode(start,end);
            return true;
        }
        TreeNode cur=root;
        while(true){
            if(end<=cur.start){
                if(cur.left==null){
                    cur.left=new TreeNode(start,end);
                    return true;
                }
                cur=cur.left;
            }else if(start>=cur.end){
                if(cur.right==null){
                    cur.right=new TreeNode(start,end);
                    return true;
                }
                cur=cur.right;
            }else{
                return false;
            }
        }
    }
}

/**
 * Your MyCalendar object will be instantiated and called as such:
 * MyCalendar obj = new MyCalendar();
 * boolean param_1 = obj.book(start,end);
 */
59.数据流的第K大数值

维护一个K大小的小顶堆,在往里面add的时候不断进行检查维护,最后堆顶元素就是答案。

class KthLargest {
    PriorityQueue<Integer> q=new PriorityQueue<>();
    int k;
    public KthLargest(int _k, int[] nums) {
        k=_k;
        for(int i:nums){
            q.offer(i);
        }
    }
    
    public int add(int val) {
        q.offer(val);
        // 维护一个K大小的小顶堆
        while(q.size()>k){
            q.poll();
        }
        // 堆顶元素就是第K大元素
        return q.peek();
    }
}

/**
 * Your KthLargest object will be instantiated and called as such:
 * KthLargest obj = new KthLargest(k, nums);
 * int param_1 = obj.add(val);
 */
60.出现频率最高的k个数字

哈希表+最小堆。

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 哈希表存频率
        HashMap<Integer,Integer> mp=new HashMap<>();
        for(int i:nums){
            mp.put(i,mp.getOrDefault(i,0)+1);
        }

        // 最小堆存频率最高的K个数
        PriorityQueue<int[]> pq=new PriorityQueue<>((a,b)->(a[1]-b[1]));
        for(Map.Entry<Integer,Integer> m:mp.entrySet()){
            if(pq.size()==k){
                if(m.getValue()>=pq.peek()[1]){
                    pq.poll();
                    pq.offer(new int[]{m.getKey(),m.getValue()});
                }
            }else{
                 pq.offer(new int[]{m.getKey(),m.getValue()});
            }
        }
        int[]ans=new int[k];
        for(int i=0;i<k;++i){
            ans[i]=pq.poll()[0];
        }
        return ans;
    }
}
61.和最小的k个数对

最大堆,根据数对和进行排序。

class Solution {
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        PriorityQueue<int[]> pq=new PriorityQueue<>((a,b)->(b[0]+b[1])-(a[0]+a[1]));
        int len1=Math.min(nums1.length,k);
        int len2=Math.min(nums2.length,k);
        for(int i=0;i<len1;i++){
            for(int j=0;j<len2;j++){
                if(pq.size()==k){
                    if(nums1[i]+nums2[j]<pq.peek()[0]+pq.peek()[1]){
                        pq.poll();
                        pq.offer(new int[]{nums1[i],nums2[j]});
                    }
                }else{
                    pq.offer(new int[]{nums1[i],nums2[j]});
                }
            }
        }
        List<List<Integer>> res=new ArrayList<>();
        while(!pq.isEmpty()){
            int[] top=pq.poll();
            res.add(Arrays.asList(top[0],top[1]));
        }
        return res;
    }
}
68.查找插入位置

二分查找

class Solution {
    //查找第一个大于等于target的位置
    public int searchInsert(int[] nums, int target) {
        int left=0;
        int right=nums.length;
        while(left<right){
            int mid=(left+right)/2;
            if(nums[mid]>=target){
                right=mid;
            }else{
                left=mid+1;
            }
        }
        return left;
    }
}
69.山峰数组的顶部

需要注意二分查找的唯一一种写法。

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int len=arr.length;
        int left=1;
        int right=len-2;
        while(left<right){
            int mid=(left+right+1)/2;
            if(arr[mid-1]<=arr[mid]){
                left=mid;
            }else{
                // arr[mid-1]>arr[mid] right=mid-1合理,因此只能有left=mid的写法
                // 否则会存在arr[mid-1]<arr[mid] left=mid+1的情况,忽略了mid也是正确的
                right=mid-1;
            }
        }
        return left;
    }   
}
70.排序数组中只出现一次的数字

二分查找,运用奇偶两段性

class Solution:
    def singleNonDuplicate(self, nums: List[int]) -> int:
        left=0
        right=len(nums)-1
        while left<right:
            mid =(left+right)//2
            # 奇数和前一个比较,偶数和后一个比较,如果比较的前面都是出现两次的话,两个值应该是相等的
            if nums[mid]!=nums[mid^1]:
                right=mid
            else:
                left=mid+1
        return nums[left]
71.按权重生成随机数

二分查找,找大于等于preSum的第一个数

class Solution:
    def __init__(self, w: List[int]):
        self.preSum=[0]*(len(w)+1)
        self.preSum[0]=0
        for i in range(len(w)):
            self.preSum[i+1]=self.preSum[i]+w[i]

    def pickIndex(self) -> int:
        # 产生一个随机数
        seed = random.randint(1,self.preSum[-1])
        # 在前缀和数组preSum中找到第一个大于等于seed的下标-1
        left=0
        right=len(self.preSum)
        while left<right:
            mid = (left+right)//2
            if self.preSum[mid]<seed:
                left=mid+1
            else:
                right=mid
        return left-1

# Your Solution object will be instantiated and called as such:
# obj = Solution(w)
# param_1 = obj.pickIndex()
72.求平方根

二分查找只能right=mid-1

class Solution:
    def mySqrt(self, x: int) -> int:
        left=0
        right=x
        while left<right:
            mid=left+(right-left+1)//2
            if mid>x//mid:
                right=mid-1
            else:
                left=mid
        return left
73.狒狒吃香蕉

二分查找,对速度进行二分,找<=h的第一个数

class Solution:
    def minEatingSpeed(self, piles: List[int], h: int) -> int:
        def check(piles: List[int],speed:int):
            cnt=0
            for p in piles:
                if p <= speed:
                    cnt +=1
                else:
                    cnt += math.ceil(p/speed)
            return cnt
        left=1
        right=max(piles)
        while left < right:
            mid= (left+right)//2
            hour=check(piles,mid)
            if hour>h:
                left=mid+1
            else:
                right=mid
        return left
74.合并区间

排序+模拟

class Solution {
    public int[][] merge(int[][] intervals) {
        ArrayList<int[]> ans=new ArrayList<>();
        Arrays.sort(intervals,(a,b)->Integer.compare(a[0],b[0]));
        int len=intervals.length;
        int start=intervals[0][0];
        int end=intervals[0][1];
        for(int i=1;i<len;i++){
            if(intervals[i][0]>end){
                ans.add(new int[]{start,end});
                start=intervals[i][0];
                end=intervals[i][1];
            }else{
                end=Math.max(end,intervals[i][1]);
            }
        }
        ans.add(new int[]{start,end});
        return ans.toArray(new int[0][0]);
    }
}
75.数组相对排序

自定义比较函数,ans.sort(),时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)

class Solution {
    public int[] relativeSortArray(int[] arr1, int[] arr2) {
        HashMap<Integer,Integer> mp=new HashMap<>();
        int len=arr2.length;
        for(int i=0;i<len;i++){
            mp.put(arr2[i],i);
        }

        // int[]转List<Integer>
        List<Integer> ans= Arrays.stream(arr1).boxed().collect(Collectors.toList());
        ans.sort(new Comparator<Integer>(){
            @Override
            public int compare(Integer a,Integer b){
                if(mp.containsKey(a)&&mp.containsKey(b)){
                    return mp.get(a)-mp.get(b);
                }else if(mp.containsKey(a)){
                    return -1;
                }else if(mp.containsKey(b)){
                    return 1;
                }else{
                    return a-b;
                }
            }
        });
        // List<Integer> 转int[]
        return ans.stream().mapToInt(x->x).toArray();
    }
}

或者计数排序,时间复杂度 O ( N ) O(N) O(N)

class Solution {
    public int[] relativeSortArray(int[] arr1, int[] arr2) {
        // 计数排序
        int[]count=new int[1001];
        for(int i:arr1){
            count[i]++;
        }
        int[]ans=new int[arr1.length];
        int k=0;
        // 先按顺序排arr2的
        for(int a:arr2){
            while(count[a]!=0){
                ans[k++]=a;
                count[a]--;
            }
        }
        // 然后按照数字大小排arr1剩下的
        for(int i=0;i<count.length;i++){
            while(count[i]!=0){
                ans[k++]=i;
                count[i]--;
            }
        }
        return ans;
    }
}
76.数组中的第k大的数字

二分减治+快速选择,根据主定理 T ( n ) = T ( n / 2 ) + O ( n ) T(n)=T(n/2)+O(n) T(n)=T(n/2)+O(n)得到时间复杂度为 O ( N ) O(N) O(N)

class Solution {
    public int findKthLargest(int[] nums, int k) {
        int len=nums.length;
        int target=len-k;
        int left=0;
        int right=len-1;
        // 二分减治
        while(left<=right){
            int mid=partition(nums,left,right);
            if(mid==target){
                return nums[target];
            }else if(mid>target){
                right=mid-1;
            }else{
                left=mid+1;
            }
        }
        return 0;
    }

    // 快速选择可以确定一个元素的最终位置
    // 就是[left,right]这个区间内和pivot进行比较,从前往后、从后往前找到冲突swap
    public int partition(int[]nums,int left,int right){
        int rd=new Random().nextInt(right-left+1)+left;
        swap(nums,left,rd);
        int pivot=nums[left];
        int le=left+1;
        int ge=right;
        while(true){
            while(le<=ge&&nums[le]<=pivot)le++;
            while(le<=ge&&nums[ge]>=pivot)ge--;
            if(le>=ge) break;
            swap(nums,le,ge);
            le++;
            ge--;
        }
        swap(nums,left,ge);
        return ge;
    }

    public void swap(int[]nums,int left,int right){
        int temp=nums[right];
        nums[right]=nums[left];
        nums[left]=temp;
    }
}
77.链表排序

链表的归并排序,需要实现「找中间节点」的拆分以及两个排序链表的合并函数

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
        if(head==null) return null;
        if(head.next==null) return head;
        // 划分成两半
        ListNode mid=split(head);
        ListNode list2=mid.next;
        mid.next=null;

        //归并排序
        ListNode left=sortList(head);
        ListNode right=sortList(list2);
        return merge(left,right);
    }

    // 找到中间节点进行划分
    public ListNode split(ListNode head){
        ListNode fast=head;
        ListNode slow=head;
        while(fast.next!=null&&fast.next.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        return slow;
    }

    // 合并两个已经排序的列表
    public ListNode merge(ListNode list1,ListNode list2){
        ListNode dummyhead=new ListNode(-1);
        ListNode cur=dummyhead;
        while(list1!=null&&list2!=null){
            if(list1.val<=list2.val){
                cur.next=list1;
                list1=list1.next;
            }else{
                cur.next=list2;
                list2=list2.next;
            }
            cur=cur.next;
        }
        cur.next=list1==null?list2:list1;
        return dummyhead.next;
    }   
}
78.合并排序链表

归并排序,前面77是针对一个链表进行排序,这里是针对K个链表整合起来进行排序,粒度不同。拆分函数是将多个链表进行拆分,而合并函数还是合并两个排序链表。时间复杂度 O ( n k l o g ( n k ) ) O(nklog(nk)) O(nklog(nk))

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists==null||lists.length==0){
            return null;
        }
        if(lists.length==1){
            return lists[0];
        }
        return splitSort(lists,0,lists.length-1);
    }

    // 归并排序
    public ListNode splitSort(ListNode[] lists, int start, int end){
        if(start>end) return null;
        if(start==end){
            return lists[start];
        }
        int mid=start+((end-start)>>1);
        ListNode left=splitSort(lists,start,mid);
        ListNode right=splitSort(lists,mid+1,end);
        // 合并两个有序链表
        return merge(left,right);
    }


    // 合并两个链表
    public ListNode merge(ListNode list1,ListNode list2){
        ListNode dummyhead=new ListNode(-1);
        ListNode cur=dummyhead;
        while(list1!=null&&list2!=null){
            if(list1.val<=list2.val){
                cur.next=list1;
                list1=list1.next;
            }else{
                cur.next=list2;
                list2=list2.next;
            }
            cur=cur.next;
        }
        cur.next=list1==null?list2:list1;
        return dummyhead.next;
    }
}
79.所有子集

经典回溯,子集问题,数组中元素互不相同

class Solution {
    LinkedList<Integer> path;
    List<List<Integer>> ans;
    public List<List<Integer>> subsets(int[] nums) {
        path=new LinkedList<>();
        ans=new ArrayList<>();
        backTracing(nums,0);
        return ans;
    }

    public void backTracing(int[]nums,int startIndex){
        // 叶子节点和非叶子节点直接加入
        ans.add(new ArrayList<Integer>(path));
        // 叶子节点返回
        if(startIndex==nums.length){
            return;
        }
        for(int i=startIndex;i<=nums.length-1;i++){
            path.add(nums[i]);
            backTracing(nums,i+1);
            path.removeLast();
        }
    }
}
80.含有k个元素的组合

经典回溯,组合问题,找满足个数的组合

class Solution {
    LinkedList<Integer> path;
    List<List<Integer>> res;
    public List<List<Integer>> combine(int n, int k) {
        path=new LinkedList<>();
        res=new ArrayList<>();
        backTracing(n,k,1);
        return res;
    }
    public void backTracing(int n,int k, int startIndex){
        // 改条件下return,因为已经没有搜索下去的必要了
        if(path.size()==k){
            res.add(new ArrayList<>(path));
            return;
        }
        // 适当的剪枝
        for(int i=startIndex;i<=n-(k-path.size())+1;i++){
            path.add(i);
            backTracing(n,k,i+1);
            path.removeLast();
        }
    }
}
81.允许重复选择元素的组合

回溯,组合总和,可以重复选择元素

class Solution {
    LinkedList<Integer> path;
    int sum;
    List<List<Integer>> res;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        path=new LinkedList<>();
        res=new ArrayList<>();
        sum=0;
        backTracing(candidates,0,target);
        return res;
    }
    
    public void backTracing(int[]candidates,int startIndex, int target){
        if(target==sum){
            res.add(new ArrayList<>(path));
            return;
        }
        // 多了个return的条件
        if(target<sum){
            return;
        }
        for(int i=startIndex;i<candidates.length;i++){
            path.add(candidates[i]);
            sum+=candidates[i];
            // 可以重复选取元素,则下一次递归的startIndex可以不用+1
            backTracing(candidates,i,target);
            sum-=candidates[i];
            path.removeLast();
        }
    }
}
82.含有重复元素集合的组合

回溯,组合总和,重复元素,需要先对数组进行排序,然后避免同层之间的元素被反复取到,甚至不需要用used数组标记。

带used的做法:

class Solution {
    boolean[] used;
    List<List<Integer>> res;
    LinkedList<Integer> path;
    int sum;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        used=new boolean[candidates.length];
        res=new ArrayList<>();
        path=new LinkedList<>();
        backTracing(candidates,target,0);
        return res;
    }
    public void backTracing(int[] candidates, int target,int startIndex){
        if(sum==target){
            res.add(new ArrayList<>(path));
            return;
        }
        if(sum>target){
            return;
        }
        for(int i=startIndex;i<candidates.length;i++){
        	// 保证在同一层比较
            if(i>startIndex&&candidates[i]==candidates[i-1]&&used[i-1]==false) continue;
            else{
                sum+=candidates[i];
                used[i]=true;
                path.addLast(candidates[i]);
                backTracing(candidates,target,i+1);
                path.removeLast();
                used[i]=false;
                sum-=candidates[i];
            }
        }
    }
}

不带used做法:

class Solution {
    List<List<Integer>> res;
    LinkedList<Integer> path;
    int sum;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        res=new ArrayList<>();
        path=new LinkedList<>();
        backTracing(candidates,target,0);
        return res;
    }
    public void backTracing(int[] candidates, int target,int startIndex){
        if(sum==target){
            res.add(new ArrayList<>(path));
            return;
        }
        if(sum>target){
            return;
        }
        for(int i=startIndex;i<candidates.length;i++){
            if(i>startIndex&&candidates[i]==candidates[i-1]) continue;
            else{
                sum+=candidates[i];
                path.addLast(candidates[i]);
                backTracing(candidates,target,i+1);
                path.removeLast();
                sum-=candidates[i];
            }
        }
    }
}
83.没有重复元素集合的全排列

回溯+排列问题+没有重复元素,排列问题不需要startIndex,每轮都从0开始取数,但是需要有used数组标记已经取过的数。

class Solution {
    LinkedList<Integer> path;
    List<List<Integer>> res;
    boolean[] used;
    public List<List<Integer>> permute(int[] nums) {
        path=new LinkedList<>();
        res=new ArrayList<>();
        used=new boolean[nums.length];
        backTracing(nums);
        return res;
    }
    public void backTracing(int[] nums){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        // 排列问题不用startIndex
        for(int i=0;i<nums.length;i++){
            // 需要跳过已经使用过的元素
            if(used[i]==true) continue;
            path.add(nums[i]);
            used[i]=true;
            backTracing(nums);
            used[i]=false;
            path.removeLast();
        }
    }
}
84.含有重复元素集合的全排列

回溯+排列问题+有重复元素,对于同一层的重复元素需要continue(不同于组合问题,这里需要used来判断的原因是,nums[i]==nums[i-1]可能出现在不同层中,因为82题i>startIndex已经限制在同一层上了),两题都是相信前面的树枝已经取到过所有情况了,排列问题没有startIndex。

class Solution {
    LinkedList<Integer> path=new LinkedList<>();
    List<List<Integer>> res=new ArrayList<>();
    boolean[]used;
    public List<List<Integer>> permuteUnique(int[] nums) {
        used=new boolean[nums.length];
        // 重复元素需要排序
        Arrays.sort(nums);
        backTracing(nums);
        return res;
    }
    public void backTracing(int[]nums){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=0;i<nums.length;i++){
            // 碰到同一层的重复元素(nums[i]==nums[i-1]&&used[i-1]==false)则continue
            if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
            // 元素已经使用过也continue
            if(used[i]==true) continue;
            path.add(nums[i]);
            used[i]=true;
            backTracing(nums);
            used[i]=false;
            path.removeLast();
        }
    }
}
85.生成匹配的括号

回溯+排列问题,这里需要检验括号的有效性,因此在回溯的过程中需要进行数量的判断。

class Solution {
    int cur;
    LinkedList<Character> path;
    List<String> res;
    public List<String> generateParenthesis(int n) {
        path=new LinkedList<>();
        res=new ArrayList<>();
        backTracing(n,n);
        return res;
    }

    public void backTracing(int left,int right){
        // 括号使用完记录结果
        if(left==0&&right==0){
            StringBuilder sb=new StringBuilder();
            for(Character c:path){
                sb.append(c);
            }
            res.add(sb.toString());
            return;
        }
        // 右边使用的括号不能多于左边使用的括号
        if(right<left){
            return;
        }
        // 记录左边使用的括号
        if(left>0){
            left--;
            path.addLast('(');
            backTracing(left,right);
            path.removeLast();
            left++;
        }
        // 记录右边使用的括号
        if(right>0){
            right--;
            path.addLast(')');
            backTracing(left,right);
            path.removeLast();
            right++;
        }
    }
}
86.分割回文子字符串

回溯+分割问题,对各种分割情况进行回文判断,只有满足回文才继续递归下去,最坏的情况为 O ( N ∗ 2 N ) O(N*2^N) O(N2N).

class Solution {
    LinkedList<String> path;
    List<List<String>> res;
    public String[][] partition(String s) {
        path=new LinkedList<>();
        res=new ArrayList<>();
        backTracing(s,0);
        // 转换成结果的形式
        String[][]ans=new String[res.size()][];
        for(int i=0;i<res.size();i++){
            int len=res.get(i).size();
            ans[i]=new String[len];
            for(int j=0;j<len;j++){
                ans[i][j]=res.get(i).get(j);
            }
        }
        return ans;
    }

    public void backTracing(String s, int startIndex){
        if(startIndex>=s.length()){
            res.add(new ArrayList<>(path));
            return;
        }
        // 字符串截取,end范围是[startIndex+1,s.length()]
        for(int i=startIndex+1;i<=s.length();i++){
            if(huiwen(s.substring(startIndex,i))){
                path.add(s.substring(startIndex,i));
                // 下一次从i开始递归
                backTracing(s,i);
                path.removeLast();
            }
        }
    } 

    // 每次截取判断回文
    public boolean huiwen(String s){
        int i=0;
        int j=s.length()-1;
        while(i<=j){
            if(s.charAt(i)!=s.charAt(j)) return false;
            i++;
            j--;
        }
        return true;
    }
}

由于是否字符子串是否回文有很多重复状态,因此可以采用记忆化搜索。

class Solution {
    LinkedList<String> path;
    List<List<String>> res;
    int[][]cache;
    public String[][] partition(String s) {
        path=new LinkedList<>();
        res=new ArrayList<>();
        cache=new int[s.length()][s.length()];
        backTracing(s,0);
        // 转换成结果的形式
        String[][]ans=new String[res.size()][];
        for(int i=0;i<res.size();i++){
            int len=res.get(i).size();
            ans[i]=new String[len];
            for(int j=0;j<len;j++){
                ans[i][j]=res.get(i).get(j);
            }
        }
        return ans;
    }

    public void backTracing(String s, int startIndex){
        if(startIndex>=s.length()){
            res.add(new ArrayList<>(path));
            return;
        }
        // 字符串截取,end范围是[startIndex+1,s.length()]
        for(int i=startIndex+1;i<=s.length();i++){
            if(huiwen(s,startIndex,i-1)==1){
                path.add(s.substring(startIndex,i));
                // 下一次从i开始递归
                backTracing(s,i);
                path.removeLast();
            }
        }
    } 

    // 记忆化搜索
    public int huiwen(String s,int i, int j){
        if(cache[i][j]!=0) return cache[i][j];
        else if(i>=j){
            cache[i][j]=1;
        }else if(s.charAt(i)!=s.charAt(j)){
            cache[i][j]=-1;
        }else{
            cache[i][j]=huiwen(s,i+1,j-1);
        }

        return cache[i][j];
    }
}
87.复原IP

回溯+分割问题,需要进行一下长度的特判,每次分割判断合理性,当首字母是0的时候直接return。

class Solution {
    LinkedList<String> path;
    List<String> res;
    public List<String> restoreIpAddresses(String s) {
        path=new LinkedList<>();
        res=new ArrayList<>();
        // 需要特殊判断一下长度,不然超出时间限制
        if(s.length()>12) return res;
        backTracing(s,0);
        return res;
    }
    public void backTracing(String s, int startIndex){
        if(startIndex>=s.length()){
            if(path.size()==4){
                StringBuilder sb=new StringBuilder();
                for(int i=0;i<4;i++){
                    sb.append(path.get(i));
                    if(i!=3) sb.append('.');
                }
                res.add(sb.toString());
            }
            return;
        }
        //在同一层上进行限制
        for(int i=startIndex+1;i<=Math.min(s.length(),startIndex+3);i++){
            int c=check(s.substring(startIndex,i));
            // 当首字母是0,本层只能分成“0”,直接return
            if(c==1){
                path.add(s.substring(startIndex,i));
                backTracing(s,i);
                path.removeLast();
                return;
            }
            if(c==2){
                path.add(s.substring(startIndex,i));
                backTracing(s,i);
                path.removeLast();
            }
        }
    }

    public int check(String s){
        if(s.charAt(0)=='0') return 1;
        if(Integer.parseInt(s)>=0&&Integer.parseInt(s)<=255) return 2;
        return -1;
    }

}

或者当首字母是0的时候不return,继续把剩下同一层的搜索完(虽然都不成立且最多2种情况),但是可以简化判断和回溯的代码。

class Solution {
    LinkedList<String> path;
    List<String> res;
    public List<String> restoreIpAddresses(String s) {
        path=new LinkedList<>();
        res=new ArrayList<>();
        // 需要特殊判断一下长度,不然超出时间限制
        if(s.length()>12) return res;
        backTracing(s,0);
        return res;
    }
    public void backTracing(String s, int startIndex){
        if(startIndex>=s.length()){
            if(path.size()==4){
                StringBuilder sb=new StringBuilder();
                for(int i=0;i<4;i++){
                    sb.append(path.get(i));
                    if(i!=3) sb.append('.');
                }
                res.add(sb.toString());
            }
            return;
        }
        //在同一层上进行限制
        for(int i=startIndex+1;i<=Math.min(s.length(),startIndex+3);i++){
            if(check(s.substring(startIndex,i))){
                path.add(s.substring(startIndex,i));
                backTracing(s,i);
                path.removeLast();
            }
        }
    }

    // boolean check
    public boolean check(String s){
        if(s.length()>=2&&s.charAt(0)=='0') return false;
        int i=Integer.parseInt(s);
        if(i>=0&&i<=255) return true;
        else return false;
    }
}
88.爬楼梯的最少成本

线性动态规划,dp[i]定义为爬上第i个台阶,但还未从当前台阶往上走所花费的费用

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n=cost.length;
        int[]dp=new int[n+1];
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=n;i++){
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
}
89.房屋偷盗

用二维数组定义了房屋偷与没偷的状态

class Solution {
    public int rob(int[] nums) {
        int len=nums.length;
        int[][]dp=new int[len][2];
        dp[0][0]=0;
        dp[0][1]=nums[0];
        for(int i=1;i<len;i++){
            dp[i][1]=dp[i-1][0]+nums[i];
            dp[i][0]=Math.max(dp[i-1][1],dp[i-1][0]);
        }
        return Math.max(dp[len-1][0],dp[len-1][1]);
    }
}

用一维数组dp[i]记录偷窃到下标为i的屋子能够产生的最大价值,偷窃第i间屋子和不偷窃第i间屋子能够产生的最大价值。

class Solution {
    public int rob(int[] nums) {
        int len=nums.length;
        // 长度为1直接返回
        if(len==1) return nums[0];

        int[]dp=new int[len];
        dp[0]=nums[0];
        dp[1]=Math.max(nums[0],nums[1]);
        for(int i=2;i<len;i++){
            dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[len-1];
    }
}
90.环形房屋偷盗

线性dp,基本思路同上,考虑首尾元素不能同时被偷的情况,将两者取最大值。

class Solution {
    public int rob(int[] nums) {
        int len=nums.length;
        // 特殊情况
        if(len==1) return nums[0];
        int[]dp=new int[len];
        int left=partition(nums,0,len-2);
        int right=partition(nums,1,len-1);
        return Math.max(left,right);
    }

    public int partition(int[]nums,int s,int e){
        // 特殊情况
        if(s==e) return nums[s];
        int[]dp=new int[nums.length];
        // 前几项dp
        dp[s]=nums[s];
        dp[s+1]=Math.max(nums[s],nums[s+1]);
        for(int i=s+2;i<=e;i++){
            dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[e];
    }
}
91.粉刷房子

线性状态dp,定义定义 dp[i][j] 为考虑下标不超过i 的房子,且最后一间房子颜色为 j 时的最小成本。定义出状态和状态转换方程

class Solution {
    public int minCost(int[][] costs) {
        int len=costs.length;
        int[][]dp=new int[len][3];
        dp[0][0]=costs[0][0];
        dp[0][1]=costs[0][1];
        dp[0][2]=costs[0][2];
        for(int i=1;i<len;i++){
            dp[i][0]=Math.min(dp[i-1][1],dp[i-1][2])+costs[i][0];
            dp[i][1]=Math.min(dp[i-1][0],dp[i-1][2])+costs[i][1];
            dp[i][2]=Math.min(dp[i-1][0],dp[i-1][1])+costs[i][2];
        }
        return Math.min(dp[len-1][0],Math.min(dp[len-1][1],dp[len-1][2]));
    }
}

考虑到后面的状态只依赖于前面的状态,因此可以用三个变量来代替动归数组。

class Solution {
    public int minCost(int[][] costs) {
        int len=costs.length;
        int a=costs[0][0];
        int b=costs[0][1];
        int c=costs[0][2];
        for(int i=1;i<len;i++){
            int d=Math.min(b,c)+costs[i][0];
            int e=Math.min(a,c)+costs[i][1];
            int f=Math.min(a,b)+costs[i][2];
            a=d;b=e;c=f;
        }
        return Math.min(a,Math.min(b,c));
    }
}
92.翻转字符

线性状态dp,重点在于相邻元素间的大小关系,定义
dp[i][0]为考虑s[0,i]翻转后为以0结尾的s[0,i]为递增序列最少翻转次数
dp[i][1]为考虑s[0,i]翻转后为以1结尾的s[0,i]为递增序列最少翻转次数
其中状态转换有一点贪心的想法,如果要考虑dp[i][0],那么只能考虑dp[i-1][0]和当前的关系,因为这样才能保持单调性,至于dp[i][1]则前面是0或者1都可以,只需要取最小。

class Solution {
    public int minFlipsMonoIncr(String s) {
        int len=s.length();
        char[]ss=s.toCharArray();
        int[][]dp=new int[len][2];
        // 初始化
        dp[0][0]=ss[0]=='0'?0:1;
        dp[0][1]=ss[0]=='1'?0:1;
        for(int i=1;i<len;i++){
            // 当前元素为1,保持单调的最小翻转次数
            dp[i][1]=Math.min(dp[i-1][0],dp[i-1][1])+(ss[i]=='0'?1:0);
            // 当前元素为0,保持单调的最小翻转次数
            dp[i][0]=dp[i-1][0]+(ss[i]=='0'?0:1);
        }
        return Math.min(dp[len-1][1],dp[len-1][0]);
    }
}
93.最长斐波那契数列

序列dp,定义二维状态,dp[i][j]表示以序号为i,j结尾的序列中最长的斐波那契数列,其中用到了哈希表查值优化。

class Solution {
    public int lenLongestFibSubseq(int[] arr) {
        int len=arr.length;
        int[][] dp=new int[len][len];
        // 初始化为长度2
        for(int i=0;i<len;i++){
            for(int j=i+1;j<len;j++){
                dp[i][j]=2;
            }
        }
        int ans=0;
        HashMap<Integer,Integer> map=new HashMap<>();
        for(int i=0;i<len;i++){
            map.put(arr[i],i);
        }
        for(int i=2;i<len;i++){
            for(int j=0;j<i;j++){
                int target=arr[i]-arr[j];
                // 哈希表进行查找优化
                if(map.containsKey(target)){
                    int k=map.get(target);
                    if(k<j){ // 这个条件很重要
                        dp[j][i]=dp[k][j]+1;
                        // 找到了满足条件的值才更新答案
                        ans=Math.max(ans,dp[j][i]);
                        // System.out.println(arr[k]+" "+arr[j]+" "+arr[i]+" "+dp[j][i]);
                    }
                }
            }
        }
        return ans;
    }
}
94.最少回文分割

一维序列dp,可以看成是最长递增子序列和判断回文子串个数的综合。 d p [ i ] dp[i] dp[i]表示以下标i结尾的子串中符合要求的最小分割数。需要用到某个区间是否为回文串的结论,因此可以先进行预处理。

class Solution {
    public int minCut(String s) {
        char[]sc=s.toCharArray();
        int len=sc.length;
        int[]dp=new int[len];
        // 初始化为最大值
        Arrays.fill(dp,0x3f3f3f3f);
        boolean[][]f=huiWen(s);
        int ans=0;
        // 下面类似最长递增子序列
        for(int i=0;i<len;i++){
        	// [0,i]已经是回文串了,则分割数是0
            if(f[0][i]){
                dp[i]=0;
                continue;
            } 
            // 否则尝试分割
            for(int j=0;j<i;j++){
                if(f[j+1][i]) dp[i]=Math.min(dp[i],dp[j]+1);
            } 
        }
        return dp[len-1];
    }


    // 回文判断预处理
    public boolean[][] huiWen(String s){
        char[]sc=s.toCharArray();
        int len=sc.length;
        boolean [][] dp=new boolean[len][len];
        for(int i=len-1;i>=0;i--){
            for(int j=i;j<len;j++){
                if(sc[i]==sc[j]&&(j-i<2||dp[i+1][j-1])){
                    dp[i][j]=true;
                }
            }
        }
        return dp;
    }
}
95.最长公共子序列

二维序列dp,带padding为0

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        char[]s1=text1.toCharArray();
        char[]s2=text2.toCharArray();
        int n1=s1.length,n2=s2.length;
        int[][]dp=new int[n1+1][n2+1];
        for(int i=0;i<n1;i++){
            for(int j=0;j<n2;j++){
                if(s1[i]==s2[j]){
                    dp[i+1][j+1]=dp[i][j]+1;
                }else{
                    dp[i+1][j+1]=Math.max(dp[i][j+1],dp[i+1][j]);
                }
            }
        }
        return dp[n1][n2];
    }
}
96.字符串交织

参考题解 d p [ i + 1 ] [ j + 1 ] dp[i+1][j+1] dp[i+1][j+1]代表以下标i结尾的s1和以下标j结尾的s2(严格包括)能否构成s3。

class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int len1=s1.length();
        int len2=s2.length();
        int len3=s3.length();
        if(len1+len2!=len3) return false;
        boolean[][] dp=new boolean[len1+1][len2+1];
        dp[0][0]=true;
        for(int i=0;i<len1;i++){
            dp[i+1][0]=dp[i][0]&&s1.charAt(i)==s3.charAt(i);
        }
        for(int j=0;j<len2;j++){
            dp[0][j+1]=dp[0][j]&&s2.charAt(j)==s3.charAt(j);
        }
        for(int i=0;i<len1;i++){
            for(int j=0;j<len2;j++){
                dp[i+1][j+1]=(dp[i][j+1]&&s1.charAt(i)==s3.charAt(i+j+1))||
                (dp[i+1][j]&&s2.charAt(j)==s3.charAt(i+j+1));
            }
        }
        return dp[len1][len2];
    }
}
97.子序列的数目

二维序列dp, d p [ i + 1 ] [ j + 1 ] dp[i+1][j+1] dp[i+1][j+1]表示以下标i结尾的s中包含多少以下标j结尾的t(不一定包括)。

class Solution {
    public int numDistinct(String s, String t) {
        int len1=s.length();
        int len2=t.length();
        int[][]dp=new int[len1+1][len2+1];
        for(int i=0;i<=len1;i++){
            dp[i][0]=1;
        }
        for(int j=1;j<=len2;j++){
            dp[0][j]=0;
        }
        for(int i=0;i<len1;i++){
            for(int j=0;j<len2;j++){
                if(s.charAt(i)==t.charAt(j)){
                    dp[i+1][j+1]=dp[i][j+1]+dp[i][j];
                }else{
                    dp[i+1][j+1]=dp[i][j+1];
                }
            }
        }
        return dp[len1][len2];
    }
}
98.路径的数目

二维线性dp, d p [ i ] [ j ] dp[i][j] dp[i][j]表示从起点开始到坐标(i,j)的路径数目,先要初始化最左侧和最上侧的路径数目,只有一种走法,所以是1。时间复杂度 O ( M ∗ N ) O(M*N) O(MN),将所有位置路径数目状态根据转移方程枚举了一遍,最后输出终点。

class Solution {
    public int uniquePaths(int m, int n) {
        int[][]dp=new int[m][n];
        for(int i=0;i<m;i++) dp[i][0]=1;
        for(int j=0;j<n;j++) dp[0][j]=1;
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}
99.最小路径之和

二维线性dp, d p [ i ] [ j ] dp[i][j] dp[i][j]表示从起点开始到坐标(i,j)的所需要的最小路径和,先要初始化最左侧和最上侧的路径和,只有一种走法,所以是累加。时间复杂度 O ( M ∗ N ) O(M*N) O(MN),将所有位置路径和状态根据转移方程枚举了一遍,最后输出终点。

class Solution {
    public int minPathSum(int[][] grid) {
        int m=grid.length;
        int n=grid[0].length;
        int[][]dp=new int[m][n];
        int sum=0;
        for(int i=0;i<m;i++){
            sum+=grid[i][0];
            dp[i][0]=sum;
        }
        sum=0;
        for(int i=0;i<n;i++){
            sum+=grid[0][i];
            dp[0][i]=sum;
        }

        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j]=grid[i][j]+Math.min(dp[i-1][j],dp[i][j-1]);
            }
        }
        return dp[m-1][n-1];
    }
}

变形的题目是输出该最短路径,那就是从右下角到左上角依次找到dp值较小的方向逆向记录,再把结果反转。

class Solution {
    public int minPathSum(int[][] grid) {
        int m=grid.length;
        int n=grid[0].length;
        int[][]dp=new int[m][n];
        int sum=0;
        for(int i=0;i<m;i++){
            sum+=grid[i][0];
            dp[i][0]=sum;
        }
        sum=0;
        for(int i=0;i<n;i++){
            sum+=grid[0][i];
            dp[0][i]=sum;
        }

        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                dp[i][j]=grid[i][j]+Math.min(dp[i-1][j],dp[i][j-1]);
            }
        }

        // 反向输出路径
        List<int[]> res=new ArrayList<>();
        int x=m-1,y=n-1;
        res.add(new int[]{x,y});
        while(x!=0||y!=0){
            if(x==0){
                res.add(new int[]{x,y-1});
                y=y-1;
            }
            else if(y==0){
                res.add(new int[]{x-1,y});
                x=x-1;
            }
            else if(dp[x-1][y]<dp[x][y-1]){
                res.add(new int[]{x-1,y});
                x=x-1;
            }else{
                res.add(new int[]{x,y-1});
                y=y-1;
            }
        }
        Collections.reverse(res);
        for(int[] r:res){
            System.out.print(grid[r[0]][r[1]]+" ");
        }

        return dp[m-1][n-1];
    }
}
100.三角形中最小路径之和

二维DP

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int len=triangle.size();
        int[][]dp=new int[len][len];
        dp[0][0]=triangle.get(0).get(0);
        for(int i=1;i<len;i++){
            for(int j=0;j<=i;j++){
                if (j==0) dp[i][j]=dp[i-1][j]+triangle.get(i).get(j);
                else if (j==i) dp[i][j]=dp[i-1][j-1]+triangle.get(i).get(j);
                else dp[i][j]=Math.min(dp[i-1][j],dp[i-1][j-1])+triangle.get(i).get(j);
            }
        }
        int ans=0x3f3f3f3f;
        for(int j=0;j<len;j++){
            ans=Math.min(ans,dp[len-1][j]);
        }
        return ans;
    }
}
101.分割等和子集

01背包问题,有点夹逼的思想在里面,找到和的一半作为背包容量,往里面放数,一个数字的价值和其大小1:1。 d p [ j ] dp[j] dp[j]代表目标和为j时,能够凑出的最大和,极限情况下能找到的话肯定 d p [ b a g ] = = b a g dp[bag]==bag dp[bag]==bag,也就是刚好分割了等和子集。

class Solution {
    public boolean canPartition(int[] nums) {
        int sum=0;
        for(int n:nums){
            sum+=n;
        }
        if(sum%2!=0) return false;
        int bag=sum/2;
        int[]dp=new int[bag+1];
        for(int i=0;i<nums.length;i++){
            for(int j=bag;j>=nums[i];j--){
                dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        return dp[bag]==bag;
    }
}
102.加减的目标值

01背包组合问题,参考官方题解,能够将neg抽象成背包容量的思想很巧妙。
在这里插入图片描述
二维dp的解法:
d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j]代表在选择下标为[0,i]的nums去组合结果j时,总共的组合数

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        //(sum-neg)-neg=target
        int sum=0;
        for(int n:nums){
            sum+=n;
        }
        // 必须保证neg为偶数且sum-target>=0
        if(target>sum||(sum-target)%2==1) return 0;
        int neg=(sum-target)/2;
        int len=nums.length;
        // dp[i+1][j]代表在选择下标为[0,i]的nums去组合结果j时,总共的组合数
        int[][]dp=new int[len+1][neg+1];
        // 由于是方法数,作为最小单元必须为1
        dp[0][0]=1;
        for(int i=0;i<nums.length;i++){
            for(int j=neg;j>=0;j--){
                // 用上nums[i]和不用上nums[i]的两种组合策略加起来
                if(j>=nums[i])dp[i+1][j]=dp[i][j]+dp[i][j-nums[i]];
                // 只能不用上nums[i]
                else dp[i+1][j]=dp[i][j];
            }
        }
        return dp[len][neg];
    }
}

改造成一维dp滚动数组:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        //(sum-neg)-neg=target
        int sum=0;
        for(int n:nums){
            sum+=n;
        }
        // 必须保证neg为偶数且sum-target>=0
        if(target>sum||(sum-target)%2==1) return 0;
        int neg=(sum-target)/2;
        int len=nums.length;
        // dp[j]代表去组合结果j时,总共的组合数
        int[]dp=new int[neg+1];
        dp[0]=1;
        for(int i=0;i<nums.length;i++){
            for(int j=neg;j>=0;j--){
                // 用上nums[i]和不用上nums[i]的两种组合策略加起来
                if(j>=nums[i])dp[j]=dp[j]+dp[j-nums[i]];
            }
        }
        return dp[neg];
    }
}
103.最少的硬币数

完全背包问题,求的是在指定容量内放无限次物品,所需要的最少物品数目。

class Solution {
    public int coinChange(int[] coins, int amount) {
        //dp[i]表示要达到重量为i时,最少的硬币个数
        int[]dp=new int[amount+1];
        int len=coins.length;
        Arrays.fill(dp,Integer.MAX_VALUE);
        dp[0]=0;
        for(int i=0;i<len;i++){
            for(int j=coins[i];j<=amount;j++){
                if(dp[j-coins[i]]!=Integer.MAX_VALUE)
                    dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
            }
        }
        return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];
    }
}
104.排列的数目

完全背包的排列问题,必须容量在外循环,物品在内循环。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        // dp[i]代表组成目标i的元素组合个数
        int[]dp=new int[target+1];
        int len=nums.length;
        dp[0]=1;
        for(int i=1;i<=target;i++){
            for(int j=0;j<len;j++){
                if(i>=nums[j]){
                    dp[i]+=dp[i-nums[j]];
                }
            }
        }
        return dp[target];
    }
}
105.岛屿最大面积

深度优先遍历中的经典岛屿问题

class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        res=0
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j]==1:
                    area=self.dfs(grid,i,j)
                    res=max(res,area)
        return res
    
    def dfs(self,grid,i,j):
        dx=[0,1,0,-1]
        dy=[-1,0,1,0]
        
        if i<0 or i>=len(grid) or j<0 or j>=len(grid[0]) or grid[i][j]==-1 or grid[i][j]==0:
            return 0
        grid[i][j]=-1
        area=0
        for k in range(4):
            # 直接将越界情况抛给下一层,不同于广度优先遍历
            x=i+dx[k]
            y=j+dy[k]
            area += self.dfs(grid,x,y)
        return 1+area
106.二分图

DFS+邻接表的数组形式存图,每个节点相邻的点都已经列出来了,要对当前的点染色,同时判断相邻的点能否进一步染色。

class Solution:
    def isBipartite(self, graph: List[List[int]]) -> bool:
        n = len(graph)
        colors= [0]*n
        for i in range(n):
        	# 一种情况不满足就return
            if colors[i]==0 and not self.dfs(graph,colors,1,i):
                return False
        return True

    def dfs(self,graph,colors,color,i):
        colors[i]=color
        # 对于相邻的点
        for j in graph[i]:
            if colors[j]==color:
                return False
            if colors[j]==0 and not self.dfs(graph,colors,-1*color,j):
                return False
        return True    
107.矩阵中的距离

和层数相关的多源BFS,求1到0的最近距离,这里转换思想,将所有0一视同仁,反而求所有0到最近的1的距离。distance初始化为0,因为节点在进队前就判断是否为1赋值了,已走路径只算了一个端点。

class Solution:
    def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
        q=deque()
        res=[[0]*len(mat[0]) for _ in range(len(mat))]
        for i in range(len(mat)):
            for j in range(len(mat[0])):
                if mat[i][j]==0:
                    mat[i][j]=-1
                    q.append((i,j))
        #初始化为0,是因为后面进队之前就赋值了
        distance=0
        while q:
            size=len(q)
            distance +=1
            for i in range(size):
                top= q.popleft()
                topx=top[0]
                topy=top[1]
                dx=[0,1,0,-1]
                dy=[1,0,-1,0]
                for k in range(4):
                    x=topx+dx[k]
                    y=topy+dy[k]
                    if x<0 or x>=len(mat) or y<0 or y>=len(mat[0]) or mat[x][y]==-1:
                        continue
                    # 进队之前就赋值
                    if mat[x][y]==1:
                        res[x][y]=distance
                    mat[x][y]=-1
                    q.append((x,y))
        return res
108.单词演变

双向BFS或者单向BFS都可以,在起点和终点都知道的情况下,我们用双向BFS作为第二解法。

双向BFS:

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        wordset=set(wordList)
        # 合法的单词序列
        if beginWord in wordset:
            wordset.remove(beginWord)
        # 如果endWord没有出现在合法的单词序列中
        if endWord not in wordset:
            return 0
        q1=deque()
        vis1=set()
        q1.append(beginWord)
        vis1.add(beginWord)
        q2=deque()
        vis2=set()
        q2.append(endWord)
        vis2.add(endWord)

        level=0
        while q1 and q2:
            if len(q1)>len(q2):
                temp1=q1
                temp2=vis1
                q1=q2
                vis1=vis2
                q2=temp1
                vis2=temp2
            
            level +=1
            size=len(q1)
            # 针对每个单词
            for _ in range(size):
                top=q1.popleft()
                # 枚举其每一位上26个字母的可能性
                listword=list(top)
                for i,c in enumerate(top):
                    for k in range(26):
                        listword[i]=chr(ord('a')+k)
                        nextword=''.join(listword)
                        # 在合法的单词序列中
                        if nextword in wordset:
                            # 下一个单词出现交集
                            if nextword in q2:
                                return level+1
                            # 下一个单词没有交集且没有被访问
                            if nextword not in vis1:
                                vis1.add(nextword)
                                q1.append(nextword)
                    # 将当前被替换位复原
                    listword[i]=c
        
        return 0

单向BFS:

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        wordset=set(wordList)
        # 合法的单词序列
        if beginWord in wordset:
            wordset.remove(beginWord)
        q=deque()
        vis=set()
        q.append(beginWord)
        vis.add(beginWord)

        level=0
        while q:
            level +=1
            size=len(q)
            # 针对每个单词
            for _ in range(size):
                top=q.popleft()
                # 枚举其每一位上26个字母的可能性
                listword=list(top)
                for i,c in enumerate(top):
                    for k in range(26):
                        listword[i]=chr(ord('a')+k)
                        nextword=''.join(listword)
                        # 在合法的单词序列中
                        if nextword in wordset:
                            # 下一个单词就是终点词
                            if nextword == endWord:
                                return level+1
                            # 下一个单词不是终点次且没有被访问
                            if nextword not in vis:
                                vis.add(nextword)
                                q.append(nextword)
                    # 将当前被替换位复原
                    listword[i]=c
        
        return 0
109.开密码锁

双向BFS:

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        # 特殊情况考虑
        if '0000'in deadends:
            return -1
        if '0000' == target:
            return 0

        deadset=set(deadends)
        q1=collections.deque()
        vis1=set()
        vis1.add('0000')
        q1.append('0000')
        q2=collections.deque()
        vis2=set()
        vis2.add(target)
        q2.append(target)

        level=-1
        while q1 and q2:
            level +=1
            if len(q1)>len(q2):
                temp1=q1
                q1=q2
                q2=temp1
                temp2=vis1
                vis1=vis2
                vis2=temp2
            size=len(q1)
            # 所有可行元素逐层出队
            for _ in range(size):
                top=q1.popleft()
                listword=list(top)
                # 针对一个word的所有位置上的元素
                for i,c in enumerate(listword):
                    # 该位置上的两种情况枚举
                    for k in {1,-1}:
                        listword[i]=str((int(c)+k+10)%10)
                        nextword=''.join(listword)
                        # 当nextword不在deadset中时
                        if nextword not in deadset:
                            # nextword在下面出现交集
                            if nextword in q2:
                                return level +1
                            if nextword not in vis1: 
                                q1.append(nextword)
                                vis1.add(nextword)
                    # 回溯还原,避免对下一次枚举产生影响
                    listword[i]=c
        return -1

单向BFS:

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        # 特殊情况考虑
        if '0000'in deadends:
            return -1
        if '0000' == target:
            return 0
        deadset=set(deadends)
        q=collections.deque()
        vis=set()
        vis.add('0000')
        q.append('0000')
        level=-1
        while q:
            level +=1
            size=len(q)
            # 所有可行元素逐层出队
            for _ in range(size):
                top=q.popleft()
                listword=list(top)
                # 针对一个word的所有位置上的元素
                for i,c in enumerate(listword):
                    # 该位置上的两种情况枚举
                    for k in {1,-1}:
                        listword[i]=str((int(c)+k+10)%10)
                        nextword=''.join(listword)
                        # 当nextword不在deadset中时
                        if nextword not in deadset:
                            if nextword==target:
                                return level +1
                            if nextword not in vis: 
                                q.append(nextword)
                                vis.add(nextword)
                    # 回溯还原,避免对下一次枚举产生影响
                    listword[i]=c
        return -1
110.所有路径

DFS暴搜

class Solution {
    LinkedList<Integer>path;
    List<List<Integer>> res;
    int n;
    int[][] graph;
    public List<List<Integer>> allPathsSourceTarget(int[][] g) {
        n=g.length-1;
        path=new LinkedList<>();
        res=new ArrayList<>();
        graph=g;
        path.add(0);
        backTracing(0);
        return res;
    }
    public void backTracing(int cur){
        if(cur==n){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i:graph[cur]){
            path.add(i);
            backTracing(i);
            path.removeLast();
        }
    }
}
111.计算除法

HashMap建图+DFS,其中boolean found来处理不能访问到的query,如果在某一个DFS分支中将found置为true,则提前剪枝结束。

class Solution {
    // 存图 Map.Entry[a,Pair(b,2)]
    HashMap<String,List<Pair>> map=new HashMap<>();
    // 表明每个query是否找到了答案
    boolean found=false;
    // 存储答案
    List<Double> res=new ArrayList<>();
    // 用于存储节点是否被访问过
    HashSet<String> vis = new HashSet<>();

    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        drawMap(equations,values);
        for(List<String> q:queries){
            // ["x","x"]不是图中的节点
            if(!map.containsKey(q.get(0))){
                res.add(-1.0);
                continue;
            }
            
            // 每次query刚开始都是没找到
            found=false;

            // ["a","a"]这种直接能返回1的结果
            vis.add(q.get(0));
            dfs(q.get(0),1.0,q.get(1));
            vis.remove(q.get(0));
        
            // 在搜索过后没有找到结果
            if(!found) res.add(-1.0);
        }
        double[] ans= new double[res.size()];
        for(int i=0;i<res.size();i++){
            ans[i]=res.get(i);
        }
        return ans;
    }

    // 根据equations和values建图
    public void drawMap(List<List<String>> equations, double[] values){
        for(int i=0;i<equations.size();i++){
            List<Pair> l1=map.getOrDefault(equations.get(i).get(0),new ArrayList<Pair>());
            l1.add(new Pair(equations.get(i).get(1),values[i]));
            map.put(equations.get(i).get(0),l1);
            List<Pair> l2=map.getOrDefault(equations.get(i).get(1),new ArrayList<Pair>());
            l2.add(new Pair(equations.get(i).get(0),1/values[i]));
            map.put(equations.get(i).get(1),l2);
        }
    }

    // 从curp节点出发, 当前权值为curw, 目标点为target
    public void dfs(String curp,double curw, String target){
        // 比较两个字符串相等不能==
        if(Objects.equals(curp,target)){
            res.add(curw);
            found=true;
            return;
        }
        for(Pair p:map.get(curp)){
            // 已经被访问过,不再访问
            if(vis.contains(p.s)) continue;
            vis.add(p.s);
            dfs(p.s,curw*p.w,target);
            vis.remove(p.s);
            // 前面的分支已经找到了,直接对同层的进行剪枝
            if(found) return;
        }
    }


    // 用于存储边权和节点 w---s
    public class Pair{
        String s;
        double w;
        public Pair(String s,double w){
            this.s=s;
            this.w=w;
        }
    }
}
112.最长递增路径

记忆化搜索DFS

class Solution {
    int[][]map;
    int[][]dir={{1,0},{-1,0},{0,1},{0,-1}};
    int m,n;
    int[][]cache;
    public int longestIncreasingPath(int[][] matrix) {
        map=matrix;
        int res=1;
        m=matrix.length;
        n=matrix[0].length;
        cache=new int[m][n];
        // 每个点出发进行dfs
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                // 前面遍历节点的时候已经把后面遍历过了(尽可能搜索到底了)
                // 因此只有后面还没有被搜索过的起点有可能变大
                if(cache[i][j]==0) res=Math.max(res,dfs(i,j));
            }
        }
        return res;
    }

    // 从matrix[px,py]出发的最长递增的长度
    public int dfs(int px,int py){
        if(cache[px][py]!=0) return cache[px][py];
        int ans=1;
        for(int i=0;i<4;i++){
            int nextx=px+dir[i][0];
            int nexty=py+dir[i][1]; 
            // 没有把return base放到下一层去,直接上一层全部判断完
            // 主要是因为只能走递增路径,放到下一层不太好判断了
            if(nextx<0||nextx>=m||nexty<0||nexty>=n) continue;
            if(map[nextx][nexty]<=map[px][py]) continue;
            ans=Math.max(ans,1+dfs(nextx,nexty));
        }
        // 当没有更max的ans直接return 1
        cache[px][py]=ans;
        return ans;
    }
    
}
113.课程顺序

拓扑排序,队列实现,和队列层数无关,因此不需要一次弹出一层。

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        // ArrayList存图
        ArrayList<Integer>[] map=new ArrayList[numCourses];
        int[]indegree = new int[numCourses];
        for(int i=0;i<numCourses;i++){
            map[i]=new ArrayList<Integer>();
        }
        // [ai,bi]表示bi->ai
        for(int[]p:prerequisites){
            map[p[1]].add(p[0]);
            indegree[p[0]]++;
        }
        Deque<Integer> que=new ArrayDeque<>();
        ArrayList<Integer> res=new ArrayList<>();
        for(int i=0;i<numCourses;i++){
            if(indegree[i]==0) que.offer(i);
        }
        while(!que.isEmpty()){
            int top=que.poll();
            res.add(top);
            for(Integer next:map[top]){
                indegree[next]--;
                if(indegree[next]==0){
                    que.offer(next);
                }
            }
        }
        int[] ans = res.stream().mapToInt(x->x).toArray();
        if(ans.length!=numCourses) return new int[]{};
        else return ans;
    }
}
114.外星文字典

拓扑排序,根据字母之间的前后关系进行拓扑中边的建立,记录所有出现过的字母,至于那些不具有前后关系的字母也需要对他们进行记录(只不过初始化入度in都为0),在建立拓扑排序的时候也将它们纳入第一层,但是不影响已经存在的先后关系。

按字母递增排序,就根据拓扑排序记录每层出队字母以及它的层数,在同层内按字母递增排序。

import java.util.*;

public class Solution {
    // HashMap存图
    HashMap<Character, ArrayList<Character>> map=new HashMap<>();
    int[]in=new int[26];
    HashSet<Character> set=new HashSet<>(); // 记录出现的字母

    public String alienOrder(String[] words) {
        // 记录所有出现的字母
        for(String w:words){
            for(char c:w.toCharArray()) set.add(c);
        }
        // 建图,只能把显然happen-before的关系定下来
        for(int k=0;k<words.length-1;k++){
            //找到第一个差异点
            String s1=words[k];
            String s2=words[k+1];
            int len1=s1.length(),len2=s2.length();
            int i=0,j=0;
            // 表示还未找到差异点
            int index=-1;
            while(i<len1&&j<len2){
                if(s1.charAt(i)!=s2.charAt(j)){
                    index=i;
                    break;
                }else{
                    i++;
                    j++;
                }
            }
            // 不具有差异
            if(index==-1&&len1>len2) return ""; // 用例错误 前缀一样结果前面长度大
            if(index==-1) continue;
            ArrayList<Character> l=map.getOrDefault(s1.charAt(i),new ArrayList<Character>());
            l.add(s2.charAt(j));
            map.put(s1.charAt(i),l);
            in[s2.charAt(j)-'a']++;
        }

        // 拓扑排序
        ArrayDeque<Character> que=new ArrayDeque<>();
        for(Character c:set){
            // 即使在不存在前后关系,刚开始初始化set里的字母in就都为0
            if(in[c-'a']==0)que.offer(c);
        }
        // 存储字母和他的权值,在权值相同时按字母递增顺序排列
        ArrayList<int[]> res=new ArrayList<>();
        int depth=-1;
        while(!que.isEmpty()){
            depth++;
            int len=que.size();
            for(int i=0;i<len;i++){
                Character top=que.poll();
                res.add(new int[]{top-'a',depth});
                if(map.get(top)==null) continue;
                for(Character next:map.get(top)){
                    in[next-'a']--;
                    if(in[next-'a']==0) que.offer(next);
                }
            }
        }
        if(res.size()!=set.size()) return "";
        Collections.sort(res,(a, b)-> a[1]==b[1]?Integer.compare(a[0],b[0]):Integer.compare(a[1],b[1]));
        StringBuilder sb=new StringBuilder();
        for(int i=0;i<res.size();i++){
            sb.append((char)('a'+res.get(i)[0]));
        }
        return sb.toString();
    }

}
116.省份数量

DFS统计图中连通域的个数,访问标记很关键。

class Solution:
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        vis=set()
        res=0
        # 从某个节点开始将与其连接的所有点都进行访问
        def dfs(i):
            for j in range(len(isConnected[i])):
                if j not in vis and isConnected[i][j]==1:
                    vis.add(j)
                    dfs(j)

        # 从每个未被访问过的节点开始进行搜索
        for i in range(len(isConnected)):
            if i not in vis:
                vis.add(i)
                dfs(i)
                res +=1
        return res
117.相似的字符串

DFS或者BFS找连通分量,一组单词就是相似的所有单词组成的一个连通分量。所以本质上就是上面的省份数量,只不过这里的isConnected是相似字符串的判断,相似则connect.

DFS查找连通分量个数,时间复杂度 O ( N 2 ) O(N^2) O(N2),循环dfs N次,每次都尝试标记所有的N个节点。

class Solution {
    HashSet<Integer> vis=new HashSet<>();
    public int numSimilarGroups(String[] strs) {
        int ans=0;
        for(int i=0;i<strs.length;i++){
            if(!vis.contains(i)){
                vis.add(i);
                dfs(i,strs);
                ans++;
            }
        }
        return ans;
    }
    
    public void dfs(int k,String[] strs){
        for(int i=0;i<strs.length;i++){
        	// 从前往后检查后面未被访问过的单词i
            if(!vis.contains(i)&&isConnect(strs[k],strs[i])){
                vis.add(i);
                dfs(i,strs);
            }
        }
    }

    public boolean isConnect(String a, String b){
        int count=0;
        for(int i=0;i<a.length();i++){
            if(a.charAt(i)!=b.charAt(i)) count++;
        }
        return count<=2; // 必定成立,必然有0,2,4...个位置不同
    }
}

BFS也可以解决连通数量问题,时间复杂度 O ( N 2 ) O(N^2) O(N2),循环bfs N次,每次在一次一次出队入队的过程中都尝试标记所有的N个节点。

class Solution {
    HashSet<Integer> vis=new HashSet<>();
    public int numSimilarGroups(String[] strs) {
        int ans=0;
        for(int i=0;i<strs.length;i++){
            if(!vis.contains(i)){
                bfs(i,strs);
                ans++;
            }
        }
        return ans;
    }


    public void bfs(int k,String[] strs){
        Deque<Integer> que=new ArrayDeque<>();
        vis.add(k);
        que.offer(k);
        while(!que.isEmpty()){
            int top=que.poll();
            // 从前往后检查后面未被访问过的单词i
            for(int i=0;i<strs.length;i++){
                if(!vis.contains(i)&&isConnect(strs[top],strs[i])){
                    vis.add(i);
                    que.offer(i);
                }
            }
        }
    }



    public boolean isConnect(String a, String b){
        int count=0;
        for(int i=0;i<a.length();i++){
            if(a.charAt(i)!=b.charAt(i)) count++;
        }
        return count<=2; // 必定成立,必然有0,2,4...个位置不同
    }
}
118.多余的边

并查集,原来无向无环的图加入了一条边以后存在了一个环,现在要删除一条边让它继续无环,当有多种删除方案时,选择 edges 中最后出现的边。

聚焦于点,从前向后遍历每一条边,边的两个节点如果不在同一个集合,就将这条边的两个节点加入同一个集合,当遍历到某条边却发现其两个节点已经在同一个集合里,再加入这条边就会成环。

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        UnionFind unionFind=new UnionFind(edges.length);
        for(int[]e:edges){
            if(unionFind.isConnect(e[0],e[1])){
                return e;
            }
            unionFind.union(e[0],e[1]);
        }
        
        return new int[0];
    }

    class UnionFind{
        private int count;
        private int[] parent;
        public UnionFind(int n){
            this.count=n;
            parent=new int[n+1];
            for(int i=1;i<=n;i++){
                parent[i]=i;
            }
        }

        public void union(int a, int b){
            int parentA=find(a);
            int parentB=find(b);
            if(parentA==parentB) return;
            parent[parentA]=parentB;
            this.count--;
        }

        // public int find(int x){
        //     if(parent[x]==x) return parent[x];
        //     else return find(parent[x]);
        // }

        public int find(int x){
            if(parent[x]!=x){
                parent[x]=find(parent[x]);
            }
            return parent[x];
        }

        public boolean isConnect(int a,int b){
            return find(a)==find(b);
        }

        public int getCount(int x){
            return this.count;
        }
    }
}
119.最长连续序列

哈希表,参考题解,其中最重要的是时间复杂度 O ( N ) O(N) O(N)的分析,并不是for, while嵌套就是 O ( N 2 ) O(N^2) O(N2)复杂度,外层循环中有些直接跳过,重点看内层循环遍历的元素数。

最坏情况是整个都可以连起来,总共循环了2N次(外层N次,不是最小数之间跳过,内层从最小的那个数开始循环N次,最小数就一个,所以只能进入内部这个循环一次,所以N+1N),最好情况下是一个都连不起来,总共循环了N次(内层虽然每层都要循环,但只循环0次,所以N+0)。最坏2N,最好N,所以时间复杂度O(N).

class Solution {
    public int longestConsecutive(int[] nums) {
        HashSet<Integer> set=new HashSet<>();
        int res=0;
        for(int n:nums){
            set.add(n);
        }

        // 并不是for,while嵌套就是O(N^2)复杂度,外层循环中有些直接跳过,重点看内层循环遍历的元素数
        for(int v:nums){
            // 当存在v-1时,最长序列的起点肯定不是当前v
            // 有可能前面最长序列的中间元素用过了v,这一步之需要跳过
            if(set.contains(v-1)) continue;
            int temp=1;
            int cur=v;
            // 该序列可以不断变长
            while(set.contains(cur+1)){
                temp++;
                cur++;
            }
            res=Math.max(res,temp);
        }
        return res;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网民工蒋大钊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值