diff --git a/.gitignore b/.gitignore index bc2f62e0..f9eb1759 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,10 @@ node_modules/ docs/.vuepress/dist/ docs/.vitepress/dist/ docs/.vitepress/cache/ +src/.lcpr_data/ dist/ coverage/ *.log pnpm-debug.log* .idea/ +/training-system/progress/ diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index e00cd3d3..b3a170f4 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -60,7 +60,7 @@ const topicOrder: string[] = [ 'dynamic-programming-state-machine', 'dynamic-programming-interval', // 进阶数据结构篇 - 'monotonic-stack', + 'monotonic-stack' ]; function getCategoryTitle(name: string): string { diff --git a/docs/docs/topic/slide-window.md b/docs/docs/topic/slide-window.md index 52144cb4..64fa8e61 100644 --- a/docs/docs/topic/slide-window.md +++ b/docs/docs/topic/slide-window.md @@ -2,7 +2,7 @@ 滑动窗口真正要训练的,不是记住几段代码,而是先把题目改写成: -1. 维护一个连续区间 `[left, right]` 或 `[left, right)`。 +1. 维护一个连续区间 `[left, right)`(左闭右开)。 2. 用少量状态描述这个区间是否满足要求。 3. 每次只让一个元素进窗口、一个元素出窗口。 4. 在移动过程中更新答案。 @@ -47,18 +47,24 @@ ```ts function fixedWindow(nums: number[], k: number): void { let left = 0; + let right = 0; const state = initState(); - for (let right = 0; right < nums.length; right++) { - add(state, nums[right]); + while (right < nums.length) { + // expand: add right element + const el = nums[right]; + right++; - if (right - left + 1 > k) { + add(state, el); + + // shrink: window exceeds k + if (right - left > k) { remove(state, nums[left]); left++; } - if (right - left + 1 === k) { - // 变体插桩:更新答案 + // update: window size equals k + if (right - left === k) { updateAnswer(left, right, state); } } @@ -88,19 +94,25 @@ function fixedWindow(nums: number[], k: number): void { ```ts function longestWindow(nums: number[]): number { let left = 0; + let right = 0; let ans = 0; const state = initState(); - for (let right = 0; right < nums.length; right++) { - add(state, nums[right]); + while (right < nums.length) { + // expand: add right element + const el = nums[right]; + right++; + + add(state, el); + // shrink: window is invalid while (needShrink(state)) { remove(state, nums[left]); left++; } - // 此时窗口重新合法 - ans = Math.max(ans, right - left + 1); + // update: window is valid again + ans = Math.max(ans, right - left); } return ans; @@ -137,16 +149,22 @@ function longestWindow(nums: number[]): number { ```ts function minWindowTemplate(s: string): string { let left = 0; + let right = 0; let bestLeft = 0; let bestLen = Infinity; const state = initState(); - for (let right = 0; right < s.length; right++) { - add(state, s[right]); + while (right < s.length) { + // expand: add right element + const ch = s[right]; + right++; + add(state, ch); + + // shrink: window is valid, keep compressing while (isValid(state)) { - if (right - left + 1 < bestLen) { - bestLen = right - left + 1; + if (right - left < bestLen) { + bestLen = right - left; bestLeft = left; } @@ -192,27 +210,33 @@ function minWindowTemplate(s: string): string { ```ts function countAtMost(nums: number[], k: number): number { let left = 0; + let right = 0; let ans = 0; const state = initState(); - for (let right = 0; right < nums.length; right++) { - add(state, nums[right]); + while (right < nums.length) { + // expand: add right element + const el = nums[right]; + right++; + + add(state, el); + // shrink: window is invalid while (needShrink(state)) { remove(state, nums[left]); left++; } - // 以 right 结尾的合法窗口共有 right - left + 1 个 - ans += right - left + 1; + // count: all valid windows ending at right - 1 + ans += right - left; } return ans; } ``` -为什么是 `right - left + 1`? -因为合法窗口为:`[left, right]`、`[left + 1, right]`、...、`[right, right]`。 +为什么是 `right - left`? +因为 `right` 已经自增,当前窗口为 `[left, right)`,合法窗口为:`[left, right)`、`[left + 1, right)`、...、`[right - 1, right)`,共 `right - left` 个。 常见技巧: @@ -242,7 +266,7 @@ exactly(k) = atMost(k) - atMost(k - 1) | 题目 | 在模板上的插桩点 | | ---- | ---------------- | | `[1358] 包含所有三种字符的子字符串数目` | 一旦窗口包含 `a`、`b`、`c`,当前 `left` 之前的起点都可贡献答案。 | -| `[713] 乘积小于 K 的子数组` | 本质可看成“至多型计数”,合法后同样累加 `right - left + 1`。 | +| `[713] 乘积小于 K 的子数组` | 本质可看成"至多型计数",合法后同样累加 `right - left`。 | ## 五、滑动窗口真正要想清楚的 3 个问题 @@ -264,11 +288,11 @@ exactly(k) = atMost(k) - atMost(k - 1) ## 易错点清单 -1. 区间定义不统一:`[left, right]` 和 `[left, right)` 混着写。 +1. 区间定义不统一:模板统一使用 `[left, right)` 左闭右开,`right` 先自增再操作,窗口大小为 `right - left`。 2. 扩窗和缩窗时忘记同步更新 `state`。 3. 更新答案的时机不对,导致少算或多算。 4. 把“最长合法窗口”和“最短覆盖窗口”的缩窗逻辑写反。 -5. 计数题不知道为什么能一次加上 `right - left + 1`。 +5. 计数题不知道为什么能一次加上 `right - left`。 6. 用对象统计频次时,字符减到 `0` 后忘记处理,导致种类数判断错误。 ## 总结:解题速查表 diff --git a/docs/index.md b/docs/index.md index 445aec87..c3f50f8b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,27 +37,27 @@ footer: false 以下专题覆盖了 LeetCode 最核心的算法知识体系,建议按顺序阅读: -| # | 专题 | 关键内容 | -| --- | --- | --- | -| 0 | [前言](/docs/topic/introduction) | 写给工程师的算法学习观 | -| 1 | [重新认识递归](/docs/topic/recursive) | 递归的本质、思维方式与代码模板 | -| 2 | [二叉树遍历算法](/docs/topic/tree) | 前/中/后序遍历框架(含 BST 中序应用) | -| 3 | [排序算法](/docs/topic/sort) | 经典排序算法对比与实现 | -| 4 | [双指针问题](/docs/topic/two-pointers) | 快慢指针、左右指针 | -| 5 | [二分搜索专题](/docs/topic/binary-search) | 统一二分搜索框架 | -| 6 | [滑动窗口算法](/docs/topic/slide-window) | 定长/不定长窗口的通用模板 | -| 7 | [前缀和算法](/docs/topic/partial-sum) | 区间求和与差分数组 | -| 8 | [回溯算法](/docs/topic/backtrack) | 排列/组合/子集的通用回溯框架 | -| 9 | [深度优先搜索](/docs/topic/depth-first-search) | 岛屿问题、连通分量的 DFS 通解 | -| 10 | [广度优先搜索](/docs/topic/breadth-first-search) | 最短路径、多源 BFS、状态空间搜索 | -| 11 | [图论算法](/docs/topic/graph) | 环检测、拓扑排序、二分图、Dijkstra | -| 12 | [贪心算法](/docs/topic/greedy) | 贪心选择性质、与 DP 的边界判断 | -| 13 | [动态规划:入门与线性/坐标模型](/docs/topic/dynamic-programming-normal) | 线性递推、网格路径、空间压缩 | -| 14 | [动态规划:背包模型](/docs/topic/dynamic-programming-backpack) | 0-1 背包、完全背包、多维背包 | -| 15 | [动态规划:序列与双序列模型](/docs/topic/dynamic-programming-subsequence) | LIS、LCS、编辑距离、匹配 DP | -| 16 | [动态规划:状态机模型](/docs/topic/dynamic-programming-state-machine) | 股票问题与阶段状态转移 | -| 17 | [动态规划:区间与划分模型](/docs/topic/dynamic-programming-interval) | 回文、切分、博弈类区间 DP | -| 18 | [单调栈算法](/docs/topic/monotonic-stack) | 下一个更大元素、柱状图的通解 | +| # | 专题 | 关键内容 | +| --- | ------------------------------------------------------------------------- | ------------------------------------- | +| 0 | [前言](/docs/topic/introduction) | 写给工程师的算法学习观 | +| 1 | [重新认识递归](/docs/topic/recursive) | 递归的本质、思维方式与代码模板 | +| 2 | [二叉树遍历算法](/docs/topic/tree) | 前/中/后序遍历框架(含 BST 中序应用) | +| 3 | [排序算法](/docs/topic/sort) | 经典排序算法对比与实现 | +| 4 | [双指针问题](/docs/topic/two-pointers) | 快慢指针、左右指针 | +| 5 | [二分搜索专题](/docs/topic/binary-search) | 统一二分搜索框架 | +| 6 | [滑动窗口算法](/docs/topic/slide-window) | 定长/不定长窗口的通用模板 | +| 7 | [前缀和算法](/docs/topic/partial-sum) | 区间求和与差分数组 | +| 8 | [回溯算法](/docs/topic/backtrack) | 排列/组合/子集的通用回溯框架 | +| 9 | [深度优先搜索](/docs/topic/depth-first-search) | 岛屿问题、连通分量的 DFS 通解 | +| 10 | [广度优先搜索](/docs/topic/breadth-first-search) | 最短路径、多源 BFS、状态空间搜索 | +| 11 | [图论算法](/docs/topic/graph) | 环检测、拓扑排序、二分图、Dijkstra | +| 12 | [贪心算法](/docs/topic/greedy) | 贪心选择性质、与 DP 的边界判断 | +| 13 | [动态规划:入门与线性/坐标模型](/docs/topic/dynamic-programming-normal) | 线性递推、网格路径、空间压缩 | +| 14 | [动态规划:背包模型](/docs/topic/dynamic-programming-backpack) | 0-1 背包、完全背包、多维背包 | +| 15 | [动态规划:序列与双序列模型](/docs/topic/dynamic-programming-subsequence) | LIS、LCS、编辑距离、匹配 DP | +| 16 | [动态规划:状态机模型](/docs/topic/dynamic-programming-state-machine) | 股票问题与阶段状态转移 | +| 17 | [动态规划:区间与划分模型](/docs/topic/dynamic-programming-interval) | 回文、切分、博弈类区间 DP | +| 18 | [单调栈算法](/docs/topic/monotonic-stack) | 下一个更大元素、柱状图的通解 | ## 📂 题解分类 diff --git "a/src/array/560.\345\222\214\344\270\272K\347\232\204\345\255\220\346\225\260\347\273\204.ts" "b/src/array/560.\345\222\214\344\270\272K\347\232\204\345\255\220\346\225\260\347\273\204.ts" index e7475128..1c0e3741 100644 --- "a/src/array/560.\345\222\214\344\270\272K\347\232\204\345\255\220\346\225\260\347\273\204.ts" +++ "b/src/array/560.\345\222\214\344\270\272K\347\232\204\345\255\220\346\225\260\347\273\204.ts" @@ -6,11 +6,11 @@ * https://leetcode.cn/problems/subarray-sum-equals-k/description/ * * algorithms - * Medium (44.27%) - * Likes: 2702 + * Medium (46.39%) + * Likes: 3130 * Dislikes: 0 - * Total Accepted: 676.5K - * Total Submissions: 1.5M + * Total Accepted: 1.1M + * Total Submissions: 2.3M * Testcase Example: '[1,1,1]\n2' * * 给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。 @@ -47,34 +47,26 @@ // @lc code=start function subarraySum(nums: number[], k: number): number { - // 先算出前缀和 - const preSum = Array(nums.length + 1).fill(0); - for (let i = 0; i < nums.length; i++) { - preSum[i + 1] = nums[i] + preSum[i]; - } + let prefixSum = 0; + let res = 0; - // 因为前缀和中任意两个数 pre[i] 与 pre[j] 的差,即为数组 i~j 的和,代表一种子数组的情况 - // 问题转换成,preSum 数组中有多少种情况使得其中两数之差为 k, 即 pre[i] - pre[j] = k - // 等价于 pre[j] = pre[i] - k,其中 i > j - const hash: Record = {}; - let result = 0; - for (let i = 0; i < preSum.length; i++) { - const preI = preSum[i]; - const preJ = preI - k; + const hashMap: Map = new Map(); + // 前缀和 0 出现了 1 次 + hashMap.set(0, 1); - // 如果找到符合要求的和 pre[j],result 中加上该种情况 - result += hash[preJ] ? hash[preJ] : 0; + for (let i = 0; i < nums.length; i++) { + prefixSum += nums[i]; - // 将当前的数字存入 hash 中方便后面以 O1 的复杂度取到 - hash[preI] = hash[preI] ? hash[preI] + 1 : 1; + // 是否有 之前的前缀和 = prefixSum - k,有多少个就加多少个到 res 里 + res += hashMap.get(prefixSum - k) ?? 0; + // 更新当前新的前缀和出现的次数 + hashMap.set(prefixSum, (hashMap.get(prefixSum) ?? 0) + 1); } - return result; + return res; } // @lc code=end (() => { - const nums = [1, 1, -1, 1, -1], - k = 1; - console.log(subarraySum(nums, k)); + LCT.func(subarraySum).auto(); })(); diff --git "a/src/backtracking/131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.ts" "b/src/backtracking/131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.ts" new file mode 100644 index 00000000..c356d8b5 --- /dev/null +++ "b/src/backtracking/131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.ts" @@ -0,0 +1,99 @@ +/* + * @lc app=leetcode.cn id=131 lang=typescript + * + * [131] 分割回文串 + * + * https://leetcode.cn/problems/palindrome-partitioning/description/ + * + * algorithms + * Medium (75.13%) + * Likes: 2203 + * Dislikes: 0 + * Total Accepted: 770.5K + * Total Submissions: 1M + * Testcase Example: '"aab"' + * + * 给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。 + * + * + * + * 示例 1: + * + * + * 输入:s = "aab" + * 输出:[["a","a","b"],["aa","b"]] + * + * + * 示例 2: + * + * + * 输入:s = "a" + * 输出:[["a"]] + * + * + * + * + * 提示: + * + * + * 1 <= s.length <= 16 + * s 仅由小写英文字母组成 + * + * + */ + +// @lc code=start +function partition(s: string): string[][] { + function isPalindrome(str: string): boolean { + let l = 0; + let r = str.length - 1; + while (l < r) { + if (str[l] !== str[r]) return false; + l++; + r--; + } + return true; + } + + const res: string[][] = []; + + function helper(start: number, path: string[]) { + if (start === s.length && path.length !== 0) { + res.push([...path]); + return; + } + for (let i = start + 1; i <= s.length; i++) { + const subStr = s.substring(start, i); + if (isPalindrome(subStr)) { + // 做选择 + path.push(subStr); + helper(i, path); + // 取消选择 + path.pop(); + } + } + } + + helper(0, []); + return res; +} +// @lc code=end + +(() => { + LCT.func(partition).auto(); + + LCT.func(partition).cases([ + { + input: ['abacaba'], + output: [ + ['a', 'b', 'a', 'c', 'a', 'b', 'a'], + ['a', 'b', 'a', 'c', 'aba'], + ['a', 'b', 'aca', 'b', 'a'], + ['a', 'bacab', 'a'], + ['aba', 'c', 'a', 'b', 'a'], + ['aba', 'c', 'aba'], + ['abacaba'] + ] + } + ]); +})(); diff --git "a/src/backtracking/39.\347\273\204\345\220\210\346\200\273\345\222\214.ts" "b/src/backtracking/39.\347\273\204\345\220\210\346\200\273\345\222\214.ts" index 6d479f60..aec9c8e4 100644 --- "a/src/backtracking/39.\347\273\204\345\220\210\346\200\273\345\222\214.ts" +++ "b/src/backtracking/39.\347\273\204\345\220\210\346\200\273\345\222\214.ts" @@ -3,14 +3,14 @@ * * [39] 组合总和 * - * https://leetcode-cn.com/problems/combination-sum/description/ + * https://leetcode.cn/problems/combination-sum/description/ * * algorithms - * Medium (72.75%) - * Likes: 1727 + * Medium (74.06%) + * Likes: 3191 * Dislikes: 0 - * Total Accepted: 398.3K - * Total Submissions: 547.5K + * Total Accepted: 1.5M + * Total Submissions: 2M * Testcase Example: '[2,3,6,7]\n7' * * 给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target @@ -51,9 +51,9 @@ * * * 1 <= candidates.length <= 30 - * 1 <= candidates[i] <= 200 - * candidate 中的每个元素都 互不相同 - * 1 <= target <= 500 + * 2 <= candidates[i] <= 40 + * candidates 的所有元素 互不相同 + * 1 <= target <= 40 * * */ @@ -61,37 +61,28 @@ // @lc code=start function combinationSum(candidates: number[], target: number): number[][] { const res: number[][] = []; - let currSum = 0; - backtrack([], 0); - return res; - - function backtrack(path: number[], start: number) { - // 满足要求,输出 - if (currSum === target) { + candidates.sort((a, b) => a - b); + function backtrack(path: number[], rest: number, start: number) { + if (rest === 0) { res.push([...path]); return; } - // 当前和已经大于 target,则直接剪枝 - if (currSum > target) { - return; - } for (let i = start; i < candidates.length; i++) { - // 做选择 - currSum += candidates[i]; + const curr = rest - candidates[i]; + if (curr < 0) break; + path.push(candidates[i]); - // 回溯 - backtrack(path, i); - // 撤销选择 - currSum -= candidates[i]; + backtrack(path, curr, i); path.pop(); } } + + backtrack([], target, 0); + return res; } // @lc code=end (() => { - const candidates = [3, 2, 6, 7], - target = 7; - console.log(combinationSum(candidates, target)); + LCT.func(combinationSum).auto(); })(); diff --git "a/src/binary-search/153.\345\257\273\346\211\276\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\346\234\200\345\260\217\345\200\274.ts" "b/src/binary-search/153.\345\257\273\346\211\276\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\346\234\200\345\260\217\345\200\274.ts" index 5ee6af66..36bc7160 100644 --- "a/src/binary-search/153.\345\257\273\346\211\276\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\346\234\200\345\260\217\345\200\274.ts" +++ "b/src/binary-search/153.\345\257\273\346\211\276\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\346\234\200\345\260\217\345\200\274.ts" @@ -3,14 +3,14 @@ * * [153] 寻找旋转排序数组中的最小值 * - * https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/description/ + * https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/description/ * * algorithms - * Medium (56.76%) - * Likes: 650 + * Medium (59.14%) + * Likes: 1353 * Dislikes: 0 - * Total Accepted: 239.8K - * Total Submissions: 421.7K + * Total Accepted: 811.5K + * Total Submissions: 1.4M * Testcase Example: '[3,4,5,1,2]' * * 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] @@ -25,6 +25,8 @@ * * 给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。 * + * 你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。 + * * * * 示例 1: @@ -57,8 +59,8 @@ * * * n == nums.length - * 1 - * -5000 + * 1 <= n <= 5000 + * -5000 <= nums[i] <= 5000 * nums 中的所有整数 互不相同 * nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转 * @@ -67,29 +69,23 @@ // @lc code=start function findMin(nums: number[]): number { - let left = 0; - let right = nums.length; - while (left < right) { - const mid = Math.floor((left + right) / 2); - // 当前值比后一个值大,说明找到了分界处 - if (nums[mid] > nums[mid + 1]) { - return nums[mid + 1]; + let l = 0; + let r = nums.length - 1; + + while (l < r) { + const mid = (l + r) >> 1; + + if (nums[mid] > nums[r]) { + l = mid + 1; } else { - // 说明此时在左侧递增序列 - if (nums[mid] > nums[left]) { - left = mid + 1; - } else { - right = mid; - } + r = mid; } } - // 所有情况下都找不到前一个值比后一个值大的情况,说明当前数组单调递增,最小值为第一个 - return nums[0]; + return nums[l]; } // @lc code=end (() => { - const nums = [4, 5, 1, 2, 3]; - console.log(findMin(nums)); + LCT.func(findMin).auto(); })(); diff --git "a/src/binary-search/33.\346\220\234\347\264\242\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204.ts" "b/src/binary-search/33.\346\220\234\347\264\242\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204.ts" index acc3ed62..d652941a 100644 --- "a/src/binary-search/33.\346\220\234\347\264\242\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204.ts" +++ "b/src/binary-search/33.\346\220\234\347\264\242\346\227\213\350\275\254\346\216\222\345\272\217\346\225\260\347\273\204.ts" @@ -3,24 +3,26 @@ * * [33] 搜索旋转排序数组 * - * https://leetcode-cn.com/problems/search-in-rotated-sorted-array/description/ + * https://leetcode.cn/problems/search-in-rotated-sorted-array/description/ * * algorithms - * Medium (43.08%) - * Likes: 1777 + * Medium (46.02%) + * Likes: 3365 * Dislikes: 0 - * Total Accepted: 424.1K - * Total Submissions: 984.4K + * Total Accepted: 1.3M + * Total Submissions: 2.9M * Testcase Example: '[4,5,6,7,0,1,2]\n0' * * 整数数组 nums 按升序排列,数组中的值 互不相同 。 * - * 在传递给函数之前,nums 在预先未知的某个下标 k(0 )上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., - * nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, - * [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。 + * 在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 向左旋转,使数组变为 [nums[k], + * nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 + * 计数)。例如, [0,1,2,4,5,6,7] 下标 3 上向左旋转后可能变为 [4,5,6,7,0,1,2] 。 * * 给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。 * + * 你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。 + * * * * 示例 1: @@ -48,73 +50,66 @@ * 提示: * * - * 1 - * -10^4 + * 1 <= nums.length <= 5000 + * -10^4 <= nums[i] <= 10^4 * nums 中的每个值都 独一无二 * 题目数据保证 nums 在预先未知的某个下标上进行了旋转 - * -10^4 - * - * + * -10^4 <= target <= 10^4 * * - * 进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗? - * */ // @lc code=start function search(nums: number[], target: number): number { - let left = 0, - right = nums.length, - start = 0; - while (left < right) { - const mid = Math.floor((left + right) / 2); - if (nums[mid] > nums[mid + 1]) { - // 说明此时找到了旋转位置,位置为 mid + 1 - start = mid + 1; - break; + let l = 0; + let r = nums.length - 1; + + // 先二分找旋转点 + while (l < r) { + const mid = (l + r) >> 1; + + if (nums[mid] > nums[r]) { + l = mid + 1; } else { - // 通过二分缩小查询范围 - if (nums[mid] > nums[left]) { - left = mid + 1; - } else { - right = mid; - } + r = mid; } } + // 现在已经找到旋转点了 + const point = l; + + // 判断在旋转点左侧还是右侧,只需要 target 与第一个元素比较大小 + const isLeft = target < nums[0] ? false : true; - if (target === nums[0]) { - // 如果target恰好等于第一个,则直接返回0索引 - return 0; - } else if (target > nums[0] && start !== 0) { - // 当target大于最左侧元素,此时target在左半边序列中查询。 - // 注意,当start=0时,此时没有左半边序列,只能从右半边序列中查询 - left = 0; - right = start - 1; + if (point === 0) { + l = 0; + r = nums.length; + } else if (isLeft) { + l = 0; + r = point; } else { - // target在右半边序列中查询 - left = start; - right = nums.length - 1; + l = point; + r = nums.length; } - // 正常二分查找 - while (left <= right) { - const mid = Math.floor((left + right) / 2); + while (l < r) { + const mid = (l + r) >> 1; if (nums[mid] < target) { - left = mid + 1; - } else if (nums[mid] > target) { - right = mid - 1; + l = mid + 1; } else { - return mid; + r = mid; } } - return -1; + + return nums[l] === target ? l : -1; } // @lc code=end (() => { - const nums = [1, 3, 5], - target = 5; - // const nums = [4, 5, 6, 7, 0, 1, 2], - // target = 0; - console.log(search(nums, target)); + LCT.func(search).auto(); + LCT.func(search).cases([ + { + input: [[1, 2, 3], 3], + output: 2 + } + ]); })(); diff --git "a/src/binary-search/74.\346\220\234\347\264\242\344\272\214\347\273\264\347\237\251\351\230\265.ts" "b/src/binary-search/74.\346\220\234\347\264\242\344\272\214\347\273\264\347\237\251\351\230\265.ts" index 9a57f5e0..9ff17513 100644 --- "a/src/binary-search/74.\346\220\234\347\264\242\344\272\214\347\273\264\347\237\251\351\230\265.ts" +++ "b/src/binary-search/74.\346\220\234\347\264\242\344\272\214\347\273\264\347\237\251\351\230\265.ts" @@ -74,11 +74,28 @@ function searchMatrix(matrix: number[][], target: number): boolean { (() => { // 从矩阵特性中,我们很容易看出,矩阵就是用一个升序数组从左至右一行一行排列而来的。 // 那么,问题显然就变成了在有序数组中寻找目标值的问题。考虑使用二分查找来解 - const matrix = [ - [1, 3, 5, 7], - [10, 11, 16, 20], - [23, 30, 34, 60] - ], - target = 3; - console.log(searchMatrix(matrix, target)); + LCT.func(searchMatrix).cases([ + { + input: [ + [ + [1, 3, 5, 7], + [10, 11, 16, 20], + [23, 30, 34, 60] + ], + 3 + ], + output: true + }, + { + input: [ + [ + [1, 3, 5, 7], + [10, 11, 16, 20], + [23, 30, 34, 60] + ], + 13 + ], + output: false + } + ]); })(); diff --git "a/src/binary-search/912.\346\216\222\345\272\217\346\225\260\347\273\204.ts" "b/src/binary-search/912.\346\216\222\345\272\217\346\225\260\347\273\204.ts" index 74c25e18..ec0b5069 100644 --- "a/src/binary-search/912.\346\216\222\345\272\217\346\225\260\347\273\204.ts" +++ "b/src/binary-search/912.\346\216\222\345\272\217\346\225\260\347\273\204.ts" @@ -47,36 +47,35 @@ // @lc code=start function sortArray(nums: number[]): number[] { - - function heapifyDown(i:number, heapSize: number) { - while(true) { - const left = i*2+1; - const right = i*2+2; + function heapifyDown(i: number, heapSize: number) { + while (true) { + const left = i * 2 + 1; + const right = i * 2 + 2; // 记录当前堆的最大值的索引 let max = i; // 父节点分别与两个子节点比较 - if(left nums[max]) max = left; - if(right nums[max]) max = right; + if (left < heapSize && nums[left] > nums[max]) max = left; + if (right < heapSize && nums[right] > nums[max]) max = right; // 如果max没变,说明当前已经形成了大顶堆了(即堆顶为最大值) - if(max === i) break; + if (max === i) break; // 将父节点与两个子节点中的最大值交换到父节点上 [nums[i], nums[max]] = [nums[max], nums[i]]; - // + // i = max; } } function sort() { // 建堆,循环完成后就是一个大顶堆 - for(let i = (nums.length - 2) >> 1; i>= 0; i--) { + for (let i = (nums.length - 2) >> 1; i >= 0; i--) { heapifyDown(i, nums.length); } - for(let i = nums.length - 1; i>0; i--) { + for (let i = nums.length - 1; i > 0; i--) { // 既然堆顶是最大值,那么把最大值对调到末尾,使得目前范围的末尾为最大值 [nums[0], nums[i]] = [nums[i], nums[0]]; // 然后减少堆的范围,即排除掉末尾有序部分。再进行堆化,找出剩下数字中最大的部分。 @@ -90,68 +89,68 @@ function sortArray(nums: number[]): number[] { } // @lc code=end - // 归并排序法 - // function merge(left: number, mid: number, right: number) { - // const temp: number[] = []; - // // 将nums[lef~right]分成两半,两两依次比对,每次将较小的那个加入到临时数组中 - // let i = left; - // let j = mid + 1; - // while (i <= mid && j <= right) { - // if (nums[i] <= nums[j]) temp.push(nums[i++]); - // else temp.push(nums[j++]); - // } - - // // 最后至多只可能有一个子数组有剩余,且里面的元素都大于temp,直接平移挪过去 - // while (i <= mid) temp.push(nums[i++]); - // while (j <= right) temp.push(nums[j++]); - - // // 此时temp是已经完成排序的nums[lef~right],直接覆盖到对应位置的值 - // for (let k = 0; k < temp.length; k++) nums[left + k] = temp[k]; - // } - - // function sort(l: number, r: number) { - // if (l >= r) return; - - // const mid = (l + r) >> 1; - // sort(l, mid); - // sort(mid + 1, r); - // merge(l, mid, r); - // } - - // 快速排序法 - // function sort(l: number, r: number) { - // if (l >= r) return; - - // // 对 [l, r] 进行一次快排 - // // 使得所有 nums[l, index-1] <= nums[index] < nums[p+1, r] - // const index = partition(l, r); - // // 之后对左右两侧的数组递归进行快排 - // sort(l, index - 1); - // sort(index + 1, r); - // } - - // // 一次快排能够达到的效果是,将数组其中一个值(pivot)的位置归位 - // function partition(left: number, right: number) { - // // 随便选取一个基准值 - // const pivot = nums[left]; - - // let i = left + 1; - // let j = right; - - // while (i <= j) { - // // 从左侧遍历,找到比基准值大的数,从右侧找比基准值小的数 - // while (i <= right && nums[i] <= pivot) i++; - // while (j > left && nums[j] >= pivot) j--; - - // // 此时交换两边的数,使得[left,i]都比pivot小,[j,right]都比pivot大; - // if (i < j) [nums[i], nums[j]] = [nums[j], nums[i]]; - // } - // // 多次重复循环使得最终i=j+1,此时交换基准值和nums[j]。 - // [nums[left], nums[j]] = [nums[j], nums[left]]; - // // 从而满足:基准值pivot左侧的值都比它小,右侧的值都比它大。即 **pivot已经完全归位**。 - // // 返回已经排好序的值的位置,这个值已经不需要调整位置了,接下来从它两侧继续递归 - // return j; - // } +// 归并排序法 +// function merge(left: number, mid: number, right: number) { +// const temp: number[] = []; +// // 将nums[lef~right]分成两半,两两依次比对,每次将较小的那个加入到临时数组中 +// let i = left; +// let j = mid + 1; +// while (i <= mid && j <= right) { +// if (nums[i] <= nums[j]) temp.push(nums[i++]); +// else temp.push(nums[j++]); +// } + +// // 最后至多只可能有一个子数组有剩余,且里面的元素都大于temp,直接平移挪过去 +// while (i <= mid) temp.push(nums[i++]); +// while (j <= right) temp.push(nums[j++]); + +// // 此时temp是已经完成排序的nums[lef~right],直接覆盖到对应位置的值 +// for (let k = 0; k < temp.length; k++) nums[left + k] = temp[k]; +// } + +// function sort(l: number, r: number) { +// if (l >= r) return; + +// const mid = (l + r) >> 1; +// sort(l, mid); +// sort(mid + 1, r); +// merge(l, mid, r); +// } + +// 快速排序法 +// function sort(l: number, r: number) { +// if (l >= r) return; + +// // 对 [l, r] 进行一次快排 +// // 使得所有 nums[l, index-1] <= nums[index] < nums[p+1, r] +// const index = partition(l, r); +// // 之后对左右两侧的数组递归进行快排 +// sort(l, index - 1); +// sort(index + 1, r); +// } + +// // 一次快排能够达到的效果是,将数组其中一个值(pivot)的位置归位 +// function partition(left: number, right: number) { +// // 随便选取一个基准值 +// const pivot = nums[left]; + +// let i = left + 1; +// let j = right; + +// while (i <= j) { +// // 从左侧遍历,找到比基准值大的数,从右侧找比基准值小的数 +// while (i <= right && nums[i] <= pivot) i++; +// while (j > left && nums[j] >= pivot) j--; + +// // 此时交换两边的数,使得[left,i]都比pivot小,[j,right]都比pivot大; +// if (i < j) [nums[i], nums[j]] = [nums[j], nums[i]]; +// } +// // 多次重复循环使得最终i=j+1,此时交换基准值和nums[j]。 +// [nums[left], nums[j]] = [nums[j], nums[left]]; +// // 从而满足:基准值pivot左侧的值都比它小,右侧的值都比它大。即 **pivot已经完全归位**。 +// // 返回已经排好序的值的位置,这个值已经不需要调整位置了,接下来从它两侧继续递归 +// return j; +// } (() => { LCT.func(sortArray).auto(); diff --git "a/src/breadth-first-search/994.\350\205\220\347\203\202\347\232\204\346\251\230\345\255\220.ts" "b/src/breadth-first-search/994.\350\205\220\347\203\202\347\232\204\346\251\230\345\255\220.ts" new file mode 100644 index 00000000..543744a6 --- /dev/null +++ "b/src/breadth-first-search/994.\350\205\220\347\203\202\347\232\204\346\251\230\345\255\220.ts" @@ -0,0 +1,116 @@ +/* + * @lc app=leetcode.cn id=994 lang=typescript + * + * [994] 腐烂的橘子 + * + * https://leetcode.cn/problems/rotting-oranges/description/ + * + * algorithms + * Medium (55.51%) + * Likes: 1152 + * Dislikes: 0 + * Total Accepted: 446.7K + * Total Submissions: 804.6K + * Testcase Example: '[[2,1,1],[1,1,0],[0,1,1]]' + * + * 在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一: + * + * + * 值 0 代表空单元格; + * 值 1 代表新鲜橘子; + * 值 2 代表腐烂的橘子。 + * + * + * 每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。 + * + * 返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。 + * + * + * + * 示例 1: + * + * + * + * + * 输入:grid = [[2,1,1],[1,1,0],[0,1,1]] + * 输出:4 + * + * + * 示例 2: + * + * + * 输入:grid = [[2,1,1],[0,1,1],[1,0,1]] + * 输出:-1 + * 解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。 + * + * + * 示例 3: + * + * + * 输入:grid = [[0,2]] + * 输出:0 + * 解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。 + * + * + * + * + * 提示: + * + * + * m == grid.length + * n == grid[i].length + * 1 <= m, n <= 10 + * grid[i][j] 仅为 0、1 或 2 + * + * + */ + +// @lc code=start +const dir = [ + [1, 0], + [-1, 0], + [0, 1], + [0, -1] +]; + +function orangesRotting(grid: number[][]): number { + const queue: Array<[number, number]> = []; + let freshCnt: number = 0; + let time = 0; + for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[0].length; j++) { + if (grid[i][j] === 2) queue.push([i, j]); + if (grid[i][j] === 1) freshCnt += 1; + } + } + + while (queue.length !== 0 && freshCnt !== 0) { + let len = queue.length; + while (len--) { + const [i, j] = queue.shift(); + bfs(i, j); + } + time++; + } + return freshCnt !== 0 ? -1 : time; + + function bfs(i: number, j: number) { + for (const [dx, dy] of dir) { + const x = i + dx; + const y = j + dy; + + if (x < 0 || x > grid.length - 1 || y < 0 || y > grid[0].length - 1) continue; + + if (grid[x][y] === 1) { + grid[x][y] = 2; + freshCnt--; + queue.push([x, y]); + } + } + } +} +// @lc code=end + +(() => { + LCT.func(orangesRotting).auto(); +})(); diff --git "a/src/depth-first-search/1011.\345\234\250D\345\244\251\345\206\205\351\200\201\350\276\276\345\214\205\350\243\271\347\232\204\350\203\275\345\212\233.ts" "b/src/depth-first-search/1011.\345\234\250D\345\244\251\345\206\205\351\200\201\350\276\276\345\214\205\350\243\271\347\232\204\350\203\275\345\212\233.ts" index 4aedda04..8bbfb9a2 100644 --- "a/src/depth-first-search/1011.\345\234\250D\345\244\251\345\206\205\351\200\201\350\276\276\345\214\205\350\243\271\347\232\204\350\203\275\345\212\233.ts" +++ "b/src/depth-first-search/1011.\345\234\250D\345\244\251\345\206\205\351\200\201\350\276\276\345\214\205\350\243\271\347\232\204\350\203\275\345\212\233.ts" @@ -85,7 +85,7 @@ function shipWithinDays(weights: number[], days: number): number { let nowDay = 1; let nowLoad = 0; - for (let weight of weights) { + for (const weight of weights) { if (nowLoad + weight > target) { nowLoad = 0; nowDay += 1; diff --git "a/src/depth-first-search/200.\345\262\233\345\261\277\346\225\260\351\207\217.ts" "b/src/depth-first-search/200.\345\262\233\345\261\277\346\225\260\351\207\217.ts" index f9b15bb1..e124965f 100644 --- "a/src/depth-first-search/200.\345\262\233\345\261\277\346\225\260\351\207\217.ts" +++ "b/src/depth-first-search/200.\345\262\233\345\261\277\346\225\260\351\207\217.ts" @@ -3,14 +3,14 @@ * * [200] 岛屿数量 * - * https://leetcode-cn.com/problems/number-of-islands/description/ + * https://leetcode.cn/problems/number-of-islands/description/ * * algorithms - * Medium (56.55%) - * Likes: 1505 + * Medium (63.89%) + * Likes: 2971 * Dislikes: 0 - * Total Accepted: 382.4K - * Total Submissions: 676.2K + * Total Accepted: 1.4M + * Total Submissions: 2.1M * Testcase Example: '[["1","1","1","1","0"],["1","1","0","1","0"],["1","1","0","0","0"],["0","0","0","0","0"]]' * * 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 @@ -25,10 +25,10 @@ * * * 输入:grid = [ - * ⁠ ["1","1","1","1","0"], - * ⁠ ["1","1","0","1","0"], - * ⁠ ["1","1","0","0","0"], - * ⁠ ["0","0","0","0","0"] + * ['1','1','1','1','0'], + * ['1','1','0','1','0'], + * ['1','1','0','0','0'], + * ['0','0','0','0','0'] * ] * 输出:1 * @@ -37,10 +37,10 @@ * * * 输入:grid = [ - * ⁠ ["1","1","0","0","0"], - * ⁠ ["1","1","0","0","0"], - * ⁠ ["0","0","1","0","0"], - * ⁠ ["0","0","0","1","1"] + * ['1','1','0','0','0'], + * ['1','1','0','0','0'], + * ['0','0','1','0','0'], + * ['0','0','0','1','1'] * ] * 输出:3 * @@ -52,7 +52,7 @@ * * m == grid.length * n == grid[i].length - * 1 + * 1 <= m, n <= 300 * grid[i][j] 的值为 '0' 或 '1' * * @@ -61,47 +61,52 @@ // @lc code=start function numIslands(grid: string[][]): number { let res = 0; - const height = grid.length; - const width = grid[0].length; - const direction = [ - [-1, 0], - [1, 0], - [0, -1], - [0, 1] - ]; - for (let i = 0; i < height; i++) { - for (let j = 0; j < width; j++) { + for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[0].length; j++) { if (grid[i][j] === '1') { + dfs(i, j); res += 1; - floodFill(i, j); } } } - return res; - function floodFill(i: number, j: number) { - // 递归越界返回 - if (i < 0 || j < 0 || i >= height || j >= width) return; - // 已遍历过,或者当前节点不是陆地时,返回 - if (grid[i][j] === '0') return; + function dfs(i: number, j: number) { + if (i < 0 || i >= grid.length || j < 0 || j > grid[0].length || grid[i][j] !== '1') return; - // 将当前点设置为已访问 grid[i][j] = '0'; - - // 分别递归其上下左右节点 - for (const [row, col] of direction) { - floodFill(row + i, col + j); - } + dfs(i - 1, j); + dfs(i + 1, j); + dfs(i, j - 1); + dfs(i, j + 1); } + + return res; } // @lc code=end (() => { - const grid = [ - ['1', '1', '0', '0', '0'], - ['1', '1', '0', '0', '0'], - ['0', '0', '1', '0', '0'], - ['0', '0', '0', '1', '1'] - ]; - console.log(numIslands(grid)); + LCT.func(numIslands).cases([ + { + input: [ + [ + ['1', '1', '1', '1', '0'], + ['1', '1', '0', '1', '0'], + ['1', '1', '0', '0', '0'], + ['0', '0', '0', '0', '0'] + ] + ], + output: 1 + }, + { + input: [ + [ + ['1', '1', '0', '0', '0'], + ['1', '1', '0', '0', '0'], + ['0', '0', '1', '0', '0'], + ['0', '0', '0', '1', '1'] + ] + ], + output: 3 + } + ]); })(); diff --git "a/src/design/146.lru\347\274\223\345\255\230.ts" "b/src/design/146.lru\347\274\223\345\255\230.ts" index 6ad0fd6a..3ca70e1b 100644 --- "a/src/design/146.lru\347\274\223\345\255\230.ts" +++ "b/src/design/146.lru\347\274\223\345\255\230.ts" @@ -3,14 +3,14 @@ * * [146] LRU 缓存 * - * https://leetcode-cn.com/problems/lru-cache/description/ + * https://leetcode.cn/problems/lru-cache/description/ * * algorithms - * Medium (52.72%) - * Likes: 2155 + * Medium (55.44%) + * Likes: 3796 * Dislikes: 0 - * Total Accepted: 341K - * Total Submissions: 646.9K + * Total Accepted: 1.1M + * Total Submissions: 2M * Testcase Example: '["LRUCache","put","put","get","put","get","put","get","get","get"]\n' + '[[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]' * @@ -70,40 +70,67 @@ // @lc code=start class LRUCache { - cache: Map; capacity: number; + + lastNode: DoublyListNode<{ key: number; val: number }>; + dummy: DoublyListNode<{ key: number; val: number }>; + hash: Map>; + constructor(capacity: number) { this.capacity = capacity; - this.cache = new Map(); + + this.dummy = new DoublyListNode({ key: -1, val: -1 }); + this.lastNode = this.dummy; + this.hash = new Map(); } get(key: number): number { - // 若未匹配值,则返回 -1 - if (!this.cache.has(key)) { - return -1; - } + const node = this.hash.get(key); + if (!node) return -1; - const value = this.cache.get(key); - // 由于进行了一次查询,更新 LRU 列表到队尾 - this.cache.delete(key); - this.cache.set(key, value); + // 删掉原有位置的 node + node.prev!.next = node.next; + if (node.next) node.next.prev = node.prev; + else this.lastNode = node.prev!; - return value; + // 将当前 node 挪到链表尾部 + node.prev = this.lastNode; + node.next = null; + this.lastNode.next = node; + this.lastNode = node; + + return node.val.val; } put(key: number, value: number): void { - // 若 LRU 中已经存在 key,则需要先进行删除 - if (this.cache.has(key)) { - this.cache.delete(key); + // 如果之前有就删掉 + const node = this.hash.get(key); + if (node) { + node.val.val = value; + node.prev!.next = node.next; + if (node.next) node.next.prev = node.prev; + else this.lastNode = node.prev!; + + // 挪到尾部 + node.prev = this.lastNode; + node.next = null; + this.lastNode.next = node; + this.lastNode = node; + return; } - this.cache.set(key, value); - // 若超出 LRU 队列长度,删去队头最久未被访问的记录 - if (this.cache.size > this.capacity) { - // map.keys() 返回一个迭代器,迭代器调用 next() 方法,返回下一个值到 value 中 - // 即表示获取 map 的第一个 key - const firstKey = this.cache.keys().next().value; - this.cache.delete(firstKey); + // 增加节点到尾部 + const newNode = new DoublyListNode({ key, val: value }, this.lastNode, null); + this.lastNode.next = newNode; + this.lastNode = newNode; + this.hash.set(key, newNode); + + // 超长了,就删掉最久没被访问的头结点 + if (this.hash.size > this.capacity) { + const lru = this.dummy.next!; + this.hash.delete(lru.val.key); + this.dummy.next = lru.next; + if (lru.next) lru.next.prev = this.dummy; } } } @@ -117,5 +144,9 @@ class LRUCache { // @lc code=end (() => { - LCT.cls(LRUCache).auto(); + LCT.cls(LRUCache).calls( + ['LRUCache', 'put', 'put', 'get', 'put', 'get', 'put', 'get', 'get', 'get'], + [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]], + [null, null, null, 1, null, -1, null, -1, 3, 4] + ); })(); diff --git "a/src/divide-and-conquer/215.\346\225\260\347\273\204\344\270\255\347\232\204\347\254\254k\344\270\252\346\234\200\345\244\247\345\205\203\347\264\240.ts" "b/src/divide-and-conquer/215.\346\225\260\347\273\204\344\270\255\347\232\204\347\254\254k\344\270\252\346\234\200\345\244\247\345\205\203\347\264\240.ts" index 9ac7e41f..2780688d 100644 --- "a/src/divide-and-conquer/215.\346\225\260\347\273\204\344\270\255\347\232\204\347\254\254k\344\270\252\346\234\200\345\244\247\345\205\203\347\264\240.ts" +++ "b/src/divide-and-conquer/215.\346\225\260\347\273\204\344\270\255\347\232\204\347\254\254k\344\270\252\346\234\200\345\244\247\345\205\203\347\264\240.ts" @@ -89,7 +89,7 @@ function findKthLargest(nums: number[], k: number): number { input: [[3, 2, 3, 1, 2, 4, 5, 5, 6], 4], output: 4 }, - { + { input: [[1], 1], output: 1 } diff --git "a/src/dynamic-programming/1449.\346\225\260\344\275\215\346\210\220\346\234\254\345\222\214\344\270\272\347\233\256\346\240\207\345\200\274\347\232\204\346\234\200\345\244\247\346\225\260\345\255\227.ts" "b/src/dynamic-programming/1449.\346\225\260\344\275\215\346\210\220\346\234\254\345\222\214\344\270\272\347\233\256\346\240\207\345\200\274\347\232\204\346\234\200\345\244\247\346\225\260\345\255\227.ts" index 19bfb49f..4712c639 100644 --- "a/src/dynamic-programming/1449.\346\225\260\344\275\215\346\210\220\346\234\254\345\222\214\344\270\272\347\233\256\346\240\207\345\200\274\347\232\204\346\234\200\345\244\247\346\225\260\345\255\227.ts" +++ "b/src/dynamic-programming/1449.\346\225\260\344\275\215\346\210\220\346\234\254\345\222\214\344\270\272\347\233\256\346\240\207\345\200\274\347\232\204\346\234\200\345\244\247\346\225\260\345\255\227.ts" @@ -3,14 +3,14 @@ * * [1449] 数位成本和为目标值的最大数字 * - * https://leetcode-cn.com/problems/form-largest-integer-with-digits-that-add-up-to-target/description/ + * https://leetcode.cn/problems/form-largest-integer-with-digits-that-add-up-to-target/description/ * * algorithms - * Hard (62.58%) - * Likes: 139 + * Hard (62.97%) + * Likes: 214 * Dislikes: 0 - * Total Accepted: 17.3K - * Total Submissions: 27.6K + * Total Accepted: 25.7K + * Total Submissions: 40.8K * Testcase Example: '[4,3,2,5,6,7,2,5,5]\n9' * * 给你一个整数数组 cost 和一个整数 target 。请你返回满足如下规则可以得到的 最大 整数: @@ -83,44 +83,36 @@ // @lc code=start function largestNumber(cost: number[], target: number): string { - const len = cost.length; - const dp = Array(target + 1).fill(null); - dp[0] = ''; - for (let i = 0; i < len; i++) { - const curCost = cost[i]; + // 设 dp[i]为 target 为 i 时,能够获取的最大整数的位数(为什么不存整数,因为整数可能很大,存位数到时候用贪心法还原整数) + const dp = Array(target + 1).fill(-Infinity); + dp[0] = 0; - for (let j = 1; j <= target; j++) { - // 之前的背包无法组成物品,丢弃 - if (j < curCost || dp[j - curCost] === null) continue; - - // 大的数字一定在前面,最终结果才最大 - const cur = String(i + 1) + dp[j - curCost]; - - dp[j] = compare(cur, dp[j]) ? cur : dp[j]; + for (let i = 0; i < cost.length; i++) { + const weight = cost[i]; + for (let j = weight; j <= target; j++) { + dp[j] = Math.max(dp[j], dp[j - weight] + 1); } } - return dp[target] === null ? '0' : dp[target]; - - function compare(a: string, b: string) { - if (b === null) return true; - const n = a.length, - m = b.length; - if (n > m) return true; - if (m > n) return false; + // 获取了最大位数,接下来就是用贪心法还原整数值了 + if (dp[target] === -Infinity) return '0'; - for (let i = 0; i < n; i++) { - if (a.charAt(i) > b.charAt(i)) return true; - else if (a.charAt(i) < b.charAt(i)) return false; + let res = ''; + let remain = target; + for (let i = 0; i < dp[target]; i++) { + for (let d = 8; d >= 0; d--) { + if (remain >= cost[d] && dp[remain - cost[d]] === dp[target] - i - 1) { + res += (d + 1).toString(); + remain -= cost[d]; + break; + } } - - return true; } + + return res; } // @lc code=end (() => { - const cost = [4, 3, 2, 5, 6, 7, 2, 5, 5], - target = 9; - console.log(largestNumber(cost, target)); + LCT.func(largestNumber).auto(); })(); diff --git "a/src/dynamic-programming/198.\346\211\223\345\256\266\345\212\253\350\210\215.ts" "b/src/dynamic-programming/198.\346\211\223\345\256\266\345\212\253\350\210\215.ts" index 349987c5..f8363415 100644 --- "a/src/dynamic-programming/198.\346\211\223\345\256\266\345\212\253\350\210\215.ts" +++ "b/src/dynamic-programming/198.\346\211\223\345\256\266\345\212\253\350\210\215.ts" @@ -3,14 +3,14 @@ * * [198] 打家劫舍 * - * https://leetcode-cn.com/problems/house-robber/description/ + * https://leetcode.cn/problems/house-robber/description/ * * algorithms - * Medium (51.96%) - * Likes: 1775 + * Medium (56.39%) + * Likes: 3507 * Dislikes: 0 - * Total Accepted: 426.4K - * Total Submissions: 820.7K + * Total Accepted: 1.6M + * Total Submissions: 2.8M * Testcase Example: '[1,2,3,1]' * * @@ -50,25 +50,18 @@ // @lc code=start function rob(nums: number[]): number { - const len = nums.length; - if (len === 1) return nums[0]; + // 设 dp[i] 为前 i 间房屋能偷到的最大金额 + const dp = Array(nums.length + 1).fill(0); + dp[1] = nums[0]; - let interval = 0; - let prev = nums[0]; - // 从第二间房开始判断,当前房间是否抢: - // 不抢,结果等于抢邻房间时的最大值 + 0; - // 抢,结果等于隔间房间的最大值 + 当前房间金额 - // 状态转移方程:Sn = Math.max(Sn-1, Sn-2 + nums[n]) - for (let i = 1; i < len; i++) { - const curr = Math.max(interval + nums[i], prev); - interval = prev; - prev = curr; + for (let i = 2; i <= nums.length; i++) { + dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]); } - return prev; + + return dp[nums.length]; } // @lc code=end (() => { - const nums = [2, 7, 9, 3, 1]; - console.log(rob(nums)); + LCT.func(rob).auto(); })(); diff --git "a/src/dynamic-programming/322.\351\233\266\351\222\261\345\205\221\346\215\242.ts" "b/src/dynamic-programming/322.\351\233\266\351\222\261\345\205\221\346\215\242.ts" index 272f81d8..8cb443eb 100644 --- "a/src/dynamic-programming/322.\351\233\266\351\222\261\345\205\221\346\215\242.ts" +++ "b/src/dynamic-programming/322.\351\233\266\351\222\261\345\205\221\346\215\242.ts" @@ -3,14 +3,14 @@ * * [322] 零钱兑换 * - * https://leetcode-cn.com/problems/coin-change/description/ + * https://leetcode.cn/problems/coin-change/description/ * * algorithms - * Medium (44.73%) - * Likes: 1617 + * Medium (52.76%) + * Likes: 3243 * Dislikes: 0 - * Total Accepted: 338K - * Total Submissions: 754.9K + * Total Accepted: 1.4M + * Total Submissions: 2.6M * Testcase Example: '[1,2,5]\n11' * * 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 @@ -41,51 +41,35 @@ * 输出:0 * * - * 示例 4: - * - * - * 输入:coins = [1], amount = 1 - * 输出:1 - * - * - * 示例 5: - * - * - * 输入:coins = [1], amount = 2 - * 输出:2 - * - * * * * 提示: * * - * 1 - * 1 - * 0 + * 1 <= coins.length <= 12 + * 1 <= coins[i] <= 2^31 - 1 + * 0 <= amount <= 10^4 * * */ // @lc code=start function coinChange(coins: number[], amount: number): number { - const dp = Array(amount + 1).fill(Number.MAX_SAFE_INTEGER); + // dp[i] 表示 获取总金额为 i 时所需的最少硬币个数 + const dp: number[] = Array(amount + 1).fill(Infinity); dp[0] = 0; - // Sn = MIN(S[n-coin] + 1) - for (let i = 1; i <= amount; i++) { - coins.forEach(coin => { - if (i - coin >= 0) { - dp[i] = Math.min(dp[i], dp[i - coin] + 1); - } - }); + for (let i = 0; i < coins.length; i++) { + const weight = coins[i]; + for (let j = weight; j <= amount; j++) { + dp[j] = Math.min(dp[j], dp[j - weight] + 1); + } } - return dp[amount] === Number.MAX_SAFE_INTEGER ? -1 : dp[amount]; + + return dp[amount] === Infinity ? -1 : dp[amount]; } // @lc code=end (() => { - const nums = [1]; - const amount = 1; - console.log(coinChange(nums, amount)); + LCT.func(coinChange).auto(); })(); diff --git "a/src/dynamic-programming/647.\345\233\236\346\226\207\345\255\220\344\270\262.ts" "b/src/dynamic-programming/647.\345\233\236\346\226\207\345\255\220\344\270\262.ts" new file mode 100644 index 00000000..5b1166af --- /dev/null +++ "b/src/dynamic-programming/647.\345\233\236\346\226\207\345\255\220\344\270\262.ts" @@ -0,0 +1,76 @@ +/* + * @lc app=leetcode.cn id=647 lang=typescript + * + * [647] 回文子串 + * + * https://leetcode.cn/problems/palindromic-substrings/description/ + * + * algorithms + * Medium (68.21%) + * Likes: 1508 + * Dislikes: 0 + * Total Accepted: 440K + * Total Submissions: 645.1K + * Testcase Example: '"abc"' + * + * 给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。 + * + * 回文字符串 是正着读和倒过来读一样的字符串。 + * + * 子字符串 是字符串中的由连续字符组成的一个序列。 + * + * + * + * 示例 1: + * + * + * 输入:s = "abc" + * 输出:3 + * 解释:三个回文子串: "a", "b", "c" + * + * + * 示例 2: + * + * + * 输入:s = "aaa" + * 输出:6 + * 解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa" + * + * + * + * 提示: + * + * + * 1 <= s.length <= 1000 + * s 由小写英文字母组成 + * + * + */ + +// @lc code=start +function countSubstrings(s: string): number { + let res = 0; + + // 设 dp[i][j] 为 i-j 字符串 是否是回文子串 + const dp: boolean[][] = Array(s.length) + .fill(0) + .map(x => Array(s.length).fill(false)); + + for (let i = s.length - 1; i >= 0; i--) { + for (let j = i; j < s.length; j++) { + if (s[i] === s[j]) { + if (j - i < 2 || dp[i + 1][j - 1] === true) { + dp[i][j] = true; + res++; + } + } + } + } + + return res; +} +// @lc code=end + +(() => { + LCT.func(countSubstrings).auto(); +})(); diff --git "a/src/dynamic-programming/70.\347\210\254\346\245\274\346\242\257.ts" "b/src/dynamic-programming/70.\347\210\254\346\245\274\346\242\257.ts" index c1e73498..4ed1a1ee 100644 --- "a/src/dynamic-programming/70.\347\210\254\346\245\274\346\242\257.ts" +++ "b/src/dynamic-programming/70.\347\210\254\346\245\274\346\242\257.ts" @@ -3,57 +3,70 @@ * * [70] 爬楼梯 * - * https://leetcode-cn.com/problems/climbing-stairs/description/ + * https://leetcode.cn/problems/climbing-stairs/description/ * * algorithms - * Easy (53.20%) - * Likes: 2049 + * Easy (55.67%) + * Likes: 4078 * Dislikes: 0 - * Total Accepted: 631.3K - * Total Submissions: 1.2M + * Total Accepted: 2.2M + * Total Submissions: 4M * Testcase Example: '2' * * 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 * * 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? * - * 注意:给定 n 是一个正整数。 + * * * 示例 1: * - * 输入: 2 - * 输出: 2 - * 解释: 有两种方法可以爬到楼顶。 - * 1. 1 阶 + 1 阶 - * 2. 2 阶 + * + * 输入:n = 2 + * 输出:2 + * 解释:有两种方法可以爬到楼顶。 + * 1. 1 阶 + 1 阶 + * 2. 2 阶 * * 示例 2: * - * 输入: 3 - * 输出: 3 - * 解释: 有三种方法可以爬到楼顶。 - * 1. 1 阶 + 1 阶 + 1 阶 - * 2. 1 阶 + 2 阶 - * 3. 2 阶 + 1 阶 + * + * 输入:n = 3 + * 输出:3 + * 解释:有三种方法可以爬到楼顶。 + * 1. 1 阶 + 1 阶 + 1 阶 + * 2. 1 阶 + 2 阶 + * 3. 2 阶 + 1 阶 + * + * + * + * + * 提示: + * + * + * 1 <= n <= 45 * * */ // @lc code=start + +// @lc code=end + function climbStairs(n: number): number { if (n === 1) return 1; if (n === 2) return 2; - let prev = 1; + let pre = 1; let cur = 2; - for (let i = 3; i <= n; i++) { - const sum = prev + cur; - prev = cur; - cur = sum; + const res = 0; + for (let i = 2; i < n; i++) { + const temp = cur; + cur = cur + pre; + pre = temp; } return cur; } -// @lc code=end (() => { - console.log(climbStairs(6)); + LCT.func(climbStairs).auto(); })(); diff --git "a/src/greedy/55.\350\267\263\350\267\203\346\270\270\346\210\217.ts" "b/src/greedy/55.\350\267\263\350\267\203\346\270\270\346\210\217.ts" index d11558ee..7fb64ccb 100644 --- "a/src/greedy/55.\350\267\263\350\267\203\346\270\270\346\210\217.ts" +++ "b/src/greedy/55.\350\267\263\350\267\203\346\270\270\346\210\217.ts" @@ -3,21 +3,19 @@ * * [55] 跳跃游戏 * - * https://leetcode-cn.com/problems/jump-game/description/ + * https://leetcode.cn/problems/jump-game/description/ * * algorithms - * Medium (43.36%) - * Likes: 1528 + * Medium (44.84%) + * Likes: 3249 * Dislikes: 0 - * Total Accepted: 367.3K - * Total Submissions: 846.9K + * Total Accepted: 1.5M + * Total Submissions: 3.4M * Testcase Example: '[2,3,1,1,4]' * - * 给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。 + * 给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 * - * 数组中的每个元素代表你在该位置可以跳跃的最大长度。 - * - * 判断你是否能够到达最后一个下标。 + * 判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。 * * * @@ -42,45 +40,29 @@ * 提示: * * - * 1 - * 0 + * 1 <= nums.length <= 10^4 + * 0 <= nums[i] <= 10^5 * * */ // @lc code=start function canJump(nums: number[]): boolean { - let furtherest = 0; + const target = nums.length - 1; + let maxReach = 0; + + for (let i = 0; i < nums.length - 1; i++) { + if (maxReach < i) return false; + + maxReach = Math.max(maxReach, i + nums[i]); - for (let i = 0; i < nums.length; i++) { - if (i > furtherest) return false; - furtherest = Math.max(furtherest, i + nums[i]); + if (maxReach >= target) return true; } - return true; + + return maxReach >= target; } // @lc code=end (() => { - const nums = [3, 2, 1, 0, 4]; - console.log(canJump(nums)); + LCT.func(canJump).auto(); })(); - -// function canJump(nums: number[]): boolean { -// const len = nums.length; -// if (len === 1) return true; - -// const dp: boolean[] = Array(len).fill(false); -// dp[0] = true; - -// for (let i = 0; i < len - 1; i++) { -// if (dp[i]) { -// let jump = nums[i]; - -// for (let j = i + 1; j <= i + jump && j < len; j++) { -// dp[j] = true; -// } -// } -// } - -// return dp[len - 1]; -// } diff --git "a/src/hash-table/438.\346\211\276\345\210\260\345\255\227\347\254\246\344\270\262\344\270\255\346\211\200\346\234\211\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.ts" "b/src/hash-table/438.\346\211\276\345\210\260\345\255\227\347\254\246\344\270\262\344\270\255\346\211\200\346\234\211\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.ts" new file mode 100644 index 00000000..fc7e863d --- /dev/null +++ "b/src/hash-table/438.\346\211\276\345\210\260\345\255\227\347\254\246\344\270\262\344\270\255\346\211\200\346\234\211\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.ts" @@ -0,0 +1,92 @@ +/* + * @lc app=leetcode.cn id=438 lang=typescript + * + * [438] 找到字符串中所有字母异位词 + * + * https://leetcode.cn/problems/find-all-anagrams-in-a-string/description/ + * + * algorithms + * Medium (54.64%) + * Likes: 1910 + * Dislikes: 0 + * Total Accepted: 982.5K + * Total Submissions: 1.8M + * Testcase Example: '"cbaebabacd"\n"abc"' + * + * 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 + * + * + * + * 示例 1: + * + * + * 输入: s = "cbaebabacd", p = "abc" + * 输出: [0,6] + * 解释: + * 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 + * 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。 + * + * + * 示例 2: + * + * + * 输入: s = "abab", p = "ab" + * 输出: [0,1,2] + * 解释: + * 起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。 + * 起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。 + * 起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。 + * + * + * + * + * 提示: + * + * + * 1 <= s.length, p.length <= 3 * 10^4 + * s 和 p 仅包含小写字母 + * + * + */ + +// @lc code=start +function findAnagrams(s: string, p: string): number[] { + const res = []; + const needs: Record = {}; + for (const ch of p) needs[ch] = (needs[ch] || 0) + 1; + const needsCount = Object.keys(needs).length; + + const window: Record = {}; + let l = 0, + r = 0; + let validCount = 0; + + while (r < s.length) { + const ch = s[r]; + r++; + + if (needs[ch]) { + window[ch] = (window[ch] || 0) + 1; + if (window[ch] === needs[ch]) validCount++; + } + + while (r - l === p.length) { + if (validCount === needsCount) res.push(l); + + const dropCh = s[l]; + l++; + + if (needs[dropCh]) { + if (window[dropCh] === needs[dropCh]) validCount--; + window[dropCh]--; + } + } + } + + return res; +} +// @lc code=end + +(() => { + LCT.func(findAnagrams).auto(); +})(); diff --git "a/src/linked-list/234.\345\233\236\346\226\207\351\223\276\350\241\250.ts" "b/src/linked-list/234.\345\233\236\346\226\207\351\223\276\350\241\250.ts" new file mode 100644 index 00000000..273f1b54 --- /dev/null +++ "b/src/linked-list/234.\345\233\236\346\226\207\351\223\276\350\241\250.ts" @@ -0,0 +1,112 @@ +/* + * @lc app=leetcode.cn id=234 lang=typescript + * + * [234] 回文链表 + * + * https://leetcode.cn/problems/palindrome-linked-list/description/ + * + * algorithms + * Easy (58.16%) + * Likes: 2273 + * Dislikes: 0 + * Total Accepted: 1.3M + * Total Submissions: 2.2M + * Testcase Example: '[1,2,2,1]' + * + * 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。 + * + * + * + * 示例 1: + * + * + * 输入:head = [1,2,2,1] + * 输出:true + * + * + * 示例 2: + * + * + * 输入:head = [1,2] + * 输出:false + * + * + * + * + * 提示: + * + * + * 链表中节点数目在范围[1, 10^5] 内 + * 0 <= Node.val <= 9 + * + * + * + * + * 进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题? + * + */ + +// @lc code=start +/** + * Definition for singly-linked list. + * class ListNode { + * val: number + * next: ListNode | null + * constructor(val?: number, next?: ListNode | null) { + * this.val = (val===undefined ? 0 : val) + * this.next = (next===undefined ? null : next) + * } + * } + */ + +function isPalindrome(head: ListNode | null): boolean { + // 找链表的中点 + function findMiddle(node: ListNode | null) { + let l = node; + let r = node; + + while (r !== null && r.next !== null) { + l = l.next; + r = r.next.next; + } + return l; + } + + // 反转链表 + function reverse(node: ListNode | null) { + let pre = null; + let cur = node; + while (cur !== null) { + const temp = cur.next; + cur.next = pre; + + pre = cur; + cur = temp; + } + return pre; + } + + const mid = findMiddle(head); + let reverseHead = reverse(mid); + + while (reverseHead !== null) { + if (head.val !== reverseHead.val) return false; + + head = head.next; + reverseHead = reverseHead.next; + } + return true; +} + +// @lc code=end + +(() => { + LCT.func(isPalindrome).cases([ + { input: List.deserialize([1, 2, 2, 1]), output: true }, + { input: List.deserialize([1, 2]), output: false }, + { input: List.deserialize([1, 2, 3, 2, 1]), output: true }, + { input: List.deserialize([1, 2, 3, 4, 5]), output: false }, + { input: List.deserialize([1, 2, 1]), output: true }, + { input: List.deserialize([1]), output: true } + ]); +})(); diff --git "a/src/sliding-window/239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.ts" "b/src/sliding-window/239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.ts" index f5411604..a0b1716e 100644 --- "a/src/sliding-window/239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.ts" +++ "b/src/sliding-window/239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.ts" @@ -6,11 +6,11 @@ * https://leetcode.cn/problems/sliding-window-maximum/description/ * * algorithms - * Hard (49.90%) - * Likes: 1640 + * Hard (50.16%) + * Likes: 3467 * Dislikes: 0 - * Total Accepted: 296.1K - * Total Submissions: 593K + * Total Accepted: 1.2M + * Total Submissions: 2.3M * Testcase Example: '[1,3,-1,-3,5,3,6,7]\n3' * * 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k @@ -57,43 +57,66 @@ // @lc code=start function maxSlidingWindow(nums: number[], k: number): number[] { - if (nums.length == 0 || k == 0) return []; - const dequeue = []; + // 注意,这里的 queue 的单调队列存的是单调队列值的下标,所对应的值是单调的,但是序号不一定 + const queue: number[] = []; const res: number[] = []; - // 初始化窗口 - for (let i = 0; i < k; i++) { - // 保持单调队列的单调递减 - while (dequeue.length !== 0 && dequeue[dequeue.length - 1] < nums[i]) { - dequeue.pop(); - } - // 加入窗口右边界的值 - dequeue.push(nums[i]); - } - // 由于滑动窗口中元素单调递减,队头一定是当前滑动窗口的最大值 - res.push(dequeue[0]); - for (let i = k; i < nums.length; i++) { - // i - k 为窗口的左边界 - const left = i - k; - // 如果这个元素在单调队列中,则直接删除 - if (dequeue[0] === nums[left]) { - dequeue.shift(); + let l = 0; + let r = 0; + while (r < nums.length) { + // 插入时清理掉队尾比较小的值,保持单调性 + while (queue.length > 0 && nums[r] > nums[queue[queue.length - 1]]) { + queue.pop(); } + queue.push(r); + r++; - // 保持单调队列的单调递减 - while (dequeue.length !== 0 && dequeue[dequeue.length - 1] < nums[i]) { - dequeue.pop(); - } - dequeue.push(nums[i]); + if (r - l >= k) { + // 窗口满足要求时,将单调队列的最大值推入 res + res.push(nums[queue[0]]); - res.push(dequeue[0]); + // 缩小窗口时,如果队头被排出 + if (l === queue[0]) { + queue.shift(); + } + l++; + } } + return res; } // @lc code=end (() => { - const nums = [1, 3, -1, -3, -1, 5, 3, 6, 7], - k = 3; - console.log(maxSlidingWindow(nums, k)); + LCT.func(maxSlidingWindow).auto(); + + LCT.func(maxSlidingWindow).cases([ + { + input: [[1, -1], 1], + output: [1, -1] + } + ]); })(); + +// let l = 0; +// let r = 0; +// while (r < nums.length) { +// while (queue.length > 0 && nums[r] > queue[queue.length - 1]) { +// queue.pop(); +// } +// queue.push(nums[r]); +// r++; + +// // 窗口满足要求 +// if (r - l >= k) { +// res.push(queue[0]); + +// // 收缩左侧 +// if (nums[l] === queue[0]) { +// queue.shift(); +// } +// l++; +// } +// } +// return res; +// } diff --git "a/src/sliding-window/3.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.ts" "b/src/sliding-window/3.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.ts" index bf72f18b..bc4aa0e7 100644 --- "a/src/sliding-window/3.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.ts" +++ "b/src/sliding-window/3.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.ts" @@ -3,17 +3,17 @@ * * [3] 无重复字符的最长子串 * - * https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/description/ + * https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/ * * algorithms - * Medium (38.27%) - * Likes: 6733 + * Medium (42.28%) + * Likes: 11404 * Dislikes: 0 - * Total Accepted: 1.4M - * Total Submissions: 3.7M + * Total Accepted: 4.1M + * Total Submissions: 9.8M * Testcase Example: '"abcabcbb"' * - * 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 + * 给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。 * * * @@ -22,7 +22,7 @@ * * 输入: s = "abcabcbb" * 输出: 3 - * 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 + * 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。 * * * 示例 2: @@ -42,19 +42,12 @@ * 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 * * - * 示例 4: - * - * - * 输入: s = "" - * 输出: 0 - * - * * * * 提示: * * - * 0 + * 0 <= s.length <= 5 * 10^4 * s 由英文字母、数字、符号和空格组成 * * @@ -62,36 +55,28 @@ // @lc code=start function lengthOfLongestSubstring(s: string): number { - const window: Record = {}; - let res = 0; - - let left = 0; - let right = 0; - while (right < s.length) { - // 扩大右边界 - const ch = s[right]; - right++; + const map: Record = {}; - // 更新滑动窗口元素 - window[ch] = window[ch] ? window[ch] + 1 : 1; + let l = 0; + let r = 0; + let res = 0; - // 当滑动窗口中该字符个数大于1,此时字串不合法,需要缩小左边界直到使该字符唯一 - while (window[ch] > 1) { - // 缩左边界 - const dropCh = s[left]; - left++; + while (r < s.length) { + const ch = s[r]; + map[ch] = map[ch] ? map[ch] + 1 : 1; + r++; - // 更新滑动窗口元素 - window[dropCh] -= 1; + while (map[ch] > 1) { + const dropCh = s[l]; + map[dropCh] -= 1; + l++; } - - // 更新合法情况的结果 - res = Math.max(res, right - left); + res = Math.max(res, r - l); } return res; } // @lc code=end (() => { - console.log(lengthOfLongestSubstring('pwwkew')); + LCT.func(lengthOfLongestSubstring).auto(); })(); diff --git "a/src/sliding-window/76.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.ts" "b/src/sliding-window/76.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.ts" index 1598982b..4ec24cd8 100644 --- "a/src/sliding-window/76.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.ts" +++ "b/src/sliding-window/76.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.ts" @@ -64,57 +64,54 @@ // @lc code=start function minWindow(s: string, t: string): string { - const initRes = s + ' '; - let res = initRes; + let l = 0; + let r = 0; + let start = 0; + let minLen = Infinity; + let validCount = 0; + + const window: Record = {}; const needs: Record = {}; - for (let i = 0; i < t.length; i++) { - const ch = t[i]; - needs[ch] = needs[ch] ? needs[ch] + 1 : 1; + for (const ch of t) { + needs[ch] = (needs[ch] ?? 0) + 1; } - const needsLength = Object.keys(needs).length; + const target = Object.keys(needs).length; - const window: Record = {}; - let left = 0; - let right = 0; - let validCount = 0; - while (right < s.length) { - // 扩右边界 - const ch = s[right]; - right++; + while (r < s.length) { + // 向右扩大窗口,将元素加入窗口 + const ch = s[r]; + r++; if (needs[ch]) { - // 更新滑动窗口元素内容以及合法指标判断 - window[ch] = window[ch] ? window[ch] + 1 : 1; - if (window[ch] === needs[ch]) { - validCount += 1; - } + window[ch] = (window[ch] ?? 0) + 1; + // 计算匹配个数,方便查询是否达成目标 + if (window[ch] === needs[ch]) validCount++; } - while (validCount === needsLength) { - // 若当前滑动窗口中字串长度小于res,则更新res字串 - if (right - left < res.length) { - res = s.substring(left, right); + while (validCount === target) { + // 处在合法区间,更新最终目标,更新要在收缩之前 + if (r - l < minLen) { + minLen = r - l; + start = l; } - // 缩左边界 - const dropCh = s[left]; - left++; + // 之后逐步缩小窗口,踢出窗口外元素 + const dropCh = s[l]; + l++; - // 更新滑动窗口元素内容以及合法指标判断 - if (needs[dropCh] && needs[dropCh] > 0) { - if (window[dropCh] === needs[ch]) validCount -= 1; - window[dropCh] -= 1; + if (needs[dropCh]) { + // 注意顺序,先判断丢弃的字符在窗口中的数量是否满足要求,再进行丢弃 + if (window[dropCh] === needs[dropCh]) validCount--; + window[dropCh]--; } } } - return res === initRes ? '' : res; + return minLen === Infinity ? '' : s.substring(start, start + minLen); } // @lc code=end (() => { - const s = 'ADOBECODEBANC', - t = 'ABC'; - console.log(minWindow(s, t)); + LCT.func(minWindow).auto(); })(); diff --git "a/src/stack/739.\346\257\217\346\227\245\346\270\251\345\272\246.ts" "b/src/stack/739.\346\257\217\346\227\245\346\270\251\345\272\246.ts" index 64a48278..a8aa7796 100644 --- "a/src/stack/739.\346\257\217\346\227\245\346\270\251\345\272\246.ts" +++ "b/src/stack/739.\346\257\217\346\227\245\346\270\251\345\272\246.ts" @@ -6,11 +6,11 @@ * https://leetcode.cn/problems/daily-temperatures/description/ * * algorithms - * Medium (69.24%) - * Likes: 1875 + * Medium (69.73%) + * Likes: 2150 * Dislikes: 0 - * Total Accepted: 660.5K - * Total Submissions: 954K + * Total Accepted: 971.2K + * Total Submissions: 1.4M * Testcase Example: '[73,74,75,71,69,72,76,73]' * * 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i @@ -51,22 +51,17 @@ // @lc code=start function dailyTemperatures(temperatures: number[]): number[] { - const len = temperatures.length; - const res: number[] = Array(len).fill(0); - const stack: number[] = []; + const res: number[] = Array(temperatures.length).fill(0); - // 本质上就是求下一个更大元素 - for (let i = len - 1; i >= 0; i--) { - // 维护一个单调递减栈 - while (stack.length > 0 && temperatures[stack[stack.length - 1]] <= temperatures[i]) { + for (let i = temperatures.length - 1; i >= 0; i--) { + while (stack.length && temperatures[stack[stack.length - 1]] <= temperatures[i]) { stack.pop(); } - // 此时栈顶元素就是第一个比当前元素大的元素,根据题意记录索引差 - if (stack.length > 0) { - res[i] = stack[stack.length - 1] - i; + if (stack.length) { + const nextHigh = stack[stack.length - 1]; + res[i] = nextHigh - i; } - stack.push(i); } @@ -75,6 +70,39 @@ function dailyTemperatures(temperatures: number[]): number[] { // @lc code=end (() => { - const temperatures = [73, 74, 75, 71, 69, 72, 76, 73]; - console.log(dailyTemperatures(temperatures)); + LCT.func(dailyTemperatures).auto(); + + LCT.func(dailyTemperatures).cases([ + { + input: [[89, 62, 70, 58, 47, 47, 46, 76, 100, 70]], + output: [8, 1, 5, 4, 3, 2, 1, 1, 0, 0] + } + ]); })(); + +// function dailyTemperatures(temperatures: number[]): number[] { +// const stack: number[] = []; +// const res: number[] = Array(temperatures.length).fill(0); + +// // 遍历方向,看你需要向前查找还是向后查找 +// for (let i = temperatures.length - 1; i >= 0; i--) { +// // 如果当前元素破坏了站的单调性,需要不断弹出栈顶直到恢复单调栈 +// // 比较大小方向,看查找的是更大值还是更小值(维护单增栈还是单减栈) +// while (stack.length > 0 && temperatures[stack[stack.length - 1]] <= temperatures[i]) { +// const top = stack.pop(); + +// // Tips: 弹出时可以按需记录,例如接雨水,它需要在弹出时用左右边界算贡献 +// // 左边界就是栈顶,右边界就是当前位置 i,弹出的 top 就是高度的低点 +// } + +// // 当栈顶有值,说明是下一个更高温度 +// if (stack.length > 0) { +// res[i] = stack[stack.length - 1] - i; +// } + +// // 如果当前元素不破坏栈的单调性,直接把 index 入栈 +// stack.push(i); +// } + +// return res; +// } diff --git "a/src/tree/113.\350\267\257\345\276\204\346\200\273\345\222\214Ii.ts" "b/src/tree/113.\350\267\257\345\276\204\346\200\273\345\222\214Ii.ts" index 1393e238..fffa5433 100644 --- "a/src/tree/113.\350\267\257\345\276\204\346\200\273\345\222\214Ii.ts" +++ "b/src/tree/113.\350\267\257\345\276\204\346\200\273\345\222\214Ii.ts" @@ -119,9 +119,17 @@ function pathSum(root: TreeNode | null, targetSum: number): number[][] { // @lc code=end (() => { - LCT.func(pathSum).auto({ - input: [Tree.deserialize] - }); + LCT.func(pathSum).cases([ + { + input: [Tree.deserialize([5, 4, 8, 11, null, 13, 4, 7, 2, null, null, 5, 1]), 22], + output: [ + [5, 4, 11, 2], + [5, 8, 4, 5] + ] + }, + { input: [Tree.deserialize([1, 2, 3]), 5], output: [] }, + { input: [Tree.deserialize([1, 2]), 0], output: [] } + ]); })(); // function pathSum(root: TreeNode | null, targetSum: number): number[][] { diff --git "a/src/tree/114.\344\272\214\345\217\211\346\240\221\345\261\225\345\274\200\344\270\272\351\223\276\350\241\250.ts" "b/src/tree/114.\344\272\214\345\217\211\346\240\221\345\261\225\345\274\200\344\270\272\351\223\276\350\241\250.ts" new file mode 100644 index 00000000..14c9abd0 --- /dev/null +++ "b/src/tree/114.\344\272\214\345\217\211\346\240\221\345\261\225\345\274\200\344\270\272\351\223\276\350\241\250.ts" @@ -0,0 +1,113 @@ +/* + * @lc app=leetcode.cn id=114 lang=typescript + * + * [114] 二叉树展开为链表 + * + * https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/description/ + * + * algorithms + * Medium (76.19%) + * Likes: 2018 + * Dislikes: 0 + * Total Accepted: 875.6K + * Total Submissions: 1.1M + * Testcase Example: '[1,2,5,3,4,null,6]' + * + * 给你二叉树的根结点 root ,请你将它展开为一个单链表: + * + * + * 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。 + * 展开后的单链表应该与二叉树 先序遍历 顺序相同。 + * + * + * + * + * 示例 1: + * + * + * 输入:root = [1,2,5,3,4,null,6] + * 输出:[1,null,2,null,3,null,4,null,5,null,6] + * + * + * 示例 2: + * + * + * 输入:root = [] + * 输出:[] + * + * + * 示例 3: + * + * + * 输入:root = [0] + * 输出:[0] + * + * + * + * + * 提示: + * + * + * 树中结点数在范围 [0, 2000] 内 + * -100 + * + * + * + * + * 进阶:你可以使用原地算法(O(1) 额外空间)展开这棵树吗? + * + */ + +// @lc code=start +/** + * Definition for a binary tree node. + * class TreeNode { + * val: number + * left: TreeNode | null + * right: TreeNode | null + * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + * } + */ + +/** + Do not return anything, modify root in-place instead. + */ +function flatten(root: TreeNode | null): void { + function traverse(node: TreeNode | null) { + if (!node) return; + arr.push(node.val); + if (node.left) traverse(node.left); + if (node.right) traverse(node.right); + } + + const arr: number[] = []; + traverse(root); + + let curr = root; + for (let i = 1; i < arr.length; i++) { + curr.left = null; + curr.right = new TreeNode(arr[i]); + curr = curr.right; + } +} +// @lc code=end + +(() => { + LCT.inPlace(flatten).cases( + [ + { + input: [Tree.deserialize([1, 2, 5, 3, 4, null, 6])], + output: '[1,"null",2,"null",3,"null",4,"null",5,"null",6]' + } + ], + { + judge: (expect: TreeNode | null, expected: string) => { + return Tree.serialize(expect) === expected; + } + } + ); +})(); diff --git "a/src/tree/236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.ts" "b/src/tree/236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.ts" new file mode 100644 index 00000000..d9af0485 --- /dev/null +++ "b/src/tree/236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.ts" @@ -0,0 +1,105 @@ +/* + * @lc app=leetcode.cn id=236 lang=typescript + * + * [236] 二叉树的最近公共祖先 + * + * https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/ + * + * algorithms + * Medium (75.00%) + * Likes: 3250 + * Dislikes: 0 + * Total Accepted: 1.3M + * Total Submissions: 1.7M + * Testcase Example: '[3,5,1,6,2,0,8,null,null,7,4]\n5\n1' + * + * 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 + * + * 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x + * 的深度尽可能大(一个节点也可以是它自己的祖先)。” + * + * + * + * 示例 1: + * + * + * 输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 + * 输出:3 + * 解释:节点 5 和节点 1 的最近公共祖先是节点 3 。 + * + * + * 示例 2: + * + * + * 输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 + * 输出:5 + * 解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。 + * + * + * 示例 3: + * + * + * 输入:root = [1,2], p = 1, q = 2 + * 输出:1 + * + * + * + * + * 提示: + * + * + * 树中节点数目在范围 [2, 10^5] 内。 + * -10^9 + * 所有 Node.val 互不相同 。 + * p != q + * p 和 q 均存在于给定的二叉树中。 + * + * + */ + +import { notDeepEqual } from 'assert'; + +// @lc code=start +/** + * Definition for a binary tree node. + * class TreeNode { + * val: number + * left: TreeNode | null + * right: TreeNode | null + * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + * } + */ + +function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null { + function traverse(node: TreeNode | null): TreeNode | null { + if (!node) return null; + if (node.val === p.val || node.val === q.val) return node; + + const left = traverse(node.left); + const right = traverse(node.right); + + if (left !== null && right !== null) return node; + if (left !== null) return left; + if (right !== null) return right; + return null; + } + + return traverse(root); +} +// @lc code=end + +(() => { + const tree = Tree.deserialize([3, 5, 1, 6, 2, 0, 8, null, null, 7, 4]); + LCT.func(lowestCommonAncestor).cases( + [ + { input: [tree, new TreeNode(5), new TreeNode(1)], output: 3 }, + { input: [tree, new TreeNode(5), new TreeNode(4)], output: 5 }, + { input: [Tree.deserialize([1, 2]), new TreeNode(1), new TreeNode(2)], output: 1 } + ], + { judge: (actual: TreeNode | null, expected: number) => actual.val === expected } + ); +})(); diff --git "a/src/trie/208.\345\256\236\347\216\260Trie\345\211\215\347\274\200\346\240\221.ts" "b/src/trie/208.\345\256\236\347\216\260Trie\345\211\215\347\274\200\346\240\221.ts" new file mode 100644 index 00000000..86132c6d --- /dev/null +++ "b/src/trie/208.\345\256\236\347\216\260Trie\345\211\215\347\274\200\346\240\221.ts" @@ -0,0 +1,57 @@ +/* + * @lc app=leetcode.cn id=208 lang=typescript + * + * [208] 实现 Trie (前缀树) + */ + +// @lc code=start +class Trie { + isEnd: boolean; + children: Map; + + constructor() { + this.isEnd = false; + this.children = new Map(); + } + + insert(word: string): void { + if (word.length === 0) { + this.isEnd = true; + return; + } + const ch = word[0]; + const trie = this.children.has(ch) ? this.children.get(ch)! : new Trie(); + trie.insert(word.slice(1)); + this.children.set(ch, trie); + } + + search(word: string): boolean { + if (word.length === 0) return this.isEnd; + const ch = word[0]; + if (!this.children.has(ch)) return false; + return this.children.get(ch)!.search(word.slice(1)); + } + + startsWith(prefix: string): boolean { + if (prefix.length === 0) return true; + const ch = prefix[0]; + if (!this.children.has(ch)) return false; + return this.children.get(ch)!.startsWith(prefix.slice(1)); + } +} +// @lc code=end + +// Tests +(() => { + const trie = new Trie(); + trie.insert('apple'); + console.log('search apple:', trie.search('apple')); // true + console.log('search app:', trie.search('app')); // false + console.log('startsWith app:', trie.startsWith('app')); // true + trie.insert('app'); + console.log('search app:', trie.search('app')); // true + console.log('search banana:', trie.search('banana')); // false + console.log('startsWith ban:', trie.startsWith('ban')); // false + trie.insert(''); + console.log('search empty:', trie.search('')); // true +})(); diff --git "a/src/trie/211.\346\267\273\345\212\240\344\270\216\346\220\234\347\264\242\345\215\225\350\257\215.ts" "b/src/trie/211.\346\267\273\345\212\240\344\270\216\346\220\234\347\264\242\345\215\225\350\257\215.ts" new file mode 100644 index 00000000..45ad2d18 --- /dev/null +++ "b/src/trie/211.\346\267\273\345\212\240\344\270\216\346\220\234\347\264\242\345\215\225\350\257\215.ts" @@ -0,0 +1,122 @@ +/* + * @lc app=leetcode.cn id=211 lang=typescript + * + * [211] 添加与搜索单词 - 数据结构设计 + * + * https://leetcode.cn/problems/design-add-and-search-words-data-structure/description/ + * + * algorithms + * Medium (51.67%) + * Likes: 639 + * Dislikes: 0 + * Total Accepted: 122.1K + * Total Submissions: 236.3K + * Testcase Example: '["WordDictionary","addWord","addWord","addWord","search","search","search","search"]\n' + + '[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]' + * + * 请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。 + * + * 实现词典类 WordDictionary : + * + * + * WordDictionary() 初始化词典对象 + * void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配 + * bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回  false 。word 中可能包含一些 + * '.' ,每个 . 都可以表示任何一个字母。 + * + * + * + * + * 示例: + * + * + * 输入: + * + * ["WordDictionary","addWord","addWord","addWord","search","search","search","search"] + * [[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]] + * 输出: + * [null,null,null,null,false,true,true,true] + * + * 解释: + * WordDictionary wordDictionary = new WordDictionary(); + * wordDictionary.addWord("bad"); + * wordDictionary.addWord("dad"); + * wordDictionary.addWord("mad"); + * wordDictionary.search("pad"); // 返回 False + * wordDictionary.search("bad"); // 返回 True + * wordDictionary.search(".ad"); // 返回 True + * wordDictionary.search("b.."); // 返回 True + * + * + * + * + * 提示: + * + * + * 1 <= word.length <= 25 + * addWord 中的 word 由小写英文字母组成 + * search 中的 word 由 '.' 或小写英文字母组成 + * 最多调用 10^4 次 addWord 和 search + * + * + */ + +// @lc code=start +class WordDictionary { + isEnd: boolean; + children: Map; + + constructor() { + this.isEnd = false; + this.children = new Map(); + } + + addWord(word: string): void { + if (word.length === 0) { + this.isEnd = true; + return; + } + const ch = word[0]; + const node = this.children.has(ch) ? this.children.get(ch)! : new WordDictionary(); + node.addWord(word.slice(1)); + this.children.set(ch, node); + } + + search(word: string): boolean { + if (word.length === 0) return this.isEnd; + + const ch = word[0]; + if (ch === '.') { + const choices: Array = Array.from(this.children.values()); + return choices.some(choice => choice.search(word.slice(1))); + } else if (!this.children.has(ch)) { + return false; + } else { + return this.children.get(ch)!.search(word.slice(1)); + } + } +} +// @lc code=end + +// Tests +(() => { + const dict = new WordDictionary(); + dict.addWord('bad'); + dict.addWord('dad'); + dict.addWord('mad'); + console.log('search pad:', dict.search('pad')); // false + console.log('search bad:', dict.search('bad')); // true + console.log('search .ad:', dict.search('.ad')); // true + console.log('search b..:', dict.search('b..')); // true + console.log('search ...:', dict.search('...')); // true + console.log('search ..:', dict.search('..')); // false + console.log('search .:', dict.search('.')); // false + console.log('search ....:', dict.search('....')); // false + + // Edge: single char with wildcard + const dict2 = new WordDictionary(); + dict2.addWord('a'); + console.log('search .:', dict2.search('.')); // true + console.log('search a:', dict2.search('a')); // true + console.log('search ..:', dict2.search('..')); // false +})(); diff --git "a/src/two-pointers/11.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.ts" "b/src/two-pointers/11.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.ts" index 5e7d1680..0f6e6315 100644 --- "a/src/two-pointers/11.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.ts" +++ "b/src/two-pointers/11.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.ts" @@ -3,18 +3,21 @@ * * [11] 盛最多水的容器 * - * https://leetcode-cn.com/problems/container-with-most-water/description/ + * https://leetcode.cn/problems/container-with-most-water/description/ * * algorithms - * Medium (62.10%) - * Likes: 3119 + * Medium (61.76%) + * Likes: 5935 * Dislikes: 0 - * Total Accepted: 610.4K - * Total Submissions: 983K + * Total Accepted: 2.1M + * Total Submissions: 3.5M * Testcase Example: '[1,8,6,2,5,4,8,3,7]' * - * 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 - * (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + * 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 + * + * 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + * + * 返回容器可以储存的最大水量。 * * 说明:你不能倾斜容器。 * @@ -36,20 +39,6 @@ * 输出:1 * * - * 示例 3: - * - * - * 输入:height = [4,3,2,1,4] - * 输出:16 - * - * - * 示例 4: - * - * - * 输入:height = [1,2,1] - * 输出:2 - * - * * * * 提示: @@ -64,19 +53,20 @@ // @lc code=start function maxArea(height: number[]): number { - let res = 0; - let l = 0; let r = height.length - 1; - while (l < r) { - // 此时最大面积是,短板高度*宽度 - const temp = Math.min(height[l], height[r]) * (r - l); - res = Math.max(res, temp); + let res = 0; - // 移动短板指针,目的是为了找到更大的面积(因为宽度会进一步减小,如果短板高度不变大的话就不可能让面积再变大了) - if (height[l] > height[r]) r--; - else l++; + while (l < r) { + const h = Math.min(height[l], height[r]); + const w = r - l; + res = Math.max(res, h * w); + if (height[l] < height[r]) { + l++; + } else { + r--; + } } return res; diff --git a/src/utils/global-utils.d.ts b/src/utils/global-utils.d.ts index 9c5c62a7..a3f3e44a 100644 --- a/src/utils/global-utils.d.ts +++ b/src/utils/global-utils.d.ts @@ -16,6 +16,13 @@ declare global { constructor(val?: T, next?: ListNode | null); } + class DoublyListNode { + val: T; + prev: DoublyListNode | null; + next: DoublyListNode | null; + constructor(val?: T, prev?: DoublyListNode | null, next?: DoublyListNode | null); + } + const Tree: { serialize(root: TreeNodeT | null): string; deserialize(data: string | Array): TreeNodeT | null; @@ -28,50 +35,45 @@ declare global { hasCycle(head: ListNode | null): boolean; }; + const DoublyList: { + serialize(head: DoublyListNode | null): Array; + deserialize(data: string | Array): DoublyListNode | null; + getNode(head: DoublyListNode | null, index: number): DoublyListNode | null; + }; + // ── LCT (LeetCode Test) ───────────────────────────────────────────── + /** Options for a test suite; `judge` overrides the default deep-equality comparison. */ + type LCTOptions = { + /** Params are `any` so each call site can annotate its own actual/expected types without casts. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + judge?: (actual: any, expected: any) => boolean; + }; + const LCT: { - /** Test a pure function: `LCT.func(fn).cases([{ input, output }, ...], options?)` or `LCT.func(fn).auto()` */ + /** Test a pure function: `LCT.func(fn).cases([{ input, output }, ...], options?)` or `.auto(options?)` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any func any>( solution: F ): { cases( - cases: ReadonlyArray<{ - input: unknown | ReadonlyArray; - output: unknown; - }>, - options?: { - input?: ((value: any) => unknown) | Array<((value: any) => unknown) | undefined>; - output?: ((actual: unknown) => unknown) | Array<((actual: unknown) => unknown) | undefined>; - } + cases: ReadonlyArray<{ input: unknown | ReadonlyArray; output: unknown }>, + options?: LCTOptions ): void; /** Auto-parse test cases from the file's comment block */ - auto(options?: { - input?: ((value: any) => unknown) | Array<((value: any) => unknown) | undefined>; - output?: ((actual: unknown) => unknown) | Array<((actual: unknown) => unknown) | undefined>; - }): void; + auto(options?: LCTOptions): void; }; - /** Test an in-place mutation function: `LCT.inPlace(fn).cases([{ input, output }, ...], options?)` or `.auto()` */ + /** Test an in-place mutation function: `LCT.inPlace(fn).cases([{ input, output }, ...], options?)` or `.auto(options?)` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any inPlace any>( solution: F ): { cases( - cases: ReadonlyArray<{ - input: unknown | ReadonlyArray; - output: unknown; - }>, - options?: { - input?: ((value: any) => unknown) | Array<((value: any) => unknown) | undefined>; - output?: ((actual: unknown) => unknown) | Array<((actual: unknown) => unknown) | undefined>; - } + cases: ReadonlyArray<{ input: unknown | ReadonlyArray; output: unknown }>, + options?: LCTOptions ): void; /** Auto-parse test cases from the file's comment block */ - auto(options?: { - input?: ((value: any) => unknown) | Array<((value: any) => unknown) | undefined>; - output?: ((actual: unknown) => unknown) | Array<((actual: unknown) => unknown) | undefined>; - }): void; + auto(options?: LCTOptions): void; }; /** Test a class (design problems): `LCT.cls(Ctor).calls(methods, inputs, expected)` or `.auto()` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -84,15 +86,7 @@ declare global { expected: ReadonlyArray ): void; /** Auto-parse test cases from the file's comment block */ - auto(options?: { - ctorInput?: ((value: any) => unknown) | Array<((value: any) => unknown) | undefined>; - callInput?: Partial< - Record unknown) | Array<((value: any) => unknown) | undefined>> - >; - callOutput?: Partial< - Record unknown) | Array<((actual: unknown) => unknown) | undefined>> - >; - }): void; + auto(): void; }; }; } diff --git a/src/utils/lct.ts b/src/utils/lct.ts index 5a8d24ee..46e329e4 100644 --- a/src/utils/lct.ts +++ b/src/utils/lct.ts @@ -6,22 +6,40 @@ type AnyFunc = (...args: any[]) => any; // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnyConstructor = new (...args: any[]) => any; -type ExpectedOrTester = T | ((actual: T) => boolean); -// Use `any` for transform input to allow direct passing of narrower functions like Tree.deserialize. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type TransformFn = (value: any) => unknown; -type TransformList = Array; -type TransformSpec = TransformFn | TransformList; +// Decides whether the actual result matches the expected value. Defaults to deep equality. +type Judge = (actual: unknown, expected: unknown) => boolean; + +type Options = { + /** + * Custom check between the actual result and the expected value (overrides deep equality). + * Params are `any` so each call site can annotate its own actual/expected types without casts. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + judge?: (actual: any, expected: any) => boolean; +}; + +// One test example: positional arguments plus an optional expected output. +type Example = { + input: unknown[]; + expected?: unknown; + hasExpected: boolean; +}; -type AutoFuncOptions = { - input?: TransformSpec; - output?: TransformSpec; +// A single runnable check: a tag for logging, the input to print, the expected +// value, and a thunk that produces the actual result. +type Row = { + tag: string; + input: ReadonlyArray; + expected?: unknown; + hasExpected: boolean; + call: () => unknown; }; -type AutoClassOptions = { - ctorInput?: TransformSpec; - callInput?: Partial>; - callOutput?: Partial>; +type ClassExample = { + methods: ReadonlyArray; + inputs: ReadonlyArray>; + expected: ReadonlyArray; + hasExpected: boolean; }; type CaseData = { @@ -54,101 +72,156 @@ class LCT { } } - private printSummary(passed: number, failed: number): void { - console.log(`\n── Summary: ${passed} passed, ${failed} failed ──\n`); + private printSummary(passed: number, failed: number, unchecked = 0): void { + const uncheckedText = unchecked > 0 ? `, ${unchecked} unchecked` : ''; + console.log(`\n── Summary: ${passed} passed, ${failed} failed${uncheckedText} ──\n`); + } + + private normalizeInput(input: unknown | ReadonlyArray): unknown[] { + return Array.isArray(input) ? [...input] : [input]; } - private isDeepEqual(a: unknown, b: unknown): boolean { + /** Default judge: deep strict equality between actual and expected. */ + private deepEqual: Judge = (actual, expected) => { try { - assert.deepStrictEqual(a, b); + assert.deepStrictEqual(actual, expected); return true; } catch { return false; } - } + }; - private applyTransforms(values: ReadonlyArray, transforms?: TransformSpec): unknown[] { - if (!transforms) return [...values]; - if (typeof transforms === 'function') { - if (values.length === 0) return []; - const [first, ...rest] = values; - return [transforms(first), ...rest]; + /** + * The single execution core: run every row, printing its input/actual/expect + * and a PASS/FAIL/ERROR line, then print a summary. + */ + private run(rows: ReadonlyArray, judge: Judge = this.deepEqual): void { + let passed = 0; + let failed = 0; + let unchecked = 0; + for (const { tag, input, expected, hasExpected, call } of rows) { + const start = performance.now(); + try { + console.log(`${tag} input: ${this.formatValue(input)}`); + const actual = call(); + const ms = (performance.now() - start).toFixed(3); + console.log(`${tag} actual: ${this.formatValue(actual)}`); + if (!hasExpected) { + console.log(`${tag} expect: `); + console.log(`${tag} ℹ️ RUN (${ms}ms)\n`); + unchecked++; + continue; + } + console.log(`${tag} expect: ${this.formatValue(expected)}`); + if (judge(actual, expected)) { + console.log(`${tag} ✅ PASS (${ms}ms)\n`); + passed++; + } else { + console.log(`${tag} ❌ FAIL (${ms}ms)\n`); + failed++; + } + } catch (error) { + const ms = (performance.now() - start).toFixed(3); + console.log(`${tag} ⚠️ ERROR (${ms}ms)\n`); + console.error(error); + failed++; + } } - return values.map((value, idx) => { - const transform = transforms[idx]; - return transform ? transform(value) : value; - }); + this.printSummary(passed, failed, unchecked); } - private applyOutputTransform(value: unknown, transforms?: TransformSpec): unknown { - if (!transforms) return value; - if (typeof transforms === 'function') return transforms(value); - if (Array.isArray(value)) { - return value.map((item, idx) => { - const transform = transforms[idx]; - return transform ? transform(item) : item; - }); - } - const transform = transforms[0]; - return transform ? transform(value) : value; + /** + * Shared driver for `func` / `inPlace`: turn examples into rows whose actual + * value comes from `getActual`, and expose `.cases()` / `.auto()`. + */ + private runner(getActual: (input: unknown[]) => unknown) { + const exec = (examples: ReadonlyArray, judge?: Judge) => + this.run( + examples.map((example, index) => ({ + tag: this.tag('case', index), + input: example.input, + expected: example.expected, + hasExpected: example.hasExpected, + call: () => getActual(example.input) + })), + judge + ); + return { + cases: (cases: ReadonlyArray, options?: Options) => + exec( + cases.map( + (testCase): Example => ({ + input: this.normalizeInput(testCase.input), + expected: testCase.output, + hasExpected: true + }) + ), + options?.judge + ), + auto: (options?: Options) => { + const examples = this.parseFuncExamples(); + if (examples.length === 0) { + console.log('⚠️ No examples found in comment or LCPR blocks'); + return; + } + exec(examples, options?.judge); + } + }; } - private normalizeInput(input: unknown | ReadonlyArray): unknown[] { - return Array.isArray(input) ? [...input] : [input]; - } + // ── Private: comment parsing ───────────────────────────────────────── - private buildAutoExpected(expected: unknown, outputTransform?: TransformSpec): ExpectedOrTester { - if (!outputTransform) return expected; - return (actual: unknown) => this.isDeepEqual(this.applyOutputTransform(actual, outputTransform), expected); + private getFileContent(): string { + return readFileSync(process.argv[1], 'utf-8'); } - private runOne( - label: string, - index: string | number, - input: unknown, - expectedOrTester: ExpectedOrTester, - getActual: () => unknown - ): boolean { - const t = this.tag(label, index); - const start = performance.now(); - try { - console.log(`${t} input: ${this.formatValue(input)}`); - - const actual = getActual(); - const elapsed = (performance.now() - start).toFixed(3); + private cleanBlockCommentLine(line: string): string { + return line + .replace(/^\s*\/\*\s?/, '') + .replace(/\s*\*\/\s*$/, '') + .replace(/^\s*\*\s?/, '') + .trim(); + } - console.log(`${t} actual: ${this.formatValue(actual)}`); + private getCommentLines(): string[] { + return Array.from(this.getFileContent().matchAll(/\/\*[\s\S]*?\*\//g)).flatMap(match => + match[0].split(/\r\n|\n|\r/).map(line => this.cleanBlockCommentLine(line)) + ); + } - if (typeof expectedOrTester === 'function') { - const name = expectedOrTester.name || 'anonymous'; - console.log(`${t} expect: `); - assert.strictEqual(expectedOrTester(actual), true); + private stripCaseLine(line: string): string { + let stripped = line.trimStart(); + while (true) { + stripped = stripped.trimStart(); + if (stripped.startsWith('//')) { + stripped = stripped.slice(2); + } else if (stripped.startsWith('#')) { + stripped = stripped.slice(1); + } else if (stripped.startsWith('--')) { + stripped = stripped.slice(2); + } else if (stripped.startsWith('*')) { + stripped = stripped.slice(1); } else { - console.log(`${t} expect: ${this.formatValue(expectedOrTester)}`); - assert.deepStrictEqual(actual, expectedOrTester); + break; } + } + return stripped.replace(/\s+$/g, ''); + } - console.log(`${t} ✅ PASS (${elapsed}ms)\n`); - return true; - } catch (error) { - const elapsed = (performance.now() - start).toFixed(3); - if (error instanceof assert.AssertionError) { - console.log(`${t} ❌ FAIL (${elapsed}ms)\n`); + private skipRawSeparators(str: string, pos: number): number { + let cursor = pos; + while (cursor < str.length) { + const current = str[cursor]; + const next = str[cursor + 1]; + if (/[,\s]/.test(current)) { + cursor++; + } else if (current === '\\' && (next === 'n' || next === 'r')) { + cursor += 2; } else { - console.log(`${t} ⚠️ ERROR (${elapsed}ms)\n`); - console.error(error); + break; } - return false; } - } - - // ── Private: comment parsing ───────────────────────────────────────── - - private getCommentLines(): string[] { - const content = readFileSync(process.argv[1], 'utf-8'); - const match = content.match(/\/\*[\s\S]*?\*\//); - if (!match) return []; - return match[0].split('\n').map(line => line.replace(/^\s*\*\s?/, '').trim()); + return cursor; } private extractJsonToken(str: string, pos: number): [string, number] { @@ -188,10 +261,63 @@ class LCT { return [str.slice(pos, end), end]; } let end = pos; - while (end < str.length && !/[,\s]/.test(str[end])) end++; + while (end < str.length && !/[,\s]/.test(str[end]) && !(str[end] === '\\' && /[nr]/.test(str[end + 1] ?? ''))) + end++; return [str.slice(pos, end), end]; } + private parseRawInputValues(raw: string): unknown[] { + const values: unknown[] = []; + let cursor = 0; + while (cursor < raw.length) { + cursor = this.skipRawSeparators(raw, cursor); + if (cursor >= raw.length) break; + const [token, end] = this.extractJsonToken(raw, cursor); + if (end <= cursor || token.length === 0) throw new Error('Unable to parse raw testcase token'); + values.push(JSON.parse(token)); + cursor = end; + } + return values; + } + + private parseLcprCaseInputs(): unknown[][] { + const results: unknown[][] = []; + const lines = this.getFileContent().split(/\r\n|\n|\r/); + let isCollecting = false; + let collectedLines: string[] = []; + + for (const line of lines) { + if (/@lcpr\s+case\s*=\s*end/.test(line)) { + if (isCollecting) { + try { + const input = this.parseRawInputValues(collectedLines.join('\n')); + if (input.length > 0) results.push(input); + } catch { + /* skip unparseable LCPR cases */ + } + } + isCollecting = false; + collectedLines = []; + continue; + } + + if (isCollecting) { + collectedLines.push(this.stripCaseLine(line)); + } + + if (/@lcpr\s+case\s*=\s*start/.test(line)) { + isCollecting = true; + collectedLines = []; + } + } + + return results; + } + + private inputKey(input: ReadonlyArray): string { + return this.formatValue(input); + } + private parseInputValues(line: string): unknown[] { const trimmed = line.trim(); if (!trimmed.includes('=')) { @@ -212,19 +338,22 @@ class LCT { return values; } - private parseFuncExamples(): Array<[unknown[], unknown]> { + private parseFuncExamples(): Example[] { const lines = this.getCommentLines(); - const results: Array<[unknown[], unknown]> = []; - for (let i = 0; i < lines.length; i++) { - const inputMatch = lines[i].match(/^输入[::]\s*(.+)/); + const results: Example[] = []; + const seenInputs = new Set(); + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const inputMatch = lines[lineIndex].match(/^(?:输入|Input)[::]\s*(.+)/i); if (!inputMatch) continue; - for (let j = i + 1; j < lines.length; j++) { - const outputMatch = lines[j].match(/^输出[::]\s*(.+)/); + for (let outputLineIndex = lineIndex + 1; outputLineIndex < lines.length; outputLineIndex++) { + const outputMatch = lines[outputLineIndex].match(/^(?:输出|Output)[::]\s*(.+)/i); if (outputMatch) { try { const inputs = this.parseInputValues(inputMatch[1]); const output = JSON.parse(outputMatch[1]); - results.push([inputs, output]); + results.push({ input: inputs, expected: output, hasExpected: true }); + seenInputs.add(this.inputKey(inputs)); } catch { /* skip unparseable examples */ } @@ -232,27 +361,44 @@ class LCT { } } } + + for (const input of this.parseLcprCaseInputs()) { + const key = this.inputKey(input); + if (seenInputs.has(key)) continue; + results.push({ input, hasExpected: false }); + seenInputs.add(key); + } + return results; } - private parseClsExample(): [string[], unknown[][], unknown[]] | null { + private isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every(item => typeof item === 'string'); + } + + private isNestedInputArray(value: unknown): value is unknown[][] { + return Array.isArray(value) && value.every(Array.isArray); + } + + private parseClsExample(): ClassExample | null { const lines = this.getCommentLines(); - for (let i = 0; i < lines.length; i++) { - if (!/^输入(?:[::]\s*)?$/.test(lines[i])) continue; + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + if (!/^(?:输入|Input)(?:[::]\s*)?$/i.test(lines[lineIndex])) continue; const inputLines: string[] = []; - let j = i + 1; - for (; j < lines.length; j++) { - if (/^输出/.test(lines[j])) break; - if (lines[j]) inputLines.push(lines[j]); + let outputLineIndex = lineIndex + 1; + for (; outputLineIndex < lines.length; outputLineIndex++) { + if (/^(?:输出|Output)/i.test(lines[outputLineIndex])) break; + if (lines[outputLineIndex]) inputLines.push(lines[outputLineIndex]); } const outputLines: string[] = []; - const outputInline = lines[j]?.match(/^输出[::]\s*(.+)/); + const outputInline = lines[outputLineIndex]?.match(/^(?:输出|Output)[::]\s*(.+)/i); if (outputInline) { outputLines.push(outputInline[1]); } else { - for (let k = j + 1; k < lines.length; k++) { - if (!lines[k] || /^(解释|示例|提示)/.test(lines[k])) break; - outputLines.push(lines[k]); + for (let nextLineIndex = outputLineIndex + 1; nextLineIndex < lines.length; nextLineIndex++) { + if (!lines[nextLineIndex] || /^(解释|示例|提示|Explanation|Example|Constraints)/i.test(lines[nextLineIndex])) + break; + outputLines.push(lines[nextLineIndex]); } } if (inputLines.length >= 2 && outputLines.length >= 1) { @@ -260,19 +406,29 @@ class LCT { const methods: string[] = JSON.parse(inputLines[0]); const inputs: unknown[][] = JSON.parse(inputLines[1]); const expected: unknown[] = JSON.parse(outputLines[0]); - return [methods, inputs, expected]; + return { methods, inputs, expected, hasExpected: true }; } catch { /* skip */ } } } + + for (const input of this.parseLcprCaseInputs()) { + const [methods, inputs, expected] = input; + if (!this.isStringArray(methods) || !this.isNestedInputArray(inputs)) continue; + if (Array.isArray(expected) && expected.length === methods.length) { + return { methods, inputs, expected, hasExpected: true }; + } + return { methods, inputs, expected: Array(methods.length).fill(undefined), hasExpected: false }; + } + return null; } // ── Public ─────────────────────────────────────────────────────────── /** - * Test a pure function with return value. + * Test a pure function against its return value. * @example * LCT.func(twoSum).cases([ * { input: [[2,7,11,15], 9], output: [0,1] }, @@ -280,100 +436,21 @@ class LCT { * ]); */ public func(solution: F) { - return { - cases: (cases: ReadonlyArray, options?: AutoFuncOptions) => { - let passed = 0; - let failed = 0; - for (const [i, item] of cases.entries()) { - const input = this.applyTransforms(this.normalizeInput(item.input), options?.input); - const expected = this.buildAutoExpected(item.output, options?.output); - if (this.runOne('case', i, input, expected, () => Reflect.apply(solution, undefined, input))) { - passed++; - } else { - failed++; - } - } - this.printSummary(passed, failed); - }, - auto: (options?: AutoFuncOptions) => { - const examples = this.parseFuncExamples(); - if (examples.length === 0) { - console.log('⚠️ No examples found in comment block'); - return; - } - let passed = 0; - let failed = 0; - for (const [i, [input, expected]] of examples.entries()) { - const transformedInput = this.applyTransforms(input, options?.input); - const expectedOrTester = this.buildAutoExpected(expected, options?.output); - if ( - this.runOne('case', i, transformedInput, expectedOrTester, () => - Reflect.apply(solution, undefined, transformedInput) - ) - ) { - passed++; - } else { - failed++; - } - } - this.printSummary(passed, failed); - } - }; + return this.runner(input => Reflect.apply(solution, undefined, input)); } /** - * Test an in-place mutation function (returns void, modifies first argument). + * Test an in-place mutation function (returns void, mutates its first argument). * @example * LCT.inPlace(moveZeroes).cases([ * { input: [[0,1,0,3,12]], output: [1,3,12,0,0] }, * ]); */ public inPlace(solution: F) { - return { - cases: (cases: ReadonlyArray, options?: AutoFuncOptions) => { - let passed = 0; - let failed = 0; - for (const [i, item] of cases.entries()) { - const args = this.applyTransforms(this.normalizeInput(item.input), options?.input); - const expected = this.buildAutoExpected(item.output, options?.output); - if ( - this.runOne('case', i, args, expected, () => { - Reflect.apply(solution, undefined, args); - return args[0]; - }) - ) { - passed++; - } else { - failed++; - } - } - this.printSummary(passed, failed); - }, - auto: (options?: AutoFuncOptions) => { - const examples = this.parseFuncExamples(); - if (examples.length === 0) { - console.log('⚠️ No examples found in comment block'); - return; - } - let passed = 0; - let failed = 0; - for (const [i, [input, expected]] of examples.entries()) { - const args = this.applyTransforms(input, options?.input); - const expectedOrTester = this.buildAutoExpected(expected, options?.output); - if ( - this.runOne('case', i, args, expectedOrTester, () => { - Reflect.apply(solution, undefined, args); - return args[0]; - }) - ) { - passed++; - } else { - failed++; - } - } - this.printSummary(passed, failed); - } - }; + return this.runner(input => { + Reflect.apply(solution, undefined, input); + return input[0]; + }); } /** @@ -386,82 +463,47 @@ class LCT { * ); */ public cls(ctor: C) { + // A null expected value (e.g. a void method) is matched loosely against null/undefined. + const judge: Judge = (actual, expected) => (expected == null ? actual == null : this.deepEqual(actual, expected)); + const exec = ({ methods, inputs, expected, hasExpected }: ClassExample) => { + if (methods.length !== inputs.length || methods.length !== expected.length) { + throw new Error('LCT.cls: methods, inputs, expected must have the same length'); + } + const instance = Reflect.construct(ctor, inputs[0], ctor); + console.log(`${this.tag('new', methods[0])} args: ${this.formatValue(inputs[0])}`); + // Index 0 is the constructor; every later call becomes one row. + this.run( + methods.slice(1).map((method, k) => { + const args = inputs[k + 1]; + return { + tag: this.tag('call', method), + input: args, + expected: expected[k + 1], + hasExpected, + call: () => { + const fn = instance[method]; + if (typeof fn !== 'function') throw new Error(`method not found: ${method}`); + return Reflect.apply(fn, instance, args); + } + }; + }), + judge + ); + }; + return { calls: ( methods: ReadonlyArray, inputs: ReadonlyArray>, expected: ReadonlyArray - ) => { - if (methods.length !== inputs.length || methods.length !== expected.length) { - throw new Error('LCT.cls: methods, inputs, expected must have the same length'); - } - - const instance = Reflect.construct(ctor, inputs[0], ctor); - console.log(`${this.tag('new', methods[0])} args: ${this.formatValue(inputs[0])}`); - - let passed = 0; - let failed = 0; - - for (let i = 1; i < methods.length; i++) { - const method = methods[i]; - const args = inputs[i]; - const exp = expected[i]; - - const fn = instance[method]; - if (typeof fn !== 'function') { - console.log(`${this.tag('call', method)} ERROR method not found`); - failed++; - continue; - } - - if ( - this.runOne('call', method, args, exp == null ? (v: unknown) => v == null : exp, () => - Reflect.apply(fn, instance, args) - ) - ) { - passed++; - } else { - failed++; - } - } - this.printSummary(passed, failed); - }, - auto: (options?: AutoClassOptions) => { + ) => exec({ methods, inputs, expected, hasExpected: true }), + auto: () => { const example = this.parseClsExample(); if (!example) { - console.log('⚠️ No class example found in comment block'); + console.log('⚠️ No class example found in comment or LCPR blocks'); return; } - const [methods, inputs, expected] = example; - if (methods.length !== inputs.length || methods.length !== expected.length) { - throw new Error('LCT.cls: methods, inputs, expected must have the same length'); - } - const ctorArgs = this.applyTransforms(inputs[0], options?.ctorInput); - const instance = Reflect.construct(ctor, ctorArgs, ctor); - console.log(`${this.tag('new', methods[0])} args: ${this.formatValue(ctorArgs)}`); - let passed = 0; - let failed = 0; - for (let i = 1; i < methods.length; i++) { - const method = methods[i]; - const args = this.applyTransforms(inputs[i], options?.callInput?.[i]); - const exp = expected[i]; - const fn = instance[method]; - if (typeof fn !== 'function') { - console.log(`${this.tag('call', method)} ERROR method not found`); - failed++; - continue; - } - const expectedOrTester = - exp == null && !options?.callOutput?.[i] - ? (v: unknown) => v == null - : this.buildAutoExpected(exp, options?.callOutput?.[i]); - if (this.runOne('call', method, args, expectedOrTester, () => Reflect.apply(fn, instance, args))) { - passed++; - } else { - failed++; - } - } - this.printSummary(passed, failed); + exec(example); } }; } diff --git a/src/utils/list.ts b/src/utils/list.ts index 369e2d7c..fa29e704 100644 --- a/src/utils/list.ts +++ b/src/utils/list.ts @@ -74,6 +74,76 @@ function hasCycle(head: ListNode | null): boolean { return false; } +export class DoublyListNode { + val: T; + prev: DoublyListNode | null; + next: DoublyListNode | null; + constructor(val?: T, prev?: DoublyListNode | null, next?: DoublyListNode | null) { + this.val = val === undefined ? (0 as T) : val; + this.prev = prev === undefined ? null : prev; + this.next = next === undefined ? null : next; + } +} + +function doublySerialize(head: DoublyListNode | null): Array { + const res: Array = []; + let curr = head; + while (curr !== null) { + res.push(curr.val); + curr = curr.next; + } + return res; +} + +function doublyDeserialize(data: string | Array): DoublyListNode | null { + try { + if (typeof data === 'string') { + data = JSON.parse(data); + } + } catch (e) { + throw Error(e instanceof Error ? e.message : String(e)); + } + if (!(data instanceof Array)) throw Error('cannot parse array'); + if (data.length === 0) return null; + + const head = new DoublyListNode(data[0]); + let curr = head; + + data.slice(1).forEach(ele => { + const node = new DoublyListNode(ele); + node.prev = curr; + curr.next = node; + curr = curr.next; + }); + + return head; +} + +function doublyGetNode(head: DoublyListNode | null, index: number): DoublyListNode | null { + let curr = head; + while (curr && index > 0) { + curr = curr.next; + index -= 1; + } + return curr; +} + +class DoublyListUtils { + public serialize(head: DoublyListNode | null): Array { + return doublySerialize(head); + } + + public deserialize(data: string | Array): DoublyListNode | null { + return doublyDeserialize(data); + } + + public getNode(head: DoublyListNode | null, index: number): DoublyListNode | null { + return doublyGetNode(head, index); + } +} + +const DoublyList = new DoublyListUtils(); + class ListUtils { public serialize(root: ListNode | null): Array { return serialize(root); @@ -97,7 +167,11 @@ const List = new ListUtils(); const listGlobal = globalThis as typeof globalThis & { ListNode?: typeof ListNode; List?: typeof List; + DoublyListNode?: typeof DoublyListNode; + DoublyList?: typeof DoublyList; }; listGlobal.ListNode = ListNode; listGlobal.List = List; +listGlobal.DoublyListNode = DoublyListNode; +listGlobal.DoublyList = DoublyList; diff --git a/training-system/GETTING-STARTED.md b/training-system/GETTING-STARTED.md new file mode 100644 index 00000000..97d95a04 --- /dev/null +++ b/training-system/GETTING-STARTED.md @@ -0,0 +1,60 @@ +# 🎯 LeetCode框架训练系统 + +> 从"知道框架"到"熟练运用"的完整解决方案 + +## 📁 文件结构 + +``` +training-system/ +├── README.md # 主训练系统 (自适应练习) +├── templates/ +│ └── algorithm-templates.md # 算法代码模板库 +├── exercises/ +│ └── framework-recognition-training.md # 框架识别练习 +└── guides/ + └── deliberate-practice-plan.md # 原版刻意练习计划 +``` + +## 🚀 快速开始 + +### 新用户 + +1. 阅读 `README.md` 了解完整训练体系 +2. 完成30秒自我评估 +3. 根据建议开始对应层级的训练 + +### 老用户 + +直接在任何会话中发送训练指令: + +- `"开始第1层训练"` - 框架识别 +- `"开始第2层训练"` - 模板套用 +- `"闪电模式"` - 快速热身 +- `"综合测试"` - 水平评估 + +## 🎯 训练体系概览 + +| 层级 | 目标 | 时长 | 适合人群 | +| ----- | ------------- | ------ | -------- | +| 第1层 | 30秒识别框架 | 15分钟 | 基础入门 | +| 第2层 | 5分钟实现代码 | 20分钟 | 识别熟练 | +| 第3层 | 识别变种题型 | 25分钟 | 基础扎实 | +| 第4层 | 多框架组合 | 35分钟 | 单一熟练 | +| 第5层 | 方案优化选择 | 30分钟 | 技术过关 | +| 第6层 | 面试场景模拟 | 45分钟 | 全面提升 | + +## 💡 使用建议 + +1. **持续练习**:每天至少完成一个训练层级 +2. **记录进度**:使用提供的记录模板追踪成果 +3. **循序渐进**:感到困难时回退一层巩固 +4. **灵活调整**:根据时间选择不同训练模式 + +## 🔗 相关资源 + +- [主项目仓库](../../README.md) - 300+道题解集合 +- [专题文档站](https://realduang.github.io/leetcode-in-javascript) - 系统性框架总结 + +--- + +**开始你的框架训练之旅!** 🚀 diff --git a/training-system/README.md b/training-system/README.md new file mode 100644 index 00000000..885e3914 --- /dev/null +++ b/training-system/README.md @@ -0,0 +1,229 @@ +# LeetCode框架训练营 - 自适应练习系统 + +> 🎯 从"知道框架"到"熟练运用"的6层递进训练体系 + +## 🚀 快速开始 (新会话必读) + +### 30秒自我评估 + +请诚实评估自己的当前水平: + +| 能力项目 | 能做到 | 基本能做到 | 做不到 | 评分 | +| --------------------------- | ------ | ---------- | ------ | -------- | +| 看到题目30秒内识别框架 | 3分 | 2分 | 1分 | \_\_\_分 | +| 5分钟内从模板写出可运行代码 | 3分 | 2分 | 1分 | \_\_\_分 | +| 识别混合型/变种框架题目 | 3分 | 2分 | 1分 | \_\_\_分 | +| 多框架组合解决复杂问题 | 3分 | 2分 | 1分 | \_\_\_分 | +| 权衡多种解法并选择最优 | 3分 | 2分 | 1分 | \_\_\_分 | +| 面试场景下完整表达思路 | 3分 | 2分 | 1分 | \_\_\_分 | + +**总分: \_\_\_/18** + +### 🎯 训练阶段建议 + +| 总分 | 建议阶段 | 训练重点 | +| ------- | -------- | ------------------- | +| 6-9分 | 第1-2层 | 识别训练 + 模板套用 | +| 10-12分 | 第3-4层 | 变种识别 + 框架组合 | +| 13-15分 | 第5-6层 | 优化选择 + 面试模拟 | +| 16-18分 | 维持巩固 | 定期复习 + 新题挑战 | + +--- + +## 📚 6层训练体系总览 + +### 🔍 第1层:框架快速识别 + +**目标**: 30秒内准确识别题型 (准确率90%+) +**训练内容**: 关键词识别 + 大量题目练习 +**毕业标准**: 连续20题识别准确率≥90% + +### ⚡ 第2层:模板快速套用 + +**目标**: 5分钟内从模板写出基础解法 +**训练内容**: 熟记代码模板 + 填充逻辑练习 +**毕业标准**: 各主流框架都能5分钟内实现 + +### 🔧 第3层:变种框架识别 + +**目标**: 识别非标准的框架变形 +**训练内容**: 变种题型训练 + 本质分析 +**毕业标准**: 混合型题目识别准确率≥80% + +### 🎭 第4层:多框架组合 + +**目标**: 复杂问题的框架组合应用 +**训练内容**: 组合题型 + 分步解决思路 +**毕业标准**: 能独立设计多步骤解决方案 + +### 💎 第5层:方案优化选择 + +**目标**: 权衡多种解法的优劣 +**训练内容**: 复杂度分析 + 场景选择 +**毕业标准**: 能分析并选择最适合的方案 + +### 🎪 第6层:面试场景模拟 + +**目标**: 完整的面试表现能力 +**训练内容**: 限时模拟 + 思路表达 +**毕业标准**: 45分钟完整解决中等难度题目 + +--- + +## 🎯 今日训练选择器 + +### 🟢 第1层训练:框架快速识别 + +**今日目标**: 识别20道题,准确率≥85% +**训练时长**: 15-20分钟 +**适合人群**: 总分6-9分,或想热身的任何阶段 + +[开始第1层训练] → 发送:"开始第1层训练" + +--- + +### 🔵 第2层训练:模板快速套用 + +**今日目标**: 完成3道不同框架的模板套用 +**训练时长**: 20-25分钟 +**适合人群**: 识别准确率≥80%,但编码不熟练 + +[开始第2层训练] → 发送:"开始第2层训练" + +--- + +### 🟡 第3层训练:变种框架识别 + +**今日目标**: 识别5道变种题型,分析本质 +**训练时长**: 25-30分钟 +**适合人群**: 基础框架熟练,遇到变种题会卡壳 + +[开始第3层训练] → 发送:"开始第3层训练" + +--- + +### 🟠 第4层训练:多框架组合 + +**今日目标**: 解决2道需要组合框架的复杂问题 +**训练时长**: 30-40分钟 +**适合人群**: 单一框架熟练,复杂题目无从下手 + +[开始第4层训练] → 发送:"开始第4层训练" + +--- + +### 🔴 第5层训练:方案优化选择 + +**今日目标**: 分析3道题的多种解法优劣 +**训练时长**: 25-35分钟 +**适合人群**: 能解题但不知道哪个方案最优 + +[开始第5层训练] → 发送:"开始第5层训练" + +--- + +### ⚫ 第6层训练:面试场景模拟 + +**今日目标**: 完整模拟1道中等难度题目 +**训练时长**: 45分钟 +**适合人群**: 技术过关,需要面试表达训练 + +[开始第6层训练] → 发送:"开始第6层训练" + +--- + +## 📊 训练记录模板 + +### 今日训练记录 + +```markdown +日期: \_**\_年**月**日 +选择训练层: 第**层 +预计时长: **分钟 +实际用时: **分钟 + +### 训练结果 + +- 目标完成情况: **_/_** +- 准确率/熟练度: \_\_\_% +- 主要收获: **\_\_\_\_** +- 发现的问题: **\_\_\_\_** + +### 明日计划 + +- 继续训练层: 第\_\_层 +- 重点改进: **\_\_\_\_** +- 预期目标: **\_\_\_\_** +``` + +--- + +## 🔄 标准训练流程 (每个会话通用) + +### Step 1: 快速评估 (1分钟) + +"我现在是什么水平?应该训练哪一层?" + +### Step 2: 选择训练 (1分钟) + +"开始第X层训练" + +### Step 3: 专项练习 (15-45分钟) + +跟随系统引导完成训练 + +### Step 4: 结果记录 (2分钟) + +记录成果,规划明日训练 + +### Step 5: 下次会话 + +带着训练记录开始新会话 + +--- + +## 🎁 特殊训练模式 + +### 🔥 闪电模式 (10分钟) + +快速热身,适合时间紧张 +发送:"闪电模式" + +### 🎯 专项突破模式 (30分钟) + +针对特定框架深度练习 +发送:"专项突破 + 框架名" (如"专项突破 滑动窗口") + +### 🏆 综合测试模式 (60分钟) + +全面评估当前水平 +发送:"综合测试" + +--- + +## 💡 使用建议 + +1. **每次新会话都先做30秒自我评估** +2. **根据建议选择合适的训练层** +3. **每天至少完成一个完整训练** +4. **记录进度,持续改进** +5. **感到困难时退回上一层巩固** +6. **感到轻松时挑战下一层** + +--- + +## 🚀 开始你的第一次训练 + +现在,请告诉我: + +1. **你的自我评估总分是多少?** +2. **你想从第几层开始训练?** + +或者直接发送训练指令: + +- "开始第1层训练" +- "开始第2层训练" +- "闪电模式" +- "综合测试" + +让我们开始建立你的算法框架肌肉记忆!🎯 diff --git a/training-system/exercises/framework-recognition-training.md b/training-system/exercises/framework-recognition-training.md new file mode 100644 index 00000000..305678d5 --- /dev/null +++ b/training-system/exercises/framework-recognition-training.md @@ -0,0 +1,150 @@ +# 框架识别训练 - 30秒挑战 + +> 目标:看到题目30秒内准确识别应该使用哪个框架 + +## 🎯 训练规则 + +1. **只看题目描述**,不看代码 +2. **计时30秒**,选择框架类型 +3. **记录答案**,然后查看正确答案 +4. **分析错误**,找出识别盲点 +5. **重复练习**,直到达到90%准确率 + +## 📝 练习题目 + +### 第一组:基础识别 + +#### Q1 + +给定一个字符串 s 和一个字符串 t ,要求在 s 中找出包含 t 所有字母的最小子串。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 滑动窗口 +**关键词:** 最小子串、包含所有字母 + +--- + +#### Q2 + +给定一个排序数组和一个目标值,在数组中找到目标值的开始位置和结束位置。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 二分搜索 +**关键词:** 排序数组、查找位置 + +--- + +#### Q3 + +给定一个数组 nums,返回该数组所有可能的子集(幂集)。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 回溯 +**关键词:** 所有可能、子集、幂集 + +--- + +#### Q4 + +给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** DFS/BFS +**关键词:** 岛屿、连通区域、二维网格 + +--- + +#### Q5 + +给你一个整数数组 coins 表示不同面额的硬币,以及一个整数 amount 表示总金额,计算出组成总金额所需的最少的硬币个数。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 动态规划 +**关键词:** 最少、组成、硬币找零 + +--- + +### 第二组:进阶识别 + +#### Q6 + +给定字符串 s 和 p,找到 s 中所有 p 的异位词的子串,返回这些子串的起始索引。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 滑动窗口 +**关键词:** 子串、异位词、固定长度 + +--- + +#### Q7 + +峰值元素是指其值大于左右相邻值的元素。给你一个整数数组 nums,找到峰值元素并返回其索引。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 二分搜索 +**关键词:** 峰值、索引(虽然数组无序,但可以用二分的思想) + +--- + +#### Q8 + +给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 滑动窗口 +**关键词:** 包含、排列、固定长度窗口 + +--- + +#### Q9 + +给定一个 n × n 的二维矩阵表示一个图像,将图像顺时针旋转 90 度。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 数组操作/数学 +**关键词:** 矩阵、旋转、原地操作 + +--- + +#### Q10 + +你是一个专业的小偷,计划偷窃沿街的房屋,不能偷窃相邻的房屋,求能偷窃到的最高金额。 + +**你的答案:** **\*\***\_**\*\*** +**正确答案:** 动态规划 +**关键词:** 最优、不相邻、选择 + +--- + +## 🔍 关键词识别表 + +| 框架类型 | 高频关键词 | 题目特征 | +| ------------ | -------------------------------------------- | ------------------------------------ | +| **滑动窗口** | 子串、子数组、最长、最短、包含、覆盖、异位词 | 连续序列问题,通常要求O(n)时间复杂度 | +| **二分搜索** | 排序数组、查找、第一个、最后一个、峰值、旋转 | 有序性质,要求O(logn)时间复杂度 | +| **回溯** | 所有可能、排列、组合、子集、N皇后、数独 | 需要枚举所有解的组合问题 | +| **DFS/BFS** | 岛屿、连通、感染、最短路径、层级遍历 | 图或网格遍历问题 | +| **动态规划** | 最优、最大、最小、方案数、硬币、背包、爬楼梯 | 最优化问题,有子问题重叠 | +| **双指针** | 两数之和、去重、回文、快慢指针 | 数组或链表的线性扫描问题 | + +## 📊 练习记录 + +第一轮练习:\_**\_/10 正确 +第二轮练习:\_\_**/10 正确 +第三轮练习:\_\_\_\_/10 正确 + +**薄弱环节分析:** + +- [ ] 滑动窗口 vs 双指针 混淆 +- [ ] 二分搜索的适用场景判断 +- [ ] 回溯 vs DFS 的区别 +- [ ] DP问题的识别 +- [ ] 其他:\***\*\_\_\_\*\*** + +## 💡 提高技巧 + +1. **抓住核心关键词**:每种框架都有其标志性词汇 +2. **分析数据结构**:数组、字符串、树、图等暗示不同方法 +3. **理解问题类型**:查找、遍历、优化、计数等 +4. **注意复杂度要求**:O(n)暗示双指针/滑动窗口,O(logn)暗示二分 +5. **建立条件反射**:大量练习形成直觉反应 diff --git a/training-system/guides/ai-trainer-rules.md b/training-system/guides/ai-trainer-rules.md new file mode 100644 index 00000000..b0fc6523 --- /dev/null +++ b/training-system/guides/ai-trainer-rules.md @@ -0,0 +1,130 @@ +# AI训练师出题规则与经验总结 + +> 记录Claude在训练过程中的出题规则、改进经验和最佳实践 + +## 📱 手机模式出题规则 + +### ✅ DO - 正确做法 + +1. **题目格式** + - 纯选择题:只需选A/B/C/D,无需敲代码 + - 概念导向:专注框架识别、策略选择、复杂度判断 + - 简洁回答:理由可用关键词,不需长篇大论 + - Claude直接出题:无需用户在手机上打开md文件 + +2. **出题内容范围** + - **严格限制在已有专题范围内**(见docs/docs/topic/): + - backtrack, binary-search, breadth-first-search, depth-first-search + - dynamic-programming-\*, graph, greedy, monotonic-stack, partial-sum + - recursive, sort, tree, two-pointers, slide-window + - 不得出现超纲算法:KMP、Rabin-Karp、并查集、平衡BST、字符串匹配等 + +3. **题目设计原则** + - 完整显示题目内容,无需额外说明 + - 避免在题目开头和选项后出现明显提示 + - 一题一题逐个进行,等待用户回答后再出下一题 + +### ❌ DON'T - 避免问题 + +1. **重复出题问题** + - **核心原则**:同一知识点至少间隔5次练习再重复 + - **原题复现**:相同或类似题目场景需间隔数周(不是数天) + - **正确巩固方式**:同知识点换全新场景,而非相似场景 + +2. **题目提示问题** + - 不要在题目开头出现"背景:"、"场景:"等多余描述 + - 不要在选项后添加明显提示或解释 + - 让用户纯粹基于题目内容做判断 + +3. **超纲内容** + - 严禁出现项目专题目录外的算法 + - 不要出现项目尚未涉及的数据结构或算法 + +## 📊 弱项追踪与题目选择 + +### 弱项状态管理 + +- **已修复**:连续正确3次以上,可减少出题频率 +- **改善中**:连续正确1-2次,保持适度练习 +- **新发现**:立即重点训练,但换不同场景考察 + +### 出题优先级 + +1. **新发现弱项**:优先考察,但避免原题 +2. **改善中弱项**:适度巩固,换场景验证 +3. **未考察项目**:适时引入,填补空白 +4. **已修复弱项**:定期复查,间隔较长 + +## 🔄 题目更新策略 + +### 知识点巩固原则 + +- ✅ **正确**:同知识点(如"堆vs平衡BST"),换场景(股票追踪 → 排行榜系统 → 任务调度) +- ❌ **错误**:同知识点,相似场景("插入/删除任意/查最大" → "插入/删除指定/查最小") + +### 原题复现时机 + +- **短期**(1-2周):绝不复现原题 +- **中期**(2-4周):可以换个角度考察同一题 +- **长期**(1个月+):可以复现原题验证记忆 + +## 💡 出题质量提升 + +### 题目设计最佳实践 + +1. **场景多样化**:系统设计、算法优化、数据结构选型、复杂度分析 +2. **考察角度多元**:不仅考算法本身,也考适用场景、性能权衡 +3. **难度梯度合理**:根据用户水平动态调整 +4. **实战导向**:贴近真实开发和面试场景 + +### 避免的出题陷阱 + +1. **提示过于明显**:让选项描述过于直白 +2. **场景过于相似**:换汤不换药的伪装 +3. **超纲内容引入**:偏离项目既定范围 +4. **重复频率过高**:同一周期内多次考察 + +## 📈 训练效果评估 + +### 成功指标 + +- 用户能快速识别框架和策略 +- 弱项状态持续改善 +- 新知识点掌握扎实 +- 长期记忆效果良好 + +### 调整信号 + +- 同类错误反复出现 → 加强基础概念 +- 某知识点长期无进展 → 换教学角度 +- 用户反馈题目质量 → 立即改进记录 + +--- + +## 📝 改进历史记录 + +### 2026-04-24 重要改进 + +**问题发现**: + +- 手机模式要求用户打开md文件,但用户在远程环境无法操作 +- 出题重复频率过高,同一知识点相邻天数多次出现 +- 题目中包含明显提示,降低了考察效果 +- 出现超纲算法(KMP、并查集等),偏离项目范围 + +**解决方案**: + +- 改为Claude直接出题,无需文件操作 +- 制定重复间隔规则:同知识点5次+间隔,原题数周间隔 +- 优化题目格式:完整题目+纯选项,无额外提示 +- 严格限制出题范围在已有专题内 + +**用户反馈**: + +> "不要总是重复出题。就算巩固知识点也要在之前5次练习以后穿插" +> "不要用原题来巩固,不然原题还没忘呢出题的意义就不大了" +> "再次提醒你注意skill里的限制要求,不要出我专题topic里讲的算法以外的题型" + +--- + +_此文件会持续更新,记录每次训练中的经验总结和改进规则_ diff --git a/training-system/guides/deliberate-practice-plan.md b/training-system/guides/deliberate-practice-plan.md new file mode 100644 index 00000000..d786befc --- /dev/null +++ b/training-system/guides/deliberate-practice-plan.md @@ -0,0 +1,171 @@ +# 框架实践突破计划 + +> 从"知道框架"到"熟练运用"的7天突破法 + +## 🎯 问题诊断 + +你现在的状态: + +- ✅ 理论框架完整 +- ✅ 题目解法正确 +- ❌ 无法快速识别题型 +- ❌ 框架套用不熟练 +- ❌ 缺少肌肉记忆 + +## 📅 7天突破计划 + +### Day 1-2: 框架识别训练 + +**目标**:30秒内识别题型 + +**训练方法**: + +1. 从每个专题选择3道最经典的题目 +2. 只看题目描述,不看代码 +3. 计时30秒,判断用什么框架 +4. 记录错误,找出识别盲点 + +**训练材料**: + +- 滑动窗口:76, 3, 438 +- 二分搜索:704, 34, 153 +- 回溯:78, 46, 39 +- DFS:200, 695, 130 +- DP:70, 322, 300 + +### Day 3-4: 框架套用训练 + +**目标**:5分钟内写出框架代码 + +**训练方法**: + +1. 先写框架骨架,再填充细节 +2. 不求最优,先求正确 +3. 每个框架练习10-15道题 + +### Day 5-6: 变种识别训练 + +**目标**:处理框架变种 + +**训练方法**: + +1. 混合不同框架的题目 +2. 练习多框架组合问题 +3. 处理边界情况 + +### Day 7: 综合测试 + +**目标**:模拟面试环境 + +## 🔧 框架速记卡片 + +### 滑动窗口 + +```javascript +// 固定长度滑动窗口 +for (let i = 0; i < nums.length - k + 1; i++) { + // 处理窗口 [i, i+k-1] +} + +// 动态长度滑动窗口 +let left = 0, + right = 0; +while (right < s.length) { + // 扩展右边界 + right++; + // 判断是否需要收缩左边界 + while (needShrink()) { + left++; + } +} +``` + +### 二分搜索 + +```javascript +// 标准二分 +let left = 0, + right = nums.length - 1; +while (left <= right) { + let mid = left + Math.floor((right - left) / 2); + if (nums[mid] == target) return mid; + else if (nums[mid] < target) left = mid + 1; + else right = mid - 1; +} + +// 搜索边界 +let left = 0, + right = nums.length; +while (left < right) { + let mid = left + Math.floor((right - left) / 2); + if (nums[mid] >= target) right = mid; + else left = mid + 1; +} +``` + +### 回溯 + +```javascript +function backtrack(path, choices) { + if (满足结束条件) { + result.push([...path]); + return; + } + + for (let choice of choices) { + // 做选择 + path.push(choice); + // 递归 + backtrack(path, newChoices); + // 撤销选择 + path.pop(); + } +} +``` + +### DFS + +```javascript +function dfs(grid, i, j) { + if (越界 || 不符合条件) return; + + // 标记已访问 + grid[i][j] = 'visited'; + + // 遍历四个方向 + dfs(grid, i + 1, j); + dfs(grid, i - 1, j); + dfs(grid, i, j + 1); + dfs(grid, i, j - 1); +} +``` + +## 📊 练习追踪表 + +| 框架 | 识别准确率 | 套用熟练度 | 薄弱环节 | 改进计划 | +| -------- | ---------- | ---------- | -------- | -------- | +| 滑动窗口 | \_/10 | \_/10 | | | +| 二分搜索 | \_/10 | \_/10 | | | +| 回溯 | \_/10 | \_/10 | | | +| DFS | \_/10 | \_/10 | | | +| BFS | \_/10 | \_/10 | | | +| DP | \_/10 | \_/10 | | | + +## ⚡ 实践技巧 + +1. **建立条件反射**:看到关键词立即想到框架 +2. **先骨架后细节**:不要一开始就追求完美 +3. **大量重复**:同一类型多练几道直到熟练 +4. **限时训练**:给自己压力,提高反应速度 +5. **总结优化**:每天练完都要总结改进 + +## 🎓 毕业标准 + +达到以下水平说明你突破了瓶颈: + +- [ ] 30秒内准确识别题型 (90%以上) +- [ ] 5分钟内写出框架代码 +- [ ] 能处理框架的常见变种 +- [ ] 面对新题有清晰的解题思路 + +记住:**框架掌握 = 识别能力 + 套用熟练度** diff --git a/training-system/templates/algorithm-templates.md b/training-system/templates/algorithm-templates.md new file mode 100644 index 00000000..a56b4f7a --- /dev/null +++ b/training-system/templates/algorithm-templates.md @@ -0,0 +1,422 @@ +# 算法框架快速模板 + +> 拿来即用的代码框架,先套模板再优化细节 + +## 🎯 使用说明 + +1. **先识别题型**(参考框架识别训练) +2. **选择对应模板**(直接复制粘贴) +3. **填充业务逻辑**(根据题目要求修改) +4. **调试优化**(处理边界情况) + +--- + +## 🪟 滑动窗口模板 + +### 固定长度滑动窗口 + +```javascript +function fixedSlidingWindow(nums, k) { + let result = []; + + // 初始化第一个窗口 + for (let i = 0; i < k; i++) { + // 处理窗口元素 + } + // 记录第一个窗口结果 + + // 滑动窗口 + for (let i = k; i < nums.length; i++) { + // 移除窗口左边元素的影响 + // 添加窗口右边新元素的影响 + // 更新结果 + } + + return result; +} +``` + +### 动态长度滑动窗口 + +```javascript +function dynamicSlidingWindow(s, condition) { + let left = 0, + right = 0; + let window = new Map(); // 或者用对象 {} + let result = initResult; + + while (right < s.length) { + // 扩展右边界 + let rightChar = s[right]; + right++; + window.set(rightChar, (window.get(rightChar) || 0) + 1); + + // 判断是否需要收缩左边界 + while (满足收缩条件) { + // 更新结果(在收缩前) + updateResult(); + + // 收缩左边界 + let leftChar = s[left]; + left++; + window.set(leftChar, window.get(leftChar) - 1); + if (window.get(leftChar) === 0) { + window.delete(leftChar); + } + } + } + + return result; +} +``` + +--- + +## 🎯 二分搜索模板 + +### 标准二分查找 + +```javascript +function binarySearch(nums, target) { + let left = 0, + right = nums.length - 1; + + while (left <= right) { + let mid = left + Math.floor((right - left) / 2); + + if (nums[mid] === target) { + return mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return -1; // 未找到 +} +``` + +### 搜索左边界 + +```javascript +function leftBound(nums, target) { + let left = 0, + right = nums.length; + + while (left < right) { + let mid = left + Math.floor((right - left) / 2); + + if (nums[mid] >= target) { + right = mid; + } else { + left = mid + 1; + } + } + + return left; // 或者检查越界后返回-1 +} +``` + +### 搜索右边界 + +```javascript +function rightBound(nums, target) { + let left = 0, + right = nums.length; + + while (left < right) { + let mid = left + Math.floor((right - left) / 2); + + if (nums[mid] > target) { + right = mid; + } else { + left = mid + 1; + } + } + + return left - 1; // 或者检查越界后返回-1 +} +``` + +--- + +## 🔄 回溯模板 + +### 排列问题 + +```javascript +function permute(nums) { + const result = []; + const used = new Array(nums.length).fill(false); + + function backtrack(path) { + // 结束条件 + if (path.length === nums.length) { + result.push([...path]); + return; + } + + // 选择列表 + for (let i = 0; i < nums.length; i++) { + if (used[i]) continue; // 剪枝 + + // 做选择 + path.push(nums[i]); + used[i] = true; + + // 递归 + backtrack(path); + + // 撤销选择 + path.pop(); + used[i] = false; + } + } + + backtrack([]); + return result; +} +``` + +### 组合问题 + +```javascript +function combine(n, k) { + const result = []; + + function backtrack(path, start) { + // 结束条件 + if (path.length === k) { + result.push([...path]); + return; + } + + // 选择列表 + for (let i = start; i <= n; i++) { + // 做选择 + path.push(i); + + // 递归 + backtrack(path, i + 1); + + // 撤销选择 + path.pop(); + } + } + + backtrack([], 1); + return result; +} +``` + +### 子集问题 + +```javascript +function subsets(nums) { + const result = []; + + function backtrack(path, start) { + // 前序遍历位置,每个节点都是一个子集 + result.push([...path]); + + // 选择列表 + for (let i = start; i < nums.length; i++) { + // 做选择 + path.push(nums[i]); + + // 递归 + backtrack(path, i + 1); + + // 撤销选择 + path.pop(); + } + } + + backtrack([], 0); + return result; +} +``` + +--- + +## 🌊 DFS模板 + +### 网格DFS + +```javascript +function dfs(grid, i, j) { + // 越界检查 + if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) { + return; + } + + // 条件检查 + if (grid[i][j] !== '期望值') { + return; + } + + // 标记已访问 + grid[i][j] = '已访问标记'; + + // 递归遍历四个方向 + dfs(grid, i + 1, j); // 下 + dfs(grid, i - 1, j); // 上 + dfs(grid, i, j + 1); // 右 + dfs(grid, i, j - 1); // 左 +} + +// 主函数 +function solve(grid) { + let result = 0; + + for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[0].length; j++) { + if (grid[i][j] === '目标值') { + dfs(grid, i, j); + result++; // 或其他处理 + } + } + } + + return result; +} +``` + +--- + +## 🚌 BFS模板 + +### 层序遍历BFS + +```javascript +function bfs(start) { + const queue = [start]; + const visited = new Set(); + visited.add(start); + + while (queue.length > 0) { + const size = queue.length; + + // 处理当前层的所有节点 + for (let i = 0; i < size; i++) { + const current = queue.shift(); + + // 处理当前节点 + + // 将下一层节点加入队列 + for (let next of getNextNodes(current)) { + if (!visited.has(next)) { + visited.add(next); + queue.push(next); + } + } + } + } +} +``` + +--- + +## 💎 动态规划模板 + +### 一维DP + +```javascript +function dp1D(n) { + // 定义dp数组 + const dp = new Array(n + 1); + + // 初始化base case + dp[0] = baseValue; + + // 状态转移 + for (let i = 1; i <= n; i++) { + dp[i] = 状态转移方程; + } + + return dp[n]; +} +``` + +### 二维DP + +```javascript +function dp2D(m, n) { + // 定义dp数组 + const dp = Array(m + 1) + .fill() + .map(() => Array(n + 1).fill(0)); + + // 初始化base case + for (let i = 0; i <= m; i++) dp[i][0] = baseValue; + for (let j = 0; j <= n; j++) dp[0][j] = baseValue; + + // 状态转移 + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + dp[i][j] = 状态转移方程; + } + } + + return dp[m][n]; +} +``` + +--- + +## 🎯 双指针模板 + +### 对撞指针 + +```javascript +function twoPointers(arr) { + let left = 0, + right = arr.length - 1; + + while (left < right) { + if (满足条件) { + // 处理结果 + left++; + right--; + } else if (需要移动左指针) { + left++; + } else { + right--; + } + } +} +``` + +### 快慢指针 + +```javascript +function fastSlowPointers(head) { + let slow = head, + fast = head; + + while (fast && fast.next) { + slow = slow.next; + fast = fast.next.next; + + // 检查条件(如环检测) + if (slow === fast) { + return true; + } + } + + return false; +} +``` + +--- + +## 🚀 使用技巧 + +1. **优先套模板**:不要从零开始写,直接改模板 +2. **关键位置标注**:把需要修改的地方用注释标出 +3. **边界条件检查**:模板处理一般情况,别忘了边界 +4. **变量命名规范**:使用有意义的变量名 +5. **逐步调试**:先保证框架正确,再优化细节 + +记住:**熟练使用模板是第一步,理解原理是第二步!**