力扣经典题目解法记录(更新中)

算法 专栏收录该内容
6 篇文章 0 订阅


力扣算法题库引用自:https://cloud.tencent.com/developer/article/167101

关于动态规划的几道基本题目:
(子序列:去除某些字符,相对顺序保持不变;子串:截取的)

  1. 最长递增子序列的长度(300 题,背下来思想,背下来解法)
    这个是子序列,不是子串。
    重点:设计dp 数组,dp[i] 为以数字nums[i] 结尾的最长递增子序列的长度。
     
        public int lengthOfLIS(int[] nums) {
    
            if (nums.length ==  0) {
                return 0;
            }
    
            // 定义dp 数组,dp[i] 表示以第i 个数字结尾,最长上升子序列的长度。
            int[] dp = new int[nums.length];
            dp[0] = 1;
            int maxAns = 1;
            // 我们从小到大计算dp 数组的值,在计算dp[i] 之前,我们已经计算出dp[0…i−1] 的值,则状态转移方程为:
            // dp[i]=max(dp[j])+1,其中0≤j<i且num[j]<num[i]
            for (int i = 1; i < nums.length; i++) {
                dp[i] = 1;
                for (int j = 0; j < i; j++) {
                    if (nums[i] > nums[j]) {
                        dp[i] = Math.max(dp[i], dp[j] + 1);
                    }
                }
                maxAns = Math.max(maxAns, dp[i]);
            }
            return maxAns;
        }
  2. 最大子数组和(题目是最大子序和,不严谨)(53 题,背下来思想,背下来解法)

    设计dp 数组,dp[i] 代表走到nums[i] 的时候,选还是不选,得到的结果是多少
    dp 数组每个元素的含义是以nums[i] (最后一个元素)结尾的最大子数组和。

    dp[i] 定义为数组nums 中以num[i] 结尾的最大连续子串和, 则有dp[i] = max(dp[i-1] + nums[i], num[i]);

    这个的理解其实也比较简单,因为dp 数组代表的就是遍历到了第i 个nums 的数字的时候最大子数组和。所以下面的逻辑也比较好解释了。


     
       public int maxSubArray(int[] nums) {
            if (nums.length == 0) {
                return 0;
            }
            // 状态只能是遍历到nums 数组每个元素的时候,最大和为多少
            int[] dp = new int[nums.length];
    
            dp[0] = nums[0];
            int ans = nums[0];
            for (int i = 1; i < nums.length; i++) {
                // 做判断,选这个数字,还是不选,是加和,还是自己
                dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
                ans = Math.max(dp[i], ans);
    
            }
            return ans;
        }
  3. 最大乘积子数组的乘积(152 题,背下来思想,背下来解法)

    因为这道题涉及乘法,乘法是负负得正,所以类似最大子数组和,但是又不太一样
    dp 数组要弄两个,一个是最大的,一个是最小的

    在遍历找ans 的时候,要分别判断nums[i] 和前一个最大的乘积、和前一个最小的乘积、当前值,哪个大或者小,来做最终的更新。
     
    public int maxProduct(int[] nums) {
            int length = nums.length;
    
            int[] maxF = new int[length];
            int[] minF = new int[length];
    
            maxF[0] = nums[0];
            minF[0] = nums[0];
    
            int ans = maxF[0];
    
            for (int i = 1; i < length; i++) {
                maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
                minF[i] = Math.min(maxF[i - 1] * nums[i], Math.min(nums[i], minF[i - 1] * nums[i]));
    
                ans = Math.max(ans, maxF[i]);
            }
            return ans;
        }

     
  4. 最小编辑距离(72 题,

    给定两个字符串s1 和s2,计算出将s1 转换成s2 所使用的最少操作数。可以对一个字符串进行如下操作:插入、删除、替换。


    理解:s1 插入或者删除,就是对应s2 的删除或者插入。修改a 或者修改b,其实上都一样。

    本质不同的操作实际上只有三种:

    在单词 A 中插入一个字符;

    在单词 B 中插入一个字符;

    修改单词 A 的一个字符。

    对于这道题,dp 数组就是将s1 转为s2 的最小操作步骤。
     

        public static int minDistance(String s1, String s2) {
            int m = s1.length();
            int n = s2.length();
            // 初始化dp 二维数组,放入初始条件
            int[][] dp = new int[m + 1][n + 1];
            for (int i = 1; i <= m; i ++) {
                dp[i][0] = i;
            }
            for (int j = 1; j <= n; j++) {
                dp[0][j] = j;
            }
            for (int i = 1; i <= m; i++) {
                for (int j = 1; j <= n; j++) {
                    if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                        // 如果字符相同,那么就是上一个位置的结果
                        dp[i][j] = dp[i - 1][j - 1];
                    } else {
                        // 如果字符不同,那么就取最小的一个
                        // A 字符串增加一个;B 字符串增加一个;A 修改一个
                        dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i - 1][j - 1])) +1;
                    }
                }
            }
            return dp[m][n];
        }


     

  5. 最长公共子序列(1143 题,

    给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

    dp[i][j] 表示遍历到两个字符串第i 个和第j 个的时候,最长公共子序列的长度

     
        // 自底向上的解法
        public int longestCommonSubsequence(String s1, String s2) {
            int m = s1.length();
            int n = s2.length();
            int[][] dp = new int[m + 1][n + 1];
            // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
            // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
            // base case: dp[0][..] = dp[..][0] = 0
            for (int i = 1; i <= m; i++) {
                for (int j = 1; j <= n; j++) {
                    // 现在 i 和 j 从 1 开始,所以要减一
                    if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                        // s1[i-1] 和 s2[j-1] 必然在 lcs 中
                        dp[i][j] = 1 + dp[i - 1][j - 1];
                    } else {
                        // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
                        dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
                    }
                }
            }
            return dp[m][n];
        }
        // 此解法可以使用状态压缩,使得空间复杂度为O(N)
    详解最长公共子序列问题,秒杀三道动态规划题目(看下面的“自底向上”解法)
  6. 最长回文子串(5 题,

    给你一个字符串 s,找到 s 中最长的回文子串。

    中心扩展法:经典面试题:最长回文子串

     



    动态规划看官方题解

    这道题对比下面那道题,求最长回文子串这个“字符串”的时候,二维数组要使用boolean 进行存储。对于求“长度” 的时候,要二维矩阵要使用数字





    此题比较重要的是,外循环要枚举“串的长度”,然后枚举左边界。

     
    class Solution {
        public String longestPalindrome(String s) {
    
            int len = s.length();
            if (len < 2) {
                return s;
            }
            int maxLen = 1;
            int begin = 0;
            // dp[i][j] 表示s[i...j] 是否为回文串
            boolean[][] dp = new boolean[len][len];
    
            for (int i = 0; i < len; i++) {
                dp[i][i] = true;
            }
            char[] charArray = s.toCharArray();
            // 递推开始
            // 枚举子串长度,长度从2 开始,长度为2、3、4、5...
            for (int L = 2; L <= len; L++) {
    
                // 枚举左边界,左边界上限设置可以宽松些
                // 上面for 循环设置的是串的长度;下面的是左边界在哪里
                for (int i = 0; i < len; i++) {
                    // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                    int j = L + i - 1;
                    // 如果右边界超限制,则退出循环
                    if (j >= len) {
                        break;
                    }
                    if (charArray[i] != charArray[j]) {
                        dp[i][j] = false;
                    } else if (charArray[i] == charArray[j]){
                        // 字符相等,且长度小于等于2
                        if (j - i <= 2) {
                            dp[i][j] = true;
                        } else {
                            // 且字符相等,长度大于2
                            dp[i][j] = dp[i + 1][j - 1];
                        }
                    }
    
    
                    // 只要dp[i][j] == true 成立,就表示字符串s[i...j] 是回文子串,此时记录长度和起始位置
                    // 每次循环要记录一下“最长的”
                    if (dp[i][j] && j - i + 1 > maxLen) {
                        maxLen = j - i + 1;
                        begin = i;
                    }
    
                }
    
            }
    
            return s.substring(begin, begin + maxLen);
        }
    }
  7. 最长回文子序列的长度(516 题,【没看懂】

    对 dp 数组的定义是:在子串s[i..j]中,最长回文子序列的长度为dp[i][j]
    子序列解题模板:最长回文子序列



    注意遍历顺序,i 从最后一个字符开始往前遍历,j 从 i + 1 开始往后遍历,这样可以保证每个子问题都已经算好了



     
    class Solution {
        public int longestPalindromeSubseq(String s) {
            int n = s.length();
            int[][] dp = new int[n][n];
            // 因为是从长度较短的子序列向长度较长的转移
            // 所以从i = n - 1 开始
            for (int i = n - 1; i >= 0; i--) {
                dp[i][i] = 1;
                char c1 = s.charAt(i);
                for (int j = i + 1; j < n; j++) {
                    char c2 = s.charAt(j);
                    if (c1 == c2) {
                        dp[i][j] = dp[i + 1][j - 1] + 2;
                    } else {
                        // 因为本身第i 个和第j 个不一致,而下面转移方程中是dp[i][j - 1] 和dp[i + 1][j] 这样,就不涉及第i 个和最后一个字符相等怎么处理这种事情了
                        dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                    }
                }
            }
            // dp[0][n - 1] 的含义就是从第0 个字符串到第n - 1 个字符串的最长回文子序列的长度
            return dp[0][n - 1];
        }
    }

最长上升子序列的个数(673 题)

给定一个未排序的整数数组,找到最长递增子序列的个数。

class Solution {
    public int findNumberOfLIS(int[] nums) {

        int n = nums.length;
        int maxLen = 0;
        int ans = 0;
        // dp[i] 为以nums[i] 结尾的最长上升子序列的长度
        int[] dp = new int[n];
        // cnt[i] 为考虑以nums[i] 结尾的最长上升子序列的个数
        int[] cnt = new int[n];

        for (int i = 0; i < n; i++) {
            dp[i] = 1;
            cnt[i] = 1;
            for (int j = 0; j < i; j++) {
                // nums[i] > nums[j] 的情况是nums[i] 可以接在nums[j] 后面形成上升子序列
                if (nums[i] > nums[j]) {
                    // 求最长上升子序列的长度,正常状态转移方程如下
                    // dp[i] = Math.max(dp[i], dp[j] + 1);
                    // 就是下面两种情况
                    // (1)if (dp[i] < dp[j] + 1);
                    // 说明以nums[i] 结尾的最长递增子序列长度是小于以nums[j] 结尾的dp[j]
                    // dp[i] 会被dp[j] + 1 直接更新
                    // 所以这个时候,cnt[i] 就直接等于cnt[j]
                    // (2)if (dp[i] == dp[j] + 1);
                    // 说明找到了一个新的符合条件的,对于cnt[i] 要直接累加cnt[j]
                    if (dp[i] < dp[j] + 1) {
                        dp[i] = dp[j] + 1;
                        cnt[i] = cnt[j];
                    } else if (dp[i] == dp[j] + 1) {
                        // 对于dp[i],因为相等,所以就不用重新赋值了
                        cnt[i] += cnt[j];
                    }
                }
            }
            // 最终结果判定 & 保存
            if (dp[i] > maxLen) {
                maxLen = dp[i];
                ans = cnt[i];
            } else if (dp[i] == maxLen) {
                ans += cnt[i];
            }
        }
        return ans;
    }
}


以下思路及代码大部分来自于上述链接“王脸小”同学,同时有一部分摘抄自“labuladong 的算法小抄,以及网上的各个链接。

鸣谢@王脸小

同时题目参考:小浩算法

基础:
1. 读题后先想想有什么思路,不要让题解局限了想法,即使暴力解法也可以。
2. 懂得“递归”的思想。递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分为符合条件的子问题,而不需要研究这个子问题是如何解决的。递归的两个特征:自我调用(为了解决子问题)和结束条件(定义了最简子问题的答案)。

递归其实是有一个栈,每一层递归都是一个压栈的操作。
对于递归的内部逻辑理解:如果进入方法之后前几行代码就是判断某个条件,然后return,这样的操作其实是最底层的出栈,然后回到上一层的栈帧中,继续执行判断条件之后的代码。

基础

排序算法

  • 快速排序
    参考链接:https://zhuanlan.zhihu.com/p/26891027

    解题思路:这个写法很简单,在纸上画一画就可以写得出来。选择最左边的一个值作为flag,然后从右边开始找。整体一个大循环,是一次遍历,将小于flag 的移到左边,大于的移到右边。大循环内部是右侧向左,左侧向右的操作。一次大循环过后,对flag 左右的两段分别进行快排。-> 整体的方向就是一个大数组 -> 以flag 左右分,分别处理 -> 整体到细节,不用合并。因为这个是二分,二分完了之后还是相同的逻辑,所以是要调用自己的。

    记住函数签名:void quickSort(int[ ] nums, int start, int end)

    5 8 7 6 3 1 2 
  • public static void quickSort(int[] nums, int start, int end) {
            int low = start;
            int high = end;
            int key = nums[low];
            while (low < high) {
                while (low < high && nums[high] >= key) {
                    high --;
                }
                if (low < high) {
                    nums[low] = nums[high];
                    low ++;
                }
                while (low < high && nums[low] <= key) {
                    low ++;
                }
                if (low < high) {
                    nums[high] = nums[low];
                    high --;
                }
            }
            nums[low] = key;
            if (low - 1 > start) {
                quickSort(nums, start, low - 1);
            }
            if (high + 1 < end) {
                quickSort(nums, high +  1, end);
            }
        }

  • 归并排序
    参考链接:【算法】排序算法之归并排序 - 知乎
    代码参考力扣讲解
    解题思路:整体思路就是一个拆分、合并的过程。

    记住函数签名:void mergeSort(int[ ] nums, int l, int r)
    class Solution {
        int[] tmp;
    
        public int[] sortArray(int[] nums) {
            tmp = new int[nums.length];
            mergeSort(nums, 0, nums.length - 1);
            return nums;
        }
    
        public void mergeSort(int[] nums, int l, int r) {
            if (l >= r) {
                return;
            }
            int mid = (l + r) >> 1;
            mergeSort(nums, l, mid);
            mergeSort(nums, mid + 1, r);
            int i = l, j = mid + 1;
            int cnt = 0;
            while (i <= mid && j <= r) {
                if (nums[i] <= nums[j]) {
                    tmp[cnt++] = nums[i++];
                } else {
                    tmp[cnt++] = nums[j++];
                }
            }
            while (i <= mid) {
                tmp[cnt++] = nums[i++];
            }
            while (j <= r) {
                tmp[cnt++] = nums[j++];
            }
            // 一共r - l + 1 个数字
            // 把nums 中这些位置的元素替换掉,替换成tmp 中排好序的数字
            for (int k = 0; k < r - l + 1; ++k) {
                nums[k + l] = tmp[k];
            }
        }
    }

    注意上面return 逻辑的理解,因为是基于递归而实现,在最后拆分为只有一个元素的时候,return 是返回到递归的上一层,然后继续执行后面的代码。(递归的思想)
     

  • 堆排序

    时间复杂度O(n log n)  最好、最坏、平均都是
    空间复杂度O(1)

    堆排序三个过程:1)建堆;2)调整;3)输出
    建堆的过程就是把一个数组维护成每个点都比叶子节点大或者小的结构

    排序,一个参数:nums[i]
    建堆,两个参数:nums[i], int len
    调整,三个参数:nums[i], int i, int len
    其中,len 代表数组长度,i 代表
    记住函数签名:
    (1)调整:heapify(int[ ] nums, int i, int length)
    (2)建堆:buildMaxHeap(int[ ] nums, int len)


    举例:5 8 7 6 3 1 2

        public static int[] sort(int[] sourceArray) {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            int len = arr.length;
    
            buildMaxHeap(arr, len);
    
            for (int i = len - 1; i > 0; i--) {
                swap(arr, 0, i);
                len--;
                heapify(arr, 0, len);
            }
            return arr;
        }
    
        // 建堆,过程就是从第一个非叶子节点开始调整
        private static void buildMaxHeap(int[] arr, int len) {
            for (int i = (int) Math.floor(len / 2) - 1; i >= 0; i--) {
                heapify(arr, i, len);
            }
        }
    
        // 对每个节点i 进行调整
        // 这是创建大顶堆的过程,创建大顶堆,然后可以进行正序排列
        private static void heapify(int[] arr, int i, int len) {
            int left = 2 * i + 1;
            int right = 2 * i + 2;
            int largest = i;
    
            if (left < len && arr[left] > arr[largest]) {
                largest = left;
            }
    
            if (right < len && arr[right] > arr[largest]) {
                largest = right;
            }
    
            if (largest != i) {
                swap(arr, i, largest);
                // 对于调整了的那个节点,还需要再继续向下进行调整
                heapify(arr, largest, len);
            }
        }
    
        private static void swap(int[] arr, int i, int j) {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }

    215 数组中第k 大的元素

     

  • 冒泡排序
  • 树的前中后遍历,迭代
    前序遍历:根左右。使用栈,先把根节点push,然后while 判断栈是否为空,不空则pop 出一个node,然后右不空push 右,左不空push 左(因为FILO)。结束。
    中序遍历:左根右。do-while 循环。先把所有左子树节点放入栈中,直到最后,是最后一个左子树节点。然后出栈,再判断其右子树,处理右子树。如果由子树继续为空,则继续pop;不空,则继续push。
    后序边拆:左右根,可以根、右、左,然后反转。
    参考:Java迭代实现二叉树的前序、中序、后序遍历_yangfeisc的专栏-CSDN博客_java二叉树的前序遍历

二分查找

做二分法需要自己理解并总结出,需要怎么进入循环,需要怎么退出循环。
即怎么判断边界条件。如果用数学方法表示mid,mid = (l + r)/2,这种是很明确的表示方法。但是到了编程的时候,就涉及mid 是应该(l + r)/2 还是(l + r + 1) / 2,判断l <= r 为结束条件,还是l < r 为结束条件。具体参考:算法浅谈——人人皆知却很多人写不对的二分法 - 知乎
二分法要注意不要造成死循环

对于区间的设置,可以使用“左闭右开”,对于每个人,设置一个自己的固定模式,这样更容易记一些。

  • 数组
    • 4. 寻找两个正序数组的中位数 (hard)
      题目要求时间复杂度Olog(m+n)。如果有log 的时间复杂度,差不多应该都是使用二分法。
      根据数组的定义,其中一个数组长度为m,另一个数组长度为n。
      如果m + n 为奇数【如7】,那么中位数就是下标为(m + n) / 2 【7/2 = 3】的数字。如果m + n 为偶数【如10】,那么中位数就是下标为(m + n) / 2 和(m + n) / 2 - 1 【4,5】的数字。可以利用整数除法向下取整的特点,求数组中下标为(m + n - 1) / 2 和(m + n) / 2 的元素,然后求出两个数字的平均值,就是数组的中位数。





       
    • 查找排序数组给定target 的左边界,查找排序数组给定target 的右边界。代码来自labuladong

      查找给定数组的左边界的下标,相当于查找排序数组中元素小于target 的元素的个数
      比如说,[0, 4, 5, 5] target = 5,那么得到的结果就是2。
      查找给定数组的左边界的下标,相当于查找排序数组中元素小于target 的元素的个数。
      所以在下面的代码中的判断,才返回的是nums.length。
      重点在于num[mid] == target 时,right = mid。将范围逐步压缩。
      
      public static int left_bound(int[] nums, int target) {
      
              if (nums[nums.length - 1] < target) {
                  return nums.length;
              }
      
              int left = 0;
              int right = nums.length;
              while (left < right) {
                  int mid = (left + right) / 2;
                  if (nums[mid] == target) {
                      right = mid;
                  } else if (nums[mid] > target) {
                      right = mid;
                  } else if (nums[mid] < target) {
                      left = mid + 1;
                  }
              }
      
              if (nums[left] != target) {
                  return -1;
              }
      
              return left;
      
          }
      
      
      
      或者
      
          public static int left_bount(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 = mid;
                  } else if (nums[mid] < target) {
                      left = mid + 1;
                  } else if (nums[mid] > target) {
                      right = mid - 1;
                  }
              }
      
              if (nums[right] == target) {
                  return right;
              } else {
                  return -1;
              }
      
          }
      
      
      注意在wile 循环里,如果有“等于”的判断,那么while 条件里,就不能是“小于等于”的判断
      
      查找右边界
      
          public static int right_bound(int[] nums, int target) {
      
              if (nums.length == 0) return -1;
              int left = 0, right = nums.length;
              while (left < right) {
                  int mid = (left + right) >> 1;
                  if (nums[mid] == target) {
                      left = mid + 1;
                  } else if (nums[mid] > target) {
                      right = mid;
                  } else if (nums[mid] < target) {
                      left = mid + 1;
                  }
              }
              return left - 1;
      
          }
      
      
      这种解法我也说不清为啥求mid 的时候要+1 就不会进入死循环。
      有时间再研究总结吧
      
          public static int right_bound(int[] nums, int target) {
      
              int left = 0;
              int right = nums.length - 1;
              while (left < right) {
                  int mid = (left + right + 1) / 2;
                  if (nums[mid] == target) {
                      left = mid;
                  } else if (nums[mid] > target) {
                      right = mid - 1;
                  } else if (nums[mid] < target) {
                      left = mid + 1;
                  }
              }
              if (nums[right] == target) {
                  return right;
              } else {
                  return -1;
              }
      
          }




       

    • 33. 搜索旋转排序数组 直接使用二分法
      一个数组,已经排过序,在某个元素那里做了旋转。要求查找出数组中是否存在某个元素。要求olog(n) 的时间复杂度。
      olog(n) 时间复杂度即说明了使用二分法。
      旋转数组是原来一个有序数组,进行了旋转,得到一个“局部有序”的数组。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]。
      这个旋转的方式是在下标K 处旋转,是数组变成(num[k], num[k + 1] ... nums[0], nums[1] ...)

      可以通过在常规的二分查找的时候,找到mid 为分割位的前半部分[1, mid],和[mid + 1,r] 哪部分是有序的,然后在有序的部分判断target 是否存在,从而根据有序的部分可以推断出如何改变二分查找的上下界。
      思路:这道题的重点思想就在,无序的那部分可能是不太好处理,我们可以转头去处理有序的那部分。每次循环通过nums[l],nums[r] 和nums[middle] 的大小,来判断l ~ middle 或者middle ~ r 的区间是否是递增的。
      class Solution {
          public int search(int[] nums, int target) {
      
              if (nums.length == 0) {
                  return -1;
              }
              int low = 0;
              int high = nums.length - 1;
      
              while (low <= high) {
                  int middle = (low + high) >> 1;
                  if (nums[middle] == target) {
                      return middle;
                  }
      
                  // 说明此区间有序
                  if (nums[low] <= nums[middle]) {
                      if (target >= nums[low] && target <= nums[middle]) {
                          high = middle - 1;
                      } else {
                          low = middle + 1;
                      }
      
                  // 说明另外一个区间有序
                  } else {
                      if(target >= nums[middle] && target <= nums[high]) {
                          low = middle + 1;
                      } else {
                          high = middle - 1;
                      }
                  }
              }
              return -1;
      
          }
      }
    •  287. 寻找重复数 dict
      包含n + 1 个数字,数字范围1 ~ n,存在一个重复的数字。找出这个数字。
      利用条件【一共有n + 1 个数字,数字的范围在1 ~ n】。

      这道题要注意的地方是,进行二分的不是数组,而是1 ~ n 这个数字区间。

       

      给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。

      假设 nums 只有 一个重复的整数 ,找出 这个重复的数 。

      你设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间。

      这道题二分的是1 ~ n 这n 个数字

       
      首先,我们让中指针指向n/2, 由题意可得,小于n/2的元素只会有(n/2)-1个。
      比如说,数组是4,1,3,2,5
      对于数组中每个元素,小于1 的元素有0 个,小于2 的元素有1 个,小于3 的元素有2个...
      
      如果超过了(n/2)-1,说明在[1,n/2)区间有重复的数字,此时我们将待查找的数据规模缩小到[1, n/2)
      如果数组没有超过(n/2)-1。说明在[n/2, n]区间有重复的数字,此时我们讲查找的数据规模缩小到[n/2, n]
      不断重复上述过程,直到找到某一个重复的数字。
      时间复杂度O(nlogn), 二分法本身是O(logn), 每次缩小规模时需要统计一次数组(O(n))
      空间复杂度O(1)
      代码中start 是数字范围1 ~ n 的1;代码中的end 是数字范围,一共n + 1 个数字,范围1 ~ n,所以最大的是nums.length - 1。
      
      
          public static int findDuplicate(int[] nums) {
              int start = 1, end = nums.length - 1;
              int mid = (start + end) / 2;
              while (start <= end){
                  int count = 0;
                  for(int num: nums){
                      // 计算小于中间数字mid 的数字个数
                      if(num < mid) {
                          count += 1;
                      } else {
                          break;
                      }
                  }
                  if( count >= mid) {
                      // 说明重复数字在此区间内
                      end = mid - 1;
                  } else {
                      // 说明重复数字在另一个区间
                      start = mid + 1;
                  }
                  mid = (start + end) / 2;
              }
      
              return mid;
          }
      
      
      
      参考:​​​​​​https://zhuanlan.zhihu.com/p/102298178
    • 34. 在排序数组中查找元素的区间
      原创(@王漂亮)提示解法:取开始下标(mid = (l + r) // 2); 取结束下标(mid = (a + b + 1) // 2)
      题目要求,o(log n) 时间复杂度,给定一个升序有序数组和一个目标值target,找出给定目标值在数组中开始和结束的位置
      要求o(log n) 即为“二分法”。考虑到要求找到target 在数组中的开始和结束位置,即找到第一个等于target 值的位置和第一个大于target 值的位置,然后减一。
      这道题的核心就是分两次,二分查找第一个等于target 的位置和第一个大于target 的位置。
      为了代码复用,定义方法binarySearch(nums, target, lower) 表示在nums 数组中二分查找target 的位置,如果lower 为true,则查找第一个大于等于target 的下标,否则查找第一个大于target 的下标。
      解释:lower = true,代表查找target 左下标那边,大于等于的原因是target 下标有可能不存在。
      因为target 有可能不存在,所以最后需要校验结果是否正确。
      8
      public int[] searchRange(int[] nums, int target) {
      
              int low = binarySearch(nums, target, true);
              int high = binarySearch(nums, target, false) - 1;
              if (low <= high && high < nums.length && nums[low] == target && nums[high] == target) {
                  return new int[]{low, high};
              }
              return new int[]{-1, -1};
      
          }
      
      
          private static int binarySearch(int[] nums, int target, boolean lower) {
              int left = 0;
              int right = nums.length - 1;
              int ans = nums.length;
              while (left <= right) {
      
                  int mid = (left + right) / 2;
                  if (lower) {
                      if (nums[mid] >= target) {
                          right = mid - 1;
                          ans = mid;
                      } else {
                          left = mid +1;
                      }
                  } else {
                      if (nums[mid] > target) {
                          right = mid -1;
                          ans = mid;
                      } else {
                          left = mid + 1;
                      }
                  }
      
              }
              return ans;
          }

  • 矩阵
    记住递增矩阵的性质。
    1. 左上最小,右下最大
    2. 搜索某个元素,可以从右上角开始,如果大于target,则向左走;小于target,则向下走
    3. 给定一个target,小于等于target 的都在左上角,大于的都在右下角
    • 240. 搜索二维矩阵 II
      原创(@王漂亮)提示解法:从左下/右上开始search, O(m+n)
      这道题注意矩阵的性质,左上到右下是依次递增的。所以从右上角开始,对每个元素和target 比较大小。
    • 378. 有序矩阵中第K小的元素
      原创(@王漂亮)提示解法:依次添加右和下,pop出其中较小的,每次pop k-1,pop k次返回
      使用矩阵的性质,矩阵左上角最小,右下角最大。同时这种矩阵如果指定一个元素target,在矩阵中可以得到,小于target 的元素都在左上角,大于的都在右下角。如果元素matrix[i][j] <= target,则matrix[0][j] ~ matrix[i][j] 的值都小于等于target。依据此思路,可以使用二分法一个个试出来有序矩阵中的第k 小的元素。
      class Solution {
          public int kthSmallest(int[][] matrix, int k) {
      
              int n = matrix.length;
              int left = matrix[0][0];
              int right = matrix[n-1][n-1];
              while (left < right) {
                  int mid = (left + right) / 2;
                  if (check(matrix, mid, k, n)) {
                      right = mid;
                  } else {
                      left = mid +1;
                  }
              }
              return left;
      
          }
      
          // 检查当前数字【mid】在矩阵中的情况
          // 返回结果:小于等于目标值mid 的数字个数是否大于等于k 个
          // 传参需要用到:矩阵,要查询的数字mid,范围k
          public boolean check(int[][] matrix, int mid, int k, int n) {
              // 从左下开始寻找,i 行,j 列。
              int i = n - 1;
              int j = 0;
              // 符合要求的个数
              int num = 0;
              while (i >= 0 && j < n) {
                  if (matrix[i][j] <= mid) {
                      // 元素上面的那些都是符合要求的
                      num += i + 1;
                      j ++;
                  } else {
                      i --;
                  }
              }
              // 因为要寻找第k 小的元素,返回一个boolean,判断寻找到的数字的个数,是否多于k 个
              return num >= k;
      
          }
      }
  • 其他
    • 69. X的平方根
      原创(@王漂亮)提示解法:在[0, x]中二分查找
      二分查找,在[0,x] 范围内。设ans 为最终答案,x 的平方根整数部分ans 就是k方 <= x 的最大k 值,因此可以对k 进行二分查找。
      class Solution {
          public int mySqrt(int x) {
              int l = 0, r = x;
              int ans = -1;
              while (l <= r) {
                  int mid = (l + r) / 2;
                  if ((long)mid * mid <= x) {
                      ans = mid;
                      l = mid + 1;
                  } else {
                      r = mid - 1;
                  }
              }
              return ans;
          }
      }

       

链表(适当使用哑结点,可命名为hair[head 上层为hair])

无法高效获取长度,无法根据偏移快速访问元素,是链表的两个劣势。然而面试的时候经常碰见诸如:

  1. 获取倒数第k个元素(两个指针,第一个指针先走k步,然后第二个指针开始。等第一个指针走到了末尾,第二个指针指向的就是倒数第k个)
  2. 获取中间位置的元素(第一个指针每次走两步,第二个指针每次走一步)
  3. 判断链表是否存在环(快慢指针)
  4. 判断环的长度与位置(3 中两个指针相遇后继续移动,再次相遇移动的次数就是环的长度)

这些相关的问题都可以通过灵活运用双指针来解决。

  • 基础题
    • 206. 反转链表
      迭代、递归。
      迭代:当前节点的next 要指向前一个节点,同时当前节点的下一个节点也要存储。prev/curr/curr = head, 然后进入while 循环,判断条件为curr 不为空。取出curr 的next,改变curr.next 指向,改变prev 的指向,改变curr 为next。while 循环结束,返回prev,即为反转的头结点。
      递归:
       
      /**
       * Definition for singly-linked list.
       * public class ListNode {
       *     int val;
       *     ListNode next;
       *     ListNode() {}
       *     ListNode(int val) { this.val = val; }
       *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
       * }
       */
      class Solution {
          public ListNode reverseList(ListNode head) {
              ListNode prev = null;
              ListNode cur = head;
              while (cur != null) {
                  ListNode next = cur.next;
                  cur.next = prev;
                  prev = cur;
                  cur = next;
              }
              return prev;
          }
      }
  • 反转链表II
    • 92
      给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从第left 到第right 位置的链表节点,返回 反转后的链表 
       
          public ListNode reverseBetween(ListNode head, int left, int right) {
              ListNode dummyNode = new ListNode(-1);
              dummyNode.next = head;
              ListNode pre = dummyNode;
              for (int i = 0; i < left - 1; i++) {
                  pre = pre.next;
              }
              ListNode cur = pre.next;
              ListNode next;
              for (int i = 0; i < right - left; i++) {
                  next = cur.next;
                  cur.next = next.next;
                  next.next = pre.next;
                  pre.next = next;
              }
              return dummyNode.next;
          }

       
  • 重难点 (M->H)
    • 138. 复制带随机指针的链表 di[node] = Node, key为原节点,val为新节点, di[node].next = di.get(node.next), O(2n)
       
      /*
      // Definition for a Node.
      class Node {
          int val;
          Node next;
          Node random;
      
          public Node(int val) {
              this.val = val;
              this.next = null;
              this.random = null;
          }
      }
      */
      
      class Solution {
          public Node copyRandomList(Node head) {
      
              if(head == null) {
                  return null;
              }
              Node pointer = head;
              // 使用这个map 做原链表和老链表的映射,每个node 一一对应
              Map<Node, Node> map = new HashMap<>();
              while (pointer != null) {
                  Node newNode = new Node(pointer.val);
                  // 新链表节点只是存于map 中,还没有连接
                  map.put(pointer, newNode);
                  pointer = pointer.next;
              }
              pointer = head;
              while (pointer != null) {
                  Node newNode = map.get(pointer);
                  // random 节点赋值
                  if (pointer.random != null) {
                      newNode.random = map.get(pointer.random);
                  }
                  // next 节点赋值
                  if (pointer.next != null) {
                      newNode.next = map.get(pointer.next);
                  }
                  pointer = pointer.next;
              }
              return map.get(head);
          }
      }

       
    • 21. 合并两个有序链表 递归,迭代(dummy_node)
      /**
       * Definition for singly-linked list.
       * public class ListNode {
       *     int val;
       *     ListNode next;
       *     ListNode() {}
       *     ListNode(int val) { this.val = val; }
       *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
       * }
       */
      class Solution {
          public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
      
              ListNode preHead = new ListNode(-1);
              ListNode prev = preHead;
              while (l1 != null && l2 != null) {
                  if (l1.val <= l2.val) {
                      prev.next = l1;
                      l1 = l1.next;
                  } else {
                      prev.next = l2;
                      l2 = l2.next;
                  }
                  prev = prev.next;
              }
      
              prev.next  = l1 == null ? l2 : l1;
      
              return preHead.next;
      
          }
      }


       
    • 23. 合并K个排序链表 for i in range(0, cnt-interval, interval*2)
      使用“堆”的数据结构完成,时间复杂度O(logKN)
      /**
       * Definition for singly-linked list.
       * public class ListNode {
       *     int val;
       *     ListNode next;
       *     ListNode() {}
       *     ListNode(int val) { this.val = val; }
       *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
       * }
       */
      class Solution {
          public ListNode mergeKLists(ListNode[] lists) {
      
              Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
              for (ListNode node : lists) {
                  if (node !=  null) {
                      pq.offer(node);
                  }
              }
              ListNode dummyHead = new ListNode(0);
              ListNode tail = dummyHead;
              while (!pq.isEmpty()) {
                  ListNode minNode = pq.poll();
                  tail.next = minNode;
                  tail = tail.next;
                  if (minNode.next != null) {
                      pq.add(minNode.next);
                  }
              }
              return dummyHead.next;
          }
      }
    • 25. K 个一组翻转链表 cnt++, cnt--

      (看官方题解就好了,看官方题解,官方题解。但是官方题解看不懂。这是一道hard)
      (看不懂)
       
          public ListNode reverseKGroup(ListNode head, int k) {
      
              ListNode hair = new ListNode(0);
              hair.next = head;
              ListNode pre = hair;
      
              while(head != null) {
                  ListNode tail = pre;
                  // 查看剩余部分是否大于k
                  for (int i = 0; i < k; i++) {
                      tail = tail.next;
                      if (tail == null) {
                          return hair.next;
                      }
                  }
      
                  ListNode next = tail.next;
                  ListNode[] reverse = myReverse(head, tail);
                  head = reverse[0];
                  tail = reverse[1];
                  // 子链表重新接回原链表
                  pre.next = head;
                  tail.next = next;
                  pre = tail;
                  head = tail.next;
              }
              return hair.next;
          }
      
          public ListNode[] myReverse(ListNode head, ListNode tail) {
              ListNode prev = tail.next;
              ListNode cur = head;
              while (prev != tail) {
                  ListNode next = cur.next;
                  cur.next = prev;
                  prev = cur;
                  cur = next;
              }
              return new ListNode[]{tail, head};
          }

       
  • 双指针技巧

    有个简单的搞法,就把slow 和fast 的起始值都设置成head 就好了。

    对于快慢指针,寻找链表的中点可以使用快慢指针的做法,快指针每次移动 2 步,慢指针每次移动 1 步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点。
    如果是奇数个节点,那么slow 就是那个中点;如果是偶数个节点,那么slow 指向的就是前半个链表的最后一个节点。
    • 141. 环形链表
      环形链表即判断链表是否有环。使用快慢指针。fast指针走两步,slow 指针走一步。
      public class Solution {
          public boolean hasCycle(ListNode head) {
      
              if (head == null || head.next == null) {
                  return false;
              }
      
              ListNode slow = head;
              ListNode fast = head.next;
      
              while (fast != null && fast.next != null) {
                  slow = slow.next;
                  fast = fast.next.next;
                  if (slow == fast) {
                      return true;
                  }
              }
      
              return false;
      
      //        第二种解法
      //        if (head == null || head.next == null) {
      //            return false;
      //        }
      //        ListNode slow = head;
      //        ListNode fast = head.next;
      //        while (slow != fast) {
      //            if (fast == null || fast.next == null) {
      //                return false;
      //            }
      //            slow = slow.next;
      //            fast = fast.next.next;
      //        }
      //        return true;
          }
      }

       
    • 142. 环形链表 II
      可以设置一个set,走过一个就存一个,如果有重复,那第一个重复的就是相交节点。
      另外就是设置快慢指针。设置快慢指针依旧是快的走两步,慢的走一步。快慢指针相遇的时候,再设置一个可以判断快慢指针的解法可以直接看题解。

      利用链表的性质解题的时候,fast 和slow 要同时指向head,然后开始向后遍历才可以

      整体过程就是,先遍历找到重合的点;然后再找相交的点。
       
          public ListNode detectCycle(ListNode head) {
              if (head == null) {
                  return null;
              }
              ListNode slow = head;
              ListNode fast = head;
              while (fast != null) {
                  if (fast.next != null) {
                      slow = slow.next;
                      fast = fast.next.next;
                  } else {
                      return null;
                  }
                  if (fast == slow) {
                      ListNode ptr = head;
                      while (ptr != slow) {
                          ptr = ptr.next;
                          slow = slow.next;
                      }
                      return ptr;
                  }
              }
              return null;
          }

       
    • 160. 相交链表 di
      对于两个链表,一个指针flagA走链表a,一个指针flagB走链表b,flagA 走完了链表A 后,再走链表b,flagB 走完了链表b 后再走链表A,两个指针走过的距离一样,那么他们就会相遇。

      此题的“相交”如图

       
          public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
      
              if (headA == null || headB == null) {
                  return null;
              }
              ListNode preA = headA;
              ListNode preB = headB;
              while (preA != preB) {
                  if (preA != null) {
                      preA = preA.next;
                  } else {
                      preA = headB;
                  }
                  if (preB != null) {
                      preB = preB.next;
                  } else {
                      preB = headA;
                  }
              }
              return preA;
          }


       
    • 19. 删除倒数第N个节点 快指针先走n步
      删除倒数第N 个节点,首先得找到这个节点。用first 指针和second 指针,first 先走n 个节点,然后second 再开始。这样当first 走到尾的时候,second 就到了倒数第n 割接点。具体实现要考虑怎么方便怎么来。
       
          public ListNode removeNthFromEnd(ListNode head, int n) {
      
              ListNode hair = new ListNode(0, head);
      
              ListNode first = head;
              ListNode second = hair;
              for (int i = 0; i < n; i++) {
                  first = first.next;
              }
      
              while (first != null) {
                  first =first.next;
                  second = second.next;
              }
      
              second.next = second.next.next;
      
              return hair.next;
      
          }

  • 其他
    • 234. 回文链表 
      原创(@王漂亮)提示解法:left数组, left从后往前,指针从前往后,依次对比,slow, fast = head, head.next
      将值复制到数组中用前后双指针法;用递归;用快慢指针,找到中间,然后将其中一个进行翻转,然后进行比较  

      做法:找到第一个链表的结尾。如果奇数个,那么第一个链表多一个。然后翻转后面的链表,进行对比

              // 找到前半部分尾节点,并翻转后半部分链表(重点)
              // 这样就可以避免了链表奇偶性的判断
      public boolean isPalindrome(ListNode head) {
      
              if (head == null) {
                  return true;
              }
              // 找到前半部分尾节点,并翻转后半部分链表(重点)
              // 这样就可以避免了链表奇偶性的判断
              ListNode firstHalfEnd = endOfFirstHalf(head);
              ListNode reversedSecondHalfStart = reverseList(firstHalfEnd.next);
              ListNode p1 = head;
              ListNode p2 = reversedSecondHalfStart;
              boolean result = true;
              while (p1!= null && p2 != null) {
                  if (p1.val != p2.val) {
                      result = false;
                      break;
                  }
                  p1 = p1.next;
                  p2 = p2.next;
              }
              return result;
          }
      
          private ListNode reverseList(ListNode head) {
              ListNode prev = null;
              ListNode cur = head;
              while (cur != null) {
                  ListNode next = cur.next;
                  cur.next = prev;
                  prev = cur;
                  cur = next;
              }
              return prev;
          }
      
          // 有可能返回的事技术个数的中间打那个;有可能返回的事偶数个,前面那个链表的最后一个节点
          private ListNode endOfFirstHalf(ListNode head) {
              ListNode fast = head;
              ListNode slow = head;
              while (fast.next != null && fast.next.next != null) {
                  fast = fast.next.next;
                  slow = slow.next;
              }
              return slow;
          }

       
    • 328. 奇偶链表
      原创(@王漂亮)提示解法:保存下even_head, odd.next, even.next = odd.next.next, even.next.next
      奇偶链表就是将一个链表中的奇数位放到前部分,偶数位节点放到后部分。解法就是分离奇偶节点,然后合并。迭代解法的终止条件就是偶数节点为空或者偶数节点的next 为空。
      class Solution {
          public ListNode oddEvenList(ListNode head) {
              if (head == null || head.next == null || head.next.next == null) {
                  return head;
              }
              ListNode evenHead = head.next;
              ListNode odd = head;
              ListNode even = evenHead;
              while (even != null && even.next != null) {
                  odd.next = even.next;
                  odd = odd.next;
                  even.next = odd.next;
                  even = even.next;
              }
              odd.next = evenHead;
              return head;
          }
      }

       
    • 2. 两数相加
      原创(@王漂亮)提示解法:迭代(dummy_node), 最后不要忘了 if carry>0: h.next = ListNode(1)
      两数字相加,注意进位就好了
      class Solution {
          public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
              int flag = 0;
              ListNode head = null;
              head = new ListNode();
              ListNode temp = head;
              while (l1 != null || l2 != null) {
      
                  int l1Val;
                  int l2Val;
                  if (l1 == null) {
                      l1Val = 0;
                  } else {
                      l1Val = l1.val;
                      l1 = l1.next;
                  }
      
                  if (l2 == null) {
                      l2Val = 0;
                  } else {
                      l2Val = l2.val;
                      l2 = l2.next;
                  }
      
                  int result = l1Val + l2Val + flag;
                  ListNode listNode = new ListNode();
                  listNode.val = result % 10;
                  flag = result / 10;
                  temp.next = listNode;
                  temp = temp.next;
              }
      
              if (flag >= 1) {
                  ListNode listNode = new ListNode();
                  listNode.val = flag;
                  temp.next = listNode;
      
              }
              return head.next;
          }
      }

       
    • 148. 排序链表
      原创(@王漂亮)提示解法:mergesort (slow, fast找到mid,再分别mergesort); merge dummynode
      (看不懂)



       

注:一般要分为两段的链表的双指针slow,fast = head, head.next; 不需要分为两段的slow,fast = head, head  对于slow 和fast,都设置head 为初始节点就行。

字符串

  • 滑动窗口
    • 76. 最小覆盖子串 - M while all(map(lambda x: s_c[x] >= t_c[x], t_c.keys())):

      题目:给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
      保证如果有答案,那么保证它是唯一的答案。
      (看不懂)(hard 做不出来)
    •     public static String minWindow(String s, String t) {
              
              Map<Character, Integer> needsMap = Maps.newHashMap();
              Map<Character, Integer> windowMap = Maps.newHashMap();
      
              Integer minLen = Integer.MAX_VALUE;
              // 用来标记结果数组在“s” 中的起始位置
              int start = 0;
      
              int left, right;
              left = right = 0;
              
              for (int i = 0; i < t.length(); i++) {
                  needsMap.put(t.charAt(i), needsMap.getOrDefault(t.charAt(i), 0) + 1);
              }
      
              int match = 0;
      
              while (right < s.length()) {
                  char c1 = s.charAt(right);
                  if (needsMap.containsKey(c1)) {
                      windowMap.put(c1, windowMap.getOrDefault(c1, 0) + 1);
                      if (windowMap.get(c1).equals(needsMap.get(c1))) {
                          match ++;
                      }
                  }
                  right ++;
      
                  while (match == needsMap.size()) {
      
                      if (right - left < minLen) {
                          start = left;
                          minLen = right - left;
                      }
      
                      // 处理缩小
                      char c2 = s.charAt(left);
                      if (needsMap.containsKey(c2)) {
                          windowMap.put(c2, windowMap.get(c2) - 1);
                          if (!windowMap.get(c2).equals(needsMap.get(c2))) {
                              match --;
                          }
                      }
                      left ++;
                  }
              }
              return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
      
          }

    • 438. 找字符串中所有字母的异位词
      (看不懂)
          public static List<Integer> findAnagrams(String s, String t) {
      
              List<Integer> result = com.google.common.collect.Lists.newArrayList();
              int left, right;
              left = right = 0;
              Map<Character, Integer> needs = Maps.newHashMap();
              Map<Character, Integer> windows = Maps.newHashMap();
      
              for (int i = 0; i < t.length(); i++) {
                  Character c = t.charAt(i);
                  needs.put(c, needs.getOrDefault(c, 0) + 1);
              }
      
              int match = 0;
              while (right < s.length()) {
      
                  char c1 = s.charAt(right);
                  if (needs.containsKey(c1)) {
                      windows.put(c1, windows.getOrDefault(c1, 0) + 1);
                      if (Objects.equals(windows.get(c1), needs.get(c1))) {
                          match ++;
                      }
                  }
                  right ++;
      
                  while (match == needs.size()) {
                      if (right - left == t.length()) {
                          result.add(left);
                      }
                      char c2 = s.charAt(left);
                      if (needs.containsKey(c2)) {
                          windows.put(c2, windows.get(c2) - 1);
                          if (windows.get(c2) < needs.get(c2)) {
                              match --;
                          }
                      }
                      left ++;
                  }
              }
              return result;
          }
    • 3. 无重复字符的最长子串 - M
      要求无重复字符的最长子串,即找出从每一个字符开始的,不包含重复字符的最长子串,最长的那个字符串即为答案。
      此题很明显特征,使用“滑动窗口”。使用两个指针,标识子串的前指针和后指针。同时还需要一个数据结构来判断是否有重复字符,可以用set。前指针移动的时候,从set 中移除;后指针移动的时候,向set 中添加。
          public static int lengthOfLongestSubString(String s) {
      
              int left, right;
              left = right = 0;
              int result = 0;
              Map<Character, Integer> windows = new HashMap<>();
              while (right < s.length()) {
                  char c1 = s.charAt(right);
                  windows.put(c1, windows.getOrDefault(c1, 0) + 1);
                  right++;
      
                  while(windows.get(c1) >1) {
                      char c2 = s.charAt(left);
                      windows.put(c2, windows.get(c2) - 1);
                      left ++;
                  }
                  result = Math.max(result, right - left);
              }
              return result;
          }

    • 340. 至多包含 K 个不同字符的最长子串 - H if not di[s[start]]: di.pop(s[start]) # 记得pop if value == 0
      (被锁)
  • DP

    最长回文子串,dp[i][j] 表示从i 到j 是否为回文子串。
    两个for 循环,第一个是循环是字符串的长度,从2 开始
    第二个for 循环是枚举左下标,从0 开始

    最长回文子序列的长度,dp[i][j] 代表字符串从i 到j 的回文子序列的长度。两次遍历,i 从最后开始。
    • 5. 最长回文子串(最长回文子数组)
      原创(@王漂亮)提示解法:- M dp[i][j], dp[0][0]=1, 要for r再for l以确保dp[i+1][j-1赋值

      如果一个子串,是回文串,并且长度大于2,那么如果将其首尾两个字母去除,它仍然是回文串。通过这种思路,可以使用动态规划的方法解决本题。使用P(i,j) 表示字符串s 的第i 到j 个字母组成的串(表示为s[i:j] )是否为回文串:
      · P(i,j) = true,如果S i ... S j 是回文串
      · P(i,j) = false,其他情况
      对于其他情况,包含两种可能
      · s[i,j] 本身不是一个回文串
      · i > j,此时s[i,j] 本身不合法
      那么可以写出动态规划的状态转移方程
      · P(i,j) = P(i+1, j-1) ∧ (Si == Sj)
      也就是说,只有s[i+1:j-1] 是回文串,并且s 的第i 和j 个字母相同时,s[i:j] 才会是回文串
      而上文是建立在子串长度大于2 的前提,我们还需要考虑动态规划的边界条件,即子串的长度为1 或者2。对于长度为1 的子串,它显然是回文串。对于长度为2 的子串,只要两个字母相同,也是一个回文串。因为可以写出其边界条件:
      · P(i, i) = true
      ` P(i, i+1) = (Si == S i+1)
      根据这个思路,就可以完成动态规划了。最终答案即为所有P(i, j) 中j - i + 1(即子串长度)的最大值。

       
      class Solution {
          public String longestPalindrome(String s) {
      
              int len = s.length();
              if (len < 2) {
                  return s;
              }
              int maxLen = 1;
              int begin = 0;
              // dp[i][j] 表示s[i...j] 是否为回文串
              boolean[][] dp = new boolean[len][len];
      
              for (int i = 0; i < len; i++) {
                  dp[i][i] = true;
              }
              char[] charArray = s.toCharArray();
              // 递推开始
              // 枚举子串长度,长度从2 开始,长度为2、3、4、5...
              for (int L = 2; L <= len; L++) {
      
                  // 枚举左边界,左边界上限设置可以宽松些
                  // 上面for 循环设置的是串的长度;下面的是左边界在哪里
                  for (int i = 0; i < len; i++) {
                      // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                      int j = L + i - 1;
                      // 如果右边界超限制,则退出循环
                      if (j >= len) {
                          break;
                      }
                      if (charArray[i] != charArray[j]) {
                          dp[i][j] = false;
                      } else if (charArray[i] == charArray[j]){
                          // 字符相等,且长度小于等于2
                          if (j - i <= 2) {
                              dp[i][j] = true;
                          } else {
                              // 且字符相等,长度大于2
                              dp[i][j] = dp[i + 1][j - 1];
                          }
                      }
      
      
                      // 只要dp[i][j] == true 成立,就表示字符串s[i...j] 是回文子串,此时记录长度和起始位置
                      // 每次循环要记录一下“最长的”
                      if (dp[i][j] && j - i + 1 > maxLen) {
                          maxLen = j - i + 1;
                          begin = i;
                      }
      
                  }
      
              }
      
              return s.substring(begin, begin + maxLen);
          }
      }



       
    • 1143. 最长公共子序列 - M dp[i+1][]j+1], dp = (max(dp[i-1][j], dp[i][j-1])) or dp[i-1][j-1]+1
      是典型的二维动态规划的问题。


       
    • 91. 解码方法 dp[i] = dp[i-1] + dp[i-2] (有条件的), 1. s[i] != "0"; 10<=s[i-1:i+1]<=26

      用dp 数组表示,dp[i] 表示到了第i 个字符的时候,共有多少种解法。
      进行到第i 个字符的时候,判断使用一个字符还是两个字符进行解码
       
      public static int numDecodings(String s) {
      
              int n = s.length();
              int[] dp = new int[n + 1];
              dp[0] = 1;
              for (int i = 1; i <= n; i++) {
                  if (s.charAt(i - 1) != '0') {
                      dp[i] = dp[i - 1];
                  }
                  // 这里包含条件"当前数字为0" 的情况
                  if (i > 1 && s.charAt(i - 2) != '0' && calculateSum(s.charAt(i - 2), s.charAt(i - 1)) <= 26) {
                      dp[i] += dp[i - 2];
                  }
              }
              return dp[n];
      
          }
      
          public static Integer calculateSum(char a, char b) {
              return (a - '0') * 10 + (b - '0');
          }

       
  • 其他高频题
    • 28. 实现 strStr() - E
       
      public static int strStr(String haystack, String needle) {
              int n = haystack.length();
              int m = needle.length();
      
              for (int i = 0; i + m <= n; i ++) {
                  boolean flag = true;
                  for (int j = 0; j < m; j++) {
                      if (haystack.charAt(i + j) != needle.charAt(j)) {
                          flag = false;
                          break;
                      }
                  }
                  if (flag) {
                      return i;
                  }
              }
              return -1;
          }
    • 14. 最长公共前缀 - E 先排序再比较first,last; for z in zip(*strs): if len(set(z)) == 1:res += z[0]

      LCP
       

      编写一个函数来查找字符串数组中的最长公共前缀。
      如果不存在公共前缀,返回空字符串 ""。

       

          public String longestCommonPrefix(String[] strs) {
              if (strs == null || strs.length == 0) {
                  return "";
              }
              // 第一个字符串的长度
              int length = strs[0].length();
              // 字符串数组的个数
              int count = strs.length;
              for (int i = 0; i < length; i++) {
                  char c = strs[0].charAt(i);
                  for (int j = 1; j < count; j++) {
                      if (i == strs[j].length() || strs[j].charAt(i) != c) {
                          return strs[0].subSequence(0, i).toString();
                      }
                  }
              }
              return strs[0];
          }

    • 125. 验证回文串 - E s[start].isalnum()
       
          public static boolean isPalindrome(String s) {
              StringBuilder stringBuilder = new StringBuilder();
              int length = s.length();
              for (int i = 0; i < length; i++) {
                  char c = s.charAt(i);
                  if (Character.isLetterOrDigit(c)) {
                      stringBuilder.append(Character.toLowerCase(c));
                  }
              }
              StringBuilder reversed = new StringBuilder(stringBuilder.toString()).reverse();
              return reversed.toString().equals(stringBuilder.toString());
          }
    • 49. 字母异位词分组 - M di["".join(sorted(s))].append(s)
       
          public List<List<String>> groupAnagrams(String[] strs) {
      
              Map<String, List<String>> map = new HashMap();
              for (String string : strs) {
                  char[] array = string.toCharArray();
                  Arrays.sort(array);
                  String key = new String(array);
                  List list = map.getOrDefault(map.get(key), new ArrayList());
                  list.add(array);
                  map.put(key, list);
              }
              return new ArrayList<List<String>>(map.values());
          }

  • 其他杂题
    • 8. 字符串转换整数 (atoi) - M try: int(s[:idx]) except: break


       
    • 227. 基本计算器 II stack存数字和+-*/,数字一添加结束就看能不能做*/,最后一起算+- 
       
          public int calculate(String s) {
      
              Deque<Integer> stack = new LinkedList<Integer>();
              char preSign = '+';
              int num = 0;
              int n = s.length();
              for (int i = 0; i < n; i++) {
                  if (Character.isDigit(s.charAt(i))) {
                      num = num * 10 + s.charAt(i) - '0';
                  }
                  if (!Character.isDigit(s.charAt(i))
                          && s.charAt(i) != ' '
                          || i == n - 1) {
                      switch(preSign) {
                          case '+':
                              stack.push(num);
                              break;
                          case '-':
                              stack.push(-num);
                              break;
                          case '*':
                              stack.push(stack.pop() * num);
                              break;
                          case '/':
                              stack.push(stack.pop() / num);
                              break;
                      }
                      preSign = s.charAt(i);
                      num = 0;
                  }
              }
              int ans = 0;
              while(!stack.isEmpty()) {
                  ans += stack.pop();
              }
              return ans;
          }

       

二叉树

  • 二叉树的构造
    • 297. 序列化与反序列化 - H se: return " ".join(res); de: nums = iter(data.split()), num = next(nums)
       
          public String serialize(TreeNode root) {
              return rserialize(root, "");
          }
        
          public TreeNode deserialize(String data) {
              String[] dataArray = data.split(",");
              List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
              return rdeserialize(dataList);
          }
      
          public String rserialize(TreeNode root, String str) {
              if (root == null) {
                  str += "None,";
              } else {
                  str += str.valueOf(root.val) + ",";
                  str = rserialize(root.left, str);
                  str = rserialize(root.right, str);
              }
              return str;
          }
        
          public TreeNode rdeserialize(List<String> dataList) {
              if (dataList.get(0).equals("None")) {
                  dataList.remove(0);
                  return null;
              }
        
              TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0)));
              dataList.remove(0);
              root.left = rdeserialize(dataList);
              root.right = rdeserialize(dataList);
          
              return root;
          }

       
    • 144. 二叉树前序遍历 根左右

       
    • 94. 二叉树中序遍历 左根右

       
    • 145. 二叉树后序遍历 左右根; 迭代(dfs+stack从上到下右到左):r, stack = [], [root] while stack:

       
    • 102. 层序遍历 单队列,q = deque([(root, layer)]),q.popleft()

       
    • 103. 锯齿形层次遍历 双队列, cur, nex = deque([root]), deque()

       
    • 二叉树的对角线遍历 递归,helper(node, layer)

       
    • 105. 前序与中序构造二叉树 递归,自调,idx = inorder.index(preorder[0])

       
    • 106. 中序与后序构造二叉树

       
  • 高频题目
    • 101. 对称二叉树 helper: isMatch(left, right)
    • 116. 填充每个节点的下一个右侧节点指针 对于任意一次递归,只需要考虑如何设置子节点的 next 属性
    • 117. 填充每个节点的下一个右侧节点指针 II 思路同上,在l&r的时候先设置好l,追加设置r or l,很复杂多看看
    • 104. 二叉树的最大深度 return max(maxdepth(root.left), maxdepth(root.right))+1
    • 662. 二叉树最大宽度 self.left[], 每层碰到的第一个节点为left, dfs(node, layer, pos*2(+1))
    • 543.二叉树的直径 helper: maxgain, self.res = max(left + right + 1, self.res)
    • 236. 二叉树的最近公共祖先 helper(root), if left + right + mid >=2: res, return left or right or mid

       
    • 112. 路径综合

      这道题类似层序遍历,可以使用广度优先搜索算法。但是不需要像层序遍历那样,计算queue 中的节点的个数。
      广度优先搜索:
       
      public boolean hasPathSum(TreeNode root, int targetSum) {
      
              if (root == null) {
                  return false;
              }
              Queue<TreeNode> nodeQueue = new LinkedList<>();
              Queue<Integer> valueQueue = new LinkedList<>();
      
              nodeQueue.offer(root);
              valueQueue.offer(root.val);
      
              while (!nodeQueue.isEmpty()) {
                  // 这个不像层序遍历,这个不需要感知queue 的长度
                  TreeNode tempNode = nodeQueue.poll();
                  Integer tempVal = valueQueue.poll();
      
                  // 如果当前节点是叶子节点,那么就进行判断
                  if (tempNode.left == null && tempNode.right == null) {
                      if (tempVal == targetSum) {
                          return true;
                      }
                      continue;
                  }
      
                  // 子节点进队列
                  if (tempNode.right != null) {
                      nodeQueue.offer(tempNode.right);
                      valueQueue.offer(tempNode.right.val + tempVal);
                  }
                  if (tempNode.left != null) {
                      nodeQueue.offer(tempNode.left);
                      valueQueue.offer(tempNode.left.val + tempVal);
                  }
              }
              return false;
          }
      递归:
       
          public boolean hasPathSum(TreeNode root, int targetSum) {
      
              if (root == null) {
                  return false;
              }
              if (root.left == null && root.right == null) {
                  return targetSum == root.val;
              }
              return hasPathSum(root.left, targetSum - root.val)
                      || hasPathSum(root.right, targetSum - root.val);
      
          }



       
    • 113. 路径总和 II



       
    • 437.路径总和 III
    • 124. 最大路径和 - H helper: maxgain, self.res = max(left + right + root.val, self.res)
  •  二叉搜索树
    • 98. 验证二叉搜索树 helper(root, low = float("-inf"), high = float("inf"))
      二叉搜索树的性质,中序遍历得到一个正序数组
    • 426. BST转排序的双向链表 中序, 处理当前节点,last.right = cur, cur.left = last
    • 450. 删除BST中的节点 - M 找到后三种情况, 无子节点/一个子节点/有两子结点(max,remove_max)
    • 删除区间内的节点

使用大顶堆做正序排序;使用小顶堆做逆序排序。

构建堆,就是从最后一个非叶子节点开始,如果构建大顶堆,就是将每个parent 与child 进行对比,如果child 大于parent,那么进行调整。同时调整到child 位置的原parent 可能还是小于其child,所以要继续调整。

堆在算法题目中的应用主要包括以下几点:

  • TopK 问题 (尤其是大数据处理)
  • 优先队列
  • 利用堆求中位数

/**
 * 调整为大顶堆(此方法只是一个调整的过程,并不是建堆的)
 * @param arr   待调整的数组
 * @param parent   当前父节点的下标
 * @param length   需要对多少个元素进行调整
 */
private static void adjustHeap(int[] arr, int parent, int length){
    //临时保存父节点
    int temp = arr[parent];
    //左子节点的下标
    int child = 2 * parent + 1;
    //如果子节点的下标大于等于当前需要比较的元素个数,则结束循环
    while(child < length){
        //判断左子节点和右子节点的大小,若右边大,则把child定位到右边
        if(child + 1 < length && arr[child] < arr[child + 1]){
            child ++;
        }
        //若child大于父节点,则交换位置,否则退出循环
        if(arr[child] > temp){
            //父子节点交换位置
            arr[parent] = arr[child];
            //因为交换位置之后,不能保证当前的子节点是它子树的最大值,所以需要继续向下比较,
            //把当前子节点设置为下次循环的父节点,同时,找到它的左子节点,继续下次循环
            parent = child;
            child = 2 * parent + 1;
        }else{
            //如果当前子节点小于等于父节点,则说明此时的父节点已经是最大值了,
            //因此无需继续循环
            break;
        }
    }
    //把当前节点值替换为最开始暂存的父节点值
    arr[parent] = temp;
}

public static void main(String[] args) {
    int[] arr = {4,1,9,3,7,8,5,6,2};
    // 构建一个大顶堆,从最下面的非叶子节点开始向上遍历
    // 这才是建堆的过程
    for (int i = arr.length/2 - 1 ; i >= 0; i--) {
        adjustHeap(arr,i,arr.length);
    }
    System.out.println(Arrays.toString(arr));
}     



//堆排序,大顶堆,升序
private static void heapSort(int[] arr){
    //构建一个大顶堆,从最下面的非叶子节点开始向上遍历
    for (int i = arr.length/2 - 1 ; i >= 0; i--) {
        adjustHeap(arr,i,arr.length);
    }
    System.out.println(Arrays.toString(arr));
    //循环执行以下操作:1.交换堆顶元素和末尾元素 2.重新调整为大顶堆
    for (int i = arr.length - 1; i > 0; i--) {
        //将堆顶最大的元素与末尾元素互换,则数组中最后的元素变为最大值
        int temp = arr[i];
        arr[i] = arr[0];
        arr[0] = temp;
        //从堆顶开始重新调整结构,使之成为大顶堆
        // i代表当前数组需要调整的元素个数,是逐渐递减的
        adjustHeap(arr,0,i);
    }

}

  • 347. 前 K 个高频元素 heapq.nlargest(k, c.keys(), key = c.get);长度为k的堆
  • 215. 数组中的第K个最大元素 堆;二分搜索牛逼
    (1)使用堆,构建大顶堆;
             使用堆的时候,一个是调整的方法
             public void adjust(int[] nums, int parent, int length){}
             另外一个是建堆的方法
             public void heapify(int[] nums){}; 这个方法里面要从第一个非叶子节点开始从下往上整
             然后是堆排序的方法,找第k 大的元素,只需要执行k 次即可。每次把最大的堆顶元素和最后一个元素替换,循环执行k 次。则数组中倒数第k 个元素就是解。

    (2)使用快排的思想,以一个key,小于的放左边,大于的放右边
            
     
  • 378. 有序矩阵中第K小的元素 堆,klogk: 一次添加右和下,pop出其中较小的,每次pop k-1,pop k次返回

    利用矩阵的性质,使用二分法
        public int kthSmallest(int[][] matrix, int k) {
            // 一共n 行
            int n = matrix.length;
            int left = matrix[0][0];
            int right = matrix[n - 1][n - 1];
            while (left < right) {
                int mid = (left + right) / 2;
                if (check(matrix, mid, k, n)) {
                    right = mid;
                } else {
                    left = mid + 1;
                }
            }
            return left;
        }
    
        public boolean check(int[][] matrix, int mid, int k ,int n) {
    
            int i = n - 1;
            int j = 0;
            int num = 0;
            while (i >=0 && j < n) {
                if (matrix[i][j] <= mid) {
                    num += i + 1;
                    j ++;
                } else {
                    i --;
                }
            }
            return num >= k;
    
        }



     
  • 218. 天际线问题(hard)

动态规划-

动态规划问题的一般形式就是“求最值”,比如求“最长递增子序列”,“最小编辑距离”。而求解最值的问题的核心就是“穷举”。这样最直观。
而动态规划的问题存在几个特点:

  1. 存在“重叠子问题”,如果暴力穷举,效率低下,所以需要“备忘录”或者“dp table”来优化穷举过程,避免不必要的计算。

  2. 存在“最优子结构”,这样才能通过子问题的最值得到原问题的最值。
    对最优子结构的理解,就是到了某一步的选择,可以得到一个最优的结果。所以说到了这个子选择的时候,结构是最优的。给它个名称,叫:最优子结构。

  3. 找出“状态转移方程”。动态规划的核心思想就是“穷举”,而动态规划问题中,穷举是通过状态转移方程来完成的。


解题步骤:
明确“状态” -> 定义dp 数组/函数的含义 -> 明确“选择” -> 明确base case
dp 数组中存的就是各个状态,通过选择各个状态,选择待选项,得到转移方程,结合base case,得到最终解。



举例: 

322. 凑零钱问题

1. 凑零钱 各个子问题间是相互独立的,互不干扰。
对于“凑零钱问题”,比如说求amount = 11 时的最少硬币数,如果凑出了amount = 10 的最少硬币数(子问题),只需要把子问题的答案加一就是原问题的答案。因为硬币的数量是不限制的,子问题之间没有相互制约,是互相独立的。
2. 列出状态转移方程。先确定“状态”,也就是原问题和子问题中变化的变量。由于硬币数量无限,所以唯一的状态就是目标金额amount。
3. 确定dp 数组的定义:当前目标金额是n,至少需要dp(n) 个硬币凑出该金额(意思就是凑出金额n 最少需要的金币数是dp[n])。
4. 然后确定“选择”并优化,也就是对于每个状态,可以做出什么选择改变当前状态。具体到此问题,无论目标金额是多少,选择就是从面额列表coins 中选择一个硬币,然后目标金额就会减少。
5. 最后明确base case。
(以上参考labuladong)


题目要求“最少”,即求最优解;要凑出总数为n 的最少枚硬币,和凑出n - coin 枚硬币是一样的问题,即存在子问题,是有重复子问题的;根据example 分析,f(11) = min(f(10), f(9), f(6)) + 1。
(参考:凑零钱问题 - 知乎

上述寥寥几行,就把一个动态规划问题的基本点都描述出来了.

  • 1、首先是有重复子问题;
  • 2、其次需要最优规划,即是子问题中的最优解再去找上一级。
  • 3、列出状态转移方程,其中最重要的就是确定谁是变化的量,这里就是总价amount
    public static int coinChange(int[] coins, int amount) {

        int[] dp = new int[amount];
        // 初始化数组,每个元素为amount + 1 的原因是为了避免混淆,初始化成别的也行,在最后做判断的时候确认一下就好
        for (int i = 0; i < amount; i++) {
            dp[i] = amount + 1;
        }

        // 第一层for 循环是对每个子结构做遍历查找,即amount 为1,为2,为3... 的时候
        for (int i = 0; i < dp.length; i++) {
            // 对每个选择做判断
            for (int c : coins) {
                if (i - c < 0) {
                    continue;
                }
                // 找出最小的
                dp[i] = Math.min(dp[i], dp[i - c] + 1);
            }

        }
        // 判断当前是否有结果,就和初始化的数字做呼应
        return dp[amount - 1] == amount ? -1 : dp[amount - 1];

    }

对于“最优子结构”,很多问题可能都具有“最优子结构”,但是不存在“重叠子问题”,所以不能被归为“动态规划系列”。如果想满足“最优子结构”,子问题间必须相互独立。

基础篇

对于动态规划,拿300 题和53 题举例子。两道题都是使用一维数组的,但是有点不一样,现在我总结不出来,理解一下,理解并总结不出来的话,那就记住好了。注意特点,300 是子序列,53 是子数组。

  • 300. 最长上升子序列
    原创(@王漂亮)提示解法:dp[n], if nums[i] > nums[j]: dp[i] = max(dp[i], dp[j]+1)

    使用一维数组dp[n],定义dp[i] 为考虑前i 个元素,以第i 个数字结尾的最长上升子序列的长度。其中nums[i] 必须被选取。
    在计算dp[i] 之前,已经计算出了dp[0 ... i-1] 的值,则状态转移方程为:
    dp[i] = max( dp[j] ) + 1, 其中 0 <= j < i 且nums[i] > nums[j]
    时间复杂度o(n方),思想就是两个for 循环,对于每一个元素,比较前面所有元素和当前nums[i] 的大小,如果小,那么就+1
     
    按照上述解题步骤:
    1)明确状态:就是题目要求,最长递增子序列的最长长度。

    2)dp 数组定义:数组存的就是各种状态,定义一维数组,dp[i] 表示以第i 个元素结尾最长上升子序列的长度。

    3)明确选择:第i 个数字选或者不选,然后写出状态转移方程。

    4)basecase:第0 个数字,第1 个数字。
     
        public int lengthOfLIS(int[] nums) {
    
            if (nums.length ==  0) {
                return 0;
            }
    
            // 定义dp 数组,dp[i] 表示以第i 个数字结尾,最长上升子序列的长度。
            int[] dp = new int[nums.length];
            dp[0] = 1;
            int maxAns = 1;
            // 我们从小到大计算 \textit{dp}dp 数组的值,在计算dp[i] 之前,我们已经计算出dp[0…i−1] 的值,则状态转移方程为:
            // dp[i]=max(dp[j])+1,其中0≤j<i且num[j]<num[i]
            for (int i = 0; i < nums.length; i++) {
                dp[i] = 1;
                for (int j = 0; j < i; j++) {
                    if (nums[i] > nums[j]) {
                        dp[i] = Math.max(dp[i], dp[j] + 1);
                    }
                }
                maxAns = Math.max(maxAns, dp[i]);
            }
            return maxAns;
        }
    题解:首先明确dp 数组所存数据的含义,假设dp[0...i-1] 都已知,想办法求出dp[i]。
    如果不能明确出dp 数组的含义,那么可能是dp 数组存储到信息不够,不足以存储下一步,需要把dp 数组扩大成二维或者三维数组。

     
  • 53. 最大子序和 dp[n], dp[i] = max(nums[i], dp[i-1] + nums[i])

    给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

    使用一维数组f[n] 来进行动态规划设计,f[i] 代表以第i 个数字结尾的“连续子数组最大和”,那么答案就是max(f[i])
    因此就是求出每个未知的f(i),然后返回f 数组中的最大值即可
    那么就是要考虑num[i] 单独成为一段,还是加入前面那一段,这取决于num[i] 和f(i-1)+num[i] 的大小。于是状态转移方程为:
    f(i) = max{f(i-1) + nums[i], nums[i]}

    按照上述四段式:状态确定;dp 数组确定(存放各个状态,用什么样的dp table);判断如何选择(状态转移方程);base case
     
    // 两种解法,一看就懂
    
    
    // 第一种
        public int maxSubArray(int[] nums) {
            if (nums.length == 0) {
                return 0;
            }
            int[] dp = new int[nums.length];
    
            dp[0] = nums[0];
            int ans = nums[0];
            for (int i = 1; i < nums.length; i++) {
    
                dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
                ans = Math.max(dp[i], ans);
    
            }
            return ans;
        }
    
    
    // 第二种
    
    
        public int maxSubArray(int[] nums) {
            int pre = 0, maxAns = nums[0];
            for (int x : nums) {
                pre = Math.max(pre + x, x);
                maxAns = Math.max(maxAns, pre);
            }
            return maxAns;
        }

     
  • 152. 最大乘积子序列 dp[n][2], return max(dp, key=lambda x:x[1])[1]
    这道题是求子序列。类比53,可以知道,53 的解法对于此题不适用。因为当前位置的最优解未必是由前一个位置的最优解得到的。
    可以根据正负性进行分类讨论。
    考虑当前一位置如,如果是一个负数,那么希望它前一个位置结尾的某个段的积也是负数,这样就可以负负得正,并且希望这个积尽可能“负得更多”;如果当前一个数字是正数,那么就希望它前一个位置结尾的某个段的积也是正数,并且希望它尽可能的大。
    所以除了f max(i),我们需要再维护一个f min(i),状态转移方程为:
    f max(i) = max{f max(i - 1) * ai, f min(i - 1) * ai, ai}
    f min(i) = min{f min(i - 1) * ai, f max(i - 1) * ai, ai}

    而此题的状态转移方程,第i 个状态只和第i - 1 个状态有关,类比53 题,所以根据“滚动数组”的思想,可以用两个变量来维护i - 1 的状态,就不用两个数组了。
     
        public int maxProduct(int[] nums) {
            int length = nums.length;
            int[] maxF = new int[length];
            int[] minF = new int[length];
            maxF[0] = nums[0];
            minF[0] = nums[0];
            int ans = maxF[0];
            for (int i = 1; i < length; i++) {
                maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
                minF[i] = Math.min(maxF[i - 1] * nums[i], Math.min(nums[i], minF[i - 1] * nums[i]));
    
                ans = Math.max(ans, maxF[i]);
            }
            return ans;
        }

  • 279. 完全平方数 dp[n+1],
    动态规划的技术建立在重用中间解的结果来计算最终解的思想之上。几乎所有动态规划,首先都会创建一个一维或多维数组DP 来保存中间子解的值,通常数组最后一个值代表最终解。此题是给定正整数n,找出最小的可以加和为此数的完全平方数的个数。

    可以依据题目要求写出状态表达式:f[i] 表示最少需要多少个数的平方来表示整数i。
    而这些数字肯定落在区间[1, 根号n] 区间。可以枚举找出这些数字,假设当前枚举到j,那么我们还需要若干数的平方,构成i - (j平方)。此时就发现了该子问题和原问题类似,只是规模变小了。

    其中f[0] = 0 为边界条件,实际上我们无法表示数字0,只是为了保证状态转移过程中遇到j 恰为(根号j)的情况合法。
    同时因为计算f[i] 时所需要用到的状态只有f[i - (j平方)],必然小于i,因此我们只需要从小到大地枚举i 来计算f[i] 即可。
     
        public int numSquares(int n) {
            int[] f = new int[n + 1];
            for (int i = 0; i <= n; i++) {
                int min = Integer.MIN_VALUE;
                for (int j = 1; j * j <= i; j++) {
                    min = Math.min(min, f[i - j * j]);
                }
                f[i] = min + 1;
            }
            return f[n];
        }



    ​​​
  • 322. 找零钱问题
     
        public static int coinChange(int[] coins, int amount) {
    
            int[] dp = new int[amount];
            // 初始化数组,每个元素为amount + 1 的原因是为了避免混淆,初始化成别的也行,在最后做判断的时候确认一下就好
            for (int i = 0; i < amount; i++) {
                dp[i] = amount + 1;
            }
    
            // 第一层for 循环是对每个子结构做遍历查找,即amount 为1,为2,为3... 的时候
            for (int i = 0; i < dp.length; i++) {
                // 对每个选择做判断
                for (int c : coins) {
                    if (i - c < 0) {
                        continue;
                    }
                    // 找出最小的
                    dp[i] = Math.min(dp[i], dp[i - c] + 1);
                }
    
            }
            // 判断当前是否有结果,就和初始化的数字做呼应
            return dp[amount - 1] == amount ? -1 : dp[amount - 1];
    
        }

  • 其他
    • 70. 爬楼梯 dp[n+1], dp[0], dp[1], dp[2] = 0, 1, 2 ;dp[i] = dp[i-2] + dp[i-1]
       
          public static int climbStairs(int n) {
              if (n == 1) {
                  return 1;
              }
              if (n == 2) {
                  return 2;
              }
              int pre = 1;
              int cur = 2;
              int result = 3;
              for (int i = 3; i <= n; i++) {
                  result = pre + cur;
                  pre = cur;
                  cur = result;
              }
              return result;
          }
    • 198. 打家劫舍 dp[n], dp[i] = max(dp[i-1], dp[i-2]+nums[i])
      一维数组进行动态规划
       
          public int rob(int[] nums) {
              if (nums == null || nums.length == 0) {
                  return 0;
              }
              int length = nums.length;
              if (nums.length == 1) {
                  return nums[0];
              }
              int dp[] = new int[length];
              dp[0] = nums[0];
              dp[1] = Math.max(nums[0], nums[1]);
              for (int i = 2; i < length; i++) {
                  dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
              }
              return dp[length -1 ];
          }
    • 62. 不同路径。题目为一个m x n 的网格,即m 行n 列,左上角有个机器人,每次只能往右或者往下移动一个格,请问移动到右下角一共有多少条路径。
      使用动态规划,用f(i, j) 表示从左上角走到(i, j) 的路径数量,其中i 和j 的范围分别是[0, m) 和[0, n)。使用二维数组进行动态规划。
      状态转移方程为:
      f(i, j) = f(i - 1, j) + f(i, j - 1)
      边界条件,第一行和第一列都是1。
       
          public int uniquePaths(int m, int n) {
      
              int[][] f = new int[m][n];
              for (int i = 0; i < m; i++) {
                  f[i][0] = 1;
              }
              for (int j = 0; j < n; j++) {
                  f[0][j] = 1;
              }
              for (int i = 1; i < m; i++) {
                  for (int j = 1; j < n; j++) {
                      f[i][j] = f[i - 1][j] + f[i][j - 1];
                  }
              }
              return f[m - 1][n - 1];
          }

       
    • 编辑距离
      给定两个字符串s1 和s2,计算出将s1 转换成s2 所使用的最少操作数。可以对一个字符串进行如下操作:插入、删除、替换。
          public static int minDistance(String s1, String s2) {
      
              int m = s1.length();
              int n = s2.length();
              int[][] dp = new int[m + 1][n + 1];
              // base case
              for (int i = 1; i <= m; i++) {
                  dp[i][0] = i;
              }
              for (int j = 1; j < n; j++) {
                  dp[0][j] = j;
              }
              for (int i = 1; i <= m; i++) {
                  for (int j = 1; j <= n; j++) {
                      if (s1.charAt(i) == s2.charAt(j)) {
                          dp[i][j] = dp[i - 1][j - 1];
                      } else {
                          dp[i][j] = Math.min(dp[i - 1][j] + 1, Math.min(dp[i][j - 1] + 1, dp[i - 1][j - 1]));
                      }
                  }
              }
              return dp[m][n];
          }
    • 高楼扔鸡蛋
      框架:这个问题有什么“状态”,有什么“选择”,然后穷举。
      1. “状态”就是当前拥有的鸡蛋数k 和需要测试的楼层数N。随着测试的进行,鸡蛋个数减少,楼层搜索范围减少,对应的就是状态的变化。
      2. “选择”其实就是选择哪层扔鸡蛋。
      3. 明确了“状态”和“选择”,就基本形成了动态规划的基本思路。肯定是个二维的dp 数组或者带有两个参数的dp 函数来表示状态转移;外加一个for 循环来遍历所有的选择,选择最优的选择更新状态。

      我们在选择第i 层扔鸡蛋之后,就可能出现两种情况:鸡蛋碎了,鸡蛋没碎。这时候,状态转移方程就来了:
      1. 鸡蛋碎了,那么鸡蛋个数K 就减一,搜索的楼层区间应该从[1, N] 变为[1, i - 1]。
      2. 鸡蛋没碎,那么鸡蛋个数K 不变,搜索的楼层区间应该从[1, N] 变为[i + 1, N]。


       

背包问题

dp对容量的init都是dp[v+1], 从空开始init 如果要减少空间的话,把dp[i]省掉,dp[v+1]的循环逆序

  • 01背包问题

    01  背包问题看这个,一看就懂:背包问题 - 知乎。只看文字,别看代码。
    这个也很不错:动态规划之背包问题系列 - 知乎
     
    class Solution {
    
        /**
         * @param v v[i] 代表第i 个物品的体积
         * @param w w[i] 代表第i 个物品的价值
         * @param V 背包的总体积
         * @return
         */
        public int findMax(int[] v, int[] w, int V) {
    
            // 物品个数
            int n = v.length;
    
            // 构造二维dp 数组。这个二维数组整体多了一个0 行和0 列,所以不用初始化。
            // 否则就要初始化第一行,小于第i 个物品体积的遍历体积都是0,大于等于第i 个物品体积的遍历体积的位置数字都是第i 个物品的价值。i = 0
            // n 个物品,所以有n 行;因为背包总体积是V,所以遍历V,从0 开始
            // 数组初始化,各个元素都是0
            int[][] f = new int[n + 1][V + 1];
    
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= V; j++) {
                    // 如果第i 个物品的体积大于当前遍历到的最大的体积,那么当前物品肯定不能选择
                    if (v[i - 1] > j) {
                        f[i][j] = f[i -1][j];
                    } else {
                        // 转移方程计算f(i,j) 的大小
                        // 分为选择还是不选择
                        f[i][j] = Math.max(f[i][j - v[i - 1]] + w[i-1], f[i - 1][j]);
                    }
                }
            }
            return f[n][V];
    
        }
    
    
        public int findMax2(int[] v, int[] w, int V) {
            // 可以优化控件,但是不能优化时间,时间还是O(n方)
            // 由状态转移方程可以看出,第i 行结果只和第i - 1 行结果有关。而为了防止数据被覆盖,所以j 要从后往前遍历
    
            int n = v.length;
            int[] dpTable = new int[V + 1];
            // 需要初始化
            for (int k = w[0]; k <= V; k++) {
                dpTable[k] = w[0];
            }
    
            for (int i = 1; i <= n; i++) {
                for (int j = V; j >=1; j--) {
                    if (v[i - 1] > j) {
                        // 如果第i 个物品的体积大于遍历体积数字,那么不变(还是上一行的结果)
                    } else {
                        // 否则做状态转移,判断选不选第i 个物品
                        dpTable[j] = Math.max(dpTable[j - v[i - 1]] + w[i - 1], dpTable[j]);
                    }
                }
            }
            return dpTable[V];
        }
    
        public static void main(String[] args) {
            Solution solution = new Solution();
            int[] v = {5,3,4,2};
            int[] w = {60,50,70,30};
    
            System.out.println(solution.findMax2(v, w, 5));
        }
    
    }
    • 416. 分割等和子集

      对于背包问题,要判断当前有几个“状态”。比如说,容量为w 的背包,n 个物品,就两个状态,用二维数组表示。如:dp[i][j]

      这道题定义dp[i][j] 为:前i 个元素是否有可以满足和为j 的子数组
      public boolean canPartition(int[] nums) {
      
              int sum = 0;
      
              int length = nums.length;
              if (length < 2) {
                  return false;
              }
      
              for (int num : nums) {
                  sum += num;
              }
      
              if (sum % 2 != 0) {
                  return false;
              }
      
              int target = sum / 2;
      
              boolean[][] dp = new boolean[length + 1][target + 1];
              dp[0][0] = true;
      
              for (int i = 1; i <= length; i++) {
                  for (int j = 1; j <= target; j++) {
      
                      if (nums[i - 1] > j) {
                          // 不能选择
                          dp[i][j] = dp[i - 1][j];
                      } else {
                          dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
                      }
      
                  }
              }
              return dp[length][target];
          }


       
    • 494. 目标和 di={0:1}, nex_di, di.get(s, 0)

      回溯:
          // 回溯解法,其中选择列表就是加法或者减法,nums 的遍历在backtrack 中使用参数“i” 实现
      
          int result = 0;
          public int findTargetSumWays(int[] nums, int S) {
      
              if (nums.length == 0) {
                  return 0;
              }
              backtrack(nums, 0, 0, S);
              return result;
          }
      
          void backtrack(int[] nums, int i, int additionResult, int target) {
              // 判断条件等于length 而不是等于length - 1 的原因是,i == nums.length 的时候,还是需要继续进行循环的
              if (i == nums.length) {
                  if (additionResult == target) {
                      result ++;
                  }
                  return;
              }
      
              additionResult += nums[i];
              backtrack(nums, i + 1, additionResult, target);
              additionResult -= nums[i];
      
              additionResult -= nums[i];
              backtrack(nums, i + 1, additionResult, target);
              additionResult += nums[i];
          }
      
      动态规划:

       

  • 完全背包问题

    也看上面的链接就好(同下面的链接)。
    对于完全背包,先看这个链接:动态规划之背包问题系列 - 知乎
    再看这个链接:https://zhuanlan.zhihu.com/p/346625269对于完全背包的状态转移方程的理解:
     

    上面那个公式含义很明确,即不选择第i 种物品。

    对于下面的公式,如果确定放,背包中应该出现至少一件第i种物品,所以F[i][j]种至少应该出现一件第i种物品,即F[i][j]=F[i][j-C[i]]+W[i]。为什么会是F[i][j-C[i]]+W[i]?因为F[i][j-C[i]]里面可能有第i种物品,也可能没有第i种物品。我们要确保F[i][j]至少有一件第i件物品,所以要预留C[i]的空间来存放一件第i种物品。
     
    class Solution {
    
        /**
         * @param v 每个物品的体积
         * @param w 每个物品的价值(重量)
         * @param V 背包的容积
         * @return 最大价值(重量)
         */
        public int findCompleteBackpackResult(int[] v, int[] w, int V) {
    
            // n 个物品
            int n = v.length;
    
            int dp[][] = new int[n + 1][V + 1];
    
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= V; j++) {
                    if (v[i - 1] > j) {
                        dp[i][j] = dp[i - 1][j];
                    } else {
                        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - v[i - 1]] + w[i - 1]);
                    }
                }
            }
            return dp[n][V];
        }
    
        public int findCompleteBackpackResult2(int[] v, int[] w, int V) {
    
            // n 个物品
            int n = v.length;
    
            int[] dp = new int[V + 1];
    
            // 初始化dp 数组
            for (int k = v[0]; k <= V; k ++) {
                // k 是当前遍历到的容量,v[0] 是第一个物品的体积,w[0] 是第一个物品的价值(重量)
                dp[k] = k / v[0] * w[0];
            }
    
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= V; j++) {
                    if (v[i - 1] > j) {
                        // 如果第i 个物品的体积大于当前遍历到的容积,则第i 个物品无法选择
                    } else {
                        dp[j] = Math.max(dp[j], dp[j - v[i - 1]] + w[i - 1]);
                    }
                }
            }
    
            return dp[V];
        }
    
        public static void main(String[] args) {
            Solution solution = new Solution();
            int[] v = {3,2,5,1,6,4};
            int[] w = {6,5,10,2,16,8};
    
            System.out.println(solution.findCompleteBackpackResult(v, w, 10));
    
            System.out.println(solution.findCompleteBackpackResult2(v, w, 10));
        }
    
    }
    • 322. 零钱兑换 if i in coins: dp[i] = 1
    • 377. 组合总和 IV if i-c>=0: dp[i] += dp[i-c]
  • 二维费用的背包问题
    • 474. 一和零 dp[i][j] = max(dp[i][j], dp[i-c["0"]][j-c["1"]]+1)

进阶算法

回溯算法

回溯算法,就是一个决策树遍历的过程。
参考:回溯算法套路详解 - 知乎
决策树解释:比如说解决一个问题,一共有三个步骤,每个步骤可以选择“是”或者“否”。就是一个三层的二叉树(不考虑根节点)。决策选择哪个,形成的一棵树,就叫决策树。
解决这个问题,只需要考虑:
1)路径,即已经做出的选择。2)选择列表,就是当前可以做的选择。3)结束条件,就是已经到达决策树底层,无法再做选择的条件。
框架:

    result = Lists.newArrayList();
    void backtrack(路径, 选择列表) {
        if 满足结束条件:
        result.add(路径)
        return

        for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择
    }

其核心就是 for 循环里面的递归,在递归调用之前“做选择”,在递归调用之后“撤销选择”
 

  • 46. 全排列 used = [False]*len(nums)
    参考这个链接:回溯算法套路详解 - 知乎,回溯算法,遍历决策树,很简单。不要看力扣上的官方讲解。
     
    class Solution {
    
        List<List<Integer>> result = new ArrayList<>();
    
        List<List<Integer>> premute(int[] nums) {
    
            LinkedList<Integer> track = new LinkedList<>();
            backTrack(nums, track);
            return result;
        }
    
        void backTrack(int[] nums, LinkedList<Integer> track) {
    
            // 出发结束条件
            if (track.size() == nums.length) {
                result.add(new ArrayList<>(track));
                return;
            }
            for (int i = 0; i < nums.length; i ++ ) {
                if (track.contains(nums[i])) {
                    continue;
                }
                track.add(nums[i]);
                backTrack(nums, track);
                track.removeLast();
            }
    
        }
    
    }


     
  • 78. 子集 for j in range(i, len(nums)): backtrack(track+[nums[j]], j+1)
    参考:https://zhuanlan.zhihu.com/p/109523146
    
    
    方法1:对于数组中每个元素,选择或者是不选择:
    
        List<List<Integer>> result = new ArrayList<>();
    
        public List<List<Integer>> subsets(int[] nums) {
    
            int len = nums.length;
    
            if (len == 0) {
                return result;
            }
            Deque<Integer> deque = new LinkedList<>();
            dfs(nums, len, 0, deque);
            return result;
    
        }
    
        public void dfs(int[] nums, int len, int index, Deque<Integer> path) {
    
            if (index == len) {
                result.add(new ArrayList<>(path));
                return;
            }
            path.addLast(nums[index]);
            dfs(nums, len, index + 1, path);
            path.removeLast();
            dfs(nums, len, index + 1, path);
    
        }
    
    
    方法2:回溯法
    
    

    参考:回溯算法团灭子集、排列、组合问题 - labuladong - 博客园
     
  • 22. 括号生成 for p in ["(", ")"]: if counter[p] < n:
     
        public List<String> generateParenthesis(int n) {
    
            List<String> resultList = new ArrayList<>();
            backtrack(resultList, new StringBuilder(), 0, 0, n);
            return resultList;
    
        }
    
        public void backtrack(List<String> ans, StringBuilder cur, int left, int right, int max) {
            if (cur.length() == max * 2) {
                ans.add(cur.toString());
                return;
            }
            if (left < max) {
                cur.append('(');
                backtrack(ans, cur, left + 1, right, max);
                cur.deleteCharAt(cur.length() - 1);
            }
            if (right < left) {
                cur.append(')');
                backtrack(ans, cur, left, right + 1, max);
                cur.deleteCharAt(cur.length() - 1);
            }
        }




     
  • 131. 分割回文串 for i in range(1, len(s)+1): if s[:i] == s[:i][::-1]: backtrack()
     

图:拓扑排序和Union Find

拓扑排序详解 通俗易懂 - 知乎 拓扑排序

  • Union Find if self.parent[idx] != idx: self.parent[idx] = self.find(self.parent[idx])
    • 200. 岛屿数量 uf = UnionFind(row*col+1) dummy_node = row*col
      可以用深度优先搜索、广度优先搜索
    • 323. 无向图中连通分量的数目
  • 拓扑排序 indegree记录流入个数, outdegree记录流出数组
    • 207. 课程表 初始化出入度, 找到入度为0的节点们, 从他们开始dfs, 不断找到入读为0的。
    • 210. 课程表 II
    • 269. 火星词典 建图,key为前序,value为后继,然后拓扑排序逐个添加入度为零的节点。

数据结构设计

  • 146. LRU缓存机制 init cache, capacity, head, tail
  • 380. 常数时间插入、删除和获取随机元素 return random.choice(arr); di的key为value, val为value在数组中的位置
  • 706. 设计哈希映射 size1000的1000个list,表头为空;Node(key, val, nex)
  • 155. 最小栈 stack, minstack
  • 295-. 数据流的中位数 最大堆+1,最小堆,每次入堆,都有从另一个堆里挤出一个元素
  • 208. 实现 Trie (前缀树)

高频系列专题

数组矩阵杂题

  • 双指针
    • 42. 接雨水 O(3n), res[i] = min(left_max, right_max), 一次性求好左边最大值和右边最大值
    • 11. 盛最多水的容器 l从前往后,r从后往前,每次移动l和r中较小的值,算当前面积
      就用双指针,先移动小的那个
  • 数组
    • 239. 滑动窗口最大值 - H

      优先队列
      单调队列


       
    • 41. 缺失的第一个正数 置换,保证数组的第x−1个元素为x
    • 51.上一个排列
    • 238. 除自身之外的乘积
      不能用除法,因为有可能出现0
      利用索引左侧所有数字的乘积乘以右侧所有数字的乘积即可
      方法:左右乘积列表
      初始化两个空数组L 和R。对于索引i,L[i] 代表左侧所有数字的乘积,R[i] 代表右侧所有数字的乘积。
      使用两个循环来初始化L 和R。
      然后迭代,可以得到结果。
  • 矩阵
    • 48. 旋转图像 - M
      (1) 对于矩阵中第i 行的第j 个元素,旋转后,它出现在第j 行,倒数第i 列的位置。利用一个新的二维矩阵,做中间存储。
      (2)  通过翻转代替旋转。先通过水平轴翻转,再通过主对角线翻转,就是顺时针旋转。
    • 54. 螺旋矩阵 - M
      一层一层地找,一层层地输出
    • 304. 2D区域和检索(不可变)
  • Math
    • 204. 计算质数

Board相关题

  • 200. 岛屿数量、547 省份数量

    这种题就是求一个图的连通分量。
    无向图G的极大连通子图称为G的连通分量( Connected Component)。任何连通图的连通分量只有一个,即是其自身,非连通的无向图有多个连通分量。

    以下是547 的解决方法如下:
    • Solution-DFS
      class Solution {
          public int findCircleNum(int[][] isConnected) {
              // int[][] isConnected 是无向图的邻接矩阵,n 为无向图的顶点数量
              int n = isConnected.length;
              // 定义 boolean 数组标识顶点是否被访问
              boolean[] visited = new boolean[n];
              // 定义 cnt 来累计遍历过的连通域的数量
              int cnt = 0;
              for (int i = 0; i < n; i++) {
                  // 若当前顶点 i 未被访问,说明又是一个新的连通域,则遍历新的连通域且cnt+=1.
                  if (!visited[i]) { 
                      cnt++;
                      dfs(i, isConnected, visited);
                  }
              }
              return cnt;
          }
      
          private void dfs(int i, int[][] isConnected, boolean[] visited) {
              // 对当前顶点 i 进行访问标记
              visited[i] = true;
              
              // 继续遍历与顶点 i 相邻的顶点(使用 visited 数组防止重复访问)
              for (int j = 0; j < isConnected.length; j++) {
                  if (isConnected[i][j] == 1 && !visited[j]) {
                      dfs(j, isConnected, visited);
                  }
              }
          }
      }
      
      
      作者:sweetiee
      链接:https://leetcode-cn.com/problems/number-of-provinces/solution/dfs-bfs-bing-cha-ji-3-chong-fang-fa-ji-s-edkl/
      来源:力扣(LeetCode)
      著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    • Solution-BFS
       
      class Solution {
          public int findCircleNum(int[][] isConnected) {
              // int[][] isConnected 是无向图的邻接矩阵,n 为无向图的顶点数量
              int n = isConnected.length;
              // 定义 boolean 数组标识顶点是否被访问
              boolean[] visited = new boolean[n];
              
              // 定义 cnt 来累计遍历过的连通域的数量
              int cnt = 0;  
              Queue<Integer> queue = new LinkedList<>();
              for (int i = 0; i < n; i++) {
                  // 若当前顶点 i 未被访问,说明又是一个新的连通域,则bfs新的连通域且cnt+=1.
                  if (!visited[i]) {
                      cnt++;
                      queue.offer(i);
                      visited[i] = true;
                      while (!queue.isEmpty()) {
                          int v = queue.poll();
                          for (int w = 0; w < n; w++) {
                              if (isConnected[v][w] == 1 && !visited[w]) {
                                  visited[w] = true;
                                  queue.offer(w);
                              }
                          }
                      }
                  }
              }
              return cnt;
          }
      } 
      
      作者:sweetiee
      链接:https://leetcode-cn.com/problems/number-of-provinces/solution/dfs-bfs-bing-cha-ji-3-chong-fang-fa-ji-s-edkl/
      来源:力扣(LeetCode)
      著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

       
    • Solution-UF(并查集)
      并查集其实就是:合并、查询功能的一种实现。
      并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。
          /**
           * 并查集,要实现的功能:合并、查找
           */
      
          public int findCircleNum(int[][] isConnected) {
              // isConnected 是n x n 矩阵
              int provinces = isConnected.length;
              // 保存每个节点对应的祖宗节点下标
              int[] parent = new int[provinces];
              for (int i = 0; i < provinces; i++) {
                  // 初始化,每个节点是是自己本身的祖宗节点
                  parent[i] = i;
              }
              // 遍历isConnected 二维数组,只需要遍历右上角就可以
              for (int i = 0; i < provinces; i++) {
                  for (int j = i + 1; j < provinces; j++) {
                      if (isConnected[i][j] == 1) {
                          union(parent, i, j);
                      }
                  }
              }
              // 最后遍历parent 数组,确定结果
              int circles = 0;
              for (int i = 0; i < provinces; i++) {
                  if (parent[i] == i) {
                      circles ++;
                  }
              }
              return circles;
      
          }
      
          // 合并功能。合并index1 和index2
          public void union(int[] parent, int index1, int index2) {
      
              // 使index1 的祖宗节点等于index2 的祖宗节点。这样两个节点就联合了。
              parent[find(parent, index1)] = find(parent, parent[index2]);
      
          }
      
          // 查找功能
          public int find(int[] parent, int index) {
              // 如果符合此条件,说明index 节点还有祖宗点,则需要继续寻找祖宗节点
              if (parent[index] != index) {
                  parent[index] = find(parent, parent[index]);
              }
              return parent[index];
          }

       

NSum及股票系列

  • NSum系列
    • 1. 2Sum

      给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

      解决方法1:一次遍历,使用map, O(n)  复杂度
      解决方法2:排序,双指针,Onlogn
      此题即在遍历nums 数组的时候,对于每一个x,寻找target - x。可以使用map 存储,对于。map 的性质,每个元素可以存储两个值(key,value)。所以遍历到x 的时候,在map 中寻找target - x,如果没找到,则将x 及x 的下标存储到map 中。

       
          public int[] twoSum(int[] nums, int target) {
      
              Map<Integer, Integer> map = new HashMap<>(nums.length);
              for (int i = 0; i < nums.length; i++) {
                  if (map.containsKey(target - nums[i])) {
                      return new int[]{i, map.get(target - nums[i])};
                  } else {
                      map.put(nums[i], i);
                  }
              }
              return null;
          }
    • 15. 3Sum 排序+双指针,On2,固定一个i,l,r从左右开始扫描
      先排序,然后对于每个元素,寻找first,这是一次遍历。然后second & third 的寻找,这是使用双指针的操作方法。
      参考题解,从O(N3) 简化成O(N2) 的过程。
       
          public List<List<Integer>> threeSum(int[] nums) {
              int n = nums.length;
              Arrays.sort(nums);
              List<List<Integer>> ans = new ArrayList<List<Integer>>();
              // 枚举a
              for (int first = 0; first < n; first ++) {
                  // 需要和上一次枚举的数不相同
                  if (first > 0 && nums[first] == nums[first - 1]) {
                      continue;
                  }
                  // c 对应的指针初始指向数组的最右端
                  int third = n - 1;
                  int target  = - nums[first];
                  // 枚举b
                  for (int second = first + 1; second < third; second ++) {
                      if (second > first + 1 && nums[second] == nums[second - 1]) {
                          continue;
                      }
                      // 保证b 的指针在c 的左侧
                      while (second < third && nums[second] + nums[third] > target) {
                          -- third;
                      }
                      // 如果指针重合,随着b 的后续增加,就不会有满足a+b+c=0 并且b<c 的c 了,可以退出循环
                      if (second == third) {
                          break;
                      }
                      if (nums[second] + nums[third] == target) {
                          List<Integer> list = new ArrayList<>();
                          list.add(nums[first]);
                          list.add(nums[second]);
                          list.add(nums[third]);
                          ans.add(list);
                      }
                  }
              }
              return ans;
          }

       
    • 18. 4Sum 和3Sum思路一样,固定两个再双指针,On3
      使用两重循环枚举前两个数字,使用双指针枚举剩下的两个数字。

       
          public static List<List<Integer>> fourSum(int[] nums, int target) {
      
              List<List<Integer>> result = new ArrayList<>();
              if (null == nums || nums.length < 4) {
                  return result;
              }
      
              Arrays.sort(nums);
      
              int length = nums.length;
              for (int i = 0; i < length - 3; i++) {
                  // 防止重复解
                  if (i > 0 && nums[i] == nums[i - 1]) {
                      continue;
                  }
                  // 数组情况前提判断
                  if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
                      break;
                  }
                  // 数组情况前提判断
                  if (nums[length - 1] + nums[length - 2] + nums[length - 3] + nums[length - 4] < target) {
                      break;
                  }
                  // 数组情况前提判断
                  if (nums[i] + nums[length - 3] + nums[length - 2] + nums[length -1] < target) {
                      continue;
                  }
                  for (int j = i + 1; j < length - 2; j++) {
      
                      // 防止重复解
                      if (j > i + 1 && nums[j] == nums[j - 1]) {
                          continue;
                      }
                      // 数组情况前提判断
                      if (nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
                          break;
                      }
                      // 数组情况前提判断
                      if (nums[i] + nums[j] + nums[length - 1] + nums[length - 2] < target) {
                          continue;
                      }
                      int left = j + 1;
                      int right = length - 1;
                      while (left < right) {
                          int sum = nums[i] + nums[j] + nums[left] + nums[right];
                          if (sum == target) {
                              // 赋值
                              result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                              // 因为有可能有多重情况,所以在这里面还需要继续执行左右指针的变动
                              while (left < right && nums[left] == nums[left + 1]) {
                                  left ++;
                              }
                              left ++;
                              while (left < right && nums[right] == nums[right - 1]) {
                                  right --;
                              }
                              right --;
                          } else if (sum > target) {
                              right --;
                          } else if (sum < target) {
                              left ++;
                          }
                      }
                  }
              }
              return result;
          }
  • 股票系列
    • 121. 买卖股票的最佳时机 维护最小值minPrice,不断更新最大收益maxProfit
      只要用一个变量记录一个历史最低价格minPrice,就可以假设股票是在那天买的,那么在第i 天卖出的利润就是price[i] - minPrice

       

      给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
      你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

      (只能买卖一次)
       
          public int maxProfit(int[] prices) {
              int ans = 0;
              int min = prices[0];
              for (int i = 1; i < prices.length; i++) {
                  if (prices[i] > min) {
                      ans = ans > prices[i] - min ? ans : prices[i] - min;
                  } else {
                      min = prices[i];
                  }
              }
              return ans;
          }

       
    • 122. 买卖股票的最佳时机 II 只要后一天比前一天大,就交易
      动态规划、贪心

      (可以买卖多次)


       

      给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格
      设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。



      动态规划:

          public int maxProfit(int[] prices) {
              int n = prices.length;
              // 定义状态dp[i][0] 表示第i 天交易完后,手里没有股票的最大利润
              // 定义状态dp[i][1] 表示第i 天交易完后,手里有股票的最大利润
              // 状态转移方程:
              // dp[i][0]=max{dp[i−1][0],dp[i−1][1]+prices[i]} 因为第i 天交易完成,股票卖出,所以要加price[i]。即当天股票卖出时的价格。
              // dp[i][1]=max{dp[i−1][1],dp[i−1][0]−prices[i]}
              int[][] dp = new int[n][2];
              dp[0][0] = 0;
              dp[0][1] = -prices[0];
              for (int i = 1; i < n; i++) {
                  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[n - 1][0];
          }
      
      
      
      // 升级版
      
          public int maxProfit(int[] prices) {
              int n = prices.length;
              int dp0 = 0, dp1 = -prices[0];
              for (int i = 1; i < n; ++i) {
                  int newDp0 = Math.max(dp0, dp1 + prices[i]);
                  int newDp1 = Math.max(dp1, dp0 - prices[i]);
                  dp0 = newDp0;
                  dp1 = newDp1;
              }
              return dp0;
          }
      
      

      贪心:

          public int maxProfit(int[] prices) {
              int ans = 0;
              int n = prices.length;
              for (int i = 1; i < n; ++i) {
                  ans += Math.max(0, prices[i] - prices[i - 1]);
              }
              return ans;
          }



       



       
    • 123. 买卖股票的最佳时机 III
      王脸小提示:dp[i][s],i为天数,s为状态


      题目:

      给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
      设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。


      动态规划,因为每天最多可以完成两笔交易,所以在任意一天结束之后,会处于五种状态之一。

      看官方题解,此题一共五个状态。只需要维护四个状态即可。

       
          public int maxProfit(int[] prices) {
              int n = prices.length;
              int buy1 = -prices[0], sell1 = 0;
              int buy2 = -prices[0], sell2 = 0;
              for (int i = 1; i < n; ++i) {
                  buy1 = Math.max(buy1, -prices[i]);
                  sell1 = Math.max(sell1, buy1 + prices[i]);
                  buy2 = Math.max(buy2, sell1 - prices[i]);
                  sell2 = Math.max(sell2, buy2 + prices[i]);
              }
              return sell2;
          }


       
    • 188. 买卖股票的最佳时机 IV dp[i][s],i为天数,s为状态

       

252. 区间(会议室)

class Solution {
    public boolean canAttendMeetings(int[][] intervals) {
        Arrays.sort(intervals, (o1, o2) -> (o1[0] == o2[0] ? o1[1] - o2[1] : o1[0] - o2[0]));

        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i][0] < intervals[i - 1][1]) {
                return false;
            }
        }
        return true;
    }
}

参考:leetcode 252.会议室 Java_云水冰的博客-CSDN博客

Arrays.sort() 的使用:Arrays.sort()的用法_Lin的博客-CSDN博客_arrays.sort()
 

253. 会议室II

首先进行一下排序,然后用一个小顶堆,维护当前每个会议室的结束时间,
然后当一个新的时间安排出现的时候,只需要判断一下是否需要新申请一个会议室,还是继续使用之前的会议室。

使用小顶堆的实现方式就是,小顶堆存了会议的结束时间,首先把第一个会议的结束时间存入,作为初始化条件。

遍历其他会议日程。如果会议开始时间(intervals[i][0])大于queue.peek(),就是大于当前最小的时间,那么就可以继续使用当前这个会议室。优先队列的操作方式就是把前一个会议日程踢出,然后add 进去当前会议的结束时间。否则不踢出。

class Solution {
    public int minMeetingRooms(int[][] intervals) {
        if(intervals == null || intervals.length == 0){
            return 0;
        }
        Arrays.sort(intervals,(o1,o2)->o1[0]-o2[0]);
        PriorityQueue<Integer> queue = new PriorityQueue<>((o1,o2)->o1-o2);
        queue.offer(intervals[0][1]);
        for(int i = 1;i < intervals.length;i++){
            if(intervals[i][0]>=queue.peek()){
                queue.poll();
            }
            queue.offer(intervals[i][1]);
        }
        return queue.size();
    }
}

参考:leetcode253. 会议室II(java):最小堆_yinianxx的博客-CSDN博客

BFS

举例:994,腐烂的桔子。

在给定的网格中,每个单元格可以有以下三个值之一:

值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。

返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。

主要就是看官方题解,主要就是细节

    class Solution {
        // dr,dc 配合使用得到 grid[r][c] 的各个方向的元素位置的确定
        // 上grid[r-1][c] 左grid[r][c-1] 下grid[r+1][c] 右grid[r][c+1]的元素
        int[] dr = new int[]{-1, 0, 1, 0};
        int[] dc = new int[]{0, -1, 0, 1};

        public int orangesRotting(int[][] grid) {
            // 获取二维数组的行数row 和 列数 column
            int R = grid.length, C = grid[0].length;

            // queue : all starting cells with rotten oranges
            Queue<Integer> queue = new ArrayDeque();
            Map<Integer, Integer> depth = new HashMap();
            for (int r = 0; r < R; ++r)
                for (int c = 0; c < C; ++c)
                    if (grid[r][c] == 2) {
                        int code = r * C + c;  // 转化为索引唯一的一维数组
                        queue.add(code); //存储腐烂橘子
                        depth.put(code, 0); //存储橘子变为腐烂时的时间,key为橘子的一维数组下标,value为变腐烂的时间
                    }

            int ans = 0;
            while (!queue.isEmpty()) {
                int code = queue.remove();
                int r = code / C, c = code % C;
                for (int k = 0; k < 4; ++k) {
                    int nr = r + dr[k];
                    int nc = c + dc[k];
                    if (0 <= nr && nr < R && 0 <= nc && nc < C && grid[nr][nc] == 1) {
                        grid[nr][nc] = 2;
                        int ncode = nr * C + nc;
                        queue.add(ncode);
                        // 计次的关键 元素 grid[r][c] 的上左下右元素得腐烂时间应该一致
                        depth.put(ncode, depth.get(code) + 1);
                        ans = depth.get(ncode);
                    }
                }
            }

           //检查grid,此时的grid能被感染已经都腐烂了,此时还新鲜的橘子无法被感染
            for (int[] row: grid)
                for (int v: row)
                    if (v == 1)
                        return -1;
            return ans;

        }
    }

 DFS

Radix/Bucket Sort

radix sort 基数排序,也叫做bucket sort。

225. 队列实现栈

使用一个队列即可实现

主要是用top_elem 标识栈顶元素,和pop 操作,比较重要。

class MyStack {

    private Queue<Integer> queue = new LinkedList<>();
    int top_elem = 0;
    public MyStack() {

    }

    // 添加元素到栈顶
    public void push(int x) {
        //x 是队列的队尾,是栈的栈顶
        queue.offer(x);
        top_elem = x;
    }

    // 队列的性质是先进先出,pop 元素的时候只能从队头取出元素
    // 但是栈是先进后出,解决办法就是
    // 把队列前面的元素全部取出,再加入到队尾,让之前队尾元素排到队头,这样就可以取出了
    public int pop() {
        int size = queue.size();
        // 留下两个元素是为了更新top_elem.
        while (size > 2) {
            queue.offer(queue.poll());
            size --;
        }
        top_elem = queue.peek();
        queue.offer(queue.poll());
        // 之前的队尾已经到了队头
        return queue.poll();
    }

    public int top() {
        return top_elem;
    }

    public boolean empty() {
        return queue.isEmpty();
    }
}

232. 栈实现队列

使用两个栈实现一个队列

添加元素的时候,把元素放到s1 里面,pop 的时候,把s2 作为中转

class MyQueue {

    private Stack<Integer> s1, s2;
    public MyQueue() {
        s1 = new Stack<>();
        s2 = new Stack<>();
    }

    public void push(int x) {
        s1.push(x);
    }

    // 对于pop 操作,也只操作s2 就可以了
    public int pop() {
        // 先调用peek,保证s2 非空
        peek();
        return s2.pop();
    }

    // 注意peek 是静态的,不会有元素的变更
    public int peek() {
        if (s2.isEmpty()) {
            // 把s1 压入s2 中
            while (!s1.isEmpty()) {
                s2.push(s1.pop());
            }
        }
        return s2.peek();
    }

    // 如果两个栈都空,就说明队列为空
    public boolean empty() {
        return s1.isEmpty() && s2.isEmpty();
    }
}

以上参考:队列实现栈|栈实现队列 - 知乎 

其他:

560. 560. 和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。
 

使用“前缀和”
前缀和指一个数组某下标之前所有数组元素的和包含其自身)。前缀和分为一维前缀和,以及二维前缀和。前缀和是一种重要的预处理,能够降低算法的时间复杂度。

(题目解法来自“labuladong”)

使用“前缀和”技巧

    public static int subArraySum(int[] nums, int k) {

        int n = nums.length;
        // 构造
        int[] preSum = new int[n + 1];
        preSum[0] = 0;
        for (int i = 0; i < n; i++) {
            preSum[i + 1] = preSum[i] + nums[i];
        }

        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j < i; j++) {
                if (preSum[i] - preSum[j] == k) {
                    ans ++;
                }
            }
        }
        return ans;
    }



优化:第⼆层 for 循环在⼲嘛呢?翻译⼀下就是, 在计算, 有⼏个 j 能够使得
sum[i] 和 sum[j] 的差为 k。 毎找到⼀个这样的 j , 就把结果加⼀。
把第二个for 循环中的判断公式移项,可以得到if(sum[j] == sum[i] - k) ans ++;
直接记录下有⼏个 sum[j] 和 sum[i] - k 相等, 直接更
新结果, 就避免了内层的 for 循环。 我们可以⽤哈希表, 在记录前缀和的同
时记录该前缀和出现的次数。

    public static int subArraySum(int[] nums, int k) {

        int n = nums.length;
        HashMap<Integer, Integer> preSum = new HashMap<>();
        preSum.put(0, 1);
        int ans = 0;
        // 数组下标0 到下标i 的和
        int sum0_i = 0;
        for (int i = 0; i < n; i++) {
            sum0_i += nums[i];
            // 题目是要求有几个和为k 的子数组
            // sum0_j 是数组下标0 到下标j 的和,也就是要找的前缀和。用sum0-i 减去sum0-j 得到的就是k
            int sum0_j = sum0_i - k;
            // 如果map 中存在这个前缀和
            if (preSum.containsKey(sum0_j)) {
                ans += preSum.get(sum0_j);
                // 然后把前缀和sum0-i 加入记录,同时记录出现次数
                preSum.put(sum0_i, preSum.getOrDefault(sum0_i, 0) + 1);
            }
        }
        return ans;
    }

给⼀棵⼆叉树, 和⼀个⽬标值, 节点上的值有正有负, 返回树中和等于⽬标值的路径条数

@Data
    class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
    }

    public int pathNum(TreeNode root, int sum) {

        if (root == null) {
            return 0;
        }
        
        int pathNumMe = pathNum(root, sum);
        int pathNumLeft = pathNum(root.left, sum);
        int pathNumRight = pathNum(root.right, sum);
        return pathNumLeft + pathNumRight + pathNumMe;
    }
    
    public static int count(TreeNode node, int sum) {
        
        if (node == null) {
            return 0;
        }
        int isMe = (node.val == sum) ? 1 : 0;
        int leftBro = count(node.left, sum - node.val);
        int rightBro = count(node.right, sum - node.val);
        return isMe + leftBro + rightBro;
        
    }

归并排序:典型的分治算法;分治,典型的递归结构。

coupang

​​​​​​九章算法 | Coupang面试题:捡苹果 - 知乎​​​​​​

 coupang面试经验|面试题(共3条)- 职朋

2020 年面试记录 [Microsoft / Coupang / CoinMarketCap] - Popco - 博客园

「韩领网络科技(上海)有限公司面试|面试题」-看准网

  • 1
    点赞
  • 0
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值