From a73b3f1714a99362cb6cab4bbdfc6e038dd04e5e Mon Sep 17 00:00:00 2001 From: krahets Date: Sat, 29 Jul 2023 02:09:22 +0800 Subject: [PATCH] build --- .../intro_to_dynamic_programming.md | 177 ++++++++++-- chapter_greedy/greedy_algorithm.md | 4 +- chapter_hashing/hash_collision.md | 266 +++++++++++++++++- chapter_tree/summary.md | 11 +- 4 files changed, 437 insertions(+), 21 deletions(-) diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming.md b/chapter_dynamic_programming/intro_to_dynamic_programming.md index e964efea9..6491115c1 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -144,17 +144,62 @@ status: new === "JS" ```javascript title="climbing_stairs_backtrack.js" - [class]{}-[func]{backtrack} + /* 回溯 */ + function backtrack(choices, state, n, res) { + // 当爬到第 n 阶时,方案数量加 1 + if (state === n) res.set(0, res.get(0) + 1); + // 遍历所有选择 + for (choice of choices) { + // 剪枝:不允许越过第 n 阶 + if (state + choice > n) break; + // 尝试:做出选择,更新状态 + backtrack(choices, state + choice, n, res); + // 回退 + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 爬楼梯:回溯 */ + function climbingStairsBacktrack(n) { + const choices = [1, 2]; // 可选择向上爬 1 或 2 阶 + const state = 0; // 从第 0 阶开始爬 + const res = new Map(); + res.set(0, 0); // 使用 res[0] 记录方案数量 + backtrack(choices, state, n, res); + return res.get(0); + } ``` === "TS" ```typescript title="climbing_stairs_backtrack.ts" - [class]{}-[func]{backtrack} + /* 回溯 */ + function backtrack( + choices: number[], + state: number, + n: number, + res: Map<0, any> + ): void { + // 当爬到第 n 阶时,方案数量加 1 + if (state === n) res.set(0, res.get(0) + 1); + // 遍历所有选择 + for (let choice of choices) { + // 剪枝:不允许越过第 n 阶 + if (state + choice > n) break; + // 尝试:做出选择,更新状态 + backtrack(choices, state + choice, n, res); + // 回退 + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 爬楼梯:回溯 */ + function climbingStairsBacktrack(n: number): number { + const choices = [1, 2]; // 可选择向上爬 1 或 2 阶 + const state = 0; // 从第 0 阶开始爬 + const res = new Map(); + res.set(0, 0); // 使用 res[0] 记录方案数量 + backtrack(choices, state, n, res); + return res.get(0); + } ``` === "C" @@ -402,17 +447,37 @@ $$ === "JS" ```javascript title="climbing_stairs_dfs.js" - [class]{}-[func]{dfs} + /* 搜索 */ + function dfs(i) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1) + dfs(i - 2); + return count; + } - [class]{}-[func]{climbingStairsDFS} + /* 爬楼梯:搜索 */ + function climbingStairsDFS(n) { + return dfs(n); + } ``` === "TS" ```typescript title="climbing_stairs_dfs.ts" - [class]{}-[func]{dfs} + /* 搜索 */ + function dfs(i: number): number { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1) + dfs(i - 2); + return count; + } - [class]{}-[func]{climbingStairsDFS} + /* 爬楼梯:搜索 */ + function climbingStairsDFS(n: number): number { + return dfs(n); + } ``` === "C" @@ -636,17 +701,49 @@ $$ === "JS" ```javascript title="climbing_stairs_dfs_mem.js" - [class]{}-[func]{dfs} + /* 记忆化搜索 */ + function dfs(i, mem) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i; + // 若存在记录 dp[i] ,则直接返回之 + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 记录 dp[i] + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* 爬楼梯:记忆化搜索 */ + function climbingStairsDFSMem(n) { + // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录 + const mem = new Array(n + 1).fill(-1); + return dfs(n, mem); + } ``` === "TS" ```typescript title="climbing_stairs_dfs_mem.ts" - [class]{}-[func]{dfs} + /* 记忆化搜索 */ + function dfs(i: number, mem: number[]): number { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i; + // 若存在记录 dp[i] ,则直接返回之 + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + const count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 记录 dp[i] + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* 爬楼梯:记忆化搜索 */ + function climbingStairsDFSMem(n: number): number { + // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录 + const mem = new Array(n + 1).fill(-1); + return dfs(n, mem); + } ``` === "C" @@ -867,13 +964,39 @@ $$ === "JS" ```javascript title="climbing_stairs_dp.js" - [class]{}-[func]{climbingStairsDP} + /* 爬楼梯:动态规划 */ + function climbingStairsDP(n) { + if (n == 1 || n == 2) return n; + // 初始化 dp 表,用于存储子问题的解 + const dp = new Array(n + 1).fill(-1); + // 初始状态:预设最小子问题的解 + dp[1] = 1; + dp[2] = 2; + // 状态转移:从较小子问题逐步求解较大子问题 + for (let i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } ``` === "TS" ```typescript title="climbing_stairs_dp.ts" - [class]{}-[func]{climbingStairsDP} + /* 爬楼梯:动态规划 */ + function climbingStairsDP(n: number): number { + if (n == 1 || n == 2) return n; + // 初始化 dp 表,用于存储子问题的解 + const dp = new Array(n + 1).fill(-1); + // 初始状态:预设最小子问题的解 + dp[1] = 1; + dp[2] = 2; + // 状态转移:从较小子问题逐步求解较大子问题 + for (let i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } ``` === "C" @@ -1054,13 +1177,35 @@ $$ === "JS" ```javascript title="climbing_stairs_dp.js" - [class]{}-[func]{climbingStairsDPComp} + /* 爬楼梯:状态压缩后的动态规划 */ + function climbingStairsDPComp(n) { + if (n == 1 || n == 2) return n; + let a = 1, + b = 2; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "TS" ```typescript title="climbing_stairs_dp.ts" - [class]{}-[func]{climbingStairsDPComp} + /* 爬楼梯:状态压缩后的动态规划 */ + function climbingStairsDPComp(n: number): number { + if (n == 1 || n == 2) return n; + let a = 1, + b = 2; + for (let i = 3; i <= n; i++) { + const tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "C" diff --git a/chapter_greedy/greedy_algorithm.md b/chapter_greedy/greedy_algorithm.md index bcc83cbd2..5c74efbda 100644 --- a/chapter_greedy/greedy_algorithm.md +++ b/chapter_greedy/greedy_algorithm.md @@ -210,9 +210,9 @@ status: new 然而,**对于某些硬币面值组合,贪心算法并不能找到最优解**。我们来看几个例子: -- **正例 $coins = [1, 5, 10, 20, 50, 100]$**:在该硬币组合下,给定任意 $amt$ ,贪心算法都可以找出最优解。 +- **正例 $coins = [1, 5, 10, 20, 50, 100]$**:在该硬币组合下,给定任意 $amt$ ,贪心算法都可以找出最优解。 - **反例 $coins = [1, 20, 50]$**:假设 $amt = 60$ ,贪心算法只能找到 $50 + 1 \times 10$ 的兑换组合,共计 $11$ 枚硬币,但动态规划可以找到最优解 $20 + 20 + 20$ ,仅需 $3$ 枚硬币。 -- **反例 $coins = [1, 49, 50]$**:假设 $amt = 98$ ,贪心算法只能找到 $50 + 1 \times 48$ 的兑换组合,共计 $48$ 枚硬币,但动态规划可以找到最优解 $49 + 49$ ,仅需 $2$ 枚硬币。 +- **反例 $coins = [1, 49, 50]$**:假设 $amt = 98$ ,贪心算法只能找到 $50 + 1 \times 48$ 的兑换组合,共计 $49$ 枚硬币,但动态规划可以找到最优解 $49 + 49$ ,仅需 $2$ 枚硬币。 ![贪心无法找出最优解的示例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png) diff --git a/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md index 9a062a3e9..e15331aa5 100644 --- a/chapter_hashing/hash_collision.md +++ b/chapter_hashing/hash_collision.md @@ -238,6 +238,8 @@ comments: true for (auto &bucket : bucketsTmp) { for (Pair *pair : bucket) { put(pair->key, pair->val); + // 释放内存 + delete pair; } } } @@ -810,7 +812,128 @@ comments: true === "Rust" ```rust title="hash_map_chaining.rs" - [class]{HashMapChaining}-[func]{} + /* 链式地址哈希表 */ + struct HashMapChaining { + size: i32, + capacity: i32, + load_thres: f32, + extend_ratio: i32, + buckets: Vec>, + } + + impl HashMapChaining { + /* 构造方法 */ + fn new() -> Self { + Self { + size: 0, + capacity: 4, + load_thres: 2.0 / 3.0, + extend_ratio: 2, + buckets: vec![vec![]; 4], + } + } + + /* 哈希函数 */ + fn hash_func(&self, key: i32) -> usize { + key as usize % self.capacity as usize + } + + /* 负载因子 */ + fn load_factor(&self) -> f32 { + self.size as f32 / self.capacity as f32 + } + + /* 删除操作 */ + fn remove(&mut self, key: i32) -> Option { + let index = self.hash_func(key); + let bucket = &mut self.buckets[index]; + + // 遍历桶,从中删除键值对 + for i in 0..bucket.len() { + if bucket[i].key == key { + let pair = bucket.remove(i); + self.size -= 1; + return Some(pair.val); + } + } + + // 若未找到 key 则返回 None + None + } + + /* 扩容哈希表 */ + fn extend(&mut self) { + // 暂存原哈希表 + let buckets_tmp = std::mem::replace(&mut self.buckets, vec![]); + + // 初始化扩容后的新哈希表 + self.capacity *= self.extend_ratio; + self.buckets = vec![Vec::new(); self.capacity as usize]; + self.size = 0; + + // 将键值对从原哈希表搬运至新哈希表 + for bucket in buckets_tmp { + for pair in bucket { + self.put(pair.key, pair.val); + } + } + } + + /* 打印哈希表 */ + fn print(&self) { + for bucket in &self.buckets { + let mut res = Vec::new(); + for pair in bucket { + res.push(format!("{} -> {}", pair.key, pair.val)); + } + println!("{:?}", res); + } + } + + /* 添加操作 */ + fn put(&mut self, key: i32, val: String) { + // 当负载因子超过阈值时,执行扩容 + if self.load_factor() > self.load_thres { + self.extend(); + } + + let index = self.hash_func(key); + let bucket = &mut self.buckets[index]; + + // 遍历桶,若遇到指定 key ,则更新对应 val 并返回 + for pair in bucket { + if pair.key == key { + pair.val = val.clone(); + return; + } + } + let bucket = &mut self.buckets[index]; + + // 若无该 key ,则将键值对添加至尾部 + let pair = Pair { + key, + val: val.clone(), + }; + bucket.push(pair); + self.size += 1; + } + + /* 查询操作 */ + fn get(&self, key: i32) -> Option<&str> { + let index = self.hash_func(key); + let bucket = &self.buckets[index]; + + // 遍历桶,若找到 key 则返回对应 val + for pair in bucket { + if pair.key == key { + return Some(&pair.val); + } + } + + // 若未找到 key 则返回 None + None + } + } ``` !!! tip @@ -1718,7 +1841,146 @@ comments: true === "Rust" ```rust title="hash_map_open_addressing.rs" - [class]{HashMapOpenAddressing}-[func]{} + /* 开放寻址哈希表 */ + struct HashMapOpenAddressing { + size: usize, + capacity: usize, + load_thres: f32, + extend_ratio: usize, + buckets: Vec>, + removed: Pair, + } + + + impl HashMapOpenAddressing { + /* 构造方法 */ + fn new() -> Self { + Self { + size: 0, + capacity: 4, + load_thres: 2.0 / 3.0, + extend_ratio: 2, + buckets: vec![None; 4], + removed: Pair { + key: -1, + val: "-1".to_string(), + }, + } + } + + /* 哈希函数 */ + fn hash_func(&self, key: i32) -> usize { + (key % self.capacity as i32) as usize + } + + /* 负载因子 */ + fn load_factor(&self) -> f32 { + self.size as f32 / self.capacity as f32 + } + + /* 查询操作 */ + fn get(&self, key: i32) -> Option<&str> { + let mut index = self.hash_func(key); + let capacity = self.capacity; + // 线性探测,从 index 开始向后遍历 + for _ in 0..capacity { + // 计算桶索引,越过尾部返回头部 + let j = (index + 1) % capacity; + match &self.buckets[j] { + // 若遇到空桶,说明无此 key ,则返回 None + None => return None, + // 若遇到指定 key ,则返回对应 val + Some(pair) if pair.key == key && pair != &self.removed => return Some(&pair.val), + _ => index = j, + } + } + + None + } + + /* 添加操作 */ + fn put(&mut self, key: i32, val: String) { + // 当负载因子超过阈值时,执行扩容 + if self.load_factor() > self.load_thres { + self.extend(); + } + + let mut index = self.hash_func(key); + let capacity = self.capacity; + + // 线性探测,从 index 开始向后遍历 + for _ in 0..capacity { + //计算桶索引,越过尾部返回头部 + let j = (index + 1) % capacity; + // 若遇到空桶、或带有删除标记的桶,则将键值对放入该桶 + match &mut self.buckets[j] { + bucket @ &mut None | bucket @ &mut Some(Pair { key: -1, .. }) => { + *bucket = Some(Pair { key, val }); + self.size += 1; + return; + } + // 若遇到指定 key ,则更新对应 val + Some(pair) if pair.key == key => { + pair.val = val; + return; + } + _ => index = j, + } + } + } + + /* 删除操作 */ + fn remove(&mut self, key: i32) { + let mut index = self.hash_func(key); + let capacity = self.capacity; + + // 遍历桶,从中删除键值对 + for _ in 0..capacity { + let j = (index + 1) % capacity; + match &mut self.buckets[j] { + // 若遇到空桶,说明无此 key ,则直接返回 + None => return, + // 若遇到指定 key ,则标记删除并返回 + Some(pair) if pair.key == key => { + *pair = Pair { + key: -1, + val: "-1".to_string(), + }; + self.size -= 1; + return; + } + _ => index = j, + } + } + } + + + /* 扩容哈希表 */ + fn extend(&mut self) { + // 暂存原哈希表 + let buckets_tmp = self.buckets.clone(); + // 初始化扩容后的新哈希表 + self.capacity *= self.extend_ratio; + self.buckets = vec![None; self.capacity]; + self.size = 0; + + // 将键值对从原哈希表搬运至新哈希表 + for pair in buckets_tmp { + if let Some(pair) = pair { + self.put(pair.key, pair.val); + } + } + } + /* 打印哈希表 */ + fn print(&self) { + for pair in &self.buckets { + match pair { + Some(pair) => println!("{} -> {}", pair.key, pair.val), + None => println!("None"), + } + } + } + } ``` ### 多次哈希 diff --git a/chapter_tree/summary.md b/chapter_tree/summary.md index 8d87321ce..8cdf13405 100644 --- a/chapter_tree/summary.md +++ b/chapter_tree/summary.md @@ -40,4 +40,13 @@ comments: true !!! question "请问如何从一组输入数据构建一个二叉搜索树?根节点的选择是不是很重要?" - 是的,构建树的方法是 `build_tree()` ,已在源代码中给出。至于根节点的选择,我们通常会将输入数据排序,然后用中点元素作为根节点,再递归地构建左右子树。这样做可以最大程度保证树的平衡性。 + 是的,构建树的方法已在二叉搜索树代码中的 `build_tree()` 方法中给出。至于根节点的选择,我们通常会将输入数据排序,然后用中点元素作为根节点,再递归地构建左右子树。这样做可以最大程度保证树的平衡性。 + +!!! question "在 Java 中,字符串对比是否一定要用 `equals()` 方法?" + + 在 Java 中,对于基本数据类型,`==` 用于对比两个变量的值是否相等。对于引用类型,两种符号的工作原理不同: + + - `==` :用来比较两个变量是否指向同一个对象,即它们在内存中的位置是否相同。 + - `equals()`:用来对比两个对象的值是否相等。 + + 因此如果要对比值,我们通常会用 `equals()` 。然而,通过 `String a = "hi"; String b = "hi";` 初始化的字符串都存储在字符串常量池中,它们指向同一个对象,因此也可以用 `a == b` 来比较两个字符串的内容。