烧鸡100二刷


前言

二刷啦

哈希部分

1. 两数之和 - 力扣(LeetCode)

思路:直接哈希表存入值和下标,然后判断target-nums[i]在不在里面,在就直接返回了

这样避免两次遍历,无敌

public int[] twoSum(int[] nums, int target) {
    Map<Integer,Integer>hash=new HashMap<Integer,Integer>();
    for(int i=0;i<nums.length;i++)
    {
        if(hash.containsKey(target-nums[i]))
        {
            return new int[]{hash.get(target-nums[i]),i};
        }
        hash.put(nums[i],i);//必须后put,不然会哦重复
    }
    return new int[0];
}

49. 字母异位词分组 - 力扣(LeetCode)

思路:异味词,排序之后值是相同的,把排序之后的str作为key,有相同key的放进同一个list里

public List<List<String>> groupAnagrams(String[] strs) {
    Map<String,List<String>>map=new HashMap<>();
    for(String str:strs)
    {
        char[]arr=str.toCharArray();//变成数组好排序
        Arrays.sort(arr);//排序
        String key=new String(arr);//排序后的作为key
        List<String>list=map.getOrDefault(key,new ArrayList<String>());//看有没有之前存过的
        list.add(str);
        map.put(key,list);//更新
    }
    return new ArrayList<List<String>>(map.values());
}

双指针

283. 移动零 - 力扣(LeetCode)

思路:把不是0的数全覆盖前面的,留到后面的全变成0

public void moveZeroes(int[] nums) {
    int i=0;
    int j=0;//这里j=1就会出错,令人不解
    while(j<nums.length)
    {
        if(nums[j]!=0)
        {
            nums[i]=nums[j];
            i++;
        }
        j++;
    }
    while(i<nums.length)
    {
        nums[i]=0;
        i++;
    }
}

11. 盛最多水的容器

思路:从两边开始往内收,然后矮的一边先收即可,因为矮的一边才影响矩形面积

public int maxArea(int[] height) {
    int left=0,right=height.length-1;
    int Maxsize=Integer.MIN_VALUE;
    while(left<right)
    {
        int size=(right-left)*Math.min(height[left],height[right]);
        Maxsize=Math.max(Maxsize,size);
        if(height[left]<height[right])
        {
            left++;
        }else
        {
            right--;
        }
    }
    return Maxsize;
}

15. 三数之和 - 力扣(LeetCode)

思路在注释里面,有一说一二刷自己会写了

public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>>res=new ArrayList<>();
    Arrays.sort(nums);
    //如果要找二元组等于0,怎么找,先排序,然后双指针,如果大于0就右边--,小于0就左边++
    //那么现在相当于对每一个数都这么来一遍
    //对每个数,取他后面的第一个和最后一个,去重很简单
    int len=nums.length;
    if(nums.length<3||nums==null)return res;
    for(int i=0;i<len;i++)
    {
        if(nums[i]>0)break;//这里都大于0了和肯定大于0
        if(i>0&&nums[i]==nums[i-1])continue;//去重
        int l=i+1,r=len-1;//i后面的第一个和最后一个
        while(l<r)
        {
            int sum=nums[i]+nums[l]+nums[r];
            if(sum==0)
            {
                 res.add(Arrays.asList(nums[i],nums[l],nums[r]));//一行
                 while(l<r&&nums[l]==nums[l+1])l++;
                 while(l<r&&nums[r]==nums[r-1])r--;//去重
                 l++;
                 r--;
            }else if(sum>0)
            {
                r--;//大了,右边缩小
            }else
            {
                l++;//小了,左边大
            }
        }
    }
    return res;
}

42. 接雨水 - 力扣(LeetCode)

思路:按列求,维护左边最大高度和右边最大高度,然后两者取小的再减去本身高度,乘1,就是这一列能存多少水

public int trap(int[] height) {
    int n=height.length,l=0,r=n-1;
    int lmax=height[l],rmax=height[r];//左侧最大高度,右侧最大高度
    int ans=0;

    while(l<=r)
    {
        if(lmax<rmax)
        {
            ans+=lmax-height[l];//乘1省略
            if(++l<=r)lmax=Math.max(height[l],lmax);//更新lmax
        }else
        {
            ans+=rmax-height[r];
            if(l<=--r)rmax=Math.max(rmax,height[r]);
        }
    }
    return ans;
}

无敌思路

滑动窗口

3. 无重复字符的最长子串

最开始的思路:i,j,滑动窗口,但是发现不行,需要一个数据结构来维护i和j之间的

那么想到了hashmap,为什么不用set,因为你要求长度,还得保存下标

public int lengthOfLongestSubstring(String s) {
    HashMap<Character,Integer>map=new HashMap<>();
    int maxlen=0;
    int left=0;
    for(int i=0;i<s.length();i++)
    {
        if(map.containsKey(s.charAt(i)))
        {
            left=Math.max(left,map.get(s.charAt(i))+1);
        }
        map.put(s.charAt(i),i);
        maxlen=Math.max(maxlen,i-left+1);
    }
    return maxlen;
}

left = Math.max(left,map.get(s.charAt(i)) + 1);
left是子串的起始位置
遇到重复元素的就把重复元素下标+1作为子串的起始位置left,即 left = map.get(s.charAt(i)) + 1; 但由于有 ‘abba’ 这样的字符,当 ‘b’ 重复时,left 已经记作2,
再次循环,遇到重复元素 ‘a’ 时 , left就会被记作1,这样子串起始位置left就从2倒退回1了 ,乱掉了。

所以为了再次循环到重复元素 ‘a’ 时,防止left 子串起始位置不倒回去,保持之前重复元素 ‘b’的值,
就对比一下老的left 和新的left = map.get(s.charAt(i)) + 1
谁大,就是正确的left , 即left = Math.max(left,map.get(s.charAt(i)) + 1);

438. 找到字符串中所有字母异位词

思路:维护一个和要找的单词的长度一样长的滑动窗口,然后滑动判断是不是异味词

复杂度取决于你如何判断是不是异味

这里选择统计窗口内各字母的个数,这样只用on

注意点:比较两个List是否相同直接Arrays.equal

public List<Integer> findAnagrams(String s, String p) {
    int slen=s.length(),plen=p.length();

    if(slen<plen)return new ArrayList<Integer>();
    List<Integer>ans=new ArrayList<>();
    int []pcount=new int[26];//统计p中每个字母出线的个数
    int []scount=new int[26];//统计窗口里的
    for(int i=0;i<plen;i++)//刚开始
    {
        pcount[p.charAt(i)-'a']++;
        scount[s.charAt(i)-'a']++;
    }

    if(Arrays.equals(scount,pcount))
    {
        ans.add(0);
    }//比较初试状态

    for(int i=0;i<slen-plen;i++)//窗口滑动
    {
        --scount[s.charAt(i)-'a'];
        ++scount[s.charAt(i+plen)-'a'];

        if(Arrays.equals(scount,pcount))
        {
            ans.add(i+1);//i是从0开始,所以要加1
        }
    }
    return ans;
}

560. 和为 K 的子数组

思路:其实类似于两数之和的哈希方法,只不过这一次变成了数组连续和

public int subarraySum(int[] nums, int k) {
    //这题吧,有点动态规划的味道
    //思路:前缀和方法,pre[i]是[0,i]里所有数的和
    //所以[j,i]的和就是pre[i]-pre[j-1]=k
    //所以我们只要对每一个i,找有没有值为pre[i]-k的pre[j]即可

    int count=0;//结果
    int pre=0;//前面数字相加的和
    HashMap<Integer,Integer>map=new HashMap<>();//记录每一个pre出现的次数
   map.put(0,1);//为什么要定义(0,1),因为考虑[1,2]然后k=3,1+2=pre=3,需要能找到3-0=3的,你不初始化就没有这个了
    for(int i=0;i<nums.length;i++)
    {   
        pre+=nums[i];
        if(map.containsKey(pre-k))//如果有前缀和等于pre-k,那么正好pre-(pre-k)=k,这一段就是我们要的
        {
            count+=map.get(pre-k);//加上出现过pre-k的次数,可能出现过不止一次,
        }
        map.put(pre,map.getOrDefault(pre,0)+1);//不管找没找到,都得更新当前pre
    }
    return count;
}

239. 滑动窗口最大值

二刷第一反应想到优先队列,但是超时了

public int[] maxSlidingWindow(int[] nums, int k) {
    //思路:直接用最大堆不就行了
    PriorityQueue<Integer> pq = new PriorityQueue<>(k, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    ArrayList<Integer>list=new ArrayList<>();
    for(int num:nums)//先把前k个写进去
    {
        if(pq.size()<k)
        {
            pq.offer(num);
        }
    }
    for(int i=0;i<nums.length-k;i++)
    {
        list.add(pq.peek());
        pq.remove(nums[i]);
        pq.offer(nums[i+k]);
    }
    list.add(pq.peek());
    int len=list.size();
    int[]res=new int[len];
    for(int i=0;i<len;i++)
    {
        res[i]=list.get(i);
    }
    return res;
}

看答案发现是用双向队列,维护一个单调递减,其实我也想过用递减栈

我是先想到递减栈,然后发现优先队列可以不用自己push然后pop排序

非常重要的思路就是:只存放下标,不存放具体的数值,这样就不用把poll的再add回去,简直无敌

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        //思路:维护存有下标的一个双向队列,如果队首的下标还在,那就说明最大值还是他
        if(nums==null||nums.length<2)return nums;
        LinkedList<Integer>queue=new LinkedList<>();//注意queue里存的是下标
        int[]res=new int[nums.length-k+1];

        for(int i=0;i<nums.length;i++)
        {
            while(!queue.isEmpty()&&nums[queue.peekLast()]<=nums[i])//比nums[i]的小的都出来
            {
                queue.pollLast();
                //因为存的是下标,所以不需要把弹出的再存回来,只需要判断这个下表是不是还在窗口内即可
            }
            queue.addLast(i);
            if(queue.peek()<=i-k)//下标在现在的窗口外面了
            {
                queue.poll();
            }
            if(i+1>=k)//已经有一个窗口的大小了
            {
                res[i+1-k]=nums[queue.peek()];
            }
        }
        return res;
    }

76. 最小覆盖子串 - 力扣(LeetCode)

看我的思路:

public String minWindow(String s, String t) {
    //思路:滑动窗口,维护start和end,然后找到一个包含所有字母的,那就记录len并且尝试更新
    //找字母的过程:start不懂,end一个一个和t[i]比较,一样了i++,否则end++
    //找打哦所以字母了,start=end+1
    int start=0,end=0;
    int i=0;
    int minlen=Integer.MAX_VALUE;
    int minstart=0;
    while(end<s.length())//还没到最末尾
    {
        if(s.charAt(end)==t.charAt(i))
        {
            if(i==t.length()-1)
            {                  
                if(end-start+1<minlen)//如果这是更小的长度
                {
                    minstart=start;
                    minlen=end-start+1;
                }
                i=0;//从头开始
                start=end+1;
                end=start;
            }else
            {
                end++;
                i++;
            }
        }else
        {
            end++;//否则找下一个
        }
    }
    return s.substring(minstart,minstart+minlen);
}

知道我的思路错在哪了吗?

比如测试用例”ADOBECODEBANC”,我的结果是”ADOBEC”,为什么,因为当我第一次找到”ADOBEC”后,start到了O, 然后一直找一直找,找到最后的C,才包含了所有字符,但是此时截取的是ODEBANC,判断比ADOBEC长,所以返回了ADOBEC

也就是说,这里无法保证是最小的长度,只对end进行处理是不行的,找到end后,还需要回头处理start

正确的思路:

class Solution {
    public String minWindow(String s, String t) {
        int[] cnt = new int[128];
        for (int i = 0; i < t.length(); i++) cnt[t.charAt(i)]++;
        int l = 0, r = 0, ansL = 0, ansR = 0, ans = Integer.MAX_VALUE, cntT = t.length();
        while (r < s.length()) {
            if (cnt[s.charAt(r++)]-- > 0) cntT--;
            while (cntT == 0) {
                if (r - l < ans) {
                    ans = r - l;
                    ansL = l;
                    ansR = r;
                }
                if (cnt[s.charAt(l++)]++ == 0) cntT++;
            }
        }
        return ans == Integer.MAX_VALUE ? "" : s.substring(ansL, ansR);
    }
}

普通数组

53. 最大子数组和 - 力扣(LeetCode)

简单动态规划,秒了

public int maxSubArray(int[] nums) {
    int len=nums.length;
    int[]dp=new int[len];
    dp[0]=nums[0];

    for(int i=1;i<len;i++)
    {
        if(dp[i-1]>0)
        {
            dp[i]=dp[i-1]+nums[i];
        }else
        {
            dp[i]=nums[i];
        }
    }
    int res=dp[0];
    for(int i=1;i<len;i++)
    {
        res=Math.max(res,dp[i]);
    }
    return res;
}

56. 合并区间 - 力扣(LeetCode)

这题目听绕道,得讲清楚

public int[][] merge(int[][] intervals) {
    if(intervals.length==0)
    {
        return new int[0][2];
    }
    Arrays.sort(intervals,new Comparator<int[]>(){
        public int compare (int[]interval1,int[]interval2){
            return interval1[0]-interval2[0];
        }
    });//先把他排序了
    //两个区间可以合并的条件:a的右端点大于其左端点或者右端点
    //但是如果a的左端点大于其左端点也可以
    //所以剩下一种情况:a的左端点大于其右端点,那就不行
    //不能合并怎么办,直接添加,因为是有序的,所以不能合并的后面所有的,对于当前的都不能合并

    //如何合并?因为已经按照左端点排序了,那么只需要更新右端点即可,右端点取最大的
    //如何遍历?
    List<int[]>merged=new ArrayList<>();
    for(int i=0;i<intervals.length;i++)
    {
        int L=intervals[i][0],R=intervals[i][1];//L:当前的左,R:当前的右
        if(merged.size()==0||merged.get(merged.size()-1)[1]<L)//如果merged为空或者其右端点小于目前比较的左端点
        {
            merged.add(new int[]{L,R});//直接添加
        }else
        {
            merged.get(merged.size()-1)[1]=Math.max(merged.get(merged.size()-1)[1],R);
            //否则,可以更新
        }
    }
    return merged.toArray(new int[merged.size()][]);
}

189. 轮转数组 - 力扣(LeetCode)

思路很清晰,但是注意k要%nums.length

public void rotate(int[] nums, int k) {
    if(nums.length==0)return;
    if(nums.length==1)return;
    k%=nums.length;//这一步非常重要,不然后面数组越界了
   reverse(nums,0,nums.length-1);
   reverse(nums,0,k-1);
    reverse(nums,k,nums.length-1);

}
public void reverse(int[]a,int l ,int r)
{
    while(l<r){
        int t=a[l];
        a[l]=a[r];
        a[r]=t;
        l++;
        r--;
    }
}

238. 除自身以外数组的乘积

这次自己用空间复杂度on的做出来了

思路就是左积乘右积

public int[] productExceptSelf(int[] nums) {
    //思路,什么勾八对角矩阵方法反正爷没记住
    int []res=new int[nums.length];
    int []L=new int[nums.length];
    int []R=new int[nums.length];
    L[0]=1;//第一个数的左积视为1
    R[nums.length-1]=1;//最后一额数的右积视为1
    for(int i=1;i<nums.length;i++)//求左边积
    {
        L[i]=nums[i-1]*L[i-1];
    }
    for(int i=nums.length-2;i>=0;i-- )//求右边积,其实这里nums.length-2可能越界,但是幸运的是测试用例没有
    {
        R[i]=nums[i+1]*R[i+1];
    }
    for(int i=0;i<nums.length;i++)//左乘右
    {
        res[i]=L[i]*R[i];
    }
    return res;
}

但是还能优化,先鸽了

41. 缺失的第一个正数

public int firstMissingPositive(int[] nums) {
    //思路:原地哈希
    //对于长度为N的数组,最小未出现正整数,要么是N+1,要么在1-N中
    //如果是有序的,那么对于1-n的i数值,i就应该出现在nums[i-1]上,所以我们可以对nums[i-1]设置标记,表示对应的i出现过
    //但是如何处理数组中的0和负数呢,我们把他先修改为N+1为什么,修改为别的会影响原来的1-N的个数
    //如何设置对nums[i-1]的标记
    //改成N+1侯,数组里全是证书了,所以我们设置为负号
    //如何规避遍历到已经被打标记的数呢
    //他的原来的数就是绝对值       
    int n=nums.length;
    for(int i=0;i<n;i++)
    {
        if(nums[i]<=0)
        {
            nums[i]=n+1;//小于等于0的全 设为n+1
        }
    }
    for(int i=0;i<n;i++)
    {
        int num=Math.abs(nums[i]);//变成原来值
        if(num<=n)
        {
            nums[num-1]=-Math.abs(nums[num-1]);//设置标记
        }
    }
    for(int i=0;i<n;i++)
    {
        if(nums[i]>0)
        {
            return i+1;//没被标记到的,那他就是没出现的最小的
        }
    }
    return n+1;
}

矩阵

73. 矩阵置零

首先想到用o(m+N)的额外空间

public void setZeroes(int[][] matrix) {
    //思路:设置两个个标识数组,表示这一行这一列有无0出线
    int m=matrix.length;//几个行
    int n=matrix[0].length;//几个列
    boolean[]ismzero=new boolean[m];//这一行有没有0
    boolean[]isnzero=new boolean[n];//这一列有没有0
    for(int i=0;i<m;i++)
    {
        for(int j=0;j<n;j++)
        {
            if(matrix[i][j]==0)
            {
                ismzero[i]=true;//这一行有0
                isnzero[j]=true;//这一列有0
            }
        }
    }
    for(int i=0;i<m;i++)
    {
        for(int j=0;j<n;j++)
        {
            if(ismzero[i]||isnzero[j])
            {
                matrix[i][j]=0;
            }
        }
    }
}

是否可以优化,答案是可以的,但我认为这个傻逼优化没有任何意义,所以爷不学了

54. 螺旋矩阵 - 力扣(LeetCode)

这题目现在可以得心应手了,思路就是改变边界

public List<Integer> spiralOrder(int[][] matrix) {
    //思路,直接改变边界即可
    //遍历顺序:右,下,左,上,如果超过任何一个边界了,则break
    int l=0;//左边
    int r=matrix[0].length-1;//右边界
    int t=0;//上
    int b=matrix.length-1;//下边界
    ArrayList<Integer>res=new ArrayList<>();
    //注意一定要是++t,--r,得先做加减再比较
    while(true)
    {
        for(int i=l;i<=r;i++) res.add(matrix[t][i]);//向右
        if(++t>b)break;
        for(int i=t;i<=b;i++)res.add(matrix[i][r]);//向下
        if(--r<l)break;
        for(int i=r;i>=l;i--)res.add(matrix[b][i]);//向左
        if(--b<t)break;
        for(int i=b;i>=t;i--)res.add(matrix[i][l]);
        if(++l>r)break;
    }
    return res;
}

240. 搜索二维矩阵 II - 力扣(LeetCode)

这个也是得心应手啦

记住从右上角开始即可

public boolean searchMatrix(int[][] matrix, int target) {
    //从右上角开始
    //x比target小,那么x这一列向上的所有 都比他小,所以往下走一行
    //x比target大,那么x这一行右边都比target大,往左走
    int i=0,j=matrix[0].length-1;
    while(i<matrix.length&&j>=0)
    {
        if(matrix[i][j]<target)
        {
            i++;
        }else if(matrix[i][j]>target)
        {
            j--;
        }
        else{
            return true;
        }
    }
    return false;
}

48. 旋转图像 - 力扣(LeetCode)

之前是嗯背公式,这一次才懂了怎么地推的

public void rotate(int[][] matrix) {
    //思路:利用tmp暂存实现原地旋转
    //知识点:i行j列->倒数第i列的第j行,n-1-i,j
    //同理i行j列,被n-1-j行,i覆盖,以此类推,分别推出每一个是被谁覆盖了
    //倒数第i列:n-i-1,n=matrix.length

    //每次先暂存i,j,然后依次覆盖
    int n=matrix.length;
    for(int i=0;i<n/2;i++)//只需要旋转一半就行了
    {
        for(int j=0;j<(n+1)/2;j++)
        {
            int t =matrix[i][j];
            matrix[i][j]=matrix[n-1-j][i];
            matrix[n-1-j][i]=matrix[n-1-i][n-1-j];
            matrix[n-1-i][n-1-j]=matrix[j][n-1-i];
            matrix[j][n-1-i]=t;
        }
    }
}

链表

160. 相交链表 - 力扣(LeetCode)

常规做法:先求两个的长度,然后较长的先走过长度之差,然后再一直走,直到相遇

但是,非常规做法

思路:

我们设A长度为a,B长度为b,相遇节点往后的长度为c

那么A如果先走完A,再从B的头走到相遇节点,会走过a+b-c的路程

同样,B先走玩B,再从A的头走到相遇节点,会走过b+a-c的路程

所以我们只需要让A先走A再从B头开始走,B先走B再从A头开始走,返回A=B的时候就行了

如果相遇自然是node,如果不相遇那就是null

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    ListNode A=headA,B=headB;
    while(A!=B)
    {
        A=A!=null?A.next:headB;
        B=B!=null?B.next:headA;
    }
    return A;
}

206. 反转链表 - 力扣(LeetCode)

鉴定为模板

public ListNode reverseList(ListNode head) {
    ListNode pre=null;
    ListNode cur=head;
    while(cur!=null)
    {
        ListNode after=cur.next;
        cur.next=pre;
        pre=cur;
        cur=after;
    }
    return pre;
}

234. 回文链表 - 力扣(LeetCode)

public boolean isPalindrome(ListNode head) {
    //思路:翻转前半部分,然后顺序比较
    //这样可以将找到中间节点和翻转,一起进行

    if(head==null||head.next==null)
    {
        return true;
    }
    ListNode slow=head,fast=head;
    ListNode pre=head,prepre=null;
    //pre相当于cur,slow相当于afer,prepre相当于pre
    while(fast!=null&&fast.next!=null)
    {
        pre=slow;
        slow=slow.next;
        fast=fast.next.next;
        pre.next=prepre;
        prepre=pre;
    }
    //处理是奇数的情况,当链表长度为奇数,此时slow会走到最中间的,不用比较的节点,而fast会走到最后一个节点
    if(fast!=null)
    {
        slow=slow.next;
    }
    while(pre!=null&&slow!=null)//比较前后是否一样
    {
        if(pre.val!=slow.val)
        {
            return false;
        }
        pre=pre.next;
        slow=slow.next;
    }
    return true;
}

141. 环形链表 - 力扣(LeetCode)

这个的原理其实是个人就懂,但是如何写,如何写while循环,判断的顺序,很有问题

public boolean hasCycle(ListNode head) {
    ListNode slow=head,fast=head;
    while(fast!=null&&fast.next!=null)//在while里判断,而不是while(1)然后if
    {
        slow=slow.next;
        fast=fast.next.next;
        if(fast==slow)
        {
            return true;
        }
    }
    return false;
}

142. 环形链表 II - 力扣(LeetCode)

这个直接被结论

1.判断有没有环

2.fast移动到开头每次走一个,slow也每次走一个直到相遇

public ListNode detectCycle(ListNode head) {
    if(head==null||head.next==null)return null;
    ListNode fast=head;
    ListNode slow=head;
    while(true)
    {   
        if(fast==null||fast.next==null)
        {
            return null;
        }
        fast=fast.next.next;
        slow=slow.next;
        if(fast==slow)
        {
            break;
        }
    }
    ListNode p=head;
    while(slow!=p)
    {
        slow=slow.next;
        p=p.next;
    }
    return p;
}

2. 两数相加 - 力扣(LeetCode)

思路在注释里面了

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    //思路,设置一个进位值,进位值等于l1.val+l2.val+上一个的进位)/10
    //保留下来的是%10
    //然后while循环不断遍历,如果最后还有进位值,那就newlistNode (1)
    ListNode pre=new ListNode(0);
    ListNode cur=pre;
    int carry=0;//进位
    while(l1!=null||l2!=null)//只需要一个不为null就可以
    {
        int x=l1==null?0:l1.val;
        int y=l2==null?0:l2.val;
        int sum=x+y+carry;
        carry=sum/10;
        sum=sum%10;
        cur.next=new ListNode(sum);
        cur=cur.next;
        if(l1!=null)
        {
            l1=l1.next;
        }
        if(l2!=null)
        {
            l2=l2.next;
        }
    }
    if(carry==1)
    {
        cur.next=new ListNode(1);
    }
    return pre.next;
}

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

思路:找倒数第n个节点很简单,但是要删除它,我们应该找倒数第n+1个节点

解决办法在注释

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy=new ListNode(0,head);
    ListNode fast=head;
    ListNode slow=dummy;//让slow先在fast后面一个,这样slow会走到要被删除节点的前一个节点
    while(n>0)
    {
        fast=fast.next;
        n--;
    }
    while(fast!=null)
    {
        fast=fast.next;
        slow=slow.next;
    }
    slow.next=slow.next.next;//这样就不用返回来再找倒数第n个节点的前一个节点了
    ListNode ans=dummy.next;
    return ans;
}

24. 两两交换链表中的节点 - 力扣(LeetCode)

这题没啥好说的,多谢几遍记住交换的 代码就行了

public ListNode swapPairs(ListNode head) {
    if(head==null||head.next==null)return  head;
    ListNode pre=new ListNode(0);
    pre.next=head;
    ListNode tmp=pre;
    while(tmp.next!=null&&tmp.next.next!=null)
    {
        ListNode l=tmp.next;
        ListNode r=tmp.next.next;
        tmp.next=r;
        l.next=r.next;
        r.next=l;
        tmp=l;
    }
    return pre.next;
}

25. K 个一组翻转链表

思路:用栈

注意事项:当count<k的时候,直接连接就行了,因为本来就是连起来的

public ListNode reverseKGroup(ListNode head, int k) {
    //思路,大小为k的栈即可,然后多出来的直接连就行了
    ListNode dummy=new ListNode(0);
    Deque<ListNode> stk=new ArrayDeque<>();
    ListNode p=dummy;
    while(true)
    {
        int count=0;
        ListNode tmp=head;
        while(tmp!=null&&count<k)//把k个push进去
        {
            count++;
            stk.add(tmp);
            tmp=tmp.next;
        }
        if(count!=k)//如果不足k个,那么直接连接,不需要弹出了,按本来的顺序就行了
        {
            p.next=head;
            break;
        }
        while(!stk.isEmpty())
        {
            p.next=stk.pollLast();//弹出,实现翻转
            p=p.next;
        }
        //最后要注意把head更新一下
        head=tmp;
    }
    return dummy.next;
}

138. 随机链表的复制 - 力扣(LeetCode)

有一说一题没看懂
有一说一太简单了

public Node copyRandomList(Node head) {
    //思路:设置哈希表,key是元节点,value是对饮的复制过的节点
    //先遍历一遍,赋值一下
    //第二次遍历连起来
    if(head==null)
    {
        return null;
    }
    Map<Node,Node> map=new HashMap<Node,Node>();
    Node p=head;
    //先复制,不忙设置random和next
    while(p!=null)
    {
        Node newNode=new Node(p.val);
        map.put(p,newNode);
        p=p.next;
    }
    p=head;
    //第二次,设置next和random
    while(p!=null)
    {
        Node newNode=map.get(p);//或者原节点对应复制的节点
        if(p.next!=null)
        {
        newNode.next=map.get(p.next);//复制的节点的next的原来的next的复制的
        }
        
        if(p.random!=null)
        {
            newNode.random=map.get(p.random);//同理
        }
        p=p.next;
    }
    return map.get(head);
}

148. 排序链表 - 力扣(LeetCode)

一眼归并排序,但是归并排序我还不是很熟,我草我是傻逼

public ListNode sortList(ListNode head) {
    if(head==null||head.next==null)
        return head;
    ListNode fast=head.next,slow=head;//有时我们可能会让快指针从头节点的下一个节点开始,这主要是为了处理链表长度为偶数的情况。如果链表长度为偶数,那么中点有两个。如果快指针从头节点开始,慢指针会停在中间两个节点的第2个;如果快指针从头节点的下一个节点开始,慢指针会停在中间两个节点的第1个。所以,是否让快指针从头节点的下一个节点开始,主要取决于你在链表长度为偶数时,希望慢指针停在哪个中点上。
    while(fast!=null&&fast.next!=null)
    {
        fast=fast.next.next;
        slow=slow.next;
    }
    ListNode tmp=slow.next;//slow是中间节点的第一个,所以slow.next是第二半
    slow.next=null;//断掉
    ListNode left=sortList(head);//递归求left
    ListNode right=sortList(tmp);//递归求right
    ListNode h=new ListNode(0);
    ListNode res=h;
    while(left!=null&&right!=null)
    {
        if(left.val<right.val)
        {
            h.next=left;
            left=left.next;
        }else
        {
            h.next=right;
            right=right.next;
        }
        h=h.next;
    }
    h.next=left!=null?left:right;//剩下的
    return res.next;
}

注意注释中关于fast的位置的讨论

23. 合并 K 个升序链表 - 力扣(LeetCode)

有了归并排序的基础,这个应该有所想法

首先最容易想到的就是,我顺序进行两两归并排序

public ListNode mergeKLists(ListNode[] lists) {
    ListNode ans=null;
    for(int i=0;i<lists.length;i++)
    {
        ans=mergeTwo(ans,lists[i]);
    }
    return ans;
}
//因为已经是有序的,所以直接归并就行了
public ListNode mergeTwo(ListNode a,ListNode b)
{
    if(a==null||b==null)
        return a!=null?a:b;
    ListNode head =new ListNode(0);
    ListNode tail=head,aptr=a,bptr=b;
    while(aptr!=null&&bptr!=null)
    {
        if(aptr.val<bptr.val)
        {
            tail.next=aptr;
            aptr=aptr.next;
        }else
        {
            tail.next=bptr;
            bptr=bptr.next;
        }
        tail=tail.next;
    }
    tail.next=(aptr!=null?aptr:bptr);//把多的连起来
    return head.next;
}

思路二:无敌

不是合并k个链表吗,那么我们用一个优先队列,大小为k,存储这k个链表中,还未被合并的元素,那么堆头就应该是还未被合并的,最小的那个,然后不断合并, 不断add即可,天才!

public ListNode mergeKLists(ListNode[] lists) {
    if(lists==null||lists.length==0)
        return null;
    PriorityQueue<ListNode> queue=new PriorityQueue<>(lists.length,new Comparator<ListNode>(){
        public int compare(ListNode o1,ListNode o2 )
        {
            if(o1.val<o2.val)return -1;//
            else if(o1.val==o2.val)return 0;//
            else return 1;//1就是你需要的顺序和原来的顺序相反
        }
    });
        ListNode dummy=new ListNode(0);
        ListNode p=dummy;
        for(ListNode node:lists)
        {
            if(node!=null)
            {
                queue.add(node);//先把每个链表的第一个插进来
            }
        }
        while(!queue.isEmpty())
        {
            p.next=queue.poll();//从队列中取出
            p=p.next;//这个时候p就是被取出的那个元素
            if(p.next!=null)
            {
                queue.add(p.next);//加入被取出p对应的的那个链表的后面一个
            }
        }
        return dummy.next;
}

146. LRU 缓存 - 力扣(LeetCode)

有一说一没写过,奶奶的

其实就是知道怎么用LinkedHashMap

class LRUCache {
    //这里我们把最进用的的放在队尾,这里put和get都算使用
    //然后借助LinkedHashHap这个强无敌的哈希链表
    LinkedHashMap<Integer,Integer>cache=new LinkedHashMap<>();
    int cap;
    public LRUCache(int capacity) {
        this.cap=capacity;
    }
    
    public int get(int key) {
        if(!cache.containsKey(key))
            return -1;
        makeRecent(key);
        return cache.get(key);

    }
    
    public void put(int key, int value) {
        if(cache.containsKey(key))//有则改变值
        {
            cache.put(key,value);
            makeRecent(key);
            return ;
        }

        if(cache.size()>=this.cap)//容量不足了,把头拿出去
        {
            int oldkey=cache.keySet().iterator().next();//迭代器一开始再头的前面一个,所以后一个就是第一个
            cache.remove(oldkey);
        }
        cache.put(key,value);
    }


    public void makeRecent(int key)//就是把他删了再放到后面
    {
        int val=cache.get(key);
        cache.remove(key);
        cache.put(key,val);
    }
}

二叉树部分

94. 二叉树的中序遍历 - 力扣(LeetCode)

直接递归

注意res写全局变量

class Solution {
        List<Integer>res=new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        
        if(root==null)
            return res;
        inorderTraversal(root.left);
        res.add(root.val);
        inorderTraversal(root.right);
        return res;
    }
}

104. 二叉树的最大深度 - 力扣(LeetCode)

依然是递归啊,先写不满足的条件就行了

public int maxDepth(TreeNode root) {
    if(root==null)return 0;//什么时候不满足,空点
    int left=maxDepth(root.left);
    int right=maxDepth(root.right);
    return Math.max(left,right)+1;
}

226. 翻转二叉树 - 力扣(LeetCode)

依然是递归

public TreeNode invertTree(TreeNode root) {
    //有一说一这题取巧了,他其实进行的是值的交换
    //什么时候不满足
    if(root==null)
        return null;
    //你可能会想,如果没有左右孩子呢?那就是null和null交换呗,一样的
    TreeNode t=root.left;
    root.left=root.right;
    root.right=t;
    invertTree(root.left);
    invertTree(root.right);
    return root;
}

101. 对称二叉树 - 力扣(LeetCode)

依然是递归,但是需要辅助函数,以及要注意,辅助函数不能直接else return false

class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root==null)  return true;
        
        return isHelper(root.left,root.right);
    }
    public boolean isHelper(TreeNode left,TreeNode right)
    {
        if(left==null&&right==null)
            return true;
        if(left==null||right==null)
            return false;
        if(left.val!=right.val)
            return false;

        return isHelper(left.left,right.right)&&isHelper(left.right,right.left);
    }
}

543. 二叉树的直径 - 力扣(LeetCode)

我的思路:直径其实就是node的左右深度之和,所以我得先实现一个求深度的代码

然后设置全局变量,在递归的过程中,更新ans的最大值即可

因为你从root开始,每一个节点都会做一次求深度

class Solution {
    //因为直径是用边数表示,所以等于左深度+右深度+自己-1,正好就是左深度加右深度
    int ans;
    public int diameterOfBinaryTree(TreeNode root) {
        ans=0;
        maxDepth(root);
        return ans;
    }
     public int maxDepth(TreeNode root) {
        if(root==null)return 0;//什么时候不满足,空点
        int left=maxDepth(root.left);
        int right=maxDepth(root.right);
        ans=Math.max(ans,left+right);//还得加上自己
        return Math.max(left,right)+1;
    }
}

102. 二叉树的层序遍历 - 力扣(LeetCode)

这个就太简单了,当然因为要分层存入,所以得记录size,这个注意一下

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res=new ArrayList<>();
    Queue<TreeNode> queue=new LinkedList<TreeNode>();
    if(root==null)
        return res;
    queue.offer(root);
    while(!queue.isEmpty())
    {
        List<Integer>level=new ArrayList<>();
        int size=queue.size();
        for(int i=0;i<size;i++)//因为要分层存入,所以记录size
        {
            TreeNode node=queue.poll();
            level.add(node.val);
            if(node.left!=null)
            {
                queue.offer(node.left);
            }
            if(node.right!=null)
            {
                queue.offer(node.right);
            }
        }
        res.add(level);
    }
    return res;
}

108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)

鉴定为易

class Solution {
    //知识点:二叉搜索树的中序遍历就是一个升序序列
    //所以该问题就是,根据中序遍历建立二叉搜索树
    //思路:中间节点就是root,然后递归建立左右子树
    
    public TreeNode sortedArrayToBST(int[] nums) {
        return helper(nums,0,nums.length-1);
    }
    public TreeNode helper(int []nums,int l,int r)
    {
        if(l>r)return null;
        int mid=(l+r)/2;
        TreeNode root=new TreeNode(nums[mid]);
        root.left=helper(nums,l,mid-1);
        root.right=helper(nums,mid+1,r);
        return root;
    }
}

230. 二叉搜索树中第K小的元素 - 力扣(LeetCode)

//方法一:中序遍历放入数组里
class Solution {
    ArrayList<Integer>list=new ArrayList<Integer>();
    public int kthSmallest(TreeNode root, int k) {
        zxbl(root);
        return list.get(k-1);
    }
    public void  zxbl(TreeNode root)
    {     
        if(root==null)return;
        zxbl(root.left);
        list.add(root.val);
        zxbl(root.right);
    }
}

199. 二叉树的右视图 - 力扣(LeetCode)

思路:中序遍历,然后,存储每一层的最后一个元素即可

public List<Integer> rightSideView(TreeNode root) {
    Queue<TreeNode>q=new LinkedList<TreeNode>();
    List<Integer>res=new ArrayList<>();
    if(root==null)
        return res;
    q.offer(root);
    while(!q.isEmpty())
    {
        int size=q.size();//先记录这一层有多少个
        while(size-->0)//size次之后,也就遍历完这一层的了
        {
            TreeNode node=q.poll();
            if(node.left!=null)q.offer(node.left);
            if(node.right!=null)q.offer(node.right);
            if(size==0)res.add(node.val);
        }
    }
    return res;
}

114. 二叉树展开为链表 - 力扣(LeetCode)

方法一:就是先序遍历放到数组里面,然后再建立

方法二:

思路:最后的链表与前序遍历相同, 我们可以试着用前序遍历来写

但是写完后会发现,当遍历右子树的时候,你的根的right已经指向根的left部分了,怎么办呢

答案是:逆前序遍历,也可以说是变心的后序遍历,变成右左根,右左根,然后右的上一个就是根了

每遍历一个节点就将当前节点的右指针更新为上一个节点,这样就不会丢失右孩子

class Solution {
    TreeNode pre=null;
    public void flatten(TreeNode root) {
        if(root==null)
        return;
        flatten(root.right);
        flatten(root.left);
        root.right=pre;
        root.left=null;
        pre=root;
    }
}

105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

哎哎哎,这题目剑指offer有,烧鸡100有,但是爷还是不能秒

思路是人人都会,就是找到根,然后递归,根就是现需遍历里的第一个

注意:此处我们选择左臂右开区间,以及递归必须先写终止条件

class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return Helper(preorder,0,preorder.length,inorder,0,inorder.length);//不能减1,因为是右开
    }
    public TreeNode Helper(int[]preorder,int pstart,int pend,int []inorder,int istart,int iend)//左闭右开
    {
        if(pstart==pend)
            return null;
        int root_val=preorder[pstart];//根节点就是前序遍历的第一个节点
        int iroot_index=0;//这个根节点在中序遍历里的位置
        for(int i=istart;i<iend;i++)//左闭右开,找到这个根在中序遍历里的位置,拆分左子树和右子树
        {
            if(root_val==inorder[i])
            {
                iroot_index=i;
                break;
            }
        }
        TreeNode root=new TreeNode(root_val);//新建根节点
        int leftlength=iroot_index-istart;//左子树的长度,用于在前序遍历中也找到左子树的部分
        root.left=Helper(preorder,pstart+1,pstart+leftlength+1,inorder,istart,iroot_index);//左比有开
        root.right=Helper(preorder,pstart+leftlength+1,pend,inorder,iroot_index+1,iend);//跳过iroot_index,因为他就是更节点
        return root;
    }
}

437. 路径总和 III - 力扣(LeetCode)

第一时间想到前缀和,前缀和的核心:map

当然深度优先搜索也是回溯,所以你必须不能忘记回溯

class Solution {
    //思路:前缀和思想,将前缀和放到map里,然后去找map里有没有curr-targetsum的
    //前缀和方法的核心:就是那个map
    public int pathSum(TreeNode root, int targetSum) {
        Map<Long,Integer> prefix=new HashMap<Long,Integer>();
        prefix.put(0L,1);//和其他前缀和方法一样,你要保证能当比如:1,2 然后你要的和就是3,那你是不是得找前缀和为0的,所以你得先放进去
        return dfs(root,prefix,0,targetSum);
    }

    public int dfs(TreeNode root,Map<Long,Integer> prefix,long curr,int targetSum)
    {
        if(root==null)
        {
            return 0;
        }
        int ret=0;
        curr+=root.val;//当前和
        ret=prefix.getOrDefault(curr-targetSum,0);//前缀和等于curr-targetsum的个数
        prefix.put(curr,prefix.getOrDefault(curr,0)+1);//把当前前缀和放进去,之前没有过就变成1,否则在基础上+1
        ret+=dfs(root.left,prefix,curr,targetSum);
        ret+=dfs(root.right,prefix,curr,targetSum);
        prefix.put(curr,prefix.getOrDefault(curr,0)-1);//回溯
        return ret;
    }
}

236. 二叉树的最近公共祖先 - 力扣(LeetCode)

这个题目,作为二刷,我首先想到,自己也是自己的祖先

所以分为三种情况,a和b,

要么a是b的祖先,要么b是a的祖先

要么a和b分在一个祖先的左右子树

然后用金典几行代码

两种都得会,来看看第一种:

class Solution {
    //本题说是找pq的公共祖先,其实是找pq的位置
    //要么p是q祖先,要么q是p祖先,要么分散在一个root的左右子树里
    //如果先找到p,那么p是祖先, 如果先找到q,那么q是祖先
    //如果这次都没找到,那就递归左右字数
    //如果左或者右为空,说明不在这个里面,只返回不空的那一个的结果就行
    //如果两个结果都不为空,那么说明分散在左右子树,root就是结果
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null)return null;
        if(root==p||root==q)return root;
        TreeNode left=lowestCommonAncestor(root.left, p, q);
        TreeNode right=lowestCommonAncestor(root.right, p, q);
        if(left!=null&&right!=null)return root;
        return left!=null?left:right;
    }
}

但是这种思路我无法理解,所以换一种:

class Solution {
    //思路,得到根节点到这两个节点的路径,然后找出路径中最后一个相同的元素即可
    //怎么找路径?dfs啊
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        List<TreeNode> a=new ArrayList<>(),b=new ArrayList<>();
        dfs(root,p,a);
        dfs(root,q,b);
        TreeNode ans=null;
        for(int i=0;i<Math.min(a.size(),b.size())&&a.get(i)==b.get(i);i++)
        {
            ans=a.get(i);
        }
        return ans;
    }

    boolean dfs(TreeNode cur,TreeNode t,List<TreeNode>path){
        if(cur==null)
            return false;
        path.add(cur);
        if(cur==t||dfs(cur.left,t,path)||dfs(cur.right,t,path))
        {   
            return true;
        }else
        {
            path.remove(path.size()-1);
            return false;
        }
    }
}

124. 二叉树中的最大路径和 - 力扣(LeetCode)

有一说一虽然是困难题,但是有了二叉树的直径的基础,还是很好想到的

class Solution {
    int max=Integer.MIN_VALUE;
    //思路:其实也就是求每个节点的左右子树的最大 顺下去的和然后相加
    public int maxPathSum(TreeNode root) {
        getmaxdeepsum(root);
        return max;
    }
    public int getmaxdeepsum(TreeNode root)
    {
        if(root==null)
            return 0;
        int leftsum=Math.max(getmaxdeepsum(root.left),0);//大于0才选取这一段
        int rightsum=Math.max(getmaxdeepsum(root.right),0);

        int price=root.val+leftsum+rightsum;//进过这一点的最大路径和
        max=Math.max(max,price);
        return root.val+Math.max(leftsum,rightsum);//返回这个结点的最大一条路径,因为如果这一个节点不是最后的中间那个节点,那么他被选取的时候只能作为左或者右路径的一部分
    }
}

图论部分

200. 岛屿数量 - 力扣(LeetCode)

可以用dfs,也可以bfs

但是爷都不会

DFS:

有一说一这挺好想的

class Solution {
    //思路:dfs,碰到一个岛屿,就把这个岛屿所有的陆地全变成'0',
    public int numIslands(char[][] grid) {
            int res=0;
        for(int i=0;i<grid.length;i++)
        {
            for(int j=0;j<grid[0].length;j++)
            {
                if(grid[i][j]=='1')
                {
                    res++;
                    dfs(grid,i,j);
                }
            }
        }
        return res;
    }

    public void dfs(char[][]grid,int i,int j)
    {
        if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]=='0')
        {
            return;
        }
        grid[i][j]='0';
        dfs(grid,i-1,j);
        dfs(grid,i+1,j);
        dfs(grid,i,j+1);
        dfs(grid,i,j-1);
    }
}

但是bfs如何写呢,众所周知树的层序遍历也就是bfs

尝试写一写

注意:至于加入过队列就表示被访问了

class Solution {
    boolean[][]visited;
    int[][] move = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};//为了遍历的时候上下左右移动
    public int numIslands(char[][] grid) {
        int res=0;
        visited = new boolean[grid.length][grid[0].length];
        for(int i=0;i<grid.length;i++)
        {
            for(int j=0;j<grid[0].length;j++)
            {
                if(!visited[i][j]&&grid[i][j]=='1')
                {
                    bfs(grid,i ,j);
                    res++;
                }
            }
        }
        return res;
    }

    public void bfs(char[][]grid,int y,int x)
    {
        //dfs因为是递归,所以需要把判断条件先写,但是bfs不是,所以不需要
        Deque<int[]>queue=new ArrayDeque<>();
        queue.offer(new int[]{y,x});//存储下标
        visited[y][x]=true;
        while(!queue.isEmpty())
        {
            int[]cur=queue.poll();
            int m=cur[0];
            int n=cur[1];
            //遍历他的上下左右
            for(int i=0;i<4;i++)
            {
                int nexty=m+move[i][0];
                int nextx=n+move[i][1];
                if(nextx < 0 || nexty == grid.length || nexty < 0 || nextx == grid[0].length) continue;
                if(!visited[nexty][nextx] && grid[nexty][nextx] == '1') {
                    queue.offer(new int[]{nexty, nextx}); 
                    visited[nexty][nextx] = true; //只要加入队列就标记为访问
                }
            }
        }
    }
}

994. 腐烂的橘子 - 力扣(LeetCode)

注意点,在普通的bfs基础上又增加了记录好橘子个数

class Solution {
    int cnt;//新鲜橘子的数量,用来最后判断能不能全部腐烂
    public int orangesRotting(int[][] grid) {
        Queue<int[]>queue=new LinkedList<>();
        //先找到所有腐烂的橘子,全部加入
        for(int i=0;i<grid.length;i++)
        {
            for(int j=0;j<grid[0].length;j++)
            {
                if(grid[i][j]==2)
                {
                    queue.add(new int[]{i,j});
                }else if(grid[i][j]==1)
                {
                    cnt++;
                }
            }
        }
        int round=0;//表示分钟数
        while(cnt>0&&!queue.isEmpty())
        {
            round++;
            int n=queue.size();//相当于层序遍历这一层的节点
            for(int i=0;i<n;i++)
            {
                int[]orange=queue.poll();
                int r=orange[0];//行
                int c=orange[1];//列
                //腐烂他的上下左右
                if(r-1>=0&&grid[r-1][c]==1)
                {
                    grid[r-1][c]=2;
                    cnt--;
                    queue.add(new int[]{r-1,c});
                }
                if(r+1<grid.length&&grid[r+1][c]==1)
                {
                    grid[r+1][c]=2;
                    cnt--;
                    queue.add(new int[]{r+1,c});
                }
                if(c-1>=0&&grid[r][c-1]==1)
                {
                    grid[r][c-1]=2;
                    cnt--;
                    queue.add(new int[]{r,c-1});
                }
                if(c+1<grid[0].length&&grid[r][c+1]==1)
                {
                    grid[r][c+1]=2;
                    cnt--;
                    queue.add(new int[]{r,c+1});
                }
            }
        }
        if(cnt>0)
        {
            return -1;//还有没腐烂的,说明不能全烂掉
        }else
        {
            return round;//返回轮数
        }
    }
}

总的来说bfs不是很难,刷两道就会了,上下左右都是套路

207. 课程表 - 力扣(LeetCode)

经典的拓扑排序问题,这题用bfs更好理解

class Solution {
    //思路:这是进店拓扑排序问题,我们只需要判断有没有环即可
    //如何判断有没有环?dfs,然后看有没有遍历到之前遍历过的节点
    //如何dfs呢,也就是说如何从给的prerequisites,得到一个图,这是难点
    //所以我们考虑bfs的思想,每次只能选入度为0的课,然后让相关课的入度-1,以此类推
    //如果最后还有入度不为0的,那么说明失败
    //所以我们需要两个数据结构:入度值和玲姐表
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        Map<Integer,Integer>inDegree=new HashMap<>();
        for(int i=0;i<num;i++)
        {
            inDegree.put(i,0);//先吧所有的课程加入
        }

        Map<Integer,List<Integer>> adj=new HashMap<>();//玲姐表,表示x指向哪几个元素

        //初始化入度
        for(int[]relate:prerequisites)
        {
            int cur=relate[1];//当前节点
            int next=relate[0];//指向的节点

            //更新入度
            inDegree.put(next,inDegree.get(next)+1);

            //更新玲姐表
            if(!adj.containsKey(cur))
            {
                adj.put(cur,new ArrayList<>());
            }
            adj.get(cur).add(next);
        }
        //队列,用于bfs
        Queue<Integer>q=new LinkedList<>();
        //先把为0的加入
        for(int key:inDegree.keySet())
        {
            if(inDegree.get(key)==0)
            {
                q.offer(key);
            }
        }

        while(!q.isEmpty())
        {
            int cur=q.poll();//取出
            if(!adj.containsKey(cur))
            {
                continue;
            }
            List<Integer> list=adj.get(cur);//获取玲姐表
            for(int k:list)//把入度-1
            {
                inDegree.put(k,inDegree.get(k)-1);
                if(inDegree.get(k)==0)//别忘了在这里把为0的放进去
                {
                    q.offer(k);
                }
            }
        }
        //检查是否还有入度不为0的
        for(int key:inDegree.keySet())
        {
            if(inDegree.get(key)!=0)
            {
                return false;
            }
        }
        return true;
    }
}

208. 实现 Trie (前缀树) - 力扣(LeetCode)

有一说一这题目一直没咋弄明白,虽然最后实现很简单

宗旨:递归+用26个空格表示26个字母,isEnd表示该单词是否结束

新建searchprefix函数寻找相同前缀的最后一个节点

class Trie {
    private Trie[] children;//孩子节点,我们用26个位置对应26个字母
    private boolean isEnd;//是否结束了

    public Trie() {
        children=new Trie[26];//26个字母
        isEnd=false;
    }
    
    public void insert(String word) {
        Trie node=this;//标记走到的节点
        for(int i=0;i<word.length();i++)
        {
            char s=word.charAt(i);
            if(node.children[s-'a']==null)
            {
                node.children[s-'a']=new Trie();
            }
            node=node.children[s-'a'];
        }
        node.isEnd=true;
    }
    
    public boolean search(String word) {
        Trie node=searchprefix(word);//找到对应的节点
        return node!=null&&node.isEnd;//判断是不是end,才能知道只是前缀相同,还是是同一个单词
    }
    
    public boolean startsWith(String prefix) {
        return searchprefix(prefix)!=null;
    }

    public Trie searchprefix(String prefix)//寻找作为前缀的最后一个节点
    {
                //和insert一样啊,就是找到为Null为止
        Trie node=this;
        for(int i=0;i<prefix.length();i++)
        {
            char ch=prefix.charAt(i);
            int idx=ch-'a';
            if(node.children[idx]==null)
            {
                return null;//没找到
            }
            node=node.children[idx];
        }
        return node;
    }
}

/**
 * Your Trie object will be instantiated and called as such:
 * Trie obj = new Trie();
 * obj.insert(word);
 * boolean param_2 = obj.search(word);
 * boolean param_3 = obj.startsWith(prefix);
 */

回溯

回溯是我的天敌,爷不会,操

让我们默念卡尔五部曲

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

46. 全排列 - 力扣(LeetCode)

我一开始怎么写的:

class Solution {
    //排列问题,也就是都可以从头开始
     List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
    LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
    public List<List<Integer>> permute(int[] nums) {
        if(nums.length==0)
        return result;
        bactracking(nums);
        return result;
    }
     void bactracking(int []nums)
     {
         if(path.size()==nums.length)
         {  
             result.add(path);
             return ;
         }
         for(int i=0;i<nums.length;i++)
         {
             if(path.contains(nums[i]))//判断是否加入过了
             {
                 continue;
             }
             path.add(nums[i]);
             bactracking(nums);
             path.removeLast();
         }
     }
}

我错哪了呢?

在你的代码中,错误在于你将 path 直接添加到 result,而在回溯的过程中,path 是会发生变化的,因此实际上你添加到 result 的是 path 的引用,而不是 path 的一个快照。这导致最终 result 中的所有列表都是相同的。

为了解决这个问题,你应该保存 path 的一个拷贝,而不是直接将它添加到 result。这可以通过创建一个新的 LinkedList 对象来实现。修改 result.add(path); 这行代码如下:

result.add(new LinkedList<>(path));

正确的,所以我的思路没错,代码随想录还用了used数组来判断是否用过

我还以为是我也得那也判断,原来是因为addpath的时候错了

正解:

class Solution {
    List<List<Integer>> result = new ArrayList<>(); // 存放符合条件结果的集合
    LinkedList<Integer> path = new LinkedList<>(); // 用来存放符合条件结果

    public List<List<Integer>> permute(int[] nums) {
        if (nums.length == 0)
            return result;
        backtrack(nums);
        return result;
    }

    void backtrack(int[] nums) {
        if (path.size() == nums.length) {
            result.add(new LinkedList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (path.contains(nums[i])) {
                continue;
            }
            path.add(nums[i]);
            backtrack(nums);
            path.removeLast();
        }
    }
}

78. 子集 - 力扣(LeetCode)

注意starindex的取用,以及什么时候add path

class Solution {
     List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
    LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
    public List<List<Integer>> subsets(int[] nums) {
        if(nums.length==0)
            return result;
        back(nums,0);
        return result;
    }
    void back(int[]nums,int staridx)
    {
        result.add(new ArrayList<>(path));
        if(staridx>=nums.length)
        {
            return;
        }
        for(int i=staridx;i<nums.length;i++)
        {
            path.add(nums[i]);
            back(nums,i+1);//这里不能再是staridx了,因为staridx就是本次遍历的
            path.removeLast();
        }
    }
}

17. 电话号码的字母组合 - 力扣(LeetCode)

class Solution {
     List<String> list = new ArrayList<>();
     StringBuilder temp = new StringBuilder();
    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) {
            return list;
        }
         String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
         back(digits,numString,0);
         return list;
    }
    void back(String digits,String[]numString,int num)
    {
        if(num==digits.length())
        {
            list.add(temp.toString());
            return;
        }
        String str=numString[digits.charAt(num)-'0'];//获取对应的字符串
        for(int i=0;i<str.length();i++)
        {
            temp.append(str.charAt(i));
            back(digits,numString,num+1);//取遍历下一个数字
            temp.deleteCharAt(temp.length()-1);//删除刚刚加进去的,然后加后面一个继续,比如刚刚加了b,现在加c
        }
    }
}

我一直没高清一个东西,就是数字对应的字符,abc,如何遍历到a,b,c呢、

就是说如何取到ad,bd,ae,be,af,bf等等

其实这就是最后回溯取到的,我上一轮取到a了,那么,回溯了把a删了,然后i++,取到b,以此类推

这下总算是弄清了

39. 组合总和 - 力扣(LeetCode)

我的想法:不用sum,而是在递归的时候用target-candidates[i]

但是我的代码有一个错误:

class Solution {
             List<List<Integer>> res = new ArrayList<>();
             List<Integer>path=new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
             Arrays.sort(candidates);
            back(candidates,target,0);
            return res;
    }

    void back(int[]candidates,int target,int starindex)
    {
        if(starindex>=candidates.length|| candidates[starindex]==target)
        {
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=starindex;i<candidates.length;i++)
        {
            if(candidates[i]>target)break;
            path.add(candidates[i]);
            back(candidates,target-candidates[i],i+1);
            path.remove(path.size()-1);
        }
    }
}
if(starindex>=candidates.length|| candidates[starindex]==target)

这里错了啊,你递归的时候减去candidates[i],判断条件不应该是if (starindex >= candidates.length || target == 0)

如果这样写candidates[starindex]==target,那么只会取到candidates[i]这一个数等于target的,其他的情况就全没了

back(candidates,target-candidates[i],i+1);

这里也错了,因为你是可以用相同元素的,所以应该是i,而不是i+1

修改之后:

class Solution {
             List<List<Integer>> res = new ArrayList<>();
             List<Integer>path=new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
             Arrays.sort(candidates);
            back(candidates,target,0);
            return res;
    }

    void back(int[]candidates,int target,int starindex)
    {
        if(starindex>=candidates.length||target==0)//你递归的时候减去candidates[i],判断条件应该是target==0
        {
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=starindex;i<candidates.length;i++)
        {
            if(candidates[i]>target)break;
            path.add(candidates[i]);
            back(candidates,target-candidates[i],i);//可以使用相同元素,所以应该是i,而不是i+1
            path.remove(path.size()-1);
        }
    }
}

22. 括号生成 - 力扣(LeetCode)

有一说一这题之前没写过

思路(核心:1. 能生成左括号:左括号剩余数量大于0 2. 能生成右括号:右括号剩余数量大于左括号剩余数量

什么时候返回:当左右括号剩余数量都等于0

class Solution {
    List<String> res=new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        if(n==0)
            return res;
        dfs("",n,n,res);
        return res;
    }

    void dfs(String cur,int left,int right,List<String>res)
    {
        if(left==0&&right==0)
        {
            res.add(cur);
            return;
        }
        if(left>right)
            return;
        
         if (left > 0) {
            dfs(cur + "(", left - 1, right, res);
        }
        if (right > 0) {
            dfs(cur + ")", left, right - 1, res);
        }
    }
}

为什么代码中没有回溯的部分?因为字符串每次变化都是新生成一个对象,所以不用回溯

79. 单词搜索 - 力扣(LeetCode)

有一说一这题放图论里更好,跟回溯除了dfs有回溯,其他没什么关系

class Solution {
    public boolean exist(char[][] board, String word) {
        int h = board.length, w = board[0].length;
        boolean[][] visited = new boolean[h][w];
        for(int i=0;i<h;i++)
        {
            for(int j=0;j<w;j++)
            {
                boolean flag=check(board,visited,i,j,word,0);
                if(flag)
                {
                    return true;
                }
            }
        }
        return false;
    }
    public boolean check(char[][]board,boolean[][]visited,int i ,int j,String s,int k)
    {
        if(board[i][j]!=s.charAt(k))
        {
            return false;
        }else if(k==s.length()-1)//长度一样了
        {
            return true;
        }
        visited[i][j]=true;
        boolean result=false;
        int[][]dirs={{0,1},{0,-1},{1,0},{-1,0}};
        for(int[]dir:dirs)
        {
            int newi=i+dir[0],newj=j+dir[1];
            if(newi>=0&&newi<board.length&&newj>=0&&newj<board[0].length)
            {   
                if(!visited[newi][newj])
                {
                boolean flag=check(board,visited,newi,newj,s,k+1);//递归查询他周边的
                if(flag)
                {
                    result=true;//说明这条路径可以找到
                    break;//不用再找了
                }
                }
            }
        }
        visited[i][j]=false;//回溯
        return result;
    }
    
}

131. 分割回文串 - 力扣(LeetCode)

我的问题在于,如何取到”aa”这种

答案:设置starindex,作为每次分割后,剩余部分,那么也就是从剩余部分继续做回溯

有一说一理解了以后简单啊

class Solution {
    List<List<String>> lists = new ArrayList<>();
    Deque<String> deque = new LinkedList<>();
    public List<List<String>> partition(String s) {
        back(s,0);
        return lists;
    }

    void back(String s,int startIndex)
    {
        if(startIndex>=s.length())
        {
            lists.add(new ArrayList<>(deque));
            return;
        }
        for(int i=startIndex;i<s.length();i++)
        {
            if(isPalindrome(s,startIndex,i))//如果是回文
            {
                deque.addLast(s.substring(startIndex,i+1));
            }else{
                continue;//不是就继续往后找
            }
            back(s,i+1);//不能重复,所以i+1
            deque.removeLast();
        }
    }



     private boolean isPalindrome(String s, int startIndex, int end) {
        for (int i = startIndex, j = end; i < j; i++, j--) {
            if (s.charAt(i) != s.charAt(j)) {
                return false;
            }
        }
        return true;
    }
}

51. N 皇后 - 力扣(LeetCode)

大名鼎鼎的n皇后问题,奶奶的

此问题难点是,之前的回溯问题全部都是一维数组,而此问题变成了二维

代码随想录的思路:第一次第一行,第二次第二行这样,一行一行取

然后行内的取,由for循环实现

这样一想其实和电话号码的字母组合逻辑是一样的

其实这题的难点在于

1.把他想成是一维的

2.怎么实现”放置皇后”,”棋盘”等抽象的动作

class Solution {
    List<List<String>>res=new ArrayList<>();
    public List<List<String>> solveNQueens(int n) {
        char[][]chessboard=new char[n][n];
        for(char[]c :chessboard)
        {
            Arrays.fill(c,'.');//初始化,一个皇后都没有
        }
        back(n,0,chessboard);
        return res;
    }

   public void back(int n,int row,char[][]chessboard )
    {
        if(row==n)//已经遍历到第n行了
        {
            res.add(Array2List(chessboard));
            return ;
        }
        //行内遍历
        for(int i=0;i<n;i++)
        {
            if(isValid(row,i,n,chessboard))
            {
                chessboard[row][i]='Q';//放皇后
                back(n,row+1,chessboard);//选下一行的
                chessboard[row][i]='.';//回溯
            }
        }
    }

    //把二维数组chessboard转换为
   public List<String> Array2List(char[][]chessboard)
    {
        ArrayList<String> list=new ArrayList<>();
        for(char[] c:chessboard)
        {
            list.add(String.copyValueOf(c));
        }
        return list;
    }


    public boolean isValid(int row,int col,int n,char[][]chessboard)
    {
        //检查列
        for(int i=0;i<row;i++)
        {
            if(chessboard[i][col]=='Q')
            {
                return false;
            }
        }
        //检查左上角
        for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--)
        {
            if(chessboard[i][j]=='Q')
            {
                return false;
            }
        }
        //检查右上角
        for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++)
        {
            if(chessboard[i][j]=='Q')
            {
                return false;
            }
        }
        return true;
    }
}

二分

众所周知,二分的边界问题非常重要,所以二刷,我得着重理解边界问题

最终决定出我的二分查找的边界,是闭是开,然后都这么写

35. 搜索插入位置 - 力扣(LeetCode)

这就是最基本的二分查找,直接写就行了

这边取[],左闭右闭区间

记住原则:就是你要保证能取到所有的数,不能有区间没取到

也不能重复边界

public int searchInsert(int[] nums, int target) {
        int left=0,right=nums.length-1;
        int ans=nums.length;//记录位置
        while(left<=right)
        {
            int mid=(left+right)/2;
            if(nums[mid]>=target)
            {
                ans=mid;
                right=mid-1;//因为你是闭区间,所以要-1
            }else
            {
                left=mid+1;
            }
        }
        return ans;
}

74. 搜索二维矩阵 - 力扣(LeetCode)

方法一:每一行去二分

方法二:观察

class Solution {
    //直接从右上角开始
    //因为行递增,列递增
    //所以如果ij<target,那么ij,左边的全小于target,下面的全小于target
    public boolean searchMatrix(int[][] matrix, int target) {
        int i=0,j=matrix[0].length-1;
        while(i<matrix.length&&j>=0)
        {
            if(matrix[i][j]==target)
            {
                return true;
            }else if(matrix[i][j]>target)
            {
                j--;
            }else if(matrix[i][j]<target)
            {
                i++;
            }
        }
        return false;
    }
}

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

我的思路:先判断有没有这个数,如果没有那就返回-1,-1

如果有,那就二分寻找左右边界

如何寻找左右边界?找到左边界的左边,右边界的右边,然后返回+1,-1即可

class Solution {
    //其实就是二分找左边界问题
    //那么我们这边都用闭区间看看
    public int[] searchRange(int[] nums, int target) {
        boolean has=false;
        for(int i=0;i<nums.length;i++)
        {
            if(nums[i]==target)
            {
                has=true;
            }
        }
        if(has==false)
        {
            return new int[]{-1,-1};
        }
        int l=getLeft(nums,target);
        int r=getRight(nums,target);
        return new int[]{l,r};
    }

    public int getLeft(int []nums,int target)
    {
        int left=0;
        int right=nums.length-1;
        while(left<=right)//也是因为是闭区间,所以有=号
        {
            int mid=(left+right)/2;
            if(nums[mid]>=target)//因为要寻找边界,所以>=的时候要更新right
            {
                right=mid-1;//闭区间,-1
            }else
            {
                left=mid+1;
            }
        }
        return right+1;
    }
    public int getRight(int[]nums,int target)
    {
        int left=0,right=nums.length-1;
        while(left<=right)
        {
            int mid=(left+right)/2;
            if(nums[mid]<=target)
            {
                left=mid+1;
            }else
            {
                right=mid-1;
            }
        }
        return left-1;
    }
}

但是有没有优化的思路呢,能不能找边界的同时就把有没有这个元素找出来,或者别的方法呢

看看他人的解答

额其实是有的,但是原理是一样的,只不过他设置了变量,然后在找边界的过程中把他赋值

class Solution {
    int[] searchRange(int[] nums, int target) {
        int leftBorder = getLeftBorder(nums, target);
        int rightBorder = getRightBorder(nums, target);
        // 情况一
        if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1};
        // 情况三
        if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1};
        // 情况二
        return new int[]{-1, -1};
    }

    int getRightBorder(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle - 1;
            } else { // 寻找右边界,nums[middle] == target的时候更新left
                left = middle + 1;
                rightBorder = left;
            }
        }
        return rightBorder;
    }

    int getLeftBorder(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
                right = middle - 1;
                leftBorder = right;
            } else {
                left = middle + 1;
            }
        }
        return leftBorder;
    }
}

感觉没我的好懂(

33. 搜索旋转排序数组 - 力扣(LeetCode)

首先我们观察旋转了的数组的特点,我们会发现

旋转之后,旋转点右侧的数组,小于旋转点之前的那个数

旋转点左侧的数组,大于等于之前那个数

要求logn,那么肯定是二分

我的选择是先找到下标,然后对左右进行二分

class Solution {
    public int search(int[] nums, int target) {
       int div= findMin(nums);//找到分割点
        int a=bs(nums,0,div,target);
        int b=bs(nums,div-1,nums.length-1,target);
        return Math.max(a,b);
    }
    public int findMin(int[] nums) {
        int left=0,right=nums.length-1;
        while(left<=right)
        {
            int mid=(left+right)/2;
            if(nums[mid]>=nums[0])
            {
                left=mid+1;
            }else
            {
                right=mid-1;
            }
        }
        return left<nums.length?left:0;
    }

    int bs(int[]nums,int left,int right,int target)
    {   
        int ans=-1;
        int mid=(left+right)/2;
        while(left<=right)
        {
            if(nums[mid]==target)
            {
                return mid;
            }else if(nums[mid]>target)
            {
                right=mid-1;
            }else
            {
                left=mid+1;
            }
        }
        return ans;
    }
}

很不幸,超时了

方法二:

每次做到这题看题解吧

class Solution {
    public int search(int[] nums, int target) {
        int left=0,right=nums.length-1;
        while(left<=right)
        {   
            int mid=(left+right)/2;
            if(nums[mid]==target)
            {
                return mid;
            }
            if(nums[mid]>=nums[0])//说明在nums[mid]第一段升序里面
            {
                if(nums[0]<=target&&target<nums[mid])//target在0和mid之间,也就是说target也在这个升序里
                {
                    right=mid-1;//更新右边,因为你是在第一个升序里面
                }else
                {
                    left=mid+1;//target不在0和mid之间,但是有1.可能在第一个升序,但是在mid的右边 2.在第二个升序里,此时无论什么情况,都是更新left
                }
            }else//mid在第二段升序里面
            {
                if(nums[nums.length-1]>=target&&target>nums[mid])//target在mid和nums.length之间,也就是说target也在这个升序里
                {
                    left=mid+1;//更新左边界
                }else
                {
                    right=mid-1;//同理
                }
            }
        }
        return -1;
    }
}

153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

这题目,观察发现,

旋转后的数组,前面的部分,大于等于nums[0],后面的部分小于nums[0]

所以我们二分,然后和nums[0]比较,如果大于等于nums[0]那说明mid在第一段里面

而最小值肯定是第二段的开头,那么 这个时候就更新left

如果小于nums[0] ,那么说明mid在第二段里,更新right

public int findMin(int[] nums) {
    int left=0,right=nums.length-1;
    while(left<=right)
    {
        int mid=(left+right)/2;
        if(nums[mid]>=nums[0])
        {
            left=mid+1;
        }else
        {
            right=mid-1;
        }
    }
    return left<nums.length?nums[left]:nums[0];
}

为什么最后是:

return left<nums.length?nums[left]:nums[0];

因为当这个数组鸭没旋转的时候,left最后会走到数组外面

4. 寻找两个正序数组的中位数 - 力扣(LeetCode)

虽然是烧鸡100二刷,但是这一题其实是一刷,草!

这一题的核心问题:找中位数如何与二分查找联系

思路:其实跟二分没勾八关系、

思路:要找中位数,也就是找第:

​ 对于奇数:(l+r)/2向下取整个

​ 对于偶数那就是要求两个

我们每次循环去掉k/2个元素,然后递归

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int n=nums1.length;
        int m=nums2.length;
         //因为数组是从索引0开始的,因此我们在这里必须+1,即索引(k+1)的数,才是第k个数。
        int left=(n+m+1)/2;
        int right=(n+m+2)/2;
        //将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k
        return (getK(nums1, 0, n - 1, nums2, 0, m - 1, left) + getK(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;
    }

    private int getK(int []nums1,int start1,int end1,int []nums2,int start2,int end2,int k)//k代表个数
    {
        int len1=end1-start1+1;
        int len2=end2-start2+1;
        //让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1 
        if (len1 > len2) return getK(nums2, start2, end2, nums1, start1, end1, k);
        if(len1==0)return nums2[start2+k-1];//返回第k小的
        
        if(k==1)return Math.min(nums1[start1],nums2[start2]);

        int i=start1+Math.min(len1,k/2)-1;//这一步是为了保证数组如果剩余长度小于k/2,那么就会移动到末尾,-1是因为k表示个数,所以作为索引计算,-1
        int j=start2+Math.min(len2,k/2)-1;

        if(nums1[i]>nums2[j])
        {
            //nums1大,那么就把nums2的前面舍去
            return getK(nums1,start1,end1,nums2,j+1,end2,k-(j-start2+1));
        }else
        {
            return getK(nums1,i+1,end1,nums2,start2,end2,k-(i-start1+1));
        }


    }
}

总结:二分部分比一刷有了提升,很不错

20. 有效的括号 - 力扣(LeetCode)

简单

public boolean isValid(String s) {
    int n=s.length();
    if(n%2==1)
        return false;
    Deque<Character>stk=new LinkedList<>();
    Map<Character,Character>mp=new HashMap<>();
    mp.put(')','(');
    mp.put(']','[');
    mp.put('}','{');
    for(int i=0;i<s.length();i++)
    {
        char ch=s.charAt(i);
        if(mp.containsKey(ch))
        {
            if(stk.isEmpty()||stk.peek()!=mp.get(ch))//遇到右括号判断对不对
            {
                return false;
            }
            stk.pop();
        }else
        {
            stk.push(ch);//只存放左括号
        }
    }
    return stk.isEmpty();//最后还有没有多余的
}

155. 最小栈 - 力扣(LeetCode)

这个也很简单,就是用两个栈,一个栈存放原来的,就是普通的栈,另一个栈保存最小值

如何达到保存最小智呢,每次push都和栈头比较呗,小了就push新的,不然还是push栈头

class MinStack {
    Deque<Integer>stk=new LinkedList<>();
    Deque<Integer>min=new LinkedList<>();

    public MinStack() {
        min=new LinkedList<>();
    }
    
    public void push(int val) {
        stk.push(val);
        if(min.isEmpty())
        {
            min.push(val);
        }else
        {
            if(val<min.peek())
            {
                min.push(val);
            }else
            {
                min.push(min.peek());
            }
        }
    }
    
    public void pop() {
        stk.pop();
        min.pop();
    }
    
    public int top() {
        return stk.peek();
    }
    
    public int getMin() {
        return min.peek();
    }
}

394. 字符串解码 - 力扣(LeetCode)

这个题目的核心思想

一句话,就是遇到[入栈,遇到]出栈

出栈的时候,出几次,append几次,要根据前面的数字来

那么我们就需要两个栈,一个记录字符串,一个记录前面的数字

public String decodeString(String s) {
    s=s.trim();//去除前后的空格什么的

    StringBuilder res=new StringBuilder();//暂存字符串

    Stack<Integer>nums=new Stack<>();
    Stack<String>strs=new Stack<>();
    int num=0;//暂存数字,直到数字读取完整
    for(int i=0;i<s.length();i++)
    {
        if(s.charAt(i)>='0'&&s.charAt(i)<='9')
        {
            num=num*10+s.charAt(i)-'0';//更新数字
        }else if(s.charAt(i)>='a'&&s.charAt(i)<='z')
        {
            res.append(s.charAt(i));
        }else if(s.charAt(i)=='[')//遇到[入栈
        {   
            nums.push(num);
            strs.add(res.toString());
            res=new StringBuilder();//从新开始
            num=0;//num一样
        }else{//遇到]出栈
            int times=nums.pop();//出几次
            String tmp=strs.pop();
            for(int j=0;j<times;j++)
            {
                tmp+=res.toString();//把res加到tmp后面times次
            }
            res=new StringBuilder(tmp);//然后更新res
        }
    }
    return res.toString();
}

739. 每日温度 - 力扣(LeetCode)

这一题呢,一眼单调栈,因为是要找第一个比他大的,那么肯定是递减站啊

注意:栈里我们只要存下标就可以了,方便计算是几天后

理解:找第一个比他大的,那么就得是要递减栈,然后遇到比他大的,就可以对栈的数一个一个比较,直到栈内有数比他还大

public int[] dailyTemperatures(int[] temperatures) {
    int len=temperatures.length;
    int []ans=new int[len];
    Deque<Integer>stk=new LinkedList<>();
    for(int i=0;i<len;i++)
    {
        int t=temperatures[i];
        while(!stk.isEmpty()&&t>temperatures[stk.peek()])//只要栈不为空,并且这个位置的温度比栈头表示的温度大,那么这个温度就是第一个比她大的温度
        {
            int index=stk.pop();//栈头温度的下标,并且把栈头温度pop
            ans[index]=i-index;//几天之后

        }
        stk.push(i);//比他小的全出去了,他又成当前最小的了
    }
    return ans;
}

84. 柱状图中最大的矩形 - 力扣(LeetCode)

虽然是烧鸡100二刷,但是这题是一刷

思考:这个题目和”盛水最多的容器”有异曲同工之妙,但是问题是

此处的高,是在left和right之间的最小值,这也是它和栈联系起来的地方

但是问题是,left可以动,right可以动, 那么这个如何pop呢

然而我发现我的思路是错的,正确答案都是枚举高度,然后看他能取到的最大宽度,呜呜呜

问题转换为当使用某个 height[i]作为矩形高度时,该矩形所能取得的最大宽度为多少

而最大宽度如何去计算?就是height[i]左边第一个比他矮的下标l,右边第一个比他矮的下标r,r-l-1就是宽度

所以我们设置两个数组,分别记录i对应的第一个比他矮的左下标,第一个比他矮的右下标

那么计算这两个数组不就和上一题“每日温度”一样吗

鉴定为易

public int largestRectangleArea(int[] heights) {
    int n=heights.length;
    int []l=new int[n];
    int []r=new int[n];
    Arrays.fill(l,-1);
    Arrays.fill(r,n);//因为下标有0,所以得先设置为-1
    Deque<Integer>stk=new ArrayDeque<>();

    for(int i=0;i<n;i++)//右边第一个比奇小的位置,所以从左边往右边遍历
    {
        while(!stk.isEmpty()&&heights[i]<heights[stk.peekLast()])//同样的这里记录下表
        {
            r[stk.pollLast()]=i;//把比他大的全pop出去,然后自己的位置,就是第一个比Last小的位置
        }
        stk.addLast(i);//找第一个比奇小,那么就是递增站
    }
    stk.clear();//清空
    for(int i=n-1;i>=0;i--)
    {
        while(!stk.isEmpty()&&heights[i]<heights[stk.peekLast()])
        {
            l[stk.pollLast()]=i;
        }
        stk.addLast(i);
    }
    //遍历,找最大的
    int ans=0;
    for(int i=0;i<n;i++)
    {
        int t=heights[i],a=l[i],b=r[i];
        ans=Math.max(ans,(b-a-1)*t);
    }
    return ans;
}

总结:通过上面两题,我们总结出了,找第一个比他小的,那就是递增栈(把比他大的全poo了),找第一个比他大的,那就是递减栈(比它 小的全pop了)

这里的注意点,就是我们要回熟练的写compare

215. 数组中的第K个最大元素 - 力扣(LeetCode)

思路很简单,我设置应该堆,先全进去,然后不断pop,第k个出来的就是第k大的,注意没有第0大的这说法,所以最后判断条件是k>1

当然这个是要最大堆,所以你的compare得重写

public int compare(Integer a, Integer b) {
	    //   以下是对比较器升序、降序的理解.
		//	 (1) 写成return a.compareTo(b) 或者 return a-b表示升序(最小堆)
		//	 (2) 写成return b.compareTo(a) 或者 return b-a表示降序(最大堆)				
			return b.compareTo(a);
		}

代码:

public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> pq=new PriorityQueue<>(new Comparator<Integer>(){
            public int compare(Integer a ,Integer b )
            {
                return b-a;
            }
        });
        for(int num:nums)
        {
            pq.add(num);
        }
        while(k>1)
        {
            pq.remove();
            k--;
        }

        return pq.peek();
}

但是这样傻逼面试官会让你自己实现堆,我认为这是司马行为, 所以如果考到只能用另一种方法

快速排序:

第k大,就是排序完后下标为N-K的元素

每次快速排序后,可以确定这个pivot的最后位置,第一大元素在N-1下标,以此类推

所以思路是:

如果pivot最后<N-K,那么在pivotindex+1~right里继续找

如果>则在left~pivot-1里找

注意这里的pivot是随机找,然后找到了还得先和left交换,最后还得还回去,我曹

class Solution {
    Random random=new Random(System.currentTimeMillis());
    //如果Pivotindex<N-K,那么到pivotindex+1~right里找
    public int findKthLargest(int[] nums, int k) {
        int len=nums.length;
        int target=len-k;
        int left=0,right=len-1;
        while(true)
        {
            int pivotIndex=partition(nums, left, right);
            if(pivotIndex<target)
            {
                left=pivotIndex+1;
            }else if(pivotIndex>target)
            {
                right=pivotIndex-1;
            }else
            {
                return nums[pivotIndex];
            }
        }
    }


    public int partition(int []nums,int left,int right)
    {
        int randomIndex=left+random.nextInt(right-left+1);//随机选一个档pivot
        //swappivot和left,把他弄到左边去
        swap(nums, randomIndex,left);
        int pivot=nums[left];
        int le=left+1;
        int ge=right;
        while(le<=ge)
        {
            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);//放回去,此时le=ge,也就是你要放的位置了
        return ge;
    }










    public void swap(int[]nums,int index1,int index2)
    {
        int t=nums[index1];
        nums[index1]=nums[index2];
        nums[index2]=t;
    }
}

347. 前 K 个高频元素 - 力扣(LeetCode)

思路:设置map,先统计每个元素出现的次数,然后重写compare方法,加进大小为k的小顶堆当中

当堆满后,新元素和堆头元素比,如果比他大,那么就把新元素加进去,堆头扔掉

全部结束后,队里剩下的就是最大的k个

:注意:如果不设置大小为k,然后用大顶堆,最后push k 次大顶堆的堆头,这样复杂度不会优于ologn

public int[] topKFrequent(int[] nums, int k) {
    Map<Integer,Integer>mp=new HashMap<>();
    for(int num:nums)
    {
        mp.put(num, mp.getOrDefault(num, 0)+1);
    }
    PriorityQueue<Integer>pq=new PriorityQueue<>(new Comparator<Integer>() {
        public int compare(Integer a,Integer b)
        {
            return mp.get(a)-mp.get(b);
        }
    });//小顶堆
    for(int key:mp.keySet())
    {
        if(pq.size()<k)
        {
            pq.add(key);
        }else if(mp.get(key)>mp.get(pq.peek()))//频率比堆顶大,才加入
        {
            pq.remove();
            pq.add(key);
        }
    }
    int []res=new int[k];
    for(int i=0;i<k;i++)
    {
        res[i]=pq.poll();
    }
    return res;
}

二刷一遍过,鉴定为掌握了

295. 数据流的中位数 - 力扣(LeetCode)

思路:求中位数,那就是说要获得最中间的两个数的和/2,如何较快的获得中间两个数呢?

建立两个堆,一个大,一个小,那么大顶堆的顶,就是前一半的最大值,小顶堆的顶就是后一半的最小值

问题是如何处理奇数的情况呢?

如果是偶数,那么两个堆大小相等,对于奇数的情况,我们人为控制较小的一半size不小于较大的一半,这样奇数情况,直接返回大堆的size即可

add的时候如何知道加入哪一个堆呢?

我们的解决方案是,先进较小的一半(大堆),然后把大堆里最大的add到最大的一半里

但是这样会导致较小的一半永远只有一个元素了

我们人为控制较小的一半size不小于较大的一半,这样奇数情况,直接返回大堆的size即可

class MedianFinder {
    //较小的一半,大堆
    PriorityQueue<Integer> minhalf=new PriorityQueue<>(new Comparator<Integer>(){
        public int compare(Integer a,Integer b)
        {
            return b-a;
        }
    });
    //较大的一半,小堆
    PriorityQueue<Integer>maxhalf=new PriorityQueue<>();
    public MedianFinder() {

    }
    
    public void addNum(int num) {
        minhalf.add(num);
        maxhalf.add(minhalf.poll());
        if(minhalf.size()<maxhalf.size())
        {
            minhalf.add(maxhalf.poll());
        }
    }
    
    public double findMedian() {
        if(minhalf.size()==maxhalf.size())
        {
            return (double)(minhalf.peek()+maxhalf.peek())/2;
        }
        else
        {
            return (double)minhalf.peek();
        }
    }
}

贪心

121. 买卖股票的最佳时机 - 力扣(LeetCode)

思路:设置res[i]表示第i天卖出股票能取得的最大收入

最大收入肯定等于:当前股票的价格-这一天之前股票的最低价

所以我们维护一个i,维护一个最低价,然后再维护一个最高利润即可

public int maxProfit(int[] prices) {
    int []res=new int[prices.length];
    int min=Integer.MAX_VALUE;
    int max=Integer.MIN_VALUE;
    for(int i=0;i<prices.length;i++)
    {
        res[i]=prices[i]-min>0?prices[i]-min:0;//先更新res[i],保证不包含当前的
        min=Math.min(min,prices[i]);
        max=Math.max(max,res[i]);
    }
    return max;
}

55. 跳跃游戏 - 力扣(LeetCode)

思路:维护跳跃的最远位置

我卡在哪了?

我认为只能知道第一轮跳跃的最远位置manlen,然后进行for(int i=0;i<maxlen)循环,更新了maxlen也没用啊

正解:还是对整个进行遍历,i如果超过了maxlen就return false不就行了

public boolean canJump(int[] nums) {
    int maxlen=0;
    for(int i=0;i<nums.length;i++)
    {
        if(i>maxlen)return false;
        maxlen=Math.max(maxlen,i+nums[i]);
    }
    return true;
}

45. 跳跃游戏 II - 力扣(LeetCode)

一开始的思路:就是每次跳到最远

但是发现这样通过不了很多测试用例

后来的思路:还是对整个进行遍历,当maxlen边长了之后才增加steps+1,还是不对,因为你无法保证变长的时候,是变的最长的那个情况

正确的思路:对maxlen中的nums[i]+i进行贪心,也就是说选择当前可以跳到的位置中,他们又能跳到的位置里,最远的

也就是关注下一次跳跃的距离,而不是这一次

public int jump(int[] nums) {
    int end=0;//当前的maxlen
    int maxPos=0;//下一次的maxPos
    int steps=0;
    for(int i=0;i<nums.length-1;i++)
    {
        maxPos=Math.max(maxPos,nums[i]+i);
        if(i==end)
        {
            end=maxPos;//更新边界,也就相当于跳到了那个位置
            steps++;
        }
    }
    return steps;
}

考虑以下情况,如果 i 的值等于 nums.length - 1,那么在下一步的迭代中,我们会更新 maxPos,但由于 i 已经等于 nums.length - 1,我们无需再进行下一步的迭代。因此,为了避免多余的一次迭代,循环条件是 i < nums.length - 1

763. 划分字母区间 - 力扣(LeetCode)

思路:遇到一个字母,就找他的lastindexof,、

然后顺着遍历,如果新的字母的lastindexof超过了当前的lastindex,那就更新边界end

如果遍历到end了,那么当前长度就从0重新开始,并且把长度加入结果list

public List<Integer> partitionLabels(String s) {
    int []last=new int[26];
    int len=s.length();
    for(int i=0;i<s.length();i++)//设置last数组,方便找
    {
        last[s.charAt(i)-'a']=i;
    }
    List<Integer>res=new ArrayList<>();
    int start=0,end=0;
    for(int i=0;i<s.length();i++)
    {
        end=Math.max(end,last[s.charAt(i)-'a']);//更新边界
        if(i==end)
        {
            res.add(end-start+1);
            start=end+1;
        }
    }
    return res;
}

动态规划

动规南中难,还是得多刷,奶奶滴

70. 爬楼梯 - 力扣(LeetCode)

这个就很简单,地推公式就是,走到这一层的方式等于走到上一层和上上一层的方式加一

public int climbStairs(int n) {
    int []dp=new int[n+1];//最后要n层,所以定义n+1
    dp[0]=1;
    dp[1]=1;//初始化两个
    for(int i=2;i<n+1;i++)
    {
        dp[i]=dp[i-1]+dp[i-2];
    }
    return dp[n];
}

198. 打家劫舍 - 力扣(LeetCode)

这个问题的核心在于,每一个房子都有两种状态,就是偷与不偷

所以我们dp[i]表示,走到这个屋子时,手上的最大金额

如果偷,dp[i]就是dp[i-2]+nums[i],不偷那就是dp[i-1]

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

二刷一遍过,无敌

279. 完全平方数 - 力扣(LeetCode)

这个的问题是怎么和动态规划联系在一起

看到输入是int n ,会自然而然想到dp[n+1],但是这个数组代表什么呢

哎,我忘了,这是背包问题!

完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?

和01背包的不同就是,遍历背包是正序遍历

二维:dp[ i] [ j ]表示0-i的物品任意放到容量为j的背包里的最大价值

一维:dp[j ]容量为j的背包所能装的最大价值

一维就是把二维的上一层状态拷贝下来了

所以这里面,容量就是n,物品就是i,物品的价值就是i*i

完全背包的公式

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

这一题

public int numSquares(int n) {
    int max=Integer.MAX_VALUE;
    int[]dp=new int[n+1];
    for(int j =0;j<n+1;j++)
    {
        dp[j]=max;//因为要求最少是几个完全平方的和,所以初始化为最大致
    }
    dp[0]=0;
    for(int i=1;i*i<n+1;i++)
    {
        for(int j=i*i;j<n+1;j++)
        {
            if(dp[j-i*i]!=max)
            {
                dp[j]=Math.min(dp[j],dp[j-i*i]+1);
            }
        }
    }
    return dp[n];
}

322. 零钱兑换 - 力扣(LeetCode)

有了上一题的基础,这一题也是完全背包问题

public int coinChange(int[] coins, int amount) {
    int []dp=new int[amount+1];
    int max=Integer.MAX_VALUE;
    for(int j=0;j<dp.length;j++)
    {
        dp[j]=max;//因为是要找最少的个数,所以初始化成最大值
    }
    dp[0]=0;
    for(int i=0;i<coins.length;i++)
    {
        for(int j=coins[i];j<amount+1;j++)
        {
            if(dp[j-coins[i]]!=max)//这一步是确保上一步是可以被凑出来的,不然你无法地推
            {
                dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
            }
        }
    }
    return dp[amount]==max?-1:dp[amount];
}

139. 单词拆分 - 力扣(LeetCode)

有了上两题的基础,这样又是经典完全背包问题

物品就是单词,容量就是什么String的长度

然后我们还是用一维,dp[j]就是长度为j的字符串能否被拆分

地推公式就是,如果[i,j]这个部分在单词数组里,并且dp[i]=true,那么dp[j]也是true

这一题只能先遍历背包,再遍历物品,为什么?

因为你的String里单词是有顺序的,你要求的并非存不存在,而是包含了顺序的,所以你得先遍历背包,也就是String

public boolean wordBreak(String s, List<String> wordDict) {

    boolean []dp=new boolean[s.length()+1];
    dp[0]=true;
    for(int i=1;i<s.length()+1;i++)//便利背包
    {
        for(int j=0;j<i&&!dp[i];j++)//严格来说这里不能叫便利物品,不过也能叫,就是不停截取,然后判断是不是物品
        {
            if(wordDict.contains(s.substring(j,i))&&dp[j])
            {
                dp[i]=true;
            }
        }
    }
    return dp[s.length()];
}

300. 最长递增子序列 - 力扣(LeetCode)

刚开始的思路:dp[i]为以i结尾的最长递增子序列的长度

递推公式,如果nums[i]比nums[i-1]大,那么等于dp[i-1]+1,否则等于dp[i-1]

然后发现错了

新的思路:

dp[i]为以i结尾的最长递增子序列的长度

递推公式,如果nums[i]比nums[i-1]大,那么等于0-i-1里面dp[j]最大的+1,否则等于1

这个过程可以优化为:

public int lengthOfLIS(int[] nums) {
    int n=nums.length;
    int[]dp=new int[n];
    Arrays.fill(dp,1);//注意此处初始化,每一个初始值应该为1
    for(int i=1;i<n;i++)
    {
        for(int j=0;j<i;j++)
        {
            if(nums[i]>nums[j])
            {
                dp[i]=Math.max(dp[i],dp[j]+1);
            }
        }
    }
    int res=0;
    for(int i=0;i<n;i++)
    {
        res=Math.max(res,dp[i]);
    }
    return res;
}

118. 杨辉三角 - 力扣(LeetCode)

这一题是一刷,南蚌

//思录:按规律来,
public List<List<Integer>> generate(int numRows) {
    List<List<Integer>> res=new ArrayList<>();
    for(int i=0;i<numRows;i++)
    {
        List<Integer>ans=new ArrayList<>();
        for(int j=0;j<=i;j++)
        {
            if(j==0||j==i)
            {
                ans.add(1);
            }else
            {
                ans.add(res.get(i-1).get(j-1)+res.get(i-1).get(j));//相加
            }
        }
        res.add(ans);
    }
    return res;
}

152. 乘积最大子数组 - 力扣(LeetCode)

思路:因为数组里面有正有负,所以我们要维护一个最大正值和最小负值(也不一定是负值,反正是最小值)

dp[i]是什么?以nums[i]为结尾的子数组的最大乘积

递推公式是什么?如果nums[i]大于0,那么就去乘最大正值,如果这个值比dp[i-1]大,那么就更新,否则dp[i]等于他本身

这种思路不对,因为题目只要求返回最大的值就行了

所以我们的dp应该对最大正值和最小负值去进行dp

maxdp[],mindp[],

地推公式,没啥地推的,就是nums[i],maxdp[i-1]** nums[i] ,mindp[i-1]* *nums[i]直接比较就行了

public int maxProduct(int[] nums) {
    int []maxdp=new int[nums.length];
    int[] mindp=new int[nums.length];

    int res=nums[0];
    maxdp[0]=mindp[0]=nums[0];
    for(int i=1;i<nums.length;i++)
    {
        maxdp[i]=Math.max(nums[i],Math.max(maxdp[i-1]*nums[i],mindp[i-1]*nums[i]));
        mindp[i]=Math.min(nums[i],Math.min(maxdp[i-1]*nums[i],mindp[i-1]*nums[i]));
        res=Math.max(res,maxdp[i]);
    }
    return res;
}

416. 分割等和子集 - 力扣(LeetCode)

思路:我上来直接先排序

后来发现错了,因为可以有 1 2 1 2这样的情况,但是你i排序后1 1 2 2这样用排序的方法是没用的!!!!

但是这个问题,细看你就会发现,这不就是背包问题么

总容量就是sum/2,然后物品就是你给的数组,就看看你给的数组里能不能装满这个背包不就行了!然后元素不能重复使用

01背包!

public boolean canPartition(int[] nums) {
    if(nums==null||nums.length==0)return false;
    int n=nums.length;
    int sum=0;
    for(int num:nums)
    {
        sum+=num;
    }
    if(sum%2!=0)return false;
    int target=sum/2;
    int[]dp=new int[target+1];//dp[i]为容量为i的背包最多能装多少容量
    for(int i=0;i<n;i++)
    {
        for(int j=target;j>=nums[i];j--)//因为是01背包,就是不能重复用数字,所以得倒序遍历
        {
            dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
        }
    }
    return dp[target]==target;
}

32. 最长有效括号 - 力扣(LeetCode)

上次通过还是二月五号,奶奶第

思路一:也就是我上次通过的思路,用栈,就是做括号匹配,过程中记录最大匹配长度

那么如何记录呢,栈存放 下标就行了,但是这里的一些细节比较反直觉,不用这种

ps:反直觉个锤子,我曹,简单的一批好吧:angry:

public int longestValidParentheses(String s) {
    if(s.length()==0)return 0;
    Deque<Integer>stk=new LinkedList<>();
    stk.push(-1);//防止一开始就是右括号,没东西可以弹出
    int max=0;
    for(int i=0;i<s.length();i++)
    {
        if(s.charAt(i)=='(')
        {
            stk.push(i);//是左括号,push进区域
        }else
        {
            stk.pop();//是右括号,弹出
            if(stk.isEmpty())
            {
                //如果是空说明没东西可以匹配,这次是不能匹配的
                stk.push(i);//把当前位置入栈,充当之前的-1
            }else
            {
                max=Math.max(max, i-stk.peek());
            }
        }
    }
    return max;
}

思路二:动态规划

public int longestValidParentheses(String s) {
    int n=s.length();
    int []dp=new int[n];
    int max_len=0;
    for(int i=1;i<n;i++)
    {
        if(s.charAt(i)==')')
        {
            if(s.charAt(i-1)=='(')
            {
                if(i<2)
                    dp[i]=2;
                else
                    dp[i]=dp[i-2]+2;
            }
            else
            {
                if(dp[i-1]>0)
                {
                    int pre_left=i-dp[i-1]-1;//上一个括号对应的左括号下标
                    if(pre_left>=0&&s.charAt(pre_left)=='(') //(())这样的
                    {
                        dp[i]=dp[i-1]+2;
                        if(pre_left-1>0)
                        {
                            dp[i]=dp[i]+dp[pre_left-1];//() (())如果他前面还有这种
                        }
                    }
                }
            }
        }
        max_len=Math.max(max_len,dp[i]);
    }
    return max_len;
}

具体思路比较复杂我这就不谢了,到时候自己看题解吧

技巧部分

136. 只出现一次的数字 - 力扣(LeetCode)

异或:同0异1

public int singleNumber(int[] nums) {
    int res=nums[0];
    for(int i=1;i<nums.length;i++)
    {
        res^=nums[i];
    }
    return res;
}

169. 多数元素 - 力扣(LeetCode)

之前没有用时间复杂度On解决问题,这次来解决一下

这里用的是投票算法

选取一个数为候选人,如果遇到和他相同的数,那么count++,否则count–,如果count=0了,那么就切换候选人

很好理解,因为你的多数元素肯定是大于一半的,如果你能等于0 ,那就说明和你不一样的,比和你一样的多,那你就是不是超过半数的元素了

public int majorityElement(int[] nums) {
    int candidate=nums[0],count=1;
    for(int i=1;i<nums.length;i++)
    {
        if(nums[i]==candidate)
        {
            count++;
        }else
        {
            count--;
        }
        if(count==0)
        {
            candidate=nums[i];
            count=1;
        }
    }
    return candidate;
}

75. 颜色分类 - 力扣(LeetCode)

思路:22222->1112222->0011222

public void sortColors(int[] nums) {
    int n0=0,n1=0;
    for(int i=0;i<nums.length;i++)
    {
        int num=nums[i];
        nums[i]=2;//先全部设为2
        if(num<2)//有多少个小于2 的,那就在前面变成多少个1
        {
            nums[n1++]=1;
        }
        if(num<1)//有多少个小于1的,那就在把前面的多少个1变成0
        {
            nums[n0++]=0;
        }
    }
}

思路2:有没有觉得很类似于“一次排序把奇数放到偶数之前”

那个题目原理就是快速排序的一次partition,这个题目也类似

先鸽了

31. 下一个排列 - 力扣(LeetCode)

这题思路很吊,建议看题解

public void nextPermutation(int[] nums) {
    int i=nums.length-2,j=nums.length-1;//从后往前找到第一个领巾的升序对
    while(i>=0)
    {
        if(nums[i]<nums[j])
            break;
        i--;j--;
    }
    if(i<0){//一直都没找到升序对,说明已经是98764321这种了,按题目要求,返回最小的,也就是直接排序了
        Arrays.sort(nums);
        return;
    }
    //找到升序对了,i就是小数,要和后面的大数交换,那么大数就是后面从后向前找,第一个比i大的
    int k=nums.length-1;//表示大数
    for(k=nums.length-1;k>=j;k--)
    {
        if(nums[i]<nums[k])
            break;
    }

    swap(nums,i,k);//交换i,k
    //让后面变成升序,使得最小
    Arrays.sort(nums,j,nums.length);
}

public void swap(int[] nums, int i, int k){
    int tmp = nums[i];
    nums[i] = nums[k];
    nums[k] = tmp;
}

287. 寻找重复数 - 力扣(LeetCode)

思路:将数组看做链表

重复的数字就是环的入口,那么我们用环形链表2的思路就很好解了

public int findDuplicate(int[] nums) {
    int slow=0,fast=0;
    slow=nums[slow];
    fast=nums[nums[fast]];
    while(slow!=fast)
    {
        slow=nums[slow];
        fast=nums[nums[fast]];
    }
    fast=0;
    while(fast!=slow)
    {
        slow=nums[slow];
        fast=nums[fast];
    }
    return fast;
}

多维动态规划

62. 不同路径 - 力扣(LeetCode)

这个就很简单,因为你只能从左边或者上面过来,所以递推公式就是

dpij=dpi-1j+dpij-1

然后初始化,第一行和第一列都只有一种走法

public int uniquePaths(int m, int n) {
    int[][]dp=new int[m][n];
    for(int i=0;i<n;i++)
        dp[0][i]=1;
    for(int j=0;j<m;j++)
        dp[j][0]=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];
}

64. 最小路径和 - 力扣(LeetCode)

这和62异曲同工之妙啊

dpij是到达ij最小的路径和

public int minPathSum(int[][] grid) {
    if(grid==null||grid.length==0||grid[0].length==0)
        return 0;
    int m=grid.length,n=grid[0].length;
    int[][]dp=new int[m][n];
    dp[0][0]=grid[0][0];
    for(int i=1;i<n;i++)
    {
        dp[0][i]=grid[0][i]+dp[0][i-1];
    }
    for(int j=1;j<m;j++)
    {
        dp[j][0]=grid[j][0]+dp[j-1][0];
    }
    for(int i=1;i<m;i++)
    {
        for(int j=1;j<n;j++)
        {
            dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
        }
    }
    return dp[m-1][n-1];

}

5. 最长回文子串 - 力扣(LeetCode)

一开始的思路:设置左右指针,但是不知道什么时候移动他们

正解:左右指针不是在首尾,而是中心扩散,这是方法一

动态规划方法

回文天然具有「状态转移」性质:一个长度严格大于 2的回文去掉头尾字符以后,剩下的部分依然是回文。反之,如果一个字符串头尾两个字符都不相等,那么这个字符串一定不是回文。「动态规划」的方法根据这样的性质得到。

dpij:i到j是不是回文(闭区间,而不是说dpi表示以i结尾的最长回文子串

递推公式也很简单,就是si等不等于sj并且dpi+1j-1也是true即可

初始化:单个字符肯定是回文,所以dpii=true

public String longestPalindrome(String s) {
    //有一说一还是动态规划好理解
    //dp[i][j]:i-j是不是回味淄川
    //递推公式:if(s[i]==s[j],dp[i][j]=dp[i+1][j-1])
    //然后再过程中维护长度和七点
    boolean[][]dp=new boolean[s.length()][s.length()];
    int begin=0;
    int maxlen=1;//至少为1
    for(int i=0;i<dp.length;i++)
    {
        dp[i][i]=true;//单个字符是回文
    }
    for(int j=1;j<s.length();j++)
    {
        for(int i=0;i<j;i++)
        {
            if(s.charAt(i)!=s.charAt(j))
            {
                dp[i][j]=false;
            }else
            {
                if(j-i<3)
                {
                    dp[i][j]=true;//只有两个字母就不能i+1,j-1了
                }
                else
                {
                    dp[i][j]=dp[i+1][j-1];
                }
            }
            if(dp[i][j]&&j-i+1>maxlen)
            {
                maxlen=j-i+1;
                begin=i;
            }
        }
    }
    return s.substring(begin,begin+maxlen);
}

1143. 最长公共子序列 - 力扣(LeetCode)

又是经典问题

注意dpi,j定义为0,i-1 0,j-1的最长公共子序列长度

为什么这么定义呢,为了方便初始化,反正你记住就行了

是为了方便当 i = 0 或者 j = 0 的时候,dp[i][j]表示的为空字符串和另外一个字符串的匹配,这样 dp[i][j] 可以初始化为 0.

如何理解递推公式:

 if(text1.charAt(i-1)==text2.charAt(j-1))
                {
                    dp[i][j]=dp[i-1][j-1]+1;//这个姮好理解
                }else
                {
                    dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);//这个呢
     
                }
比如 abcde ace 
    ab ab 这个时候是1
    那么abc ace maxlen可能是什么呢
    可能是abc 和ac 那就是2
    也有可能是ab 和ace 那就是1
    然后比较发现2更大,所以取更大的
public int longestCommonSubsequence(String text1, String text2) {
    int l1=text1.length();
    int l2=text2.length();
    int[][]dp=new int[l1+1][l2+1];//表示以i-1,j-1为结尾的,他们的公共子序列长度,因为你最后要能取到dpl1l2,表示l1-1,l2-1为结尾的,所以你初始化得到l1+1,l2+1
    for(int i=1;i<=l1;i++)
    {
        for(int j=1;j<=l2;j++)
        {
            if(text1.charAt(i-1)==text2.charAt(j-1))
            {
                dp[i][j]=dp[i-1][j-1]+1;
            }else
            {
                dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);
            }
        }
    }
    return dp[l1][l2];
}

72. 编辑距离 - 力扣(LeetCode)

这个题目的关键在于:

  1. 添加元素和删除元素,他们加的次数都是1,所以没必要区分

    所以这样就变成了,删除和修改两个操作

    删除:要么word1删,要么word2删,这两个得分开来

    修改:随便哪个修改,结果都是一样的,反正都变得一样了

  2. 你无需具体知道每一步到底是用删除还是修改,只需要把所有的情狂都考虑到,然后取最小的情况就行了

    dp[i][j]:还是i-1,j-1的字符串,编辑的最小距离
    初始化:dp[i][0]全是i,因为你是和空字符串比,dp[0][j]也是
    递推公式:
    如果不同
    	当word1删,那就变成了dp[i-1][j],word2删,就是dp[i][j-1]
    	当修改,那就是dp[i-1][j-1]
    	然后取最小的加一
    如果相同
    	那不用操作
public int minDistance(String word1, String word2) {
    int[][]dp=new int[word1.length()+1][word2.length()+1];
    
    for(int i=0;i<=word1.length();i++)//也是小于等于
    {
        dp[i][0]=i;
    }
    for(int j=0;j<=word2.length();j++)
    {
        dp[0][j]=j;
    }

    for(int i=1;i<=word1.length();i++)
    {
        for(int j=1;j<=word2.length();j++)
        {
            if(word1.charAt(i-1)==word2.charAt(j-1))//因为表示的是i-1,j-1的编辑距离,所以要比较i-1和j-1
            {
                dp[i][j]=dp[i-1][j-1];
            }else
            {
                dp[i][j]=Math.min(Math.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
            }
        }
    }
    return dp[word1.length()][word2.length()];
}

  目录