From d6f1c0efb21ab59a8fe0ac3f8e1d2e4abeacbb61 Mon Sep 17 00:00:00 2001 From: labuladong Date: Sun, 7 Apr 2024 19:25:28 +0800 Subject: [PATCH] update content --- README.md | 21 +- 多语言解法代码/solution_code.md | 790 +++++++++++++++++-- 算法思维系列/二分查找详解.md | 44 +- 算法思维系列/回溯算法详解修订版.md | 1 + 算法思维系列/学习数据结构和算法的高效方法.md | 2 + 算法思维系列/滑动窗口技巧进阶.md | 3 +- 6 files changed, 804 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 9f995c9..b7140ce 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,6 @@ PDF 共两本,一本《labuladong 的算法秘籍》类似教材,帮你系 - ### [本站简介](https://labuladong.github.io/article/fname.html?fname=home) ### [准备工作:安装刷题全家桶](https://labuladong.github.io/article/fname.html?fname=全家桶简介) @@ -134,10 +133,28 @@ PDF 共两本,一本《labuladong 的算法秘籍》类似教材,帮你系 * [配套 JetBrains 刷题插件](https://labuladong.github.io/article/fname.html?fname=jb插件简介) * [算法可视化面板简介(必读)](https://labuladong.github.io/article/fname.html?fname=可视化简介) * [使用可视化面板的 JavaScript 基础](https://labuladong.github.io/article/fname.html?fname=面板js基础) - * [学习本站所需的 Java 基础](https://labuladong.github.io/article/fname.html?fname=网站Java基础) * [30 天刷题打卡挑战(可选)](https://labuladong.github.io/article/fname.html?fname=打卡挑战简介) +### [新手入门:数据结构基础](https://labuladong.github.io/algo/) + * [本章导读](https://labuladong.github.io/article/fname.html?fname=数据结构基础简介) + * [学习本站所需的 Java 基础](https://labuladong.github.io/article/fname.html?fname=网站Java基础) + * [手把手带你实现动态数组](https://labuladong.github.io/algo/) + * [数组(顺序存储)基本原理](https://labuladong.github.io/article/fname.html?fname=数组基础) + * [动态数组代码实现](https://labuladong.github.io/article/fname.html?fname=数组实现) + * [手把手带你实现链表](https://labuladong.github.io/algo/) + * [链表(链式存储)基本原理](https://labuladong.github.io/article/fname.html?fname=链表基础) + * [链表代码实现](https://labuladong.github.io/article/fname.html?fname=链表实现) + * [手把手带你实现队列/栈](https://labuladong.github.io/algo/) + * [队列/栈基本原理](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=队列栈数组实现) + * [双端队列(Deque)原理及实现](https://labuladong.github.io/article/fname.html?fname=双端队列原理实现) + * [正在更新 ing](https://labuladong.github.io/article/fname.html?fname=更新中) + + ### [第零章、核心框架汇总](https://labuladong.github.io/algo/) + * [本章导读](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=链表技巧) diff --git a/多语言解法代码/solution_code.md b/多语言解法代码/solution_code.md index d99c030..4a577d5 100644 --- a/多语言解法代码/solution_code.md +++ b/多语言解法代码/solution_code.md @@ -7041,6 +7041,302 @@ class Solution: https://leetcode.cn/problems/add-two-numbers-ii 的多语言解法👆 +https://leetcode.cn/problems/additive-number 的多语言解法👇 + +```cpp +// by chatGPT (cpp) +class Solution { +public: + bool isAdditiveNumber(string num) { + // 穷举前两个数字 + int n = num.size(); + for (int i = 1; i <= n; i++) { + for (int j = i + 1; j <= n; j++) { + string first = num.substr(0, i); + string second = num.substr(i, j-i); + if (isValid(num, first, second)) { + return true; + } + } + } + return false; + } + +private: + // 定义:num 前两个数字分别是 first 和 second,判断 num 是否满足累加数的性质 + bool isValid(string num, string first, string second) { + if ((first[0] == '0' && first.size() > 1) + || (second[0] == '0' && second.size() > 1)) { + // 0 开头的数字,只能是 0 本身 + return false; + } + string sumStr = strAdd(first, second); + string next = num.substr(first.size() + second.size()); + if (next.find(sumStr) != 0) { + // 不满足累加数的性质 + return false; + } + if (next == sumStr) { + // 已经匹配完整个字符串 + return true; + } + // 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + // 你也可以改用迭代写法,一样的 + return isValid(num.substr(first.size()), second, sumStr); + } + + // 模拟加法竖式运算,具体可以看下这道题 + // https://leetcode-cn.com/problems/add-strings/ + string strAdd(string a, string b) { + int n = a.size(), m = b.size(); + int i = n - 1, j = m - 1, add = 0; + string builder; + while (i >= 0 || j >= 0 || add != 0) { + int x = i >= 0 ? a[i] - '0' : 0; + int y = j >= 0 ? b[j] - '0' : 0; + int result = x + y + add; + builder.push_back(result % 10 + '0'); + add = result / 10; + i--; + j--; + } + reverse(builder.begin(), builder.end()); + return builder; + } +}; +``` + +```go +// by chatGPT (go) +import ( + "strconv" + "strings" +) + +// isAdditiveNumber - 穷举前两个数字 +func isAdditiveNumber(num string) bool { + n := len(num) + for i := 1; i <= n; i++ { + for j := i + 1; j <= n; j++ { + first := num[:i] + second := num[i:j] + if isValid(num, first, second) { + return true + } + } + } + return false +} + +// isValid - 定义:num 前两个数字分别是 first 和 second,判断 num 是否满足累加数的性质 +func isValid(num string, first string, second string) bool { + if (strings.HasPrefix(first, "0") && len(first) > 1) || + (strings.HasPrefix(second, "0") && len(second) > 1) { + // 0 开头的数字,只能是 0 本身 + return false + } + sumStr := strAdd(first, second) + next := num[len(first)+len(second):] + if !strings.HasPrefix(next, sumStr) { + // 不满足累加数的性质 + return false + } + if next == sumStr { + // 已经匹配完整个字符串 + return true + } + // 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + // 你也可以改用迭代写法,一样的 + return isValid(num[len(first):], second, sumStr) +} + +// strAdd - 模拟加法竖式运算,具体可以看下这道题 +// https://leetcode.cn/problems/add-strings/ +func strAdd(a, b string) string { + i, j, carry := len(a)-1, len(b)-1, 0 + ans := "" + for i >= 0 || j >= 0 { + x, _ := strconv.Atoi(string(a[i])) + y, _ := strconv.Atoi(string(b[j])) + sum := x + y + carry + tmp := sum % 10 + carry = sum / 10 + ans = strconv.Itoa(tmp) + ans + i-- + j-- + } + if carry > 0 { + ans = "1" + ans + } + return ans +} +``` + +```java +// by labuladong (java) +class Solution { + public boolean isAdditiveNumber(String num) { + // 穷举前两个数字 + int n = num.length(); + for (int i = 1; i <= n; i++) { + for (int j = i + 1; j <= n; j++) { + String first = num.substring(0, i); + String second = num.substring(i, j); + if (isValid(num, first, second)) { + return true; + } + } + } + return false; + } + + // 定义:num 前两个数字分别是 first 和 second,判断 num 是否满足累加数的性质 + boolean isValid(String num, String first, String second) { + if (first.startsWith("0") && first.length() > 1 + || second.startsWith("0") && second.length() > 1) { + // 0 开头的数字,只能是 0 本身 + return false; + } + String sumStr = strAdd(first, second); + String next = num.substring(first.length() + second.length()); + if (!next.startsWith(sumStr)) { + // 不满足累加数的性质 + return false; + } + if (next.equals(sumStr)) { + // 已经匹配完整个字符串 + return true; + } + // 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + // 你也可以改用迭代写法,一样的 + return isValid(num.substring(first.length()), second, sumStr); + } + + // 模拟加法竖式运算,具体可以看下这道题 + // https://leetcode.cn/problems/add-strings/ + String strAdd(String a, String b) { + int n = a.length(), m = b.length(); + int i = n - 1, j = m - 1, add = 0; + StringBuilder builder = new StringBuilder(); + while (i >= 0 || j >= 0 || add != 0) { + int x = i >= 0 ? a.charAt(i) - '0' : 0; + int y = j >= 0 ? b.charAt(j) - '0' : 0; + int result = x + y + add; + builder.append(result % 10); + add = result / 10; + i--; + j--; + } + return builder.reverse().toString(); + } +} +``` + +```javascript +// by chatGPT (javascript) +var isAdditiveNumber = function(num) { + // 穷举前两个数字 + let n = num.length; + for (let i = 1; i <= n; i++) { + for (let j = i + 1; j <= n; j++) { + let first = num.substring(0, i); + let second = num.substring(i, j); + if (isValid(num, first, second)) { + return true; + } + } + } + return false; +} + +var isValid = function(num, first, second) { + if ((first.startsWith("0") && first.length > 1) + || (second.startsWith("0") && second.length > 1)) { + // 0 开头的数字,只能是 0 本身 + return false; + } + let sumStr = strAdd(first, second); + let next = num.substring(first.length + second.length); + if (!next.startsWith(sumStr)) { + // 不满足累加数的性质 + return false; + } + if (next === sumStr) { + // 已经匹配完整个字符串 + return true; + } + // 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + // 你也可以改用迭代写法,一样的 + return isValid(num.substring(first.length), second, sumStr); +} + +var strAdd = function(a, b) { + let n = a.length, m = b.length; + let i = n - 1, j = m - 1, add = 0; + let builder = []; + while (i >= 0 || j >= 0 || add != 0) { + let x = i >= 0 ? a.charAt(i) - '0' : 0; + let y = j >= 0 ? b.charAt(j) - '0' : 0; + let result = x + y + add; + builder.push(result % 10); + add = Math.floor(result / 10); + i--; + j--; + } + return builder.reverse().join(''); +} +``` + +```python +# by chatGPT (python) +class Solution: + def isAdditiveNumber(self, num): + # 穷举前两个数字 + n = len(num) + for i in range(1, n + 1): + for j in range(i + 1, n + 1): + first = num[0 : i] + second = num[i : j] + if self.isValid(num, first, second): + return True + return False + + def isValid(self, num, first, second): + # 定义:num 前两个数字分别是 first 和 second,判断 num 是否满足累加数的性质 + if (first.startswith("0") and len(first) > 1) or (second.startswith("0") and len(second) > 1): + # 0 开头的数字,只能是 0 本身 + return False + sumStr = self.strAdd(first, second) + next = num[len(first) + len(second):] + if not next.startswith(sumStr): + # 不满足累加数的性质 + return False + if next == sumStr: + # 已经匹配完整个字符串 + return True + # 根据递归函数的定义,继续匹配后面的三个数字,我这里用递归的方式去比较,因为容易写 + # 你也可以改用迭代写法,一样的 + return self.isValid(num[len(first):], second, sumStr) + + def strAdd(self, a, b): + # 模拟加法竖式运算,具体可以看下这道题 + # https://leetcode.cn/problems/add-strings/ + n, m = len(a), len(b) + i, j, add = n - 1, m - 1, 0 + builder = [] + while i >= 0 or j >= 0 or add != 0: + x = int(a[i]) if i >= 0 else 0 + y = int(b[j]) if j >= 0 else 0 + result = x + y + add + builder.append(result % 10) + add = result // 10 + i -= 1 + j -= 1 + return ''.join(str(i) for i in builder[::-1]) +``` + +https://leetcode.cn/problems/additive-number 的多语言解法👆 + https://leetcode.cn/problems/advantage-shuffle 的多语言解法👇 ```cpp @@ -22934,7 +23230,8 @@ class Solution2 { public List findDuplicates(int[] nums) { List res = new LinkedList<>(); for (int num : nums) { - // 注意索引,元素大小从 1 开始,有一位索引偏移 + // 注意索引,nums 中元素大小从 1 开始, + // 而索引是从 0 开始的,所以有一位索引偏移 if (nums[Math.abs(num) - 1] < 0) { // 之前已经把对应索引的元素变成负数了, // 这说明 num 重复出现了两次 @@ -23145,10 +23442,11 @@ class Solution { class Solution2 { public List findDisappearedNumbers(int[] nums) { for (int num : nums) { - // 注意索引,元素大小从 1 开始,有一位索引偏移 + // 注意索引,nums 中元素大小从 1 开始, + // 而索引是从 0 开始的,所以有一位索引偏移 if (nums[Math.abs(num) - 1] < 0) { // 之前已经把对应索引的元素变成负数了, - // 这说明 num 重复出现了两次,但我们不用做,让索引继续保持负数 + // 这说明 num 重复出现了两次,但我们什么都不用做,让索引继续保持负数 } else { // 把索引 num - 1 置为负数 nums[Math.abs(num) - 1] *= -1; @@ -24474,13 +24772,13 @@ class PriorityQueue { import heapq class Solution: - def kSmallestPairs(self, nums1, nums2, k): + def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]: # 存储三元组 (num1[i], nums2[i], i) # i 记录 nums2 元素的索引位置,用于生成下一个节点 - pq = [] + pq = [] for i in range(len(nums1)): - heapq.heappush(pq, (nums1[i] + nums2[0], nums2[0], 0)) - + heapq.heappush(pq, [nums1[i], nums2[0], 0]) + res = [] # 执行合并多个有序链表的逻辑 while pq and k > 0: @@ -24488,14 +24786,12 @@ class Solution: k -= 1 # 链表中的下一个节点加入优先级队列 next_index = cur[2] + 1 - # 获取第一个链表节点 - node = cur[0] - cur[1] if next_index < len(nums2): - heapq.heappush(pq, (node + nums2[next_index], nums2[next_index], next_index)) - - pair = [node, cur[1]] + heapq.heappush(pq, [cur[0], nums2[next_index], next_index]) + + pair = [cur[0], cur[1]] res.append(pair) - + return res ``` @@ -25492,6 +25788,91 @@ class NestedIterator: https://leetcode.cn/problems/flatten-nested-list-iterator 的多语言解法👆 +https://leetcode.cn/problems/flip-game 的多语言解法👇 + +```java +// by labuladong (java) +class Solution { + public List generatePossibleNextMoves(String currentState) { + List res = new ArrayList<>(); + char[] arr = currentState.toCharArray(); + for (int i = 1; i < arr.length; i++) { + if (arr[i] == '+' && arr[i - 1] == '+') { + // 做选择 + arr[i] = '-'; + arr[i - 1] = '-'; + res.add(new String(arr)); + // 撤销选择 + arr[i] = '+'; + arr[i - 1] = '+'; + } + } + return res; + } +} +``` + +https://leetcode.cn/problems/flip-game 的多语言解法👆 + +https://leetcode.cn/problems/flip-game-ii 的多语言解法👇 + +```java +// by labuladong (java) +class Solution { + + // 直接把 293 的代码 copy 过来,生成所有可能的下一步 + List generatePossibleNextMoves(String currentState) { + List res = new ArrayList<>(); + char[] arr = currentState.toCharArray(); + for (int i = 1; i < arr.length; i++) { + if (arr[i] == '+' && arr[i - 1] == '+') { + // 做选择 + arr[i] = '-'; + arr[i - 1] = '-'; + res.add(new String(arr)); + // 撤销选择 + arr[i] = '+'; + arr[i - 1] = '+'; + } + } + return res; + } + + // 备忘录 + Map memo = new HashMap<>(); + + public boolean canWin(String currentState) { + // 记入备忘录 + memo.clear(); + return dp(currentState); + } + + // 定义:输入字符串 s,返回在此局面下先手是否可能赢 + boolean dp(String s) { + if (memo.containsKey(s)) { + // 之前遇到过这种局面,直接返回结果 + return memo.get(s); + } + boolean res = false; + List nextStates = generatePossibleNextMoves(s); + for (String nextState : nextStates) { + // 只要有一个是 false,说明当前玩家赢了 + boolean win = dp(nextState); + // 后序位置,做判断 + if (!win) { + res = true; + break; + } + } + // 记入备忘录 + memo.put(s, res); + return res; + } +} +``` + +https://leetcode.cn/problems/flip-game-ii 的多语言解法👆 + https://leetcode.cn/problems/freedom-trail 的多语言解法👇 ```cpp @@ -28926,31 +29307,27 @@ class Solution { ```javascript // by chatGPT (javascript) var rob = function(root) { - // memo 需要提到函数外侧,否则每次递归重新声明 memo,相当于备忘录没有生效 const memo = new Map(); - let dp = (node) => { - if (node === null) { - return 0; - } - // 利用备忘录消除重叠子问题 - if (memo.has(node)) { - return memo.get(node); - } - // 抢,然后去下下家 - let do_it = node.val + - ((node.left === null) ? - 0 : - dp(node.left.left) + dp(node.left.right)) + - ((node.right === null) ? - 0 : - dp(node.right.left) + dp(node.right.right)); - // 不抢,然后去下家 - let not_do = dp(node.left) + dp(node.right); - let res = Math.max(do_it, not_do); - memo.set(node, res); - return res; + if (root === null) { + return 0; } - return dp(root) + // 利用备忘录消除重叠子问题 + if (memo.has(root)) { + return memo.get(root); + } + // 抢,然后去下下家 + let do_it = root.val + + ((root.left === null) ? + 0 : + rob(root.left.left) + rob(root.left.right)) + + ((root.right === null) ? + 0 : + rob(root.right.left) + rob(root.right.right)); + // 不抢,然后去下家 + let not_do = rob(root.left) + rob(root.right); + let res = Math.max(do_it, not_do); + memo.set(root, res); + return res; }; ``` @@ -32972,6 +33349,93 @@ https://leetcode.cn/problems/lMSNwu 的多语言解法👆 https://leetcode.cn/problems/letter-case-permutation 的多语言解法👇 +```cpp +// by chatGPT (cpp) +class Solution { +public: + string track; + vector res; + + vector letterCasePermutation(string s) { + backtrack(s, 0); + return res; + } + + void backtrack(string s, int index) { + if (index == s.size()) { + res.push_back(track); + return; + } + + if ('0' <= s[index] && s[index] <= '9') { + // s[index] 是数字 + // 做选择 + track.push_back(s[index]); + backtrack(s, index + 1); + // 撤销选择 + track.pop_back(); + } else { + // s[index] 是字母 + + // 小写字母,做选择 + track.push_back(tolower(s[index])); + backtrack(s, index + 1); + // 撤销选择 + track.pop_back(); + + // 大写字母,做选择 + track.push_back(toupper(s[index])); + backtrack(s, index + 1); + // 撤销选择 + track.pop_back(); + } + } +}; +``` + +```go +// by chatGPT (go) +func letterCasePermutation(s string) []string { + var track []byte + var res []string + + var backtrack func(s string, index int) + backtrack = func(s string, index int) { + if index == len(s) { + res = append(res, string(track)) + return + } + + if '0' <= s[index] && s[index] <= '9' { + // s[index] 是数字 + // 做选择 + track = append(track, s[index]) + backtrack(s, index + 1) + // 撤销选择 + track = track[:len(track)-1] + } else { + // s[index] 是字母 + + // 小写字母,做选择 + track = append(track, byte(unicode.ToLower(rune(s[index])))) + backtrack(s, index + 1) + // 撤销选择 + track = track[:len(track)-1] + + // 大写字母,做选择 + track = append(track, byte(unicode.ToUpper(rune(s[index])))) + backtrack(s, index + 1) + // 撤销选择 + track = track[:len(track)-1] + } + } + + backtrack(s, 0) + + return res +} +``` + ```java // by labuladong (java) class Solution { @@ -33015,6 +33479,88 @@ class Solution { } ``` +```javascript +// by chatGPT (javascript) +var Solution = function() { + this.track = []; + this.res = []; + + this.letterCasePermutation = function(s) { + this.backtrack(s, 0); + return this.res; + } + + this.backtrack = function(s, index) { + if (index == s.length) { + this.res.push(this.track.join('')); + return; + } + + if ('0' <= s.charAt(index) && s.charAt(index) <= '9') { + // s[index] 是数字 + // 做选择 + this.track.push(s.charAt(index)); + this.backtrack(s, index + 1); + // 撤销选择 + this.track.pop(); + } else { + // s[index] 是字母 + + // 小写字母,做选择 + this.track.push(s.charAt(index).toLowerCase()); + this.backtrack(s, index + 1); + // 撤销选择 + this.track.pop(); + + // 大写字母,做选择 + this.track.push(s.charAt(index).toUpperCase()); + this.backtrack(s, index + 1); + // 撤销选择 + this.track.pop(); + } + } +} +``` + +```python +# by chatGPT (python) +class Solution: + def __init__(self): + self.track = "" + self.res = [] + + def letterCasePermutation(self, s: str) -> List[str]: + self.backtrack(s, 0) + return self.res + + def backtrack(self, s, index): + if index == len(s): + self.res.append(self.track) + return + + if '0' <= s[index] <= '9': + # s[index] 是数字 + # 做选择 + self.track += s[index] + self.backtrack(s, index + 1) + # 撤销选择 + self.track = self.track[:-1] + else: + # s[index] 是字母 + + # 小写字母,做选择 + self.track += s[index].lower() + self.backtrack(s, index + 1) + # 撤销选择 + self.track = self.track[:-1] + + # 大写字母,做选择 + self.track += s[index].upper() + self.backtrack(s, index + 1) + # 撤销选择 + self.track = self.track[:-1] +``` + https://leetcode.cn/problems/letter-case-permutation 的多语言解法👆 https://leetcode.cn/problems/letter-combinations-of-a-phone-number 的多语言解法👇 @@ -40399,7 +40945,70 @@ func minDepth(root *TreeNode) int { ```java // by labuladong (java) +// 「迭代」的递归思路 class Solution { + private int minDepth = Integer.MAX_VALUE; + private int currentDepth = 0; + + public int minDepth(TreeNode root) { + if (root == null) { + return 0; + } + traverse(root); + return minDepth; + } + + private void traverse(TreeNode root) { + if (root == null) { + return; + } + + // 做选择:在进入节点时增加当前深度 + currentDepth++; + + // 如果当前节点是叶子节点,更新最小深度 + if (root.left == null && root.right == null) { + minDepth = Math.min(minDepth, currentDepth); + } + + traverse(root.left); + traverse(root.right); + + // 撤销选择:在离开节点时减少当前深度 + currentDepth--; + } +} + +// 「分解问题」的递归思路 +class Solution2 { + public int minDepth(TreeNode root) { + // 基本情况:如果节点为空,返回深度为0 + if (root == null) { + return 0; + } + + // 递归计算左子树的最小深度 + int leftDepth = minDepth(root.left); + // 递归计算右子树的最小深度 + int rightDepth = minDepth(root.right); + + // 特殊情况处理:如果左子树为空,返回右子树的深度加1 + if (leftDepth == 0) { + return rightDepth + 1; + } + // 特殊情况处理:如果右子树为空,返回左子树的深度加1 + if (rightDepth == 0) { + return leftDepth + 1; + } + + // 计算并返回最小深度:左右子树深度的最小值加1 + return Math.min(leftDepth, rightDepth) + 1; + } +} + + +// BFS 的思路 +class Solution3 { public int minDepth(TreeNode root) { if (root == null) return 0; Queue q = new LinkedList<>(); @@ -48667,6 +49276,89 @@ class Solution { } ``` +```javascript +// by chatGPT (javascript) +var minimumEffortPath = function(heights) { + + // Dijkstra 算法,计算 (0, 0) 到 (m - 1, n - 1) 的最小体力消耗 + let m = heights.length, + n = heights[0].length, + // 定义:从 (0, 0) 到 (i, j) 的最小体力消耗是 effortTo[i][j] + effortTo = Array.from({ length: m }, () => Array(n).fill(Number.MAX_SAFE_INTEGER)), + // 方向数组,上下左右的坐标偏移量 + dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]], + // 优先级队列,effortFromStart 较小的排在前面 + pq = []; + + // 从起点 (0, 0) 开始进行 BFS + pq.push(new State(0, 0, 0)); + + // base case,起点到起点的最小消耗就是 0 + effortTo[0][0] = 0; + + class State { + // 矩阵中的一个位置 + // 从起点 (0, 0) 到当前位置的最小体力消耗(距离) + constructor(x, y, effortFromStart) { + this.x = x; + this.y = y; + this.effortFromStart = effortFromStart; + } + } + + function adj(matrix, x, y) { + let m = matrix.length, n = matrix[0].length; + // 存储相邻节点 + let neighbors = []; + for (let dir of dirs) { + let nx = x + dir[0]; + let ny = y + dir[1]; + if (nx >= m || nx < 0 || ny >= n || ny < 0) { + // 索引越界 + continue; + } + neighbors.push([nx, ny]); + } + return neighbors; + } + + while (pq.length != 0) { + let curState = pq.shift(); + let curX = curState.x; + let curY = curState.y; + let curEffortFromStart = curState.effortFromStart; + + // 到达终点提前结束 + if (curX == m - 1 && curY == n - 1) { + return curEffortFromStart; + } + + if (curEffortFromStart > effortTo[curX][curY]) { + continue; + } + + // 将 (curX, curY) 的相邻坐标装入队列 + for (let neighbor of adj(heights, curX, curY)) { + let nextX = neighbor[0]; + let nextY = neighbor[1]; + // 计算从 (curX, curY) 达到 (nextX, nextY) 的消耗 + let effortToNextNode = Math.max( + effortTo[curX][curY], + Math.abs(heights[curX][curY] - heights[nextX][nextY]) + ); + // 更新 dp table + if (effortTo[nextX][nextY] > effortToNextNode) { + effortTo[nextX][nextY] = effortToNextNode; + pq.push(new State(nextX, nextY, effortToNextNode)); + } + } + } + + // 正常情况不会达到这个 return + return -1; +}; +``` + ```python # by chatGPT (python) import heapq @@ -53531,17 +54223,18 @@ class Solution { int count = 0; while (fast < nums.length) { if (nums[fast] != nums[slow]) { + // 此时,对于 nums[0..slow] 来说,nums[fast] 是一个新的元素,加进来 slow++; nums[slow] = nums[fast]; } else if (slow < fast && count < 2) { - // 当一个元素重复次数不到 2 次时,也 + // 此时,对于 nums[0..slow] 来说,nums[fast] 重复次数小于 2,也加进来 slow++; nums[slow] = nums[fast]; } fast++; count++; if (fast < nums.length && nums[fast] != nums[fast - 1]) { - // 遇到不同的元素 + // fast 遇到新的不同的元素时,重置 count count = 0; } } @@ -58478,18 +59171,25 @@ func abs(num int) int { // by labuladong (java) class Solution { public int findRepeatNumber(int[] nums) { + // 先把 nums 数组中的所有元素都加一,避免 0 的影响 + for (int i = 0; i < nums.length; i++) { + nums[i] = nums[i] + 1; + } + for (int num : nums) { - if (nums[Math.abs(num)] < 0) { + // 该元素对应的索引 + int index = Math.abs(num) - 1; + if (nums[index] < 0) { // 之前已经把对应索引的元素变成负数了, // 这说明 num 重复出现了两次 - return Math.abs(num); + // 注意结果要减一 + return Math.abs(num) - 1; } else { // 把索引 num 的元素置为负数 - nums[Math.abs(num)] *= -1; + nums[index] *= -1; } } - // 如果没有在 for 循环中返回,说明重复的那个元素是 0 - return 0; + return -1; } } ``` @@ -62832,13 +63532,13 @@ class Solution { // 加入起点 q.offer(start); visited[start[0]][start[1]] = true; + int step = 0; // 启动 BFS 算法框架 while (!q.isEmpty()) { int[] cur = q.poll(); // 向四个方向扩展 for (int[] dir : dirs) { int x = cur[0], y = cur[1]; - int step = 0; // 和其他题目不同的是,这里一直走到墙,而不是只走一步,同时要记录走过的步数 while (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == 0) { x += dir[0]; @@ -71075,4 +71775,4 @@ class Solution: return s[n:] + s[:n] ``` -https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof 的多语言解法👆 +https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof 的多语言解法👆 \ No newline at end of file diff --git a/算法思维系列/二分查找详解.md b/算法思维系列/二分查找详解.md index fcd4ba2..ed76536 100644 --- a/算法思维系列/二分查找详解.md +++ b/算法思维系列/二分查找详解.md @@ -31,9 +31,11 @@ 先给大家讲个笑话乐呵一下: -有一天阿东到图书馆借了 `N` 本书,出图书馆的时候,警报响了,于是保安把阿东拦下,要检查一下哪本书没有登记出借。阿东正准备把每一本书在报警器下过一下,以找出引发警报的书。但是保安露出不屑的眼神:你连二分查找都不会吗? +有一天阿东到图书馆借了 `N` 本书,出图书馆的时候,警报响了,于是保安把阿东拦下,要检查一下哪本书没有登记出借。阿东正准备把每一本书在报警器下过一下,以找出引发警报的书,但是保安露出不屑的眼神:你连二分查找都不会吗? -于是保安把书分成两堆,让第一堆过一下报警器,报警器响;于是再把这堆书分成两堆…… 最终,检测了 `logN` 次之后,保安成功的找到了那本引起警报的书,露出了得意和嘲讽的笑容。于是阿东背着剩下的书走了。 +于是保安把书分成两堆,让第一堆过一下报警器,报警器响,这说明引起报警的书包含在里面;于是再把这堆书分成两堆,把第一堆过一下报警器,报警器又响,继续分成两堆…… + +最终,检测了 `logN` 次之后,保安成功的找到了那本引起警报的书,露出了得意和嘲讽的笑容。于是阿东背着剩下的书走了。 从此,图书馆丢了 `N - 1` 本书(手动狗头)。 @@ -200,9 +202,32 @@ int left_bound(int[] nums, int target) { 有点绕晕了是吧?这个 `left_bound` 函数明明是搜索左边界的,但是当 `target` 不存在的时候,却返回的是大于 `target` 的最小索引。这个结论不用死记,你要是拿不准,简单举个例子就能得到这个结论了。 -所以说,二分搜索这个东西思路很简单,细节是魔鬼嘛,里面的坑太多了。要是真想考你,肯定可以把你考到怀疑人生。 +所以跟你说二分搜索这个东西思路很简单,细节是魔鬼嘛,里面的坑太多了。要是真想考你,总有办法可以把你考到怀疑人生。 -我的建议是,如果你必须手写二分代码,那么你一定要了解清楚代码的种种行为,本文总结的框架就是在帮你理清这里面的细节。如果非必要,不要自己手写,尽肯能用编程语言提供的标准库函数,可以节约时间,而且标准库函数的行为在文档里都有明确的说明,不容易出错。 +不是我故意把代码模板总结的这么复杂,而是二分搜索本身就很复杂,这些细节是不可能绕开的,如果你之前没有了解过这些细节,只能说明你之前学得不扎实。就算不用我总结的模板,你也必须搞清楚当 `target` 不存在时算法的行为,否则出了 bug 你都不知道咋改,真有这么严重。 + +话说回来,`left_bound` 的这个行为有一个好处。比方说现在让你写一个 `floor` 函数: + +```java +// 在一个有序数组中,找到「小于 target 的最大元素的索引」 +// 比如说输入 nums = [1,2,2,2,3],target = 2,函数返回 0,因为 1 是小于 2 的最大元素。 +// 再比如输入 nums = [1,2,3,5,6],target = 4,函数返回 2,因为 3 是小于 4 的最大元素。 +int floor(int[] nums, int target); +``` + +那么就可以直接用 `left_bound` 函数来实现: + +```java +int floor(int[] nums, int target) { + // 当 target 不存在,比如输入 [4,6,8,10], target = 7 + // left_bound 返回 2,减一就是 1,元素 6 就是小于 7 的最大元素 + // 当 target 存在,比如输入 [4,6,8,8,8,10], target = 8 + // left_bound 返回 2,减一就是 1,元素 6 就是小于 8 的最大元素 + return left_bound(nums, target) - 1; +} +``` + +最后,我的建议是,如果你必须手写二分代码,那么你一定要了解清楚代码的种种行为,本文总结的框架就是在帮你理清这里面的细节。如果非必要,不要自己手写,尽肯能用编程语言提供的标准库函数,可以节约时间,而且标准库函数的行为在文档里都有明确的说明,不容易出错。 ::: @@ -216,15 +241,16 @@ while (left < right) { if (left < 0 || left >= nums.length) { return -1; } +// 提示:其实上面的 if 中 left < 0 这个判断可以省略,因为对于这个算法,left 不可能小于 0 +// 你看这个算法执行的逻辑,left 初始化就是 0,且只可能一直往右走,那么只可能在右侧越界 +// 不过我这里就同时判断了,因为在访问数组索引之前保证索引在左右两端都不越界是一个好习惯,没有坏处 +// 另一个好处是让二分的模板更统一,降低你的记忆成本,因为等会儿寻找右边界的时候也有类似的出界判断 + + // 判断一下 nums[left] 是不是 target return nums[left] == target ? left : -1; ``` -::: tip - -其实对于这个算法,`left` 不可能小于 0。你可以想象一下算法执行的逻辑,`left` 初始化就是 0,且只可能一直往右走,那么只可能在右侧越界。不过在访问数组索引之前保证索引在左右两端都不越界是一个很好的编程习惯,没有坏处,我这里就同时判断了。这样做的另一个好处是可以让二分的模板更统一,降低你的记忆成本。 - -::: **3、为什么 `left = mid + 1`,`right = mid` ?和之前的算法不一样**? diff --git a/算法思维系列/回溯算法详解修订版.md b/算法思维系列/回溯算法详解修订版.md index 9e54dce..304fe18 100644 --- a/算法思维系列/回溯算法详解修订版.md +++ b/算法思维系列/回溯算法详解修订版.md @@ -437,6 +437,7 @@ def backtrack(...): - [球盒模型:回溯算法穷举的两种视角](https://labuladong.github.io/article/fname.html?fname=回溯两种视角) - [目标和问题:背包问题的变体](https://labuladong.github.io/article/fname.html?fname=targetSum) - [算法学习和心流体验](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=扎气球) diff --git a/算法思维系列/学习数据结构和算法的高效方法.md b/算法思维系列/学习数据结构和算法的高效方法.md index ebf5ab0..6890cbf 100644 --- a/算法思维系列/学习数据结构和算法的高效方法.md +++ b/算法思维系列/学习数据结构和算法的高效方法.md @@ -379,8 +379,10 @@ N 叉树的遍历框架,找出来了吧?你说,树这种结构重不重要 - [如何判断回文链表](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=数组基础) - [浅谈存储系统:LSM 树设计原理](https://labuladong.github.io/article/fname.html?fname=LSM树) - [环检测及拓扑排序算法](https://labuladong.github.io/article/fname.html?fname=拓扑排序) + - [用链表实现队列/栈](https://labuladong.github.io/article/fname.html?fname=队列栈链表实现) - [目标和问题:背包问题的变体](https://labuladong.github.io/article/fname.html?fname=targetSum) - [算法学习和心流体验](https://labuladong.github.io/article/fname.html?fname=心流) - [算法时空复杂度分析实用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度) diff --git a/算法思维系列/滑动窗口技巧进阶.md b/算法思维系列/滑动窗口技巧进阶.md index 3bd9845..d4ca330 100644 --- a/算法思维系列/滑动窗口技巧进阶.md +++ b/算法思维系列/滑动窗口技巧进阶.md @@ -173,7 +173,7 @@ for (int i = 0; i < s.size(); i++) ::: tip 为什么要「左闭右开」区间 -理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。 +**理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的**。 因为这样初始化 `left = right = 0` 时区间 `[0, 0)` 中没有元素,但只要让 `right` 向右移动(扩大)一位,区间 `[0, 1)` 就包含一个元素 `0` 了。 @@ -498,6 +498,7 @@ int lengthOfLongestSubstring(string s) { - [我的刷题心得:算法的本质](https://labuladong.github.io/article/fname.html?fname=算法心得) - [本站简介](https://labuladong.github.io/article/fname.html?fname=home) - [滑动窗口算法延伸:Rabin Karp 字符匹配算法](https://labuladong.github.io/article/fname.html?fname=rabinkarp) + - [环形数组技巧](https://labuladong.github.io/article/fname.html?fname=环形数组技巧) - [算法时空复杂度分析实用指南](https://labuladong.github.io/article/fname.html?fname=时间复杂度)