常用算法案例分析(一)

1、动态规划

动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三个步骤:

  1. 第一步骤定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?
  2. 第二步骤状态转移方程,找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]……dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步。
  3. 第三步骤找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。

有了初始值,并且有了数组元素之间的关系式,那么我们就可以得到 dp[n] 的值了而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了

- 案例一
在这里插入图片描述

class Solution {
    //解题思路:动态规划
    //状态转移方程:比如,如果想指导amout为11时的最少硬币数,如果你凑出amout=10的最少硬币数,你只需要加一就可以得到amount=11的最少硬币数。
    public int coinChange(int[] coins, int amount) {
        //自底而上求
        int max=amount+1;
        //dp[n]:当amout为n时,凑成的硬币最少个数
        int[] dp=new int[amount+1];
        //把数组中的元素都变为max
        Arrays.fill(dp,max);
        dp[0]=0;
        for(int i=1;i<=amount;i++){
            for(int j=0;j<coins.length;j++){
                //判断条件
                if(coins[j]<=i){
                    dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
                }
            }
        }
        return dp[amount]==amount+1?-1:dp[amount];
    }
}

2、回溯法

解决⼀个回溯问题,实际上就是⼀个决策树的遍历过程。你只需要思考 3 个问题:

  • 路径:也就是已经做出的选择。
  • 选择列表:也就是你当前可以做的选择。
  • 结束条件:也就是到达决策树底层,无法再做选择的条件。

回溯算法的框架:

在这里插入图片描述

  • 案例一

在这里插入图片描述

class Solution {
    private List<List<Integer>> list1=new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if(candidates.length==0){
            return list1;
        }
        int sum=0;
        int index=0;
        Arrays.sort(candidates);
        List<Integer> list=new LinkedList<>();
        backtrace(candidates,target,list,sum,index);
        return list1;
    }
    public void backtrace(int[] arr,int target,List<Integer> list,int sum,int index){
        //结束条件
        if(sum==target){
            list1.add(new LinkedList(list));
            return;
        }
        for(int i=index;i<arr.length;i++){
            if(target-sum<arr[i]){
                break;
            }
            list.add(arr[i]);
            sum=sum+arr[i];
            backtrace(arr,target,list,sum,i);
            list.remove(list.size()-1);
            sum=sum-arr[i];
        }

    }
}
  • 案例二
    在这里插入图片描述
class Solution {
    private List<List<Integer>> list=new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        if(nums.length<1){
            return list;
        }
        int[] flag=new int[nums.length];
        backtrace(nums,flag,nums.length,new LinkedList<>());
        return list;
    }
    private void backtrace(int[] num,int[] flag,int len,List<Integer> list2){
        if(list2.size()==len){
            list.add(new ArrayList<>(list2));
            return;
        }
        for(int i=0;i<num.length;i++){
            if(flag[i]==1){
                continue;
            }
            flag[i]=1;
            list2.add(num[i]);
            backtrace(num,flag,len,list2);
            flag[i]=0;
            list2.remove(list2.size()-1);
        }
    }
}
  • 案例三
    在这里插入图片描述
class Solution {
    // 每步要么增加一个左括号,要么增加一个右括号,是一个二叉的选择,所以暴搜很容易写出来,就是dfs(left - 1, right, curStr + "("); dfs(left, right - 1, curStr + ")"); 
    //但是并不是每个分支都是符合要求的(正确的括号匹配),比如如果right使用的比left多的话就已经不是正确括号了,没必须继续dfs这个分支了,所以加上if来剪枝哈~
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        dfs(n, n, "");
        return res;
    }
    private void dfs(int left, int right, String curStr) {
        if (left == 0 && right == 0) { // 左右括号都不剩余了,递归终止
            res.add(curStr);
            return;
        }

        if (left > 0) { // 如果左括号还剩余的话,可以拼接左括号
            dfs(left - 1, right, curStr + "(");
        }
        if (right > left) { // 如果右括号剩余多于左括号剩余的话,可以拼接右括号
            dfs(left, right - 1, curStr + ")");
        }
}
}

3、深度优先遍历算法

在这里插入图片描述

class Solution {
    //深度优先遍历
    private int[][] nums=new int[][]{{1,0},{0,1},{-1,0},{0,-1}};
    public int numIslands(char[][] grid) {
        if(grid.length==0){
            return 0;
        }
        int row=grid.length;
        int col=grid[0].length;
        //岛屿的个数
        int count=0;
        //记录被遍历过的位置
        boolean[][] arr=new boolean[row][col];
        for(int i=0;i<row;i++){
            for(int j=0;j<col;j++){
                if(grid[i][j]=='1'&&arr[i][j]==false){
                    count++;
                    dfs(arr,grid,i,j,row,col);
                }
            }
        }
        return count;
    }
    public void dfs(boolean[][] arr,char[][] grid,int x,int y,int row,int col){
        arr[x][y]=true;
        for(int[] num:nums){
            int curRow=num[0]+x;
            int curCol=num[1]+y;
            if(judgeRange(curRow,curCol,row,col)&&arr[curRow][curCol]==false&&grid[curRow][curCol]=='1'){
                dfs(arr,grid,curRow,curCol,row,col);
            }
        }
    }
    public boolean judgeRange(int x,int y,int row,int col){
        if(x>=0&&x<row&&y>=0&&y<col){
            return true;
        }
        return false;
    }
}

4、广度优先遍历算法

在这里插入图片描述

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    //解题思路:广度优先遍历
    //广度优先遍历是按层层推进的方式,遍历每一层的节点;题目要求的是返回每一层的节点值,所以这题用广度优先来做非常合适
    //广度优先需要用队列作为辅助结构,我们先将根节点放到队列中,然后不断遍历队列
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> list=new LinkedList<>();
        if(root==null){
            return list;
        }
        LinkedList<TreeNode> deque=new LinkedList<>();
        //将根节点放入队列中,然后不断遍历队列
        deque.add(root);
        while(deque.size()>0){
            //获取当前队列的长度,这个长度相当于 当前这一层的节点个数
            int len=deque.size();
            List<Integer> list1=new LinkedList<>();
            //将队列中的元素都拿出来(也就是获取这一层的节点),放到临时list中
            //如果节点的左/右子树不为空,也放入队列中
            for(int i=0;i<len;i++){
                TreeNode node=deque.poll();
                list1.add(node.val);
                if(node.left!=null){
                   deque.add(node.left);
                }
                if(node.right!=null){
                deque.add(node.right);
                }
            }
            //将临时list加入最终返回结果中
            list.add(list1);
        }
        return list;

    }
}

5、滑动窗口算法

在这里插入图片描述

class Solution {
    //滑动窗口,left=right=0
    //向右移动窗口,直到[left,right]包含t,然后不断增大left,直到[left,right]不包含t
    //不断重复上一步骤,直到遍历结束
    public String minWindow(String s, String t) {
        //保存t中的每个字符,以及字符出现的次数
        int[] arr1=new int[128];
        //保存s中的目标字符,以及出现的次数
        int[] arr2=new int[128];
        int left=0;
        int right=0;
        for(int i=0;i<t.length();i++){
            arr1[t.charAt(i)]++;
        }
        int count=0;
        int start=0;
        int min=s.length()+1;
        while(right<s.length()){
            //如果当前字符不是目标字符,则直接进入下一轮循环
            if(arr1[s.charAt(right)]==0){
                right++;
                continue;
            }
            if(arr2[s.charAt(right)]<arr1[s.charAt(right)]){               
                count++;
            }
            arr2[s.charAt(right)]++;
            right++;
            //当窗口内包含t的全部字符,则开始从左侧缩小窗口
            while(count==t.length()){
                //若窗口符合条件,且长度比较小,则对min重新赋值
                if(right-left<min){
                    start=left;
                    min=right-left;
                }
                //起点字符不是目标字符
                if(arr1[s.charAt(left)]==0){
                    left++;
                    continue;
                }
                //起点字符是目标字符,且arr1[s.charAt(left)]==arr2[s.charAt(left)]
                if(arr1[s.charAt(left)]==arr2[s.charAt(left)]){
                    count--;                   
                }
                arr2[s.charAt(left)]--;
                left++;         
            }

        }
        if(min==s.length()+1){
            return "";
        }else{
            return s.substring(start,start+min);
        }

    }
}

在这里插入图片描述

class Solution {
    //滑动窗口,left=right=0
    //向右移动窗口,直到[left,right]包含t,然后不断增大left,直到[left,right]不包含t
    //不断重复上一步骤,直到遍历结束
    public boolean checkInclusion(String s1, String s2) {
        //窗口的起点
        int left=0;
        //窗口的额终点
        int right=0;
        //目标字符在窗口内出现的次数
        int count=0;
        //因为字符一共只有127个
        int[] arr1=new int[128];
        int[] arr2=new int[128];
        //把目标字符串中的我字符全部添加到数组arr1中
        for(int i=0;i<s1.length();i++){
            arr1[s1.charAt(i)]++;
        }
        while(right<s2.length()){
            //若当前字符不是目标字符,则扩大窗口,进入下一轮循环
            if(arr1[s2.charAt(right)]==0){
                right++;
                continue;
            }
            //若当前字符是目标字符,且该字符的出现次数小于s1中目标字符出现的次数
            if(arr2[s2.charAt(right)]<arr1[s2.charAt(right)]){
                count++;
            }
            arr2[s2.charAt(right)]++;
            right++;
            //若s1的全部字符全部出现在窗口内
            while(count==s1.length()){
                //若窗口的长度等于s1的长度,且窗口内包含全部的s1字符
                if(right-left==s1.length()){
                    return true;
                }
                //当前字符不是目标字符
                if(arr1[s2.charAt(left)]==0){
                    left++;
                    continue;
                }
                //若当前字符是目标字符
                if(arr1[s2.charAt(left)]==arr2[s2.charAt(left)]){
                    count--;
                }
                arr2[s2.charAt(left)]--;
                left++;
                
            }
        }
        return false;

    }
}

在这里插入图片描述

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s.length()==0) return 0;
        HashMap<Character, Integer> map = new HashMap<Character, Integer>();
        int max = 0;
        int left = 0;
        for(int i = 0; i < s.length(); i ++){
            /**
            1、首先,判断当前字符是否包含在map中,如果不包含,将该字符添加到map(字符,字符在数组下标),
             此时没有出现重复的字符,左指针不需要变化。此时不重复子串的长度为:i-left+1,与原来的maxLen比较,取最大值;

            2、如果当前字符 ch 包含在 map中,此时有2类情况:
             1)当前字符包含在当前有效的子段中,如:abca,当我们遍历到第二个a,当前有效最长子段是 abc,我们又遍历到a,
             那么此时更新 left 为 map.get(a)+1=1,当前有效子段更新为 bca;
             2)当前字符不包含在当前最长有效子段中,如:abba,我们先添加a,b进map,此时left=0,我们再添加b,发现map中包含b,
             而且b包含在最长有效子段中,就是1)的情况,我们更新 left=map.get(b)+1=2,此时子段更新为 b,而且map中仍然包含a,              map.get(a)=0;
             随后,我们遍历到a,发现a包含在map中,且map.get(a)=0,如果我们像1)一样处理,就会发现 left=map.get(a)+1=1,实际              上,left此时
             应该不变,left始终为2,子段变成 ba才对。

             为了处理以上2类情况,我们每次更新left,left=Math.max(left , map.get(ch)+1).
             另外,更新left后,不管原来的 s.charAt(i) 是否在最长子段中,我们都要将 s.charAt(i) 的位置更新为当前的i,
             因此此时新的 s.charAt(i) 已经进入到 当前最长的子段中!
             */ 
            if(map.containsKey(s.charAt(i))){
                left = Math.max(left,map.get(s.charAt(i)) + 1);
            }
            map.put(s.charAt(i),i);
            max = Math.max(max,i-left+1);
        }
        return max;
        
    }
}

在这里插入图片描述

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        //窗口的左右索引
        int left=0;
        int right=0;
        //窗口内目标字符出现的次数
        int count=0;
        //存储结果
        List<Integer> list=new LinkedList<>();
        //字符最多为127个
        int[] arr1=new int[128];
        int[] arr2=new int[128];
        //把目标字符串的字符保存到数组中
        for(int i=0;i<p.length();i++){
            arr1[p.charAt(i)]++;
        }
        while(right<s.length()){
            //当前字符不是目标字符
            if(arr1[s.charAt(right)]==0){
                right++;
                continue;
            }
            //当前字符是目标字符
            if(arr2[s.charAt(right)]<arr1[s.charAt(right)]){
                count++;
            }
            arr2[s.charAt(right)]++;
            right++;
            while(count==p.length()){
                if(right-left==p.length()){
                        list.add(left);                   
                }
                if(arr1[s.charAt(left)]==0){
                    left++;
                    continue;
                }
                if(arr1[s.charAt(left)]==arr2[s.charAt(left)]){
                    count--;
                }
                arr2[s.charAt(left)]--;
                left++;
                
            }
        }
        return list;

    }
}

6、二分法

在这里插入图片描述

class Solution {
    //二分法,先判断两个区间哪个是有序的区间,然后根据两个区间的性质判断target可能出现在那个区间
    public int search(int[] nums, int target) {
        if(nums.length==0){
            return -1;
        }else if(nums.length==1){
            if(nums[0]==target){
                return 0;
            }else{
                return -1;
            }
        }
        int l=0;
        int r=nums.length-1;
        while(l<=r){
            int mid = (l + r) / 2;
            if (nums[mid] == target) return mid;
            if (nums[l] <= nums[mid]) {
                if (nums[l] <= target && target < nums[mid]) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                if (nums[mid] < target && target <= nums[r]) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }
        return -1;

    }
}

1、为什么while循环的条件是<=,而不是<?
答:因为初始化 right 的赋值是 nums.length - 1 ,即最后⼀个元素的索引,⽽不是 nums.length 。

这⼆者可能出现在不同功能的⼆分查找中,区别是:前者相当于两端都闭区间 [left, right] ,后者相当于左闭右开区间 [left, right) ,因为索引⼤⼩为 nums.length 是越界的。

我们这个算法中使⽤的是前者 [left, right] 两端都闭的区间。这个区间其实就是每次进⾏搜索的区间。

7、双指针技巧汇总

我把双指针技巧再分为两类,⼀类是「快慢指针」,⼀类是「左右指针」前者解决主要解决链表中的问题,⽐如典型的判定链表中是否包含环;后者主要解决数组(或者字符串)中的问题,⽐如⼆分查找。

7.1、快慢指针的用法

7.1.1判断链表中是否有环

经典解法就是⽤两个指针,⼀个跑得快,⼀个跑得慢。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针⼀圈,和慢指针相遇,说明链表含有环
在这里插入图片描述

7.1.2已知链表中含有环,返回这个环的起始位置

在这里插入图片描述

ListNode detectCycle(ListNode head) {
        ListNode fast, slow;
        fast = slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) break;
        }
        slow = head;
        while (slow != fast) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

可以看到,当快慢指针相遇时,让其中任⼀个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。这是为什么呢?

第⼀次相遇时,假设慢指针 slow ⾛了 k 步,那么快指针 fast ⼀定⾛了 2k步,也就是说⽐ slow 多⾛了 k 步(也就是环的⻓度
在这里插入图片描述
设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k -m,也就是说如果从 head 前进 k - m 步就能到达环起点。巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点

在这里插入图片描述
所以,只要我们把快慢指针中的任⼀个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了

7.1.3寻找链表的中点

类似上⾯的思路,我们还可以让快指针⼀次前进两步,慢指针⼀次前进⼀步,当快指针到达链表尽头时,慢指针就处于链表的中间位置

while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
// slow 就在中间位置
return slow;

当链表的⻓度是奇数时,slow 恰巧停在中点位置;如果⻓度是偶数,slow最终的位置是中间偏右

7.1.4链表的归并排序

在这里插入图片描述

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    /**
     * 参考:Sort List——经典(链表中的归并排序) https://www.cnblogs.com/qiaozhoulin/p/4585401.html
     * 
     * 归并排序法:在动手之前一直觉得空间复杂度为常量不太可能,因为原来使用归并时,都是 O(N)的,
     * 需要复制出相等的空间来进行赋值归并。对于链表,实际上是可以实现常数空间占用的(链表的归并
     * 排序不需要额外的空间)。利用归并的思想,递归地将当前链表分为两段,然后merge,分两段的方
     * 法是使用 fast-slow 法,用两个指针,一个每次走两步,一个走一步,知道快的走到了末尾,然后
     * 慢的所在位置就是中间位置,这样就分成了两段。merge时,把两段头部节点值比较,用一个 p 指向
     * 较小的,且记录第一个节点,然后 两段的头一步一步向后走,p也一直向后走,总是指向较小节点,
     * 直至其中一个头为NULL,处理剩下的元素。最后返回记录的头即可。
     * 
     * 主要考察3个知识点,
     * 知识点1:归并排序的整体思想
     * 知识点2:找到一个链表的中间节点的方法
     * 知识点3:合并两个已排好序的链表为一个新的有序链表
     */
    public ListNode sortList(ListNode head) {
        return head==null?null:megerSort(head);
    }
    public ListNode megerSort(ListNode head){
        if(head==null||head.next==null){
            return head;
        }
        //慢指针的位置
        ListNode low=head;
        //快指针的位置
        ListNode fast=head;
        //记录当前慢指针的位置
        ListNode pre=null;
        while(fast!=null&&fast.next!=null){
            pre=low;
            low=low.next;
            fast=fast.next.next;
        }
        //断开链表
        pre.next=null;
        ListNode l=megerSort(head);
        ListNode r=megerSort(low);
        return  meger(l,r);
    }
    public ListNode meger(ListNode l,ListNode r){
        ListNode root=new ListNode(0);
        ListNode curRoot=root;
        while(l!=null&&r!=null){
            if(l.val<r.val){
                curRoot.next=l;
                curRoot=curRoot.next;
                l=l.next;
            }else{
                curRoot.next=r;
                curRoot=curRoot.next;
                r=r.next;
            }
        }
        while(l!=null){
            curRoot.next=l;
            curRoot=curRoot.next;
            l=l.next;
        }
        while(r!=null){
            curRoot.next=r;
            curRoot=curRoot.next;
            r=r.next;
        }
        return root.next;
    }
}

7.1.5寻找链表的倒数第k个元素

我们的思路还是使⽤快慢指针,让快指针先⾛ k 步,然后快慢指针开始同速前进。这样当快指针⾛到链表末尾 null 时,慢指针所在的位置就是倒数第 k个链表节点(为了简化,假设 k 不会超过链表⻓度):

ListNode slow, fast;
slow = fast = head;
while (k-- > 0)
   fast = fast.next;
while (fast != null) {
   slow = slow.next;
   fast = fast.next;
}
return slow;

7.1、左右指针的用法

左右指针在数组中实际是指两个索引值,⼀般初始化为 left = 0, right =nums.length - 1 。

7.2.1二分查找

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

7.2.2两数之和

int[] twoSum(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left < right) {
            int sum = nums[left] + nums[right];
            if (sum == target) {
            // 题⽬要求的索引是从 1 开始的
                return new int[]{left + 1, right + 1};
            } else if (sum < target) {
                left++; // 让 sum ⼤⼀点
            } else if (sum > target) {
                right--; // 让 sum ⼩⼀点
            }
        }
        return new int[]{-1, -1};
    }

需要注意的是:如果是求中点的值,在int left = 0, right = nums.length - 1;的情况下,left<=right,但在这种情况况下是left<right

7.2.3反转数组

void reverse(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
        // swap(nums[left], nums[right])
            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
            left++; right--;
        }
    }

8、买股票问题通用解法

动态规划求解

  • 定义数组含义:dp[i][k][0 or 1],第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态( 1 表示持有股票,0 表示没有持有股票)。
  • 状态转移方程:

在这里插入图片描述
根据这个图,我们来写一下状态转移方程

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
              max(   选择 rest  ,             选择 sell      )

解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
              max(   选择 rest  ,           选择 buy         )

解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。

这个解释应该很清楚了,如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的(只能选择一种方式减1,即买的时候减1或者卖的时候减1)

在,我们已经完成了动态规划中最困难的一步:状态转移方程。如果之前的内容你都可以理解,那么你已经可以秒杀所有问题了,只要套这个框架就行了。不过还差最后一点点,就是定义 base case,即最简单的情况。

dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。

把上面的状态转移方程总结一下:

base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity

状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

8.1、买股票的最佳时机

在这里插入图片描述
解题思路:因为只交易一次k=1,所以

dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) 
            = max(dp[i-1][1][1], -prices[i])
解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。

现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
可以进行进一步化简去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])

当i=0时,dp[0][0]=max(0,负无穷)=0;
当i=0时,dp[0][1]=max(负无穷,-prices[i])=-prices[i]

所以这一题的代码为:

class Solution {
    /**
    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
    dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
    **/
    public int maxProfit(int[] prices) {
        if(prices.length<2){
            return 0;
        }
        int[][] dp=new int[prices.length][2];
        for(int i=0;i<prices.length;i++){
            if(i==0){
                dp[i][0]=0;
                dp[i][1]=-prices[i];
            }else{
                dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
                dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
            }
        }
        return dp[prices.length-1][0];
    }
}

8.2、买股票的最佳时机`

class Solution {
    //dp[i][k][0]=Math.max(dp[i-1][k][0],dp[i-1][k][1]+prices[i]);
    //dp[i][k][1]=Math.max(dp[i-1][k][1],dp[i-1][k-1][1]-prices[i]);
    //因为这里k是无穷大所以状态转移方程为:
    //dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
    //dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
    public int maxProfit(int[] prices) {
        if(prices.length<2){
            return 0;
        }
        int[][] dp=new int[prices.length][2];
        for(int i=0;i<prices.length;i++){
            if(i==0){
                //当i=0时,dp[0][0]=max{dp[-1][0],dp[-1][1]+prices[i]}=max{0,负无穷}=0
                //dp[0][1]=max{dp[-1][1],dp[-1][0]-prices[i]}=max{负无穷,-prices[i]}=-prices[i]
                dp[i][0]=0;
                dp[i][1]=-prices[i];
                continue;
            }
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
            
        }
        return dp[prices.length-1][0];
    }
}

8.3、买股票的最佳时机(三)

在这里插入图片描述

class Solution {
    //dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
    //dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
    //当k=2时,就不能进行优化把k去掉
    public int maxProfit(int[] prices) {
        if(prices.length<2){
            return 0;
        }
        int[][][] dp=new int[prices.length][3][2];
        for(int i=0;i<prices.length;i++){
            for(int k=1;k<=2;k++){
                if(i==0){
                    dp[i][k][0]=0;
                    dp[i][k][1]=-prices[0];
                }else{
                    dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
                    dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
                }
            }
        }
        return dp[prices.length-1][2][0];
    }
}

8.4、买股票的最佳时机(四)

在这里插入图片描述
分析:有了上一题 k = 2 的铺垫,这题应该和上一题的第一个解法没啥区别。但是出现了一个超内存的错误,原来是传入的 k 值会非常大,dp 数组太大了。现在想想,交易次数 k 最多有多大呢?

一次交易由买入和卖出构成,至少需要两天。所以说有效的限制 k 应该不超过 n/2如果超过,就没有约束作用了,相当于 k = +infinity。这种情况是之前解决过的。

class Solution {
    public int maxProfit(int k, int[] prices) {
        //这里k不是无穷大,所以状态方程为:
        //dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
        //dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
        if(prices.length<2){
            return 0;
        }
        if(k>prices.length/2){
            return maxProfit_nonek(prices);
        }
        int[][][] dp=new int[prices.length][k+1][2];
        for(int i=0;i<prices.length;i++){
            for(int j=1;j<=k;j++){
                if(i==0){
                    dp[i][j][0]=0;
                    dp[i][j][1]=-prices[0];
                }else{
                    dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]);
                    dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]);
                }
            }
        }
        return dp[prices.length-1][k][0];
    }
    private int maxProfit_nonek(int[] price){
        //这里相当于k无穷大,所以状态转移方程为:
        //dp[i][0]=max{dp[i-1][0],dp[i-1][1]+price[i]}
        //dp[i][1]=max{dp[i-1][1],dp[i-1][0]-price[i]}
        int[][] dp=new int[price.length][2];
        for(int i=0;i<price.length;i++){
            if(i==0){
                dp[i][0]=0;
                dp[i][1]=-price[0];
            }else{
                dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+price[i]);
                dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-price[i]);
            }
        }
        return dp[price.length-1][0];
    }
}

8.5、最佳买卖股票实际含冷冻期

每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1
class Solution {
    //dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]);
    //dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]);
    //每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
    //dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
    //dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i]);
    public int maxProfit(int[] prices) {
        if(prices.length<2){
            return 0;
        }
        int[][] dp=new int[prices.length][2];
        for(int i=0;i<prices.length;i++){
            if(i==0){
                dp[i][0]=0;
                dp[i][1]=-prices[i];
            }else if(i==1){
                dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
                dp[i][1]=Math.max(dp[0][1],-prices[i]);
            }else{
                dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
                dp[i][1] = Math.max(dp[i-1][1], dp[i-2][0] - prices[i]);
            }
        }
        return dp[prices.length-1][0];
    }
}

8.6、最佳买卖股票实际含手续费

在这里插入图片描述

j为无穷大,每笔交易需要付手续费,所以状态方程为

dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]-fee);
class Solution {
    //dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]);
    //dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]);
    //j为无穷大,每笔交易需要付手续费,所以状态方程为:
    //dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
    //dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]-fee);
    public int maxProfit(int[] prices, int fee) {
        if(prices.length<2){
            return 0;
        }
        int[][] dp=new int[prices.length][2];
        for(int i=0;i<prices.length;i++){
            if(i==0){
                dp[i][0]=0;
                dp[i][1]=-prices[i]-fee;
            }else{
                dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
                dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]-fee);
            }
        }
        return dp[prices.length-1][0];
    }
}

9、打家劫舍(三)

在这里插入图片描述

class Solution {
    //首先想到的是用递归解决,但是为了避免重复的计算,这里使用HashMap保存结果
    private HashMap<TreeNode,Integer> map=new HashMap<>();
    public int rob(TreeNode root) {
        //结束条件
        if(root==null){
            return 0;
        }
        if(map.containsKey(root)){
            return map.get(root);
        }
        //接下来要分情况讨论,当前节点root是否偷窃,max=Math.max(偷窃root节点,不偷窃root节点)
        //若偷窃root节点
        int tem1=root.val+(root.left==null?0:rob(root.left.left)+rob(root.left.right))+(root.right==null?0:rob(root.right.left)+rob(root.right.right));
        //若不偷窃root节点
        int tem2=rob(root.left)+rob(root.right);
        //保存结果,避免重复的计算
        map.put(root,Math.max(tem1,tem2));
        return Math.max(tem1,tem2);
    }
}

10、多数之和

思路:先排序,在使用双指针

10.1、两数之和

在这里插入图片描述
思路:题目要求返回,满足条件的索引,所以不能使用先排序,后双指针的方法。这里采用一遍哈希表。

class Solution {
    //一遍哈希表
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement)) {
                return new int[] { map.get(complement), i };
            }
            map.put(nums[i], i);
        }
        throw new IllegalArgumentException("No two sum solution");
    }
}

10.2、三数之和

在这里插入图片描述
核心思路:先排序,后双指针

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> list1=new ArrayList<>();
        if(nums.length<3){
            return list1;
        }
        //对数组进行排序
        Arrays.sort(nums);
        //双指针法,首先遍历整个数组
        for(int i=0;i<nums.length;i++){
            if(nums[i]>0){
                //若大于0,则后面不可能出现和等于0的数
                break;
            }
            //若元素重复,则直接进入下一轮循环
            if(i-1>=0&&nums[i]==nums[i-1]){
                continue;
            }
            //第一个指针的位置
            int left=i+1;
            //第二个指针的位置
            int right=nums.length-1;
            //双指针法的循环条件
            while(left<right){
                //若三个数之和大于0,右指针左移
                if(nums[i]+nums[left]+nums[right]>0){
                    right--;
                }else if(nums[i]+nums[left]+nums[right]<0){
                    //若三个数之和小于0,左指针右移
                    left++;
                }else{
                    //若三数之和等于0,直接添加
                    List<Integer> list=new ArrayList<>();
                    list.add(nums[i]);
                    list.add(nums[left]);
                    list.add(nums[right]);
                    list1.add(list);
                    int tem1=nums[right];
                    int tem2=nums[left];
                    right--;
                    left++;
                    //若指针在移动过程中出现相同的元素,则直接跳过,直到出现下一个不相等的元素
                    while(right>=0&&nums[right]==tem1){
                        right--;
                    }
                    while(left<nums.length&&nums[left]==tem2){
                        left++;
                    }
                    
                }
            }
            
        }
        return list1;

    }
    

}

10.3、四数之和

在这里插入图片描述

class Solution {
    //思路:和三数之和解题方法一样
    //利用两个for循环,固定四个元素中的最小的两个元素a和b,之后采用双指针法,left=a+1,right=len-1
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> list=new LinkedList<>();
        if(nums.length<4){
            return list;
        }
        //排序
        Arrays.sort(nums);
        for(int i=0;i<nums.length-3;i++){
            //排除相等的元素
            if(i-1>=0&&nums[i]==nums[i-1]){
                continue;
            }
            for(int j=i+1;j<nums.length-2;j++){
                //排除相等的元素
                if(j-1>i&&nums[j]==nums[j-1]){
                continue;
                }
                int tem=target-nums[i]-nums[j];
                int left=j+1;
                int right=nums.length-1;
                while(left<right){
                    if(nums[left]+nums[right]==tem){
                        List<Integer> list1=new LinkedList<>();
                        list1.add(nums[i]);
                        list1.add(nums[j]);
                        list1.add(nums[left]);
                        list1.add(nums[right]);
                        list.add(list1);
                        int curleft=nums[left];
                        int curright=nums[right];
                        left++;
                        right--;
                        //取出相同的元素
                        while(left<nums.length&&nums[left]==curleft){
                            left++;
                        }
                        while(right>=0&&nums[right]==curright){
                            right--;
                        }
                    }else if(nums[left]+nums[right]>tem){
                        right--;
                    }else{
                        left++;
                    }
                }
            }

        }
        return list;
    }
}

版权声明:本文为weixin_43181521原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>