diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/LCS.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/LCS.md" index 82ce0d37378028ee2771bd5220edb50f35a6c89e..836c36d7f83645c69520f206ab65715769c7e051 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/LCS.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/LCS.md" @@ -38,6 +38,7 @@ title: '详解最长公共子序列问题,秒杀三道动态规划题目' 给你输入两个字符串 `s1` 和 `s2`,请你找出他们俩的最长公共子序列,返回这个子序列的长度。函数签名如下: + ```java int longestCommonSubsequence(String s1, String s2); ``` diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" index a9a046cf4c099216265154d5aa1304ccaf6200e1..ffa1c847a88baa40f4cb831c0090af3da1f815b8 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\345\212\250\346\200\201\350\247\204\345\210\222\344\271\213KMP\345\255\227\347\254\246\345\214\271\351\205\215\347\256\227\346\263\225.md" @@ -43,6 +43,7 @@ KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法 力扣第 28 题「实现 strStr」就是字符串匹配问题,暴力的字符串匹配算法很容易写,看一下它的运行逻辑: + ```java // 暴力匹配(伪码) int search(String pat, String txt) { @@ -117,6 +118,7 @@ pat = "aaab" 明白了 `dp` 数组只和 `pat` 有关,那么我们这样设计 KMP 算法就会比较漂亮: + ```java public class KMP { private int[][] dp; @@ -208,6 +210,7 @@ pat 应该转移到状态 2 根据我们这个 dp 数组的定义和刚才状态转移的过程,我们可以先写出 KMP 算法的 search 函数代码: + ```java public int search(String txt) { int M = pat.length(); @@ -285,6 +288,7 @@ for 0 <= j < M: 如果之前的内容你都能理解,恭喜你,现在就剩下一个问题:影子状态 `X` 是如何得到的呢?下面先直接看完整代码吧。 + ```java public class KMP { private int[][] dp; @@ -360,6 +364,7 @@ for (int i = 0; i < N; i++) { 至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧: + ```java public class KMP { private int[][] dp; diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\203\214\345\214\205\351\227\256\351\242\230.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\203\214\345\214\205\351\227\256\351\242\230.md" index 6a5bc81cad78b67f7194eff864f57c86a2701f94..6a7aea39788bae56a54ba490009689dc427e1b70 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\203\214\345\214\205\351\227\256\351\242\230.md" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\347\263\273\345\210\227/\350\203\214\345\214\205\351\227\256\351\242\230.md" @@ -29,7 +29,7 @@ title: '动态规划之背包问题' 举个简单的例子,输入如下: -``` +```py N = 3, W = 4 wt = [2, 1, 3] val = [4, 2, 3] @@ -124,6 +124,7 @@ return dp[N][W] 我用 Java 写的代码,把上面的思路完全翻译了一遍,并且处理了 `w - wt[i-1]` 可能小于 0 导致数组索引越界的问题: + ```java int knapsack(int W, int N, int[] wt, int[] val) { assert N == wt.length; diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" index cdd3394912d0f1181c222437c3301eaa354c2f41..850ddf27598396deef51f9882af1059596628849 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" @@ -234,6 +234,12 @@ void traverse(ListNode head) { **二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 [回溯算法核心框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 和 [动态规划核心框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶)**。 +> tip:这里说一下我的函数命名习惯:二叉树中用遍历思路解题时函数签名一般是 `void traverse(...)`,没有返回值,靠更新外部变量来计算结果,而用分解问题思路解题时函数名根据该函数具体功能而定,而且一般会有返回值,返回值是子问题的计算结果。 +> +> 与此对应的,你会发现我在 [回溯算法核心框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 中给出的函数签名一般也是没有返回值的 `void backtrack(...)`,而在 [动态规划核心框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 中给出的函数签名是带有返回值的 `dp` 函数。这也说明它俩和二叉树之间千丝万缕的联系。 +> +> 虽然函数命名没有什么硬性的要求,但我还是建议你也遵循我的这种风格,这样更能突出函数的作用和解题的思维模式,便于你自己理解和运用。 + 当时我是用二叉树的最大深度这个问题来举例,重点在于把这两种思路和动态规划和回溯算法进行对比,而本文的重点在于分析这两种思路如何解决二叉树的题目。 力扣第 104 题「二叉树的最大深度」就是最大深度的题目,所谓最大深度就是根节点到「最远」叶子节点的最长路径上的节点数,比如输入这棵二叉树,算法应该返回 3: @@ -710,6 +716,7 @@ class Solution { - [两种思路解决单词拼接问题](https://labuladong.github.io/article/fname.html?fname=单词拼接) - [二叉树(递归)专题课](https://labuladong.github.io/article/fname.html?fname=tree课程简介) - [前缀树算法模板秒杀五道算法题](https://labuladong.github.io/article/fname.html?fname=trie) + - [后序遍历的妙用](https://labuladong.github.io/article/fname.html?fname=后序遍历) - [回溯算法秒杀所有排列/组合/子集问题](https://labuladong.github.io/article/fname.html?fname=子集排列组合) - [回溯算法解题套路框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) - [在插件中解锁二叉树专属题解](https://labuladong.github.io/article/fname.html?fname=解锁tree插件) diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" index 884edf58c05e805ed3e756b7e384977971441dae..b3aad41e8bce46e7317e876916140a6250d22280 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\345\215\225\350\260\203\346\240\210.md" @@ -34,6 +34,7 @@ title: '特殊数据结构:单调栈' 现在给你出这么一道题:输入一个数组 `nums`,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。函数签名如下: + ```java int[] nextGreaterElement(int[] nums); ``` @@ -48,6 +49,7 @@ int[] nextGreaterElement(int[] nums); 这个情景很好理解吧?带着这个抽象的情景,先来看下代码。 + ```java int[] nextGreaterElement(int[] nums) { int n = nums.length; @@ -83,12 +85,14 @@ int[] nextGreaterElement(int[] nums) { 这道题给你输入两个数组 `nums1` 和 `nums2`,让你求 `nums1` 中的元素在 `nums2` 中的下一个更大元素,函数签名如下: + ```java int[] nextGreaterElement(int[] nums1, int[] nums2) ``` 其实和把我们刚才的代码改一改就可以解决这道题了,因为题目说 `nums1` 是 `nums2` 的子集,那么我们先把 `nums2` 中每个元素的下一个更大元素算出来存到一个映射里,然后再让 `nums1` 中的元素去查表即可: + ```java int[] nextGreaterElement(int[] nums1, int[] nums2) { // 记录 nums2 中每个元素的下一个更大元素 @@ -116,6 +120,7 @@ int[] nextGreaterElement(int[] nums) { 给你一个数组 `temperatures`,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。函数签名如下: + ```java int[] dailyTemperatures(int[] temperatures); ``` @@ -126,6 +131,7 @@ int[] dailyTemperatures(int[] temperatures); 相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧: + ```java int[] dailyTemperatures(int[] temperatures) { int n = temperatures.length; @@ -156,6 +162,7 @@ int[] dailyTemperatures(int[] temperatures) { 我们一般是通过 % 运算符求模(余数),来模拟环形特效: + ```java int[] arr = {1,2,3,4,5}; int n = arr.length, index = 0; @@ -176,6 +183,7 @@ while (true) { 有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,**我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果**。直接看代码吧: + ```java int[] nextGreaterElements(int[] nums) { int n = nums.length; diff --git "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" index e65e077c74d3cda77414c3aff8de24eb66808e69..b107ebdde2a6ac44f3f6eb82316f635a71a53ab8 100644 --- "a/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" +++ "b/\346\225\260\346\215\256\347\273\223\346\236\204\347\263\273\345\210\227/\351\200\222\345\275\222\345\217\215\350\275\254\351\223\276\350\241\250\347\232\204\344\270\200\351\203\250\345\210\206.md" @@ -106,6 +106,7 @@ head.next.next = head; 接下来: + ```java head.next = null; return last; @@ -117,6 +118,7 @@ return last; 1、递归函数要有 base case,也就是这句: + ```java if (head == null || head.next == null) { return head; @@ -127,6 +129,7 @@ if (head == null || head.next == null) { 2、当链表递归反转之后,新的头结点是 `last`,而之前的 `head` 变成了最后一个节点,别忘了链表的末尾要指向 null: + ```java head.next = null; ``` diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md" index ce3d5d16897fa8b736a56f9a202bd9a83a301e02..2031b4438aa946ea3c0fc371f5535367e0bb5fb1 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/BFS\350\247\243\345\206\263\346\273\221\345\212\250\346\213\274\345\233\276.md" @@ -75,6 +75,7 @@ title: 'BFS 算法秒杀各种益智游戏' 对于这道题,题目说输入的数组大小都是 2 x 3,所以我们可以直接手动写出来这个映射: + ```java // 记录一维字符串的相邻索引 int[][] neighbor = new int[][]{ @@ -99,6 +100,7 @@ int[][] neighbor = new int[][]{ 至此,我们就把这个问题完全转化成标准的 BFS 问题了,借助前文 [BFS 算法框架](https://labuladong.github.io/article/fname.html?fname=BFS框架) 的代码框架,直接就可以套出解法代码了: + ```java public int slidingPuzzle(int[][] board) { int m = 2, n = 3; diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" index 6db37c3c1896abf7ca2997f935db0bd83531fab5..285c7990f0cff5bf3898d95b76e0c442a2f74be9 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\255\227\347\254\246\344\270\262\344\271\230\346\263\225.md" @@ -59,7 +59,8 @@ title: '字符串乘法' 明白了这一点,就可以用代码模仿出这个计算过程了: -```java + +```cpp string multiply(string num1, string num2) { int m = num1.size(), n = num2.size(); // 结果最多为 m + n 位数 diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" index 2a461c7301c2bd04d6bddeef39c7a16c684d0036..2400c8d1b577fbcfb7b8dc604dc69c6ee0f9be67 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\345\270\270\347\224\250\347\232\204\344\275\215\346\223\215\344\275\234.md" @@ -104,6 +104,7 @@ boolean f = ((x ^ y) < 0); // false 我在 [单调栈解题套路](https://labuladong.github.io/article/fname.html?fname=单调栈) 中介绍过环形数组,其实就是利用求模(余数)的方式让数组看起来头尾相接形成一个环形,永远都走不完: + ```java int[] arr = {1,2,3,4}; int index = 0; @@ -117,6 +118,7 @@ while (true) { 但模运算 `%` 对计算机来说其实是一个比较昂贵的操作,所以我们可以用 `&` 运算来求余数: + ```java int[] arr = {1,2,3,4}; int index = 0; @@ -136,6 +138,7 @@ while (true) { 答案是,如果你使用 `%` 求模的方式,那么当 `index` 小于 0 之后求模的结果也会出现负数,你需要特殊处理。但通过 `&` 与运算的方式,`index` 不会出现负数,依然可以正常工作: + ```java int[] arr = {1,2,3,4}; int index = 0; @@ -167,6 +170,7 @@ while (true) { 就是让你返回 `n` 的二进制表示中有几个 1。因为 `n & (n - 1)` 可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到 `n` 变成 0 为止。 + ```java int hammingWeight(int n) { int res = 0; @@ -192,6 +196,7 @@ int hammingWeight(int n) { 如果使用 `n & (n-1)` 的技巧就很简单了(注意运算符优先级,括号不可以省略): + ```java boolean isPowerOfTwo(int n) { if (n <= 0) return false; @@ -213,6 +218,7 @@ boolean isPowerOfTwo(int n) { 对于这道题目,我们只要把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素: + ```java int singleNumber(int[] nums) { int res = 0; @@ -241,6 +247,7 @@ int singleNumber(int[] nums) { 题目的意思可以这样理解:现在有个等差数列 `0, 1, 2,..., n`,其中少了某一个数字,请你把它找出来。那这个数字不就是 `sum(0,1,..n) - sum(nums)` 嘛? + ```java int missingNumber(int[] nums) { int n = nums.length; @@ -277,6 +284,7 @@ int missingNumber(int[] nums) { 如何找这个落单的数字呢,**只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下**,也就达到了我们的目的: + ```java int missingNumber(int[] nums) { int n = nums.length; diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" index 9e746989354ee732371a4f02cb7173a7847fd05e..d735edff7f3d4856ab46d54e5825b9065368244b 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\346\264\227\347\211\214\347\256\227\346\263\225.md" @@ -26,7 +26,9 @@ title: '洗牌算法' 我知道大家会各种花式排序算法,但是如果叫你打乱一个数组,你是否能做到胸有成竹?即便你拍脑袋想出一个算法,怎么证明你的算法就是正确的呢?乱序算法不像排序算法,结果唯一可以很容易检验,因为「乱」可以有很多种,你怎么能证明你的算法是「真的乱」呢? 所以我们面临两个问题: + 1. 什么叫做「真的乱」? + 2. 设计怎样的算法来打乱数组才能做到「真的乱」? 这种算法称为「随机乱置算法」或者「洗牌算法」。 @@ -37,6 +39,7 @@ title: '洗牌算法' 此类算法都是靠随机选取元素交换来获取随机性,直接看代码(伪码),该算法有 4 种形式,都是正确的: + ```java // 得到一个在闭区间 [min, max] 内的随机整数 int randInt(int min, int max); @@ -71,6 +74,7 @@ void shuffle(int[] arr) { 我们先用这个准则分析一下**第一种写法**的正确性: + ```java // 假设传入这样一个 arr int[] arr = {1,3,5,7,9}; @@ -113,6 +117,7 @@ for 循环第二轮迭代时,`i = 1`,`rand` 的取值范围是 `[1, 4]`, 如果读者思考过洗牌算法,可能会想出如下的算法,但是**这种写法是错误的**: + ```java void shuffle(int[] arr) { int n = arr.length(); @@ -151,6 +156,7 @@ void shuffle(int[] arr) { 每次进行洗牌算法后,就把得到的打乱结果对应的频数加一,重复进行 100 万次,如果每种结果出现的总次数差不多,那就说明每种结果出现的概率应该是相等的。写一下这个思路的伪代码: + ```java void shuffle(int[] arr); diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" index d5a1d226f23c3ab445ccf18d002cf26c639ba90c..6b54b9708c18d87d7c5980796ff738020d6e5c23 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\347\203\247\351\245\274\346\216\222\345\272\217.md" @@ -43,6 +43,7 @@ title: '烧饼排序' 为什么说这个问题有递归性质呢?比如说我们需要实现这样一个函数: + ```java // cakes 是一堆烧饼,函数会将前 n 个烧饼排序 void sort(int[] cakes, int n); @@ -82,45 +83,48 @@ base case:`n == 1` 时,排序 1 个饼时不需要翻转。 只要把上述的思路用代码实现即可,唯一需要注意的是,数组索引从 0 开始,而我们要返回的结果是从 1 开始算的。 + ```java -// 记录反转操作序列 -LinkedList res = new LinkedList<>(); +class Solution { + // 记录反转操作序列 + LinkedList res = new LinkedList<>(); -List pancakeSort(int[] cakes) { - sort(cakes, cakes.length); - return res; -} + List pancakeSort(int[] cakes) { + sort(cakes, cakes.length); + return res; + } -void sort(int[] cakes, int n) { - // base case - if (n == 1) return; - - // 寻找最大饼的索引 - int maxCake = 0; - int maxCakeIndex = 0; - for (int i = 0; i < n; i++) - if (cakes[i] > maxCake) { - maxCakeIndex = i; - maxCake = cakes[i]; - } - - // 第一次翻转,将最大饼翻到最上面 - reverse(cakes, 0, maxCakeIndex); - res.add(maxCakeIndex + 1); - // 第二次翻转,将最大饼翻到最下面 - reverse(cakes, 0, n - 1); - res.add(n); - - // 递归调用 - sort(cakes, n - 1); -} + void sort(int[] cakes, int n) { + // base case + if (n == 1) return; + + // 寻找最大饼的索引 + int maxCake = 0; + int maxCakeIndex = 0; + for (int i = 0; i < n; i++) + if (cakes[i] > maxCake) { + maxCakeIndex = i; + maxCake = cakes[i]; + } + + // 第一次翻转,将最大饼翻到最上面 + reverse(cakes, 0, maxCakeIndex); + res.add(maxCakeIndex + 1); + // 第二次翻转,将最大饼翻到最下面 + reverse(cakes, 0, n - 1); + res.add(n); -void reverse(int[] arr, int i, int j) { - while (i < j) { - int temp = arr[i]; - arr[i] = arr[j]; - arr[j] = temp; - i++; j--; + // 递归调用 + sort(cakes, n - 1); + } + + void reverse(int[] arr, int i, int j) { + while (i < j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + i++; j--; + } } } ``` diff --git "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md" "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md" index 8e5d9d536c0a4f54e98bfdcb54da46cc690f73e3..061ac3b0256dcf43e3f17a2a5ec96fda8381f332 100644 --- "a/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md" +++ "b/\347\256\227\346\263\225\346\200\235\347\273\264\347\263\273\345\210\227/\351\233\206\345\220\210\345\210\222\345\210\206.md" @@ -37,6 +37,7 @@ title: '经典回溯算法:集合划分问题' 函数签名如下: + ```java boolean canPartitionKSubsets(int[] nums, int k); ``` @@ -139,6 +140,7 @@ void traverse(int[] nums, int index) { 那么回到这道题,以数字的视角,选择 `k` 个桶,用 for 循环写出来是下面这样: + ```java // k 个桶(集合),记录每个桶装的数字之和 int[] bucket = new int[k]; @@ -155,6 +157,7 @@ for (int index = 0; index < nums.length; index++) { 如果改成递归的形式,就是下面这段代码逻辑: + ```java // k 个桶(集合),记录每个桶装的数字之和 int[] bucket = new int[k]; @@ -179,6 +182,7 @@ void backtrack(int[] nums, int index) { 虽然上述代码仅仅是穷举逻辑,还不能解决我们的问题,但是只要略加完善即可: + ```java // 主函数 boolean canPartitionKSubsets(int[] nums, int k) { @@ -236,6 +240,7 @@ boolean backtrack( 主要看 `backtrack` 函数的递归部分: + ```java for (int i = 0; i < bucket.length; i++) { // 剪枝 @@ -257,6 +262,7 @@ for (int i = 0; i < bucket.length; i++) { 所以可以在之前的代码中再添加一些代码: + ```java boolean canPartitionKSubsets(int[] nums, int k) { // 其他代码不变 @@ -284,6 +290,7 @@ boolean canPartitionKSubsets(int[] nums, int k) { 这个思路可以用下面这段代码表示出来: + ```java // 装满所有桶为止 while (k > 0) { @@ -305,6 +312,7 @@ while (k > 0) { 那么我们也可以把这个 while 循环改写成递归函数,不过比刚才略微复杂一些,首先写一个 `backtrack` 递归函数出来: + ```java boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int target); @@ -318,6 +326,7 @@ boolean backtrack(int k, int bucket, 根据这个函数定义,可以这样调用 `backtrack` 函数: + ```java boolean canPartitionKSubsets(int[] nums, int k) { // 排除一些基本情况 @@ -341,6 +350,7 @@ boolean canPartitionKSubsets(int[] nums, int k) { 下面的代码就实现了这个逻辑: + ```java boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int target) { @@ -423,6 +433,7 @@ boolean backtrack(int k, int bucket, 看下代码实现,只要稍微改一下 `backtrack` 函数即可: + ```java // 备忘录,存储 used 数组的状态 HashMap memo = new HashMap<>(); @@ -465,65 +476,68 @@ boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int 看下最终的解法代码: + ```java -public boolean canPartitionKSubsets(int[] nums, int k) { - // 排除一些基本情况 - if (k > nums.length) return false; - int sum = 0; - for (int v : nums) sum += v; - if (sum % k != 0) return false; - - int used = 0; // 使用位图技巧 - int target = sum / k; - // k 号桶初始什么都没装,从 nums[0] 开始做选择 - return backtrack(k, 0, nums, 0, used, target); -} - -HashMap memo = new HashMap<>(); - -boolean backtrack(int k, int bucket, - int[] nums, int start, int used, int target) { - // base case - if (k == 0) { - // 所有桶都被装满了,而且 nums 一定全部用完了 - return true; - } - if (bucket == target) { - // 装满了当前桶,递归穷举下一个桶的选择 - // 让下一个桶从 nums[0] 开始选数字 - boolean res = backtrack(k - 1, 0, nums, 0, used, target); - // 缓存结果 - memo.put(used, res); - return res; - } - - if (memo.containsKey(used)) { - // 避免冗余计算 - return memo.get(used); +class Solution { + public boolean canPartitionKSubsets(int[] nums, int k) { + // 排除一些基本情况 + if (k > nums.length) return false; + int sum = 0; + for (int v : nums) sum += v; + if (sum % k != 0) return false; + + int used = 0; // 使用位图技巧 + int target = sum / k; + // k 号桶初始什么都没装,从 nums[0] 开始做选择 + return backtrack(k, 0, nums, 0, used, target); } - for (int i = start; i < nums.length; i++) { - // 剪枝 - if (((used >> i) & 1) == 1) { // 判断第 i 位是否是 1 - // nums[i] 已经被装入别的桶中 - continue; + HashMap memo = new HashMap<>(); + + boolean backtrack(int k, int bucket, + int[] nums, int start, int used, int target) { + // base case + if (k == 0) { + // 所有桶都被装满了,而且 nums 一定全部用完了 + return true; } - if (nums[i] + bucket > target) { - continue; + if (bucket == target) { + // 装满了当前桶,递归穷举下一个桶的选择 + // 让下一个桶从 nums[0] 开始选数字 + boolean res = backtrack(k - 1, 0, nums, 0, used, target); + // 缓存结果 + memo.put(used, res); + return res; } - // 做选择 - used |= 1 << i; // 将第 i 位置为 1 - bucket += nums[i]; - // 递归穷举下一个数字是否装入当前桶 - if (backtrack(k, bucket, nums, i + 1, used, target)) { - return true; + + if (memo.containsKey(used)) { + // 避免冗余计算 + return memo.get(used); } - // 撤销选择 - used ^= 1 << i; // 使用异或运算将第 i 位恢复 0 - bucket -= nums[i]; - } - return false; + for (int i = start; i < nums.length; i++) { + // 剪枝 + if (((used >> i) & 1) == 1) { // 判断第 i 位是否是 1 + // nums[i] 已经被装入别的桶中 + continue; + } + if (nums[i] + bucket > target) { + continue; + } + // 做选择 + used |= 1 << i; // 将第 i 位置为 1 + bucket += nums[i]; + // 递归穷举下一个数字是否装入当前桶 + if (backtrack(k, bucket, nums, i + 1, used, target)) { + return true; + } + // 撤销选择 + used ^= 1 << i; // 使用异或运算将第 i 位恢复 0 + bucket -= nums[i]; + } + + return false; + } } ``` diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" index 53de2216c5d5382c31fd1d74c99a1492e24c0f06..a1bbb6132609bb2271014f3602e3d2b62f71e7f5 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\270\200\350\241\214\344\273\243\347\240\201\350\247\243\345\206\263\347\232\204\346\231\272\345\212\233\351\242\230.md" @@ -51,6 +51,7 @@ title: '一行代码就能解决的算法题' 这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输。所以这道题的解法非常简单: + ```java boolean canWinNim(int n) { // 如果上来就踩到 4 的倍数,那就认输吧 @@ -85,6 +86,7 @@ boolean canWinNim(int n) { 这道题又涉及到两人的博弈,也可以用动态规划算法暴力试,比较麻烦。但我们只要对规则深入思考,就会大惊失色:只要你足够聪明,你是必胜无疑的,因为你是先手。 + ```java boolean stoneGame(int[] piles) { return true; @@ -119,6 +121,7 @@ boolean stoneGame(int[] piles) { 我们当然可以用一个布尔数组表示这些灯的开关情况,然后模拟这些操作过程,最后去数一下就能出结果。但是这样显得没有灵性,最好的解法是这样的: + ```java int bulbSwitch(int n) { return (int)Math.sqrt(n); diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" index be836057782402b2ce19e10bb3826237a3da866e..ae97d9a8ecf9c57db285f8bf92aa592b7d44b365 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\344\272\214\345\210\206\346\237\245\346\211\276\345\210\244\345\256\232\345\255\220\345\272\217\345\210\227.md" @@ -34,9 +34,11 @@ title: '二分查找高效判定子序列' 举两个例子: +``` s = "abc", t = "**a**h**b**gd**c**", return true. s = "axc", t = "ahbgdc", return false. +``` 题目很容易理解,而且看起来很简单,但很难想到这个问题跟二分查找有关吧? @@ -44,6 +46,7 @@ s = "axc", t = "ahbgdc", return false. 首先,一个很简单的解法是这样的: + ```java boolean isSubsequence(String s, String t) { int i = 0, j = 0; @@ -67,6 +70,7 @@ boolean isSubsequence(String s, String t) { 如果给你一系列字符串 `s1,s2,...` 和字符串 `t`,你需要判定每个串 `s` 是否是 `t` 的子序列(可以假定 `s` 较短,`t` 很长)。 + ```java boolean[] isSubsequence(String[] sn, String t); ``` @@ -79,6 +83,7 @@ boolean[] isSubsequence(String[] sn, String t); 二分思路主要是对 `t` 进行预处理,用一个字典 `index` 将每个字符出现的索引位置按顺序存储下来: + ```java int m = s.length(), n = t.length(); ArrayList[] index = new ArrayList[256]; @@ -111,6 +116,7 @@ for (int i = 0; i < n; i++) { 什么意思呢,就是说如果在数组 `[0,1,3,4]` 中搜索元素 2,算法会返回索引 2,也就是元素 3 的位置,元素 3 是数组中大于 2 的最小元素。所以我们可以利用二分搜索避免线性扫描。 + ```java // 查找左侧边界的二分查找 int left_bound(ArrayList arr, int target) { @@ -134,6 +140,7 @@ int left_bound(ArrayList arr, int target) { 这里以单个字符串 `s` 为例,对于多个字符串 `s`,可以把预处理部分抽出来。 + ```java boolean isSubsequence(String s, String t) { int m = s.length(), n = t.length(); @@ -173,12 +180,14 @@ boolean isSubsequence(String s, String t) { 函数签名如下: + ```java int numMatchingSubseq(String s, String[] words) ``` 我们直接把上一道题的代码稍微改改即可完成这道题: + ```java int numMatchingSubseq(String s, String[] words) { // 对 s 进行预处理 diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" index bdecbf464c4aceeba40b86c891b57e96adba3405..d2fbc9554331fd2d7375aa4ecc18f5aefda84801 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\210\244\346\226\255\345\233\236\346\226\207\351\223\276\350\241\250.md" @@ -28,6 +28,7 @@ title: '如何高效判断回文链表' **寻找**回文串的核心思想是从中心向两端扩展: + ```java // 在 s 中寻找以 s[left] 和 s[right] 为中心的最长回文串 String palindrome(String s, int left, int right) { @@ -47,6 +48,7 @@ String palindrome(String s, int left, int right) { 而**判断**一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要[双指针技巧](https://labuladong.github.io/article/fname.html?fname=双指针技巧),从两端向中间逼近即可: + ```java boolean isPalindrome(String s) { // 一左一右两个指针相向而行 @@ -94,6 +96,7 @@ boolean isPalindrome(ListNode head); 对于二叉树的几种遍历方式,我们再熟悉不过了: + ```java void traverse(TreeNode root) { // 前序遍历代码 @@ -106,6 +109,7 @@ void traverse(TreeNode root) { 在 [学习数据结构的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法) 中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,**链表其实也可以有前序遍历和后序遍历**: + ```java void traverse(ListNode head) { // 前序遍历代码 @@ -116,6 +120,7 @@ void traverse(ListNode head) { 这个框架有什么指导意义呢?如果我想正序打印链表中的 `val` 值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作: + ```java /* 倒序打印单链表中的元素值 */ void traverse(ListNode head) { @@ -128,6 +133,7 @@ void traverse(ListNode head) { 说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能: + ```java // 左侧指针 ListNode left; @@ -159,6 +165,7 @@ boolean traverse(ListNode right) { **1、先通过 [双指针技巧](https://labuladong.github.io/article/fname.html?fname=链表技巧) 中的快慢指针来找到链表的中点**: + ```java ListNode slow, fast; slow = fast = head; @@ -182,6 +189,7 @@ if (fast != null) **3、从`slow`开始反转后面的链表,现在就可以开始比较回文串了**: + ```java ListNode left = head; ListNode right = reverse(slow); @@ -199,6 +207,7 @@ return true; 至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中 `reverse` 函数很容易实现: + ```java boolean isPalindrome(ListNode head) { ListNode slow, fast; diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" index 939ddac74691b00803e79db937d1534b98d4b787..d27893d3ec9d9db707679076ed5e41293604a443 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\256\211\346\216\222\344\274\232\350\256\256\345\256\244.md" @@ -33,6 +33,7 @@ title: '扫描线技巧解决会议室安排问题' 函数签名如下: + ```java // 返回需要申请的会议室数量 int minMeetingRooms(int[][] meetings); @@ -152,38 +153,42 @@ int minMeetingRooms(int[][] meetings) { 然后就简单了,扫描线从左向右前进,遇到红点就对计数器加一,遇到绿点就对计数器减一,计数器 `count` 的最大值就是答案: + ```java -int minMeetingRooms(int[][] meetings) { - int n = meetings.length; - int[] begin = new int[n]; - int[] end = new int[n]; - for(int i = 0; i < n; i++) { - begin[i] = meetings[i][0]; - end[i] = meetings[i][1]; - } - Arrays.sort(begin); - Arrays.sort(end); - - // 扫描过程中的计数器 - int count = 0; - // 双指针技巧 - int res = 0, i = 0, j = 0; - while (i < n && j < n) { - if (begin[i] < end[j]) { - // 扫描到一个红点 - count++; - i++; - } else { - // 扫描到一个绿点 - count--; - j++; +class Solution { + int minMeetingRooms(int[][] meetings) { + int n = meetings.length; + int[] begin = new int[n]; + int[] end = new int[n]; + for(int i = 0; i < n; i++) { + begin[i] = meetings[i][0]; + end[i] = meetings[i][1]; + } + Arrays.sort(begin); + Arrays.sort(end); + + // 扫描过程中的计数器 + int count = 0; + // 双指针技巧 + int res = 0, i = 0, j = 0; + while (i < n && j < n) { + if (begin[i] < end[j]) { + // 扫描到一个红点 + count++; + i++; + } else { + // 扫描到一个绿点 + count--; + j++; + } + // 记录扫描过程中的最大值 + res = Math.max(res, count); } - // 记录扫描过程中的最大值 - res = Math.max(res, count); + + return res; } - - return res; } + ``` 这里使用的是 [双指针技巧](https://labuladong.github.io/article/fname.html?fname=双指针技巧),根据 `i, j` 的相对位置模拟扫描线前进的过程。 diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md" index cf64bc3f1e2d6628f91ef1f6d550add55a93cd81..ff57abda85b6a7b7c4d15126d8ebf56c879bb818 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\262\233\345\261\277\351\242\230\347\233\256.md" @@ -39,6 +39,7 @@ title: 'DFS 算法秒杀岛屿系列题目' 根据 [学习数据结构和算法的框架思维](https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法),完全可以根据二叉树的遍历框架改写出二维矩阵的 DFS 代码框架: + ```java // 二叉树遍历框架 void traverse(TreeNode root) { @@ -70,6 +71,7 @@ void dfs(int[][] grid, int i, int j, boolean[][] visited) { 这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和前文 [图遍历框架](https://labuladong.github.io/article/fname.html?fname=图) 的代码很类似: + ```java // 方向数组,分别代表上、下、左、右 int[][] dirs = new int[][]{{-1,0}, {1,0}, {0,-1}, {0,1}}; @@ -105,6 +107,7 @@ void dfs(int[][] grid, int i, int j, boolean[][] visited) { 我们说连成片的陆地形成岛屿,那么请你写一个算法,计算这个矩阵 `grid` 中岛屿的个数,函数签名如下: + ```java int numIslands(char[][] grid); ``` @@ -115,43 +118,46 @@ int numIslands(char[][] grid); 思路很简单,关键在于如何寻找并标记「岛屿」,这就要 DFS 算法发挥作用了,我们直接看解法代码: + ```java -// 主函数,计算岛屿数量 -int numIslands(char[][] grid) { - int res = 0; - int m = grid.length, n = grid[0].length; - // 遍历 grid - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (grid[i][j] == '1') { - // 每发现一个岛屿,岛屿数量加一 - res++; - // 然后使用 DFS 将岛屿淹了 - dfs(grid, i, j); +class Solution { + // 主函数,计算岛屿数量 + int numIslands(char[][] grid) { + int res = 0; + int m = grid.length, n = grid[0].length; + // 遍历 grid + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == '1') { + // 每发现一个岛屿,岛屿数量加一 + res++; + // 然后使用 DFS 将岛屿淹了 + dfs(grid, i, j); + } } } + return res; } - return res; -} -// 从 (i, j) 开始,将与之相邻的陆地都变成海水 -void dfs(char[][] grid, int i, int j) { - int m = grid.length, n = grid[0].length; - if (i < 0 || j < 0 || i >= m || j >= n) { - // 超出索引边界 - return; - } - if (grid[i][j] == '0') { - // 已经是海水了 - return; + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(char[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return; + } + if (grid[i][j] == '0') { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = '0'; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); } - // 将 (i, j) 变成海水 - grid[i][j] = '0'; - // 淹没上下左右的陆地 - dfs(grid, i + 1, j); - dfs(grid, i, j + 1); - dfs(grid, i - 1, j); - dfs(grid, i, j - 1); } ``` @@ -175,6 +181,7 @@ void dfs(char[][] grid, int i, int j) { 函数签名如下: + ```java int closedIsland(int[][] grid) ``` @@ -189,52 +196,55 @@ int closedIsland(int[][] grid) 有了这个思路,就可以直接看代码了,注意这题规定 `0` 表示陆地,用 `1` 表示海水: + ```java -// 主函数:计算封闭岛屿的数量 -int closedIsland(int[][] grid) { - int m = grid.length, n = grid[0].length; - for (int j = 0; j < n; j++) { - // 把靠上边的岛屿淹掉 - dfs(grid, 0, j); - // 把靠下边的岛屿淹掉 - dfs(grid, m - 1, j); - } - for (int i = 0; i < m; i++) { - // 把靠左边的岛屿淹掉 - dfs(grid, i, 0); - // 把靠右边的岛屿淹掉 - dfs(grid, i, n - 1); - } - // 遍历 grid,剩下的岛屿都是封闭岛屿 - int res = 0; - for (int i = 0; i < m; i++) { +class Solution { + // 主函数:计算封闭岛屿的数量 + int closedIsland(int[][] grid) { + int m = grid.length, n = grid[0].length; for (int j = 0; j < n; j++) { - if (grid[i][j] == 0) { - res++; - dfs(grid, i, j); + // 把靠上边的岛屿淹掉 + dfs(grid, 0, j); + // 把靠下边的岛屿淹掉 + dfs(grid, m - 1, j); + } + for (int i = 0; i < m; i++) { + // 把靠左边的岛屿淹掉 + dfs(grid, i, 0); + // 把靠右边的岛屿淹掉 + dfs(grid, i, n - 1); + } + // 遍历 grid,剩下的岛屿都是封闭岛屿 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 0) { + res++; + dfs(grid, i, j); + } } } + return res; } - return res; -} -// 从 (i, j) 开始,将与之相邻的陆地都变成海水 -void dfs(int[][] grid, int i, int j) { - int m = grid.length, n = grid[0].length; - if (i < 0 || j < 0 || i >= m || j >= n) { - return; - } - if (grid[i][j] == 1) { - // 已经是海水了 - return; + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 1) { + // 已经是海水了 + return; + } + // 将 (i, j) 变成海水 + grid[i][j] = 1; + // 淹没上下左右的陆地 + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); } - // 将 (i, j) 变成海水 - grid[i][j] = 1; - // 淹没上下左右的陆地 - dfs(grid, i + 1, j); - dfs(grid, i, j + 1); - dfs(grid, i - 1, j); - dfs(grid, i, j - 1); } ``` @@ -246,36 +256,40 @@ void dfs(int[][] grid, int i, int j) { 其实思路都是一样的,先把靠边的陆地淹掉,然后去数剩下的陆地数量就行了,注意第 1020 题中 `1` 代表陆地,`0` 代表海水: + ```java -int numEnclaves(int[][] grid) { - int m = grid.length, n = grid[0].length; - // 淹掉靠边的陆地 - for (int i = 0; i < m; i++) { - dfs(grid, i, 0); - dfs(grid, i, n - 1); - } - for (int j = 0; j < n; j++) { - dfs(grid, 0, j); - dfs(grid, m - 1, j); - } - - // 数一数剩下的陆地 - int res = 0; - for (int i = 0; i < m; i++) { +class Solution { + int numEnclaves(int[][] grid) { + int m = grid.length, n = grid[0].length; + // 淹掉靠边的陆地 + for (int i = 0; i < m; i++) { + dfs(grid, i, 0); + dfs(grid, i, n - 1); + } for (int j = 0; j < n; j++) { - if (grid[i][j] == 1) { - res += 1; + dfs(grid, 0, j); + dfs(grid, m - 1, j); + } + + // 数一数剩下的陆地 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + res += 1; + } } } + + return res; } - return res; + // 和之前的实现类似 + void dfs(int[][] grid, int i, int j) { + // ... + } } -// 和之前的实现类似 -void dfs(int[][] grid, int i, int j) { - // ... -} ``` 篇幅所限,具体代码我就不写了,我们继续看其他的岛屿题目。 @@ -284,6 +298,7 @@ void dfs(int[][] grid, int i, int j) { 这是力扣第 695 题「岛屿的最大面积」,`0` 表示海水,`1` 表示陆地,现在不让你计算岛屿的个数了,而是让你计算最大的那个岛屿的面积,函数签名如下: + ```java int maxAreaOfIsland(int[][] grid) ``` @@ -298,40 +313,43 @@ int maxAreaOfIsland(int[][] grid) 我们可以给 `dfs` 函数设置返回值,记录每次淹没的陆地的个数,直接看解法吧: + ```java -int maxAreaOfIsland(int[][] grid) { - // 记录岛屿的最大面积 - int res = 0; - int m = grid.length, n = grid[0].length; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (grid[i][j] == 1) { - // 淹没岛屿,并更新最大岛屿面积 - res = Math.max(res, dfs(grid, i, j)); +class Solution { + int maxAreaOfIsland(int[][] grid) { + // 记录岛屿的最大面积 + int res = 0; + int m = grid.length, n = grid[0].length; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + // 淹没岛屿,并更新最大岛屿面积 + res = Math.max(res, dfs(grid, i, j)); + } } } + return res; } - return res; -} -// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 -int dfs(int[][] grid, int i, int j) { - int m = grid.length, n = grid[0].length; - if (i < 0 || j < 0 || i >= m || j >= n) { - // 超出索引边界 - return 0; - } - if (grid[i][j] == 0) { - // 已经是海水了 - return 0; - } - // 将 (i, j) 变成海水 - grid[i][j] = 0; + // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 + int dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + // 超出索引边界 + return 0; + } + if (grid[i][j] == 0) { + // 已经是海水了 + return 0; + } + // 将 (i, j) 变成海水 + grid[i][j] = 0; - return dfs(grid, i + 1, j) - + dfs(grid, i, j + 1) - + dfs(grid, i - 1, j) - + dfs(grid, i, j - 1) + 1; + return dfs(grid, i + 1, j) + + dfs(grid, i, j + 1) + + dfs(grid, i - 1, j) + + dfs(grid, i, j - 1) + 1; + } } ``` @@ -355,45 +373,48 @@ int dfs(int[][] grid, int i, int j) { 依据这个思路,可以直接写出下面的代码: + ```java -int countSubIslands(int[][] grid1, int[][] grid2) { - int m = grid1.length, n = grid1[0].length; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (grid1[i][j] == 0 && grid2[i][j] == 1) { - // 这个岛屿肯定不是子岛,淹掉 - dfs(grid2, i, j); +class Solution { + int countSubIslands(int[][] grid1, int[][] grid2) { + int m = grid1.length, n = grid1[0].length; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid1[i][j] == 0 && grid2[i][j] == 1) { + // 这个岛屿肯定不是子岛,淹掉 + dfs(grid2, i, j); + } } } - } - // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量 - int res = 0; - for (int i = 0; i < m; i++) { - for (int j = 0; j < n; j++) { - if (grid2[i][j] == 1) { - res++; - dfs(grid2, i, j); + // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量 + int res = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid2[i][j] == 1) { + res++; + dfs(grid2, i, j); + } } } + return res; } - return res; -} -// 从 (i, j) 开始,将与之相邻的陆地都变成海水 -void dfs(int[][] grid, int i, int j) { - int m = grid.length, n = grid[0].length; - if (i < 0 || j < 0 || i >= m || j >= n) { - return; - } - if (grid[i][j] == 0) { - return; - } + // 从 (i, j) 开始,将与之相邻的陆地都变成海水 + void dfs(int[][] grid, int i, int j) { + int m = grid.length, n = grid[0].length; + if (i < 0 || j < 0 || i >= m || j >= n) { + return; + } + if (grid[i][j] == 0) { + return; + } - grid[i][j] = 0; - dfs(grid, i + 1, j); - dfs(grid, i, j + 1); - dfs(grid, i - 1, j); - dfs(grid, i, j - 1); + grid[i][j] = 0; + dfs(grid, i + 1, j); + dfs(grid, i, j + 1); + dfs(grid, i - 1, j); + dfs(grid, i, j - 1); + } } ``` @@ -405,6 +426,7 @@ void dfs(int[][] grid, int i, int j) { 力扣第 694 题「不同的岛屿数量」,题目还是输入一个二维矩阵,`0` 表示海水,`1` 表示陆地,这次让你计算 **不同的 (distinct)** 岛屿数量,函数签名如下: + ```java int numDistinctIslands(int[][] grid) ``` @@ -423,6 +445,7 @@ int numDistinctIslands(int[][] grid) 因为遍历顺序是写死在你的递归函数里面的,不会动态改变: + ```java void dfs(int[][] grid, int i, int j) { // 递归顺序: @@ -453,6 +476,7 @@ void dfs(int[][] grid, int i, int j) { 所以我们需要稍微改造 `dfs` 函数,添加一些函数参数以便记录遍历顺序: + ```java void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) { int m = grid.length, n = grid[0].length; @@ -476,6 +500,7 @@ void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) { `dir` 记录方向,`dfs` 函数递归结束后,`sb` 记录着整个遍历顺序。有了这个 `dfs` 函数就好办了,我们可以直接写出最后的解法代码: + ```java int numDistinctIslands(int[][] grid) { int m = grid.length, n = grid[0].length; diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" index 4d4fca36adef0f6f3921f9848171e85c2f11e1f2..63261de057e7f7860f21a640ad43427c533ffef8 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\345\272\247\344\275\215\350\260\203\345\272\246.md" @@ -33,6 +33,7 @@ title: '如何调度考生的座位' 也就是请你实现下面这样一个类: + ```java class ExamRoom { // 构造函数,传入座位总数 N @@ -87,47 +88,52 @@ class ExamRoom { 这个问题还用到一个常用的编程技巧,就是使用一个「虚拟线段」让算法正确启动,这就和链表相关的算法需要「虚拟头结点」一个道理。 + ```java -// 将端点 p 映射到以 p 为左端点的线段 -private Map startMap; -// 将端点 p 映射到以 p 为右端点的线段 -private Map endMap; -// 根据线段长度从小到大存放所有线段 -private TreeSet pq; -private int N; - -public ExamRoom(int N) { - this.N = N; - startMap = new HashMap<>(); - endMap = new HashMap<>(); - pq = new TreeSet<>((a, b) -> { - // 算出两个线段的长度 - int distA = distance(a); - int distB = distance(b); - // 长度更长的更大,排后面 - return distA - distB; - }); - // 在有序集合中先放一个虚拟线段 - addInterval(new int[] {-1, N}); -} +class ExamRoom { + // 将端点 p 映射到以 p 为左端点的线段 + private Map startMap; + // 将端点 p 映射到以 p 为右端点的线段 + private Map endMap; + // 根据线段长度从小到大存放所有线段 + private TreeSet pq; + private int N; + + public ExamRoom(int N) { + this.N = N; + startMap = new HashMap<>(); + endMap = new HashMap<>(); + pq = new TreeSet<>((a, b) -> { + // 算出两个线段的长度 + int distA = distance(a); + int distB = distance(b); + // 长度更长的更大,排后面 + return distA - distB; + }); + // 在有序集合中先放一个虚拟线段 + addInterval(new int[] {-1, N}); + } -/* 去除一个线段 */ -private void removeInterval(int[] intv) { - pq.remove(intv); - startMap.remove(intv[0]); - endMap.remove(intv[1]); -} + /* 去除一个线段 */ + private void removeInterval(int[] intv) { + pq.remove(intv); + startMap.remove(intv[0]); + endMap.remove(intv[1]); + } -/* 增加一个线段 */ -private void addInterval(int[] intv) { - pq.add(intv); - startMap.put(intv[0], intv); - endMap.put(intv[1], intv); -} + /* 增加一个线段 */ + private void addInterval(int[] intv) { + pq.add(intv); + startMap.put(intv[0], intv); + endMap.put(intv[1], intv); + } -/* 计算一个线段的长度 */ -private int distance(int[] intv) { - return intv[1] - intv[0] - 1; + /* 计算一个线段的长度 */ + private int distance(int[] intv) { + return intv[1] - intv[0] - 1; + } + + // ... } ``` @@ -138,37 +144,41 @@ private int distance(int[] intv) { 有了上述铺垫,主要 API `seat` 和 `leave` 就可以写了: ```java -public int seat() { - // 从有序集合拿出最长的线段 - int[] longest = pq.last(); - int x = longest[0]; - int y = longest[1]; - int seat; - if (x == -1) { // 情况一 - seat = 0; - } else if (y == N) { // 情况二 - seat = N - 1; - } else { // 情况三 - seat = (y - x) / 2 + x; +class ExamRoom { + // ... + + public int seat() { + // 从有序集合拿出最长的线段 + int[] longest = pq.last(); + int x = longest[0]; + int y = longest[1]; + int seat; + if (x == -1) { // 情况一 + seat = 0; + } else if (y == N) { // 情况二 + seat = N - 1; + } else { // 情况三 + seat = (y - x) / 2 + x; + } + // 将最长的线段分成两段 + int[] left = new int[] {x, seat}; + int[] right = new int[] {seat, y}; + removeInterval(longest); + addInterval(left); + addInterval(right); + return seat; } - // 将最长的线段分成两段 - int[] left = new int[] {x, seat}; - int[] right = new int[] {seat, y}; - removeInterval(longest); - addInterval(left); - addInterval(right); - return seat; -} -public void leave(int p) { - // 将 p 左右的线段找出来 - int[] right = startMap.get(p); - int[] left = endMap.get(p); - // 合并两个线段成为一个线段 - int[] merged = new int[] {left[0], right[1]}; - removeInterval(left); - removeInterval(right); - addInterval(merged); + public void leave(int p) { + // 将 p 左右的线段找出来 + int[] right = startMap.get(p); + int[] left = endMap.get(p); + // 合并两个线段成为一个线段 + int[] merged = new int[] {left[0], right[1]}; + removeInterval(left); + removeInterval(right); + addInterval(merged); + } } ``` @@ -201,14 +211,19 @@ pq = new TreeSet<>((a, b) -> { 除此之外,还要改变 `distance` 函数,**不能简单地让它计算一个线段两个端点间的长度,而是让它计算该线段中点和端点之间的长度**。 + ```java -private int distance(int[] intv) { - int x = intv[0]; - int y = intv[1]; - if (x == -1) return y; - if (y == N) return N - 1 - x; - // 中点和端点之间的长度 - return (y - x) / 2; +class ExamRoom { + // ... + + private int distance(int[] intv) { + int x = intv[0]; + int y = intv[1]; + if (x == -1) return y; + if (y == N) return N - 1 - x; + // 中点和端点之间的长度 + return (y - x) / 2; + } } ``` diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" index a62da251995115f545cd9842d23c5f91ec7a9c90..7b612fa3c57f9a31ada608bf1b378b6c1309f702 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\211\223\345\215\260\347\264\240\346\225\260.md" @@ -29,6 +29,7 @@ title: '如何高效寻找素数' 比如力扣第 204 题「计数质数」,让你写这样一个函数: + ```java // 返回区间 [2, n) 中有几个素数 int countPrimes(int n) @@ -39,6 +40,7 @@ int countPrimes(int n) 你会如何写这个函数?我想大家应该会这样写: + ```java int countPrimes(int n) { int count = 0; @@ -100,6 +102,7 @@ Wikipedia 的这个 GIF 很形象: 看到这里,你是否有点明白这个排除法的逻辑了呢?先看我们的第一版代码: + ```java int countPrimes(int n) { boolean[] isPrime = new boolean[n]; @@ -150,21 +153,25 @@ for (int j = i * i; j < n; j += i) 这样,素数计数的算法就高效实现了,其实这个算法有一个名字,叫做 Sieve of Eratosthenes。看下完整的最终代码: + ```java -int countPrimes(int n) { - boolean[] isPrime = new boolean[n]; - Arrays.fill(isPrime, true); - for (int i = 2; i * i < n; i++) - if (isPrime[i]) - for (int j = i * i; j < n; j += i) - isPrime[j] = false; - - int count = 0; - for (int i = 2; i < n; i++) - if (isPrime[i]) count++; - - return count; +class Solution { + public int countPrimes(int n) { + boolean[] isPrime = new boolean[n]; + Arrays.fill(isPrime, true); + for (int i = 2; i * i < n; i++) + if (isPrime[i]) + for (int j = i * i; j < n; j += i) + isPrime[j] = false; + + int count = 0; + for (int i = 2; i < n; i++) + if (isPrime[i]) count++; + + return count; + } } + ``` **该算法的时间复杂度比较难算**,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是: diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" index d6fab4759e219acecd9c3f7fface7388dd95302c..0f1506fd56b4d230ae2a387ebd1a9785e53ecdda 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\216\245\351\233\250\346\260\264.md" @@ -34,6 +34,7 @@ title: '接雨水问题详解' 就是用一个数组表示一个条形图,问你这个条形图最多能接多少水。 + ```java int trap(int[] height); ``` @@ -70,6 +71,7 @@ water[i] = min( 这就是本问题的核心思路,我们可以简单写一个暴力算法: + ```java int trap(int[] height) { int n = height.length; @@ -98,29 +100,32 @@ int trap(int[] height) { **我们开两个数组 `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 `i` 左边最高的柱子高度,`r_max[i]` 表示位置 `i` 右边最高的柱子高度**。预先把这两个数组计算好,避免重复计算: + ```java -int trap(int[] height) { - if (height.length == 0) { - return 0; +class Solution { + int trap(int[] height) { + if (height.length == 0) { + return 0; + } + int n = height.length; + int res = 0; + // 数组充当备忘录 + int[] l_max = new int[n]; + int[] r_max = new int[n]; + // 初始化 base case + l_max[0] = height[0]; + r_max[n - 1] = height[n - 1]; + // 从左向右计算 l_max + for (int i = 1; i < n; i++) + l_max[i] = Math.max(height[i], l_max[i - 1]); + // 从右向左计算 r_max + for (int i = n - 2; i >= 0; i--) + r_max[i] = Math.max(height[i], r_max[i + 1]); + // 计算答案 + for (int i = 1; i < n - 1; i++) + res += Math.min(l_max[i], r_max[i]) - height[i]; + return res; } - int n = height.length; - int res = 0; - // 数组充当备忘录 - int[] l_max = new int[n]; - int[] r_max = new int[n]; - // 初始化 base case - l_max[0] = height[0]; - r_max[n - 1] = height[n - 1]; - // 从左向右计算 l_max - for (int i = 1; i < n; i++) - l_max[i] = Math.max(height[i], l_max[i - 1]); - // 从右向左计算 r_max - for (int i = n - 2; i >= 0; i--) - r_max[i] = Math.max(height[i], r_max[i + 1]); - // 计算答案 - for (int i = 1; i < n - 1; i++) - res += Math.min(l_max[i], r_max[i]) - height[i]; - return res; } ``` @@ -132,6 +137,7 @@ int trap(int[] height) { 首先,看一部分代码: + ```java int trap(int[] height) { int left = 0, right = height.length - 1; @@ -152,26 +158,29 @@ int trap(int[] height) { 明白了这一点,直接看解法: + ```java -int trap(int[] height) { - int left = 0, right = height.length - 1; - int l_max = 0, r_max = 0; +class Solution { + int trap(int[] height) { + int left = 0, right = height.length - 1; + int l_max = 0, r_max = 0; - int res = 0; - while (left < right) { - l_max = Math.max(l_max, height[left]); - r_max = Math.max(r_max, height[right]); + int res = 0; + while (left < right) { + l_max = Math.max(l_max, height[left]); + r_max = Math.max(r_max, height[right]); - // res += min(l_max, r_max) - height[i] - if (l_max < r_max) { - res += l_max - height[left]; - left++; - } else { - res += r_max - height[right]; - right--; + // res += min(l_max, r_max) - height[i] + if (l_max < r_max) { + res += l_max - height[left]; + left++; + } else { + res += r_max - height[right]; + right--; + } } + return res; } - return res; } ``` @@ -212,6 +221,7 @@ if (l_max < r_max) { 函数签名如下: + ```java int maxArea(int[] height); ``` @@ -242,22 +252,25 @@ min(height[left], height[right]) * (right - left) 先直接看解法代码吧: + ```java -int maxArea(int[] height) { - int left = 0, right = height.length - 1; - int res = 0; - while (left < right) { - // [left, right] 之间的矩形面积 - int cur_area = Math.min(height[left], height[right]) * (right - left); - res = Math.max(res, cur_area); - // 双指针技巧,移动较低的一边 - if (height[left] < height[right]) { - left++; - } else { - right--; +class Solution { + public int maxArea(int[] height) { + int left = 0, right = height.length - 1; + int res = 0; + while (left < right) { + // [left, right] 之间的矩形面积 + int cur_area = Math.min(height[left], height[right]) * (right - left); + res = Math.max(res, cur_area); + // 双指针技巧,移动较低的一边 + if (height[left] < height[right]) { + left++; + } else { + right--; + } } + return res; } - return res; } ``` diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" index 95d5ff1a024bb9e7b6fa71e7ccf595dc8a9a7f47..de831aa55c39e6726f2880abb149580ff84a88b3 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\346\260\264\345\241\230\346\212\275\346\240\267.md" @@ -40,6 +40,7 @@ title: '随机算法之水塘抽样算法' **先说结论,当你遇到第 `i` 个元素时,应该有 `1/i` 的概率选择该元素,`1 - 1/i` 的概率保持原有的选择**。看代码容易理解这个思路: + ```java /* 返回链表中一个随机节点的值 */ int getRandom(ListNode head) { @@ -72,6 +73,7 @@ int getRandom(ListNode head) { **同理,如果要随机选择 `k` 个数,只要在第 `i` 个元素处以 `k/i` 的概率选择该元素,以 `1 - k/i` 的概率保持原有选择即可**。代码如下: + ```java /* 返回链表中 k 个随机节点的值 */ int[] getRandom(ListNode head, int k) { diff --git "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" index db59d228adad106a51381388f6bb33fecb6c961f..a8c4140d7b5fece71b78960c76305466dba6b9b8 100644 --- "a/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" +++ "b/\351\253\230\351\242\221\351\235\242\350\257\225\347\263\273\345\210\227/\347\274\272\345\244\261\345\222\214\351\207\215\345\244\215\347\232\204\345\205\203\347\264\240.md" @@ -29,6 +29,7 @@ title: '如何寻找缺失和重复的元素' 给一个长度为 `N` 的数组 `nums`,其中本来装着 `[1..N]` 这 `N` 个元素,无序。但是现在出现了一些错误,`nums` 中的一个元素出现了重复,也就同时导致了另一个元素的缺失。请你写一个算法,找到 `nums` 中的重复元素和缺失元素的值。 + ```java // 返回两个数字,分别是 {dup, missing} int[] findErrorNums(int[] nums); @@ -70,6 +71,7 @@ O(N) 的时间复杂度遍历数组是无法避免的,所以我们可以想想 对于这个现象,我们就可以翻译成代码了: + ```java int[] findErrorNums(int[] nums) { int n = nums.length; @@ -95,6 +97,7 @@ int[] findErrorNums(int[] nums) { 这个问题就基本解决了,别忘了我们刚才为了方便分析,假设元素是 `[0..N-1]`,但题目要求是 `[1..N]`,所以只要简单修改两处地方即可得到原题的答案: + ```java int[] findErrorNums(int[] nums) { int n = nums.length;