diff --git a/chapter_backtracking/subset_sum_problem.md b/chapter_backtracking/subset_sum_problem.md index 71c511474..b00d00da9 100644 --- a/chapter_backtracking/subset_sum_problem.md +++ b/chapter_backtracking/subset_sum_problem.md @@ -729,7 +729,7 @@ comments: true /* 求解子集和 I */ function subsetSumI(nums, target) { const state = []; // 状态(子集) - nums.sort(); // 对 nums 进行排序 + nums.sort((a, b) => a - b); // 对 nums 进行排序 const start = 0; // 遍历起始点 const res = []; // 结果列表(子集列表) backtrack(state, target, nums, start, res); @@ -773,7 +773,7 @@ comments: true /* 求解子集和 I */ function subsetSumI(nums: number[], target: number): number[][] { const state = []; // 状态(子集) - nums.sort(); // 对 nums 进行排序 + nums.sort((a, b) => a - b); // 对 nums 进行排序 const start = 0; // 遍历起始点 const res = []; // 结果列表(子集列表) backtrack(state, target, nums, start, res); @@ -1230,7 +1230,7 @@ comments: true /* 求解子集和 II */ function subsetSumII(nums, target) { const state = []; // 状态(子集) - nums.sort(); // 对 nums 进行排序 + nums.sort((a, b) => a - b); // 对 nums 进行排序 const start = 0; // 遍历起始点 const res = []; // 结果列表(子集列表) backtrack(state, target, nums, start, res); @@ -1279,7 +1279,7 @@ comments: true /* 求解子集和 II */ function subsetSumII(nums: number[], target: number): number[][] { const state = []; // 状态(子集) - nums.sort(); // 对 nums 进行排序 + nums.sort((a, b) => a - b); // 对 nums 进行排序 const start = 0; // 遍历起始点 const res = []; // 结果列表(子集列表) backtrack(state, target, nums, start, res); diff --git a/chapter_computational_complexity/iteration_and_recursion.md b/chapter_computational_complexity/iteration_and_recursion.md index 9a7659aae..e3b43aa67 100644 --- a/chapter_computational_complexity/iteration_and_recursion.md +++ b/chapter_computational_complexity/iteration_and_recursion.md @@ -1470,7 +1470,24 @@ status: new === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{forLoopRecur} + /* 使用迭代模拟递归 */ + int forLoopRecur(int n) { + // 使用一个显式的栈来模拟系统调用栈 + Stack stack = new Stack(); + int res = 0; + // 递:递归调用 + for (int i = n; i > 0; i--) { + // 通过“入栈操作”模拟“递” + stack.Push(i); + } + // 归:返回结果 + while (stack.Count > 0) { + // 通过“出栈操作”模拟“归” + res += stack.Pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "Go" @@ -1488,13 +1505,47 @@ status: new === "JS" ```javascript title="recursion.js" - [class]{}-[func]{forLoopRecur} + /* 递归转化为迭代 */ + function forLoopRecur(n) { + // 使用一个显式的栈来模拟系统调用栈 + const stack = []; + let res = 0; + // 递:递归调用 + for (let i = 1; i <= n; i++) { + // 通过“入栈操作”模拟“递” + stack.push(i); + } + // 归:返回结果 + while (stack.length) { + // 通过“出栈操作”模拟“归” + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{forLoopRecur} + /* 递归转化为迭代 */ + function forLoopRecur(n: number): number { + // 使用一个显式的栈来模拟系统调用栈 + const stack: number[] = []; + let res: number = 0; + // 递:递归调用 + for (let i = 1; i <= n; i++) { + // 通过“入栈操作”模拟“递” + stack.push(i); + } + // 归:返回结果 + while (stack.length) { + // 通过“出栈操作”模拟“归” + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "Dart" diff --git a/chapter_dynamic_programming/dp_problem_features.md b/chapter_dynamic_programming/dp_problem_features.md index fefd24a3f..2c7de1b46 100644 --- a/chapter_dynamic_programming/dp_problem_features.md +++ b/chapter_dynamic_programming/dp_problem_features.md @@ -514,10 +514,10 @@ $$ 不难发现,此问题已不满足无后效性,状态转移方程 $dp[i] = dp[i-1] + dp[i-2]$ 也失效了,因为 $dp[i-1]$ 代表本轮跳 $1$ 阶,但其中包含了许多“上一轮跳 $1$ 阶上来的”方案,而为了满足约束,我们就不能将 $dp[i-1]$ 直接计入 $dp[i]$ 中。 -为此,我们需要扩展状态定义:**状态 $[i, j]$ 表示处在第 $i$ 阶、并且上一轮跳了 $j$ 阶**,其中 $j \in \{1, 2\}$ 。此状态定义有效地区分了上一轮跳了 $1$ 阶还是 $2$ 阶,我们可以据此来决定下一步该怎么跳。 +为此,我们需要扩展状态定义:**状态 $[i, j]$ 表示处在第 $i$ 阶、并且上一轮跳了 $j$ 阶**,其中 $j \in \{1, 2\}$ 。此状态定义有效地区分了上一轮跳了 $1$ 阶还是 $2$ 阶,我们可以据此来判断当前状态是从何而来的。 -- 当 $j$ 等于 $1$ ,即上一轮跳了 $1$ 阶时,这一轮只能选择跳 $2$ 阶。 -- 当 $j$ 等于 $2$ ,即上一轮跳了 $2$ 阶时,这一轮可选择跳 $1$ 阶或跳 $2$ 阶。 +- 当上一轮跳了 $1$ 阶时,上上一轮只能选择跳 $2$ 阶,即 $dp[i, 1]$ 只能从 $dp[i-1, 2]$ 转移过来。 +- 当上一轮跳了 $2$ 阶时,上上一轮可选择跳 $1$ 阶或跳 $2$ 阶,即 $dp[i, 2]$ 可以从 $dp[i-2, 1]$ 或 $dp[i-2, 2]$ 转移过来。 如图 14-9 所示,在该定义下,$dp[i, j]$ 表示状态 $[i, j]$ 对应的方案数。此时状态转移方程为: diff --git a/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md index 1be9e2c4b..457279c0a 100644 --- a/chapter_hashing/hash_collision.md +++ b/chapter_hashing/hash_collision.md @@ -1741,21 +1741,17 @@ comments: true ```csharp title="hash_map_open_addressing.cs" /* 开放寻址哈希表 */ class HashMapOpenAddressing { - int size; // 键值对数量 - int capacity; // 哈希表容量 - double loadThres; // 触发扩容的负载因子阈值 - int extendRatio; // 扩容倍数 - Pair[] buckets; // 桶数组 - Pair removed; // 删除标记 + private int size; // 键值对数量 + private int capacity = 4; // 哈希表容量 + private double loadThres = 2.0 / 3; // 触发扩容的负载因子阈值 + private int extendRatio = 2; // 扩容倍数 + private Pair[] buckets; // 桶数组 + private Pair TOMBSTONE = new Pair(-1, "-1"); // 删除标记 /* 构造方法 */ public HashMapOpenAddressing() { size = 0; - capacity = 4; - loadThres = 2.0 / 3.0; - extendRatio = 2; buckets = new Pair[capacity]; - removed = new Pair(-1, "-1"); } /* 哈希函数 */ @@ -1768,20 +1764,42 @@ comments: true return (double)size / capacity; } + /* 搜索 key 对应的桶索引 */ + private int findBucket(int key) { + int index = hashFunc(key); + int firstTombstone = -1; + // 线性探测,当遇到空桶时跳出 + while (buckets[index] != null) { + // 若遇到 key ,返回对应桶索引 + if (buckets[index].key == key) { + // 若之前遇到了删除标记,则将键值对移动至该索引 + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index]; + buckets[index] = TOMBSTONE; + return firstTombstone; // 返回移动后的桶索引 + } + return index; // 返回桶索引 + } + // 记录遇到的首个删除标记 + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index; + } + // 计算桶索引,越过尾部返回头部 + index = (index + 1) % capacity; + } + // 若 key 不存在,则返回添加点的索引 + return firstTombstone == -1 ? index : firstTombstone; + } + /* 查询操作 */ public string get(int key) { - int index = hashFunc(key); - // 线性探测,从 index 开始向后遍历 - for (int i = 0; i < capacity; i++) { - // 计算桶索引,越过尾部返回头部 - int j = (index + i) % capacity; - // 若遇到空桶,说明无此 key ,则返回 null - if (buckets[j] == null) - return null; - // 若遇到指定 key ,则返回对应 val - if (buckets[j].key == key && buckets[j] != removed) - return buckets[j].val; + // 搜索 key 对应的桶索引 + int index = findBucket(key); + // 若找到键值对,则返回对应 val + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + return buckets[index].val; } + // 若键值对不存在,则返回 null return null; } @@ -1791,42 +1809,26 @@ comments: true if (loadFactor() > loadThres) { extend(); } - int index = hashFunc(key); - // 线性探测,从 index 开始向后遍历 - for (int i = 0; i < capacity; i++) { - // 计算桶索引,越过尾部返回头部 - int j = (index + i) % capacity; - // 若遇到空桶、或带有删除标记的桶,则将键值对放入该桶 - if (buckets[j] == null || buckets[j] == removed) { - buckets[j] = new Pair(key, val); - size += 1; - return; - } - // 若遇到指定 key ,则更新对应 val - if (buckets[j].key == key) { - buckets[j].val = val; - return; - } + // 搜索 key 对应的桶索引 + int index = findBucket(key); + // 若找到键值对,则覆盖 val 并返回 + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index].val = val; + return; } + // 若键值对不存在,则添加该键值对 + buckets[index] = new Pair(key, val); + size++; } /* 删除操作 */ public void remove(int key) { - int index = hashFunc(key); - // 线性探测,从 index 开始向后遍历 - for (int i = 0; i < capacity; i++) { - // 计算桶索引,越过尾部返回头部 - int j = (index + i) % capacity; - // 若遇到空桶,说明无此 key ,则直接返回 - if (buckets[j] == null) { - return; - } - // 若遇到指定 key ,则标记删除并返回 - if (buckets[j].key == key) { - buckets[j] = removed; - size -= 1; - return; - } + // 搜索 key 对应的桶索引 + int index = findBucket(key); + // 若找到键值对,则用删除标记覆盖它 + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index] = TOMBSTONE; + size--; } } @@ -1840,7 +1842,7 @@ comments: true size = 0; // 将键值对从原哈希表搬运至新哈希表 foreach (Pair pair in bucketsTmp) { - if (pair != null && pair != removed) { + if (pair != null && pair != TOMBSTONE) { put(pair.key, pair.val); } } @@ -1849,10 +1851,12 @@ comments: true /* 打印哈希表 */ public void print() { foreach (Pair pair in buckets) { - if (pair != null) { - Console.WriteLine(pair.key + " -> " + pair.val); - } else { + if (pair == null) { Console.WriteLine("null"); + } else if (pair == TOMBSTONE) { + Console.WriteLine("TOMBSTONE"); + } else { + Console.WriteLine(pair.key + " -> " + pair.val); } } } @@ -2503,12 +2507,12 @@ comments: true ```rust title="hash_map_open_addressing.rs" /* 开放寻址哈希表 */ struct HashMapOpenAddressing { - size: usize, - capacity: usize, - load_thres: f32, - extend_ratio: usize, - buckets: Vec>, - removed: Pair, + size: usize, // 键值对数量 + capacity: usize, // 哈希表容量 + load_thres: f64, // 触发扩容的负载因子阈值 + extend_ratio: usize, // 扩容倍数 + buckets: Vec>, // 桶数组 + TOMBSTONE: Option, // 删除标记 } @@ -2521,10 +2525,7 @@ comments: true load_thres: 2.0 / 3.0, extend_ratio: 2, buckets: vec![None; 4], - removed: Pair { - key: -1, - val: "-1".to_string(), - }, + TOMBSTONE: Some(Pair {key: -1, val: "-1".to_string()}), } } @@ -2534,27 +2535,46 @@ comments: true } /* 负载因子 */ - fn load_factor(&self) -> f32 { - self.size as f32 / self.capacity as f32 + fn load_factor(&self) -> f64 { + self.size as f64 / self.capacity as f64 + } + + /* 搜索 key 对应的桶索引 */ + fn find_bucket(&mut self, key: i32) -> usize { + let mut index = self.hash_func(key); + let mut first_tombstone = -1; + // 线性探测,当遇到空桶时跳出 + while self.buckets[index].is_some() { + // 若遇到 key,返回对应的桶索引 + if self.buckets[index].as_ref().unwrap().key == key { + // 若之前遇到了删除标记,则将建值对移动至该索引 + if first_tombstone != -1 { + self.buckets[first_tombstone as usize] = self.buckets[index].take(); + self.buckets[index] = self.TOMBSTONE.clone(); + return first_tombstone as usize; // 返回移动后的桶索引 + } + return index; // 返回桶索引 + } + // 记录遇到的首个删除标记 + if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE { + first_tombstone = index as i32; + } + // 计算桶索引,越过尾部返回头部 + index = (index + 1) % self.capacity; + } + // 若 key 不存在,则返回添加点的索引 + if first_tombstone == -1 { index } else { first_tombstone as usize } } /* 查询操作 */ - 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, - } + fn get(&mut self, key: i32) -> Option<&str> { + // 搜索 key 对应的桶索引 + let index = self.find_bucket(key); + // 若找到键值对,则返回对应 val + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + return self.buckets[index].as_ref().map(|pair| &pair.val as &str); } - + // 若键值对不存在,则返回 null None } @@ -2564,57 +2584,29 @@ comments: true 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, - } + // 搜索 key 对应的桶索引 + let index = self.find_bucket(key); + // 若找到键值对,则覆盖 val 并返回 + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + self.buckets[index].as_mut().unwrap().val = val; + return; } + // 若键值对不存在,则添加该键值对 + self.buckets[index] = Some(Pair { key, val }); + self.size += 1; } /* 删除操作 */ 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, - } + // 搜索 key 对应的桶索引 + let index = self.find_bucket(key); + // 若找到键值对,则用删除标记覆盖它 + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + self.buckets[index] = self.TOMBSTONE.clone(); + self.size -= 1; } } - /* 扩容哈希表 */ fn extend(&mut self) { // 暂存原哈希表 @@ -2626,17 +2618,24 @@ comments: true // 将键值对从原哈希表搬运至新哈希表 for pair in buckets_tmp { - if let Some(pair) = pair { - self.put(pair.key, pair.val); + if pair.is_none() || pair == self.TOMBSTONE { + continue; } + let pair = pair.unwrap(); + + 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"), + if pair.is_none() { + println!("null"); + } else if pair == &self.TOMBSTONE { + println!("TOMBSTONE"); + } else { + let pair = pair.as_ref().unwrap(); + println!("{} -> {}", pair.key, pair.val); } } } diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md index ef90039b5..ffd5c87b4 100755 --- a/chapter_hashing/hash_map.md +++ b/chapter_hashing/hash_map.md @@ -1329,6 +1329,7 @@ index = hash(key) % capacity ```rust title="array_hash_map.rs" /* 键值对 */ + #[derive(Debug, Clone, PartialEq)] pub struct Pair { pub key: i32, pub val: String, diff --git a/chapter_stack_and_queue/summary.md b/chapter_stack_and_queue/summary.md index 94340bcd1..f1b0ddc91 100644 --- a/chapter_stack_and_queue/summary.md +++ b/chapter_stack_and_queue/summary.md @@ -7,7 +7,7 @@ comments: true ### 1.   重点回顾 - 栈是一种遵循先入后出原则的数据结构,可通过数组或链表来实现。 -- 从时间效率角度看,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会降低至 $O(n)$ 。相比之下,基于链表实现的栈具有更为稳定的效率表现。 +- 从时间效率角度看,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会劣化至 $O(n)$ 。相比之下,基于链表实现的栈具有更为稳定的效率表现。 - 在空间效率方面,栈的数组实现可能导致一定程度的空间浪费。但需要注意的是,链表节点所占用的内存空间比数组元素更大。 - 队列是一种遵循先入先出原则的数据结构,同样可以通过数组或链表来实现。在时间效率和空间效率的对比上,队列的结论与前述栈的结论相似。 - 双向队列是一种具有更高自由度的队列,它允许在两端进行元素的添加和删除操作。