diff --git a/DSA/README.md b/DSA/README.md index 8ee22ab..5e5365a 100644 --- a/DSA/README.md +++ b/DSA/README.md @@ -1,14 +1,5 @@ # 学习数据结构与算法 -从 230903 开始,做 POTD 时,无法在 40 分钟内 AC 的一律 ❌,禁止任何留恋。 - -文件夹说明: - -- lecture 内容来自 [算法老师左神的 B 站视频](https://www.bilibili.com/video/BV13g41157hK/)。 -- POTD 内容来自 [geeksforgeeks 的 Problem Of The Day](https://practice.geeksforgeeks.org/problem-of-the-day)。确保每天更新 -- [AcWing](https://www.acwing.com/activity/1/competition/) 暂时只记录周赛题目。 -- [LeeCode](https://leetcode.cn/problemset/all/) 暂不确定。 - TODO: - [ ] 整理仓库。仓库现已变得臃肿了,笔记中的某些内容已经变得过于啰嗦。 @@ -17,189 +8,7 @@ TODO: 学习技巧: -- 写算法时,永远记得考虑边界问题 -- 人的优势在于使用工具。不要认为只靠想就做出一道算法题有多厉害,真正厉害的应该是能画出来,理清逻辑,并且能给别人讲明白。所以要多画图! - -## 基础班内容 - -### lecture01 - -- 时间复杂度 -- 选择排序、冒泡排序 -- 异或运算 - - 交换两个数 - - 求一个出现奇数次的数字 - - 求两个出现奇数次的数字 -- 二分法 - - 在有序数组中查找某数的位置 - - 在有序数组中查找最左侧/最右侧的位置 - - 求取局部最小值/最大值 -- 对数器 -- 插入排序 - -### lecture02 - -- 递归 - - master 公式计算时间复杂度 -- 归并排序 - - 小和问题 -- 快速排序 - - 二项划分 - - 三项划分 - -### lecture03 - -- 数组实现完全二叉树 -- 堆:大根堆和小根堆(优先级队列) - - heapify - - heapInsert - - 堆存储中位数 - - 堆排序 - - 完全二叉树转换为大根堆 - - 堆扩容 -- 比较器 -- 非比较排序(桶排序) - - 计数排序 - - 基数排序 - - 基数排序优化 - -### lecture04 - -- 排序算法总结(时间复杂度、稳定性) -- 哈希表(map, set)、有序表 -- 链表 - - 反转链表 - - 打印有序链表公共部分 - - 快慢指针 - - 单链表回文 - - 单链表划分 - - 复制含有随机指针节点的链表 - - 判断链表成环 - - 判断链表相交 -- 二叉树(节点形式) - - 递归遍历二叉树(先序、中序、后序) - - 非递归遍历二叉树(先序、中序、后序) - - 层序遍历 - - 求取最大宽度 - -### lecture05 - -- 二叉树 - - 搜索二叉树 - - 完全二叉树 - - 满二叉树 - - 平衡二叉树 - - 二叉树递归套路(树形 DP) - - 最近公共祖先节点 - - 查找(中序遍历)后继结点 - - 二叉树的序列化和反序列化(先序、后序和层序) - - 折纸问题 - -### lecture06 - -- 图 - - 基本概念(顶点、边、无向图、有向图、邻接表、邻接矩阵、出度、入度、权重、邻接点、邻接边) - - 转换图结构 - - 宽度优先遍历 - - 深度优先遍历 - - 拓扑排序 - - 最小生成树 - - Kruskal 算法 - - Prim 算法 - - 最短路径(Dijkstra 算法) - -### lecture07 - -- 字符串前缀树 -- 贪心算法 - - 安排会议顺序 - - 最小字典序 - - 哈夫曼编码 - - 项目利润 -- 暴力递归 - - 汉诺塔问题 - - 打印字符串全部子序列 - - 打印字符串全排列 - - 先手后手问题 - - 逆转栈 - - 数字转26进制所有结果 - - 暴力背包 - - N 皇后问题 - -## 基础提升班内容 - -### lecture08 - -- 哈希 - - 哈希的特性(一致性、抗碰撞性、离散型、高效性) - - 哈希表原理 - - 找出出现次数最多的数字 - - 设计 RandomPool 结构 - - 布隆过滤器 - - 位图 - - 一致性哈希原理 - - 虚拟节点技术 - -### lecture09 - -- 并查集 - - 岛问题 - - 实现并查集 - - 并行处理岛问题 -- KMP 算法 - -### lecture10 - -- manacher 算法 - - 回文子串 -- 滑动窗口 - - 双端队列 - - 维护最大值 -- 单调栈 - - 求两侧的第一个小于/大于自己的值 - -### lecture11 - -- 树形 DP - - 求一棵树的最大距离 - - 最大快乐值 -- Morris 遍历二叉树 - - 先序中序后序 - - 判断搜索二叉树 -- 大数据 - - 低内存查找没出现过的数字 - -### lecture12 - -- 大数据(资源限制) - - TOP 词汇 - - 统计出现两次的数字 - - 外排序 -- 位运算 - - 最大值 - - 判断 2 的幂次 - - 加减乘除 - -### lecture13 - -- 动态规划 - - 机器人步数问题 - - 最少硬币 - -### lecture14 - -- 动态规划 - - 先手后手 - - 象棋跳马(三维) - - 生存概率(三维) - - 凑零钱(斜率优化) - -### lecture15 - -- 有序表 - - 搜索二叉树 - - 树的左旋和右旋 - - 平衡二叉树(AVL 树) - - SB 树 - - 红黑树(不必深研) - - 跳表(多链表) +- 多画 +- 多看 +- 多敲 +- 睡觉 diff --git "a/DSA/____\345\205\250\346\226\260\347\254\224\350\256\260/KMP.md" "b/DSA/____\345\205\250\346\226\260\347\254\224\350\256\260/KMP.md" index 4293228..6d2a761 100644 --- "a/DSA/____\345\205\250\346\226\260\347\254\224\350\256\260/KMP.md" +++ "b/DSA/____\345\205\250\346\226\260\347\254\224\350\256\260/KMP.md" @@ -172,6 +172,8 @@ const getNext2 = (pattern) => { k++ j++ // 我们是统一右移了一位,所以这里是在 j++ 后再赋值 + // 换句话说,我们这里求的 next[j] 的值,其实是 + // t0...!tj 字符串(不包含 tj)的最长相同前后缀长度 next[j] = k } else { // 加快处理。这里有动态规划的思想 @@ -216,7 +218,7 @@ const getNext1 = (pattern) => { const next = [0] let j = 1 while (j < pattern.length) { - if (pattern[k] === pattern[j]) { + if (pattern[k] === pattern[j]) { // 相同,则可以直接在前人的基础上 + 1 next[j] = k + 1 k++ @@ -230,6 +232,8 @@ const getNext1 = (pattern) => { j++ } } + // 如果你在这里往头插入一个任意元素,那么 next 数组就变成了 + // 第二种 next 数组了。 return next } ``` diff --git "a/DSA/____\345\205\250\346\226\260\347\254\224\350\256\260/\344\272\214\345\217\211\346\240\221\347\232\204\351\201\215\345\216\206.md" "b/DSA/____\345\205\250\346\226\260\347\254\224\350\256\260/\344\272\214\345\217\211\346\240\221\347\232\204\351\201\215\345\216\206.md" index 13d4ce7..28b2494 100644 --- "a/DSA/____\345\205\250\346\226\260\347\254\224\350\256\260/\344\272\214\345\217\211\346\240\221\347\232\204\351\201\215\345\216\206.md" +++ "b/DSA/____\345\205\250\346\226\260\347\254\224\350\256\260/\344\272\214\345\217\211\346\240\221\347\232\204\351\201\215\345\216\206.md" @@ -121,47 +121,26 @@ def pre_order_binary_tree(root, print_arr): stack.append(head.right) if head.right is not None else None stack.append(head.left) if head.left is not None else None ``` -```js -function preorderTraversal(root) { +```ts +function preorderTraversal(root: TreeNode | null): number[] { const ans = [] - if (!root) return ans + // 先序遍历,一个头换两个子 const stack = [root] while (stack.length > 0) { const head = stack.pop() - ans.push(head.val) - - // 记得入栈是先右后左哦 - if (head.right) stack.push(head.right) - if (head.left) stack.push(head.left) - } - - return ans -} -``` -```js -/** - * @param {TreeNode} root - * @return {number[]} - */ -var preorderTraversal = function(root) { - const ans = [] - - const stack = [root] - while (stack.length > 0){ - const head = stack.pop() - // 我们可以选择在这里进行空节点的判断 + // 在这里判断,代码更简洁。 + // 但要知道,简洁的代价就是所有空节点都会经历入栈出栈,吞吐量翻倍 if (!head) continue ans.push(head.val) - - // 颠倒 + // 注意这里是颠倒的 stack.push(head.right) stack.push(head.left) } return ans -} +}; ``` ::: @@ -191,52 +170,32 @@ def pos_order_binary_tree(root, print_arr): while len(collect) != 0: print_arr.append(collect.pop().val) ``` -```js -function postorderTraversal(root) { - if (!root) return [] - - const ans = [] +```ts +function postorderTraversal(root: TreeNode | null): number[] { + const ansReversed = [] + // 后序,就是前序的相反结果 const stack = [root] - const postStack = [] while (stack.length > 0) { const head = stack.pop() - postStack.push(head.val) + if (!head) continue - // 这里就是负负得正,所以是 left, right + // 等会我们还会从 ansReversed 中反向取出,所以 + // 要先让头入栈,这样,这样头才会最晚出栈(输出) + ansReversed.push(head.val) + + // 负负得正 // 第一个负,指的是先序遍历中,stack 需要反着入栈 - // 第二个负,指的是我们后面对 postStack 再次取反 + // 第二个负,指的是我们后面对 ansReversed 再次出栈 // 所以这里需要再负一次,结果就变成了正! - if (head.left) stack.push(head.left) - if (head.right) stack.push(head.right) - } - return postStack.toReversed() -} -``` -```js -/** - * @param {TreeNode} root - * @return {number[]} - */ -var postorderTraversal = function(root) { - - const reversedAns = [] - const stack = [root] - while (stack.length > 0) { - const head = stack.pop() - if (!head) continue - - // 后序,要的是 左右头 - // 所以我们的 reversedAns 中应该存储 头右左 - reversedAns.push(head.val) - // 这里是放入栈中,我们需要右左,所以放入时应该是左右 stack.push(head.left) stack.push(head.right) } // 出栈后返回的结果就是后序遍历结果 左右头 - return reversedAns.toReversed() - -} + ansReversed.reverse() + return ansReversed + // return ansReversed.toReversed() 该 api 较新 +}; ``` ::: @@ -273,26 +232,31 @@ def in_order_binary_tree(root, print_arr): # 同时“头”给出右子节点的位置 p = p.right ``` -```js -function inorderTraversal(root) { - const inorder = [] +```ts +function inorderTraversal(root: TreeNode | null): number[] { + const ans = [] + // 中序遍历,先左后头,那就是不断压左 const stack = [] let p = root while (stack.length > 0 || p) { // 2. 因为在添加之前,我们已经进行了判断 if (p) { + // 不断压左 stack.push(p) p = p.left } else { - // 1. 我们这里保证 head 不为 null - const head = stack.pop() - inorder.push(head.val) + // 此时,cur 既是左,也是头 + // 1. 并且保证 cur 非空 + const cur = stack.pop() + ans.push(cur.val) - p = head.right + // 最后才是右 + p = cur.right } } - return inorder -} + return ans + +}; ``` ::: diff --git a/DSA/code/other/KMP.js b/DSA/code/other/KMP.js index 1c6de3b..6bf4767 100644 --- a/DSA/code/other/KMP.js +++ b/DSA/code/other/KMP.js @@ -1,7 +1,8 @@ check(bruteForce) -check(KMP) check(KMP1) check(KMP2) +check(KMP2_1) +check(KMP2_2) check(KMP22) check(KMP3) console.log('✅'); @@ -51,6 +52,45 @@ function KMP22(str, pattern) { return -1 } function KMP2(str, pattern) { + const getNext2 = pattern => { + // 这里的实现,next[0] 不能任意 + // 因为后面的 kmp 中会访问到 next[0] + // 这里专门弄个 -1,其实就是简化了代码量 + const next = [-1] + let k = -1 + let j = 0 + while (j < pattern.length) { + if (k === -1 || pattern[k] === pattern[j]) { + j++ + k++ + next[j] = k + } else { + k = next[k] + } + } + return next + } + + const next = getNext2(pattern) + + let i = 0 + let j = 0 + while (i < str.length && j < pattern.length) { + if (j === -1 || str[i] === pattern[j]) { + i++ + j++ + } else { + j = next[j] + } + } + + if (j === pattern.length) { + return i - j + } + return -1 +} +/** 第二种 next 数组的第二种实现 */ +function KMP2_1(str, pattern) { const getNext2 = (pattern) => { let k = -1 let j = 0 @@ -88,6 +128,51 @@ function KMP2(str, pattern) { return -1 } +/** 第二种 next 数组的第三种实现 */ +function KMP2_2(str, pattern) { + const getNext2 = pattern => { + const next = [0] + let k = 0 + let j = 1 + while (j < pattern.length) { + if (pattern[k] === pattern[j]) { + next[j] = k + 1 + j++ + k++ + } else if (k > 0) { + k = next[k - 1] + } else { + next[j] = 0 + j++ + } + } + // 其实可以简单的往头添加一个元素,就变成第二个 next 了 + // 而且这个元素是无所谓的,因为我们的 KMP 中永远不会调用 + // 到 next[0] 的值。 + next.unshift(null) + return next + } + const next = getNext2(pattern) + + let i = 0 + let j = 0 + while (i < str.length && j < pattern.length) { + if (str[i] === pattern[j]) { + i++ + j++ + } else if (j > 0) { + // 换成第二个 next 后,这里就可以是 next[j] 了 + j = next[j] + } else { + i++ + } + } + + if (j === pattern.length) { + return i - j + } + return -1 +} function KMP1(str, pattern) { const getNext1 = (pattern) => { let k = 0 @@ -99,7 +184,7 @@ function KMP1(str, pattern) { k++ j++ } else if (k > 0) { - // 这里是 k - 1 哦。计算算一下 bba 的 next 就知道了 + // 这里是易错点。求第一个 next 数组时,记得这里要 k-1。自己算一下 aab 就知道了 k = next[k - 1] } else { next[j] = 0 @@ -117,7 +202,9 @@ function KMP1(str, pattern) { i++ j++ } else if (j > 0) { - // 这里变成了 j - 1 + // 易错点。这里也是一样的道理,我们需要 j - 1 + // 因为当前的 si tj 是不相同的,但 si-1 和 tj-1 是相等的 + // 我们需要需要查找的前后缀是不包含 j 所在字符的 j = next[j - 1] } else { i++ @@ -176,60 +263,6 @@ function KMP3(str, pattern) { - - - - - - - - - - - - - - - - - - - - -function KMP(str, pattern) { - const getNext = (pattern) => { - let k = -1, j = 0 - const next = [-1] - while (j < pattern.length) { - if (k === -1 || pattern[k] === pattern[j]) { - k++ - j++ - next[j] = k - } else { - k = next[k] - } - } - return next - } - - const next = getNext(pattern) - let i = 0, j = 0 - while (i < str.length && j < pattern.length) { - if (j === -1 || str[i] === pattern[j]) { - i++ - j++ - } else { - j = next[j] - } - } - - if (j >= pattern.length) { - return i - pattern.length - } - - return -1 -} - function bruteForce(str, pattern) { let [i, j] = [0, 0] while (i < str.length - j) { diff --git a/DSA/code/sort/sort_quick-1.mjs b/DSA/code/sort/sort_quick-1.mjs index efcd1f6..0e93164 100644 --- a/DSA/code/sort/sort_quick-1.mjs +++ b/DSA/code/sort/sort_quick-1.mjs @@ -21,7 +21,7 @@ function quick_sort(arr, leftIndex, rightIndex) { let l = leftIndex - 1 let r = rightIndex + 1 while (l < r) { - // 这里的 while 为什么不加等号呢? + // 这里的 do while 为什么不加等号呢? // 因为加了等号会导致 l,r 范围溢出 [leftIndex, rightIndex] // 比如为 r 添加等号:arr[r] >= pivot // 那么当 arr = [1, 3, 2], pivot 取 1 时,r 最终会等于 -1 @@ -30,8 +30,6 @@ function quick_sort(arr, leftIndex, rightIndex) { // 并不是说溢出了就不能处理,而是因为我们后面讨论的前提 // 是 l, r 最终都还是在 [leftIndex, rightIndex] 范围内 - - // 同样的,你如果还要问为什么用 do while 不用 while do l++; while (arr[l] < pivot); do r--; while (arr[r] > pivot); @@ -46,15 +44,15 @@ function quick_sort(arr, leftIndex, rightIndex) { // 出循环后,此时的 l 和 r 不一定相同,它们还可能是 r+1 === l // 原因是每轮循环中,l 和 r 的变化大小是不确定的! - // 所以,不要看着 while 条件是 l < r,就以为除了循环后这两个就相等! + // 所以,不要看到 while 条件是 l < r,就以为除了循环后这两个就相等! - // 因为 l 和 r 之间的关系,只可能是 + // 此时的 l 和 r 之间的关系,只可能是 // [ l ] // [ r ] // 或者 // [ l ] // [ r ] - // 然后,再来考虑边界情况 + // 现在,来考虑边界情况 // l 和 r 的范围肯定都是在 [leftIndex, rightIndex] 内的 // 问题是 l 是否可能等于 leftIndex // 而 r 是否可能等于 rightIndex