diff --git a/chapter_array_and_linkedlist/array.md b/chapter_array_and_linkedlist/array.md index 4c986d422..284c8e4d9 100755 --- a/chapter_array_and_linkedlist/array.md +++ b/chapter_array_and_linkedlist/array.md @@ -106,6 +106,12 @@ comments: true List nums = [1, 3, 2, 5, 4]; ``` +=== "Rust" + + ```rust title="array.rs" + + ``` + ## 4.1.1.   数组优点 **在数组中访问元素非常高效**。由于数组元素被存储在连续的内存空间中,因此计算数组元素的内存地址非常容易。给定数组首个元素的地址和某个元素的索引,我们可以使用以下公式计算得到该元素的内存地址,从而直接访问此元素。 @@ -270,6 +276,19 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Rust" + + ```rust title="array.rs" + /* 随机返回一个数组元素 */ + fn random_access(nums: &[i32]) -> i32 { + // 在区间 [0, nums.len()) 中随机抽取一个数字 + let random_index = rand::thread_rng().gen_range(0..nums.len()); + // 获取并返回随机元素 + let random_num = nums[random_index]; + random_num + } + ``` + ## 4.1.2.   数组缺点 **数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。 @@ -457,6 +476,22 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Rust" + + ```rust title="array.rs" + /* 扩展数组长度 */ + fn extend(nums: Vec, enlarge: usize) -> Vec { + // 初始化一个扩展长度后的数组 + let mut res: Vec = vec![0; nums.len() + enlarge]; + // 将原数组中的所有元素复制到新 + for i in 0..nums.len() { + res[i] = nums[i]; + } + // 返回扩展后的新数组 + res + } + ``` + **数组中插入或删除元素效率低下**。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。 ![数组插入元素](array.assets/array_insert_element.png) @@ -616,6 +651,20 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Rust" + + ```rust title="array.rs" + /* 在数组的索引 index 处插入元素 num */ + fn insert(nums: &mut Vec, num: i32, index: usize) { + // 把索引 index 以及之后的所有元素向后移动一位 + for i in (index + 1..nums.len()).rev() { + nums[i] = nums[i - 1]; + } + // 将 num 赋给 index 处元素 + nums[index] = num; + } + ``` + 删除元素也类似,如果我们想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。 ![数组删除元素](array.assets/array_remove_element.png) @@ -755,6 +804,18 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Rust" + + ```rust title="array.rs" + /* 删除索引 index 处元素 */ + fn remove(nums: &mut Vec, index: usize) { + // 把索引 index 之后的所有元素向前移动一位 + for i in index..nums.len() - 1 { + nums[i] = nums[i + 1]; + } + } + ``` + 总结来看,数组的插入与删除操作有以下缺点: - **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(n)$ ,其中 $n$ 为数组长度。 @@ -951,6 +1012,23 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Rust" + + ```rust title="array.rs" + /* 遍历数组 */ + fn traverse(nums: &[i32]) { + let mut _count = 0; + // 通过索引遍历数组 + for _ in 0..nums.len() { + _count += 1; + } + // 直接遍历数组 + for _ in nums { + _count += 1; + } + } + ``` + **数组查找**。通过遍历数组,查找数组内的指定元素,并输出对应索引。 === "Java" @@ -1096,6 +1174,20 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Rust" + + ```rust title="array.rs" + /* 在数组中查找指定元素 */ + fn find(nums: &[i32], target: i32) -> Option { + for i in 0..nums.len() { + if nums[i] == target { + return Some(i); + } + } + None + } + ``` + ## 4.1.4.   数组典型应用 数组是最基础的数据结构,在各类数据结构和算法中都有广泛应用。 diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index 7e99e9e13..489d8a1e6 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -169,6 +169,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="" + + ``` + !!! question "尾节点指向什么?" 我们将链表的最后一个节点称为「尾节点」,其指向的是“空”,在 Java, C++, Python 中分别记为 $\text{null}$ , $\text{nullptr}$ , $\text{None}$ 。在不引起歧义的前提下,本书都使用 $\text{None}$ 来表示空。 @@ -366,6 +372,12 @@ comments: true n3.next = n4; ``` +=== "Rust" + + ```rust title="linked_list.rs" + + ``` + ## 4.2.1.   链表优点 **链表中插入与删除节点的操作效率高**。例如,如果我们想在链表中间的两个节点 `A` , `B` 之间插入一个新节点 `P` ,我们只需要改变两个节点指针即可,时间复杂度为 $O(1)$ ;相比之下,数组的插入操作效率要低得多。 @@ -494,6 +506,18 @@ comments: true } ``` +=== "Rust" + + ```rust title="linked_list.rs" + /* 在链表的节点 n0 之后插入节点 P */ + #[allow(non_snake_case)] + pub fn insert(n0: &Rc>>, P: Rc>>) { + let n1 = n0.borrow_mut().next.take(); + P.borrow_mut().next = n1; + n0.borrow_mut().next = Some(P); + } + ``` + 在链表中删除节点也非常方便,只需改变一个节点的指针即可。如下图所示,尽管在删除操作完成后,节点 `P` 仍然指向 `n1` ,但实际上 `P` 已经不再属于此链表,因为遍历此链表时无法访问到 `P` 。 ![链表删除节点](linked_list.assets/linkedlist_remove_node.png) @@ -659,6 +683,22 @@ comments: true } ``` +=== "Rust" + + ```rust title="linked_list.rs" + /* 删除链表的节点 n0 之后的首个节点 */ + #[allow(non_snake_case)] + pub fn remove(n0: &Rc>>) { + if n0.borrow().next.is_none() {return}; + // n0 -> P -> n1 + let P = n0.borrow_mut().next.take(); + if let Some(node) = P { + let n1 = node.borrow_mut().next.take(); + n0.borrow_mut().next = n1; + } + } + ``` + ## 4.2.2.   链表缺点 **链表访问节点效率较低**。如上节所述,数组可以在 $O(1)$ 时间下访问任意元素。然而,链表无法直接访问任意节点,这是因为系统需要从头节点出发,逐个向后遍历直至找到目标节点。例如,若要访问链表索引为 `index`(即第 `index + 1` 个)的节点,则需要向后遍历 `index` 轮。 @@ -819,6 +859,19 @@ comments: true } ``` +=== "Rust" + + ```rust title="linked_list.rs" + /* 访问链表中索引为 index 的节点 */ + pub fn access(head: Rc>>, index: i32) -> Rc>> { + if index <= 0 {return head}; + if let Some(node) = &head.borrow_mut().next { + return access(node.clone(), index - 1); + } + return head; + } + ``` + **链表的内存占用较大**。链表以节点为单位,每个节点除了保存值之外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多的内存空间。 ## 4.2.3.   链表常用操作 @@ -1005,6 +1058,19 @@ comments: true } ``` +=== "Rust" + + ```rust title="linked_list.rs" + /* 在链表中查找值为 target 的首个节点 */ + pub fn find(head: Rc>>, target: T, index: i32) -> i32 { + if head.borrow().val == target {return index}; + if let Some(node) = &head.borrow_mut().next { + return find(node.clone(), target, index + 1); + } + return -1; + } + ``` + ## 4.2.4.   常见链表类型 **单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的指针(引用)两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向空 $\text{None}$ 。 @@ -1184,6 +1250,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="" + + ``` + ![常见链表种类](linked_list.assets/linkedlist_common_types.png)

Fig. 常见链表种类

diff --git a/chapter_array_and_linkedlist/list.md b/chapter_array_and_linkedlist/list.md index 5dd12b9fa..8fc96f91a 100755 --- a/chapter_array_and_linkedlist/list.md +++ b/chapter_array_and_linkedlist/list.md @@ -120,6 +120,12 @@ comments: true List list = [1, 3, 2, 5, 4]; ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **访问与更新元素**。由于列表的底层数据结构是数组,因此可以在 $O(1)$ 时间内访问和更新元素,效率很高。 === "Java" @@ -228,6 +234,12 @@ comments: true list[1] = 0; // 将索引 1 处的元素更新为 0 ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **在列表中添加、插入、删除元素**。相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但插入和删除元素的效率仍与数组相同,时间复杂度为 $O(N)$ 。 === "Java" @@ -436,6 +448,12 @@ comments: true list.removeAt(3); // 删除索引 3 处的元素 ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **遍历列表**。与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。 === "Java" @@ -603,6 +621,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **拼接两个列表**。给定一个新列表 `list1` ,我们可以将该列表拼接到原列表的尾部。 === "Java" @@ -694,6 +718,12 @@ comments: true list.addAll(list1); // 将列表 list1 拼接到 list 之后 ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **排序列表**。排序也是常用的方法之一。完成列表排序后,我们便可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法。 === "Java" @@ -772,6 +802,12 @@ comments: true list.sort(); // 排序后,列表元素从小到大排列 ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + ## 4.3.2.   列表实现 * 为了帮助加深对列表的理解,我们在此提供一个简易版列表实现。需要关注三个核心点: @@ -1918,3 +1954,114 @@ comments: true } } ``` + +=== "Rust" + + ```rust title="my_list.rs" + /* 列表类简易实现 */ + #[allow(dead_code)] + struct MyList { + nums: Vec, // 数组(存储列表元素) + capacity: usize, // 列表容量 + size: usize, // 列表长度(即当前元素数量) + extend_ratio: usize, // 每次列表扩容的倍数 + } + + #[allow(unused,unused_comparisons)] + impl MyList { + /* 构造方法 */ + pub fn new(capacity: usize) -> Self { + let mut vec = Vec::new(); + vec.resize(capacity, 0); + Self { + nums: vec, + capacity, + size: 0, + extend_ratio: 2, + } + } + + /* 获取列表长度(即当前元素数量)*/ + pub fn size(&self) -> usize { + return self.size; + } + + /* 获取列表容量 */ + pub fn capacity(&self) -> usize { + return self.capacity; + } + + /* 访问元素 */ + pub fn get(&self, index: usize) -> i32 { + // 索引如果越界则抛出异常,下同 + if index < 0 || index >= self.size {panic!("索引越界")}; + return self.nums[index]; + } + + /* 更新元素 */ + pub fn set(&mut self, index: usize, num: i32) { + if index < 0 || index >= self.size {panic!("索引越界")}; + self.nums[index] = num; + } + + /* 尾部添加元素 */ + pub fn add(&mut self, num: i32) { + // 元素数量超出容量时,触发扩容机制 + if self.size == self.capacity() { + self.extend_capacity(); + } + self.nums[self.size] = num; + // 更新元素数量 + self.size += 1; + } + + /* 中间插入元素 */ + pub fn insert(&mut self, index: usize, num: i32) { + if index < 0 || index >= self.size() {panic!("索引越界")}; + // 元素数量超出容量时,触发扩容机制 + if self.size == self.capacity() { + self.extend_capacity(); + } + // 将索引 index 以及之后的元素都向后移动一位 + for j in (index..self.size).rev() { + self.nums[j + 1] = self.nums[j]; + } + self.nums[index] = num; + // 更新元素数量 + self.size += 1; + } + + /* 删除元素 */ + pub fn remove(&mut self, index: usize) -> i32 { + if index < 0 || index >= self.size() {panic!("索引越界")}; + let num = self.nums[index]; + // 将索引 index 之后的元素都向前移动一位 + for j in (index..self.size - 1) { + self.nums[j] = self.nums[j + 1]; + } + // 更新元素数量 + self.size -= 1; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + pub fn extend_capacity(&mut self) { + // 新建一个长度为原数组 extend_ratio 倍的新数组,并将原数组拷贝到新数组 + let new_capacity = self.capacity * self.extend_ratio; + self.nums.resize(new_capacity, 0); + // 更新列表容量 + self.capacity = new_capacity; + } + + /* 将列表转换为数组 */ + pub fn to_array(&mut self) -> Vec { + // 仅转换有效长度范围内的列表元素 + let mut nums = Vec::new(); + for i in 0..self.size { + nums.push(self.get(i)); + } + nums + } + } + ``` diff --git a/chapter_backtracking/backtracking_algorithm.md b/chapter_backtracking/backtracking_algorithm.md index 89a8fd3d6..68c22ed3b 100644 --- a/chapter_backtracking/backtracking_algorithm.md +++ b/chapter_backtracking/backtracking_algorithm.md @@ -165,6 +165,25 @@ comments: true [class]{}-[func]{preOrder} ``` +=== "Rust" + + ```rust title="preorder_traversal_i_compact.rs" + /* 前序遍历:例题一 */ + fn pre_order(res: &mut Vec>>, root: Option>>) { + if root.is_none() { + return; + } + if let Some(node) = root { + if node.borrow().val == 7 { + // 记录解 + res.push(node.clone()); + } + pre_order(res, node.borrow().left.clone()); + pre_order(res, node.borrow().right.clone()); + } + } + ``` + ![在前序遍历中搜索节点](backtracking_algorithm.assets/preorder_find_nodes.png)

Fig. 在前序遍历中搜索节点

@@ -370,6 +389,29 @@ comments: true [class]{}-[func]{preOrder} ``` +=== "Rust" + + ```rust title="preorder_traversal_ii_compact.rs" + /* 前序遍历:例题二 */ + fn pre_order(res: &mut Vec>>>, path: &mut Vec>>, root: Option>>) { + if root.is_none() { + return; + } + if let Some(node) = root { + // 尝试 + path.push(node.clone()); + if node.borrow().val == 7 { + // 记录解 + res.push(path.clone()); + } + pre_order(res, path, node.borrow().left.clone()); + pre_order(res, path, node.borrow().right.clone()); + // 回退 + path.remove(path.len() - 1); + } + } + ``` + 在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。 观察该过程,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为逆向的。 @@ -628,6 +670,32 @@ comments: true [class]{}-[func]{preOrder} ``` +=== "Rust" + + ```rust title="preorder_traversal_iii_compact.rs" + /* 前序遍历:例题三 */ + fn pre_order(res: &mut Vec>>>, path: &mut Vec>>, root: Option>>) { + // 剪枝 + if root.is_none() || root.as_ref().unwrap().borrow().val == 3 { + return; + } + if let Some(node) = root { + // 尝试 + path.push(node.clone()); + if node.borrow().val == 7 { + // 记录解 + res.push(path.clone()); + path.remove(path.len() - 1); + return; + } + pre_order(res, path, node.borrow().left.clone()); + pre_order(res, path, node.borrow().right.clone()); + // 回退 + path.remove(path.len() - 1); + } + } + ``` + 剪枝是一个非常形象的名词。在搜索过程中,**我们“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而实现搜索效率的提高。 ![根据约束条件剪枝](backtracking_algorithm.assets/preorder_find_constrained_paths.png) @@ -902,6 +970,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="" + + ``` + 接下来,我们基于框架代码来解决例题三。状态 `state` 为节点遍历路径,选择 `choices` 为当前节点的左子节点和右子节点,结果 `res` 是路径列表。 === "Java" @@ -1351,6 +1425,56 @@ comments: true [class]{}-[func]{backtrack} ``` +=== "Rust" + + ```rust title="preorder_traversal_iii_template.rs" + /* 判断当前状态是否为解 */ + fn is_solution(state: &mut Vec>>) -> bool { + return !state.is_empty() && state.get(state.len() - 1).unwrap().borrow().val == 7; + } + + /* 记录解 */ + fn record_solution(state: &mut Vec>>, res: &mut Vec>>>) { + res.push(state.clone()); + } + + /* 判断在当前状态下,该选择是否合法 */ + fn is_valid(_: &mut Vec>>, choice: Rc>) -> bool { + return choice.borrow().val != 3; + } + + /* 更新状态 */ + fn make_choice(state: &mut Vec>>, choice: Rc>) { + state.push(choice); + } + + /* 恢复状态 */ + fn undo_choice(state: &mut Vec>>, _: Rc>) { + state.remove(state.len() - 1); + } + + /* 回溯算法:例题三 */ + fn backtrack(state: &mut Vec>>, choices: &mut Vec>>, res: &mut Vec>>>) { + // 检查是否为解 + if is_solution(state) { + // 记录解 + record_solution(state, res); + } + // 遍历所有选择 + for choice in choices { + // 剪枝:检查选择是否合法 + if is_valid(state, choice.clone()) { + // 尝试:做出选择,更新状态 + make_choice(state, choice.clone()); + // 进行下一轮选择 + backtrack(state, &mut vec![choice.borrow().left.clone().unwrap(), choice.borrow().right.clone().unwrap()], res); + // 回退:撤销选择,恢复到之前的状态 + undo_choice(state, choice.clone()); + } + } + } + ``` + 根据题意,当找到值为 7 的节点后应该继续搜索,**因此我们需要将记录解之后的 `return` 语句删除**。下图对比了保留或删除 `return` 语句的搜索过程。 ![保留与删除 return 的搜索过程对比](backtracking_algorithm.assets/backtrack_remove_return_or_not.png) diff --git a/chapter_backtracking/n_queens_problem.md b/chapter_backtracking/n_queens_problem.md index ab853c514..8cdcb70bb 100644 --- a/chapter_backtracking/n_queens_problem.md +++ b/chapter_backtracking/n_queens_problem.md @@ -506,6 +506,62 @@ comments: true [class]{}-[func]{nQueens} ``` +=== "Rust" + + ```rust title="n_queens.rs" + /* 回溯算法:N 皇后 */ + fn backtrack(row: usize, n: usize, state: &mut Vec>, res: &mut Vec>>, + cols: &mut [bool], diags1: &mut [bool], diags2: &mut [bool]) { + // 当放置完所有行时,记录解 + if row == n { + let mut copy_state: Vec> = Vec::new(); + for s_row in state.clone() { + copy_state.push(s_row); + } + res.push(copy_state); + return; + } + // 遍历所有列 + for col in 0..n { + // 计算该格子对应的主对角线和副对角线 + let diag1 = row + n - 1 - col; + let diag2 = row + col; + // 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后 + if !cols[col] && !diags1[diag1] && !diags2[diag2] { + // 尝试:将皇后放置在该格子 + state.get_mut(row).unwrap()[col] = "Q".into(); + (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true); + // 放置下一行 + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // 回退:将该格子恢复为空位 + state.get_mut(row).unwrap()[col] = "#".into(); + (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false); + } + } + } + + /* 求解 N 皇后 */ + fn n_queens(n: usize) -> Vec>> { + // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位 + let mut state: Vec> = Vec::new(); + for _ in 0..n { + let mut row: Vec = Vec::new(); + for _ in 0..n { + row.push("#".into()); + } + state.push(row); + } + let mut cols = vec![false; n]; // 记录列是否有皇后 + let mut diags1 = vec![false; 2 * n - 1]; // 记录主对角线是否有皇后 + let mut diags2 = vec![false; 2 * n - 1]; // 记录副对角线是否有皇后 + let mut res: Vec>> = Vec::new(); + + backtrack(0, n, &mut state, &mut res, &mut cols, &mut diags1, &mut diags2); + + res + } + ``` + 逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n, n-1, \cdots, 2, 1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。 数组 `state` 使用 $O(n^2)$ 空间,数组 `cols` , `diags1` , `diags2` 皆使用 $O(n)$ 空间。最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。因此,**空间复杂度为 $O(n^2)$** 。 diff --git a/chapter_backtracking/permutations_problem.md b/chapter_backtracking/permutations_problem.md index d69003387..80c5f382c 100644 --- a/chapter_backtracking/permutations_problem.md +++ b/chapter_backtracking/permutations_problem.md @@ -361,6 +361,41 @@ comments: true [class]{}-[func]{permutationsI} ``` +=== "Rust" + + ```rust title="permutations_i.rs" + /* 回溯算法:全排列 I */ + fn backtrack(mut state: Vec, choices: &[i32], selected: &mut [bool], res: &mut Vec>) { + // 当状态长度等于元素数量时,记录解 + if state.len() == choices.len() { + res.push(state); + return; + } + // 遍历所有选择 + for i in 0..choices.len() { + let choice = choices[i]; + // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素 + if !selected[i] { + // 尝试:做出选择,更新状态 + selected[i] = true; + state.push(choice); + // 进行下一轮选择 + backtrack(state.clone(), choices, selected, res); + // 回退:撤销选择,恢复到之前的状态 + selected[i] = false; + state.remove(state.len() - 1); + } + } + } + + /* 全排列 I */ + fn permutations_i(nums: &mut [i32]) -> Vec> { + let mut res = Vec::new(); // 状态(子集) + backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res); + res + } + ``` + ## 13.2.2.   考虑相等元素的情况 !!! question @@ -718,6 +753,43 @@ comments: true [class]{}-[func]{permutationsII} ``` +=== "Rust" + + ```rust title="permutations_ii.rs" + /* 回溯算法:全排列 II */ + fn backtrack(mut state: Vec, choices: &[i32], selected: &mut [bool], res: &mut Vec>) { + // 当状态长度等于元素数量时,记录解 + if state.len() == choices.len() { + res.push(state); + return; + } + // 遍历所有选择 + let mut duplicated = HashSet::::new(); + for i in 0..choices.len() { + let choice = choices[i]; + // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素 + if !selected[i] && !duplicated.contains(&choice) { + // 尝试:做出选择,更新状态 + duplicated.insert(choice); // 记录选择过的元素值 + selected[i] = true; + state.push(choice); + // 进行下一轮选择 + backtrack(state.clone(), choices, selected, res); + // 回退:撤销选择,恢复到之前的状态 + selected[i] = false; + state.remove(state.len() - 1); + } + } + } + + /* 全排列 II */ + fn permutations_ii(nums: &mut [i32]) -> Vec> { + let mut res = Vec::new(); + backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res); + res + } + ``` + 假设元素两两之间互不相同,则 $n$ 个元素共有 $n!$ 种排列(阶乘);在记录结果时,需要复制长度为 $n$ 的列表,使用 $O(n)$ 时间。因此,**时间复杂度为 $O(n!n)$** 。 最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。`selected` 使用 $O(n)$ 空间。同一时刻最多共有 $n$ 个 `duplicated` ,使用 $O(n^2)$ 空间。**因此空间复杂度为 $O(n^2)$** 。 diff --git a/chapter_backtracking/subset_sum_problem.md b/chapter_backtracking/subset_sum_problem.md index e80796218..42bf90b41 100644 --- a/chapter_backtracking/subset_sum_problem.md +++ b/chapter_backtracking/subset_sum_problem.md @@ -273,6 +273,41 @@ comments: true [class]{}-[func]{subsetSumINaive} ``` +=== "Rust" + + ```rust title="subset_sum_i_naive.rs" + /* 回溯算法:子集和 I */ + fn backtrack(mut state: Vec, target: i32, total: i32, choices: &[i32], res: &mut Vec>) { + // 子集和等于 target 时,记录解 + if total == target { + res.push(state); + return; + } + // 遍历所有选择 + for i in 0..choices.len() { + // 剪枝:若子集和超过 target ,则跳过该选择 + if total + choices[i] > target { + continue; + } + // 尝试:做出选择,更新元素和 total + state.push(choices[i]); + // 进行下一轮选择 + backtrack(state.clone(), target, total + choices[i], choices, res); + // 回退:撤销选择,恢复到之前的状态 + state.pop(); + } + } + + /* 求解子集和 I(包含重复子集) */ + fn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec> { + let state = Vec::new(); // 状态(子集) + let total = 0; // 子集和 + let mut res = Vec::new(); // 结果列表(子集列表) + backtrack(state, target, total, nums, &mut res); + res + } + ``` + 向以上代码输入数组 $[3, 4, 5]$ 和目标元素 $9$ ,输出结果为 $[3, 3, 3], [4, 5], [5, 4]$ 。**虽然成功找出了所有和为 $9$ 的子集,但其中存在重复的子集 $[4, 5]$ 和 $[5, 4]$** 。 这是因为搜索过程是区分选择顺序的,然而子集不区分选择顺序。如下图所示,先选 $4$ 后选 $5$ 与先选 $5$ 后选 $4$ 是两个不同的分支,但两者对应同一个子集。 @@ -580,6 +615,44 @@ comments: true [class]{}-[func]{subsetSumI} ``` +=== "Rust" + + ```rust title="subset_sum_i.rs" + /* 回溯算法:子集和 I */ + fn backtrack(mut state: Vec, target: i32, choices: &[i32], start: usize, res: &mut Vec>) { + // 子集和等于 target 时,记录解 + if target == 0 { + res.push(state); + return; + } + // 遍历所有选择 + // 剪枝二:从 start 开始遍历,避免生成重复子集 + for i in start..choices.len() { + // 剪枝一:若子集和超过 target ,则直接结束循环 + // 这是因为数组已排序,后边元素更大,子集和一定超过 target + if target - choices[i] < 0 { + break; + } + // 尝试:做出选择,更新 target, start + state.push(choices[i]); + // 进行下一轮选择 + backtrack(state.clone(), target - choices[i], choices, i, res); + // 回退:撤销选择,恢复到之前的状态 + state.pop(); + } + } + + /* 求解子集和 I */ + fn subset_sum_i(nums: &mut [i32], target: i32) -> Vec> { + let state = Vec::new(); // 状态(子集) + nums.sort(); // 对 nums 进行排序 + let start = 0; // 遍历起始点 + let mut res = Vec::new(); // 结果列表(子集列表) + backtrack(state, target, nums, start, &mut res); + res + } + ``` + 如下图所示,为将数组 $[3, 4, 5]$ 和目标元素 $9$ 输入到以上代码后的整体回溯过程。 ![子集和 I 回溯过程](subset_sum_problem.assets/subset_sum_i.png) @@ -903,6 +976,49 @@ comments: true [class]{}-[func]{subsetSumII} ``` +=== "Rust" + + ```rust title="subset_sum_ii.rs" + /* 回溯算法:子集和 II */ + fn backtrack(mut state: Vec, target: i32, choices: &[i32], start: usize, res: &mut Vec>) { + // 子集和等于 target 时,记录解 + if target == 0 { + res.push(state); + return; + } + // 遍历所有选择 + // 剪枝二:从 start 开始遍历,避免生成重复子集 + // 剪枝三:从 start 开始遍历,避免重复选择同一元素 + for i in start..choices.len() { + // 剪枝一:若子集和超过 target ,则直接结束循环 + // 这是因为数组已排序,后边元素更大,子集和一定超过 target + if target - choices[i] < 0 { + break; + } + // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过 + if i > start && choices[i] == choices[i - 1] { + continue; + } + // 尝试:做出选择,更新 target, start + state.push(choices[i]); + // 进行下一轮选择 + backtrack(state.clone(), target - choices[i], choices, i, res); + // 回退:撤销选择,恢复到之前的状态 + state.pop(); + } + } + + /* 求解子集和 II */ + fn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec> { + let state = Vec::new(); // 状态(子集) + nums.sort(); // 对 nums 进行排序 + let start = 0; // 遍历起始点 + let mut res = Vec::new(); // 结果列表(子集列表) + backtrack(state, target, nums, start, &mut res); + res + } + ``` + 下图展示了数组 $[4, 4, 5]$ 和目标元素 $9$ 的回溯过程,共包含四种剪枝操作。请你将图示与代码注释相结合,理解整个搜索过程,以及每种剪枝操作是如何工作的。 ![子集和 II 回溯过程](subset_sum_problem.assets/subset_sum_ii.png) diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md index 129ce4d50..5a1378240 100755 --- a/chapter_computational_complexity/space_complexity.md +++ b/chapter_computational_complexity/space_complexity.md @@ -286,6 +286,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="" + + ``` + ## 2.3.2.   推算方法 空间复杂度的推算方法与时间复杂度大致相同,只是将统计对象从“计算操作数量”转为“使用空间大小”。与时间复杂度不同的是,**我们通常只关注「最差空间复杂度」**,这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。 @@ -418,6 +424,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="" + + ``` + **在递归函数中,需要注意统计栈帧空间**。例如,函数 `loop()` 在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行过程中会同时存在 $n$ 个未返回的 `recur()` ,从而占用 $O(n)$ 的栈帧空间。 === "Java" @@ -633,6 +645,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="" + + ``` + ## 2.3.3.   常见类型 设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列) @@ -895,6 +913,28 @@ $$ } ``` +=== "Rust" + + ```rust title="space_complexity.rs" + /* 常数阶 */ + #[allow(unused)] + fn constant(n: i32) { + // 常量、变量、对象占用 O(1) 空间 + const A: i32 = 0; + let b = 0; + let nums = vec![0; 10000]; + let node = ListNode::new(0); + // 循环中的变量占用 O(1) 空间 + for i in 0..n { + let c = 0; + } + // 循环中的函数占用 O(1) 空间 + for i in 0..n { + function(); + } + } + ``` + ### 线性阶 $O(n)$ 线性阶常见于元素数量与 $n$ 成正比的数组、链表、栈、队列等。 @@ -1140,6 +1180,27 @@ $$ } ``` +=== "Rust" + + ```rust title="space_complexity.rs" + /* 线性阶 */ + #[allow(unused)] + fn linear(n: i32) { + // 长度为 n 的数组占用 O(n) 空间 + let mut nums = vec![0; n as usize]; + // 长度为 n 的列表占用 O(n) 空间 + let mut nodes = Vec::new(); + for i in 0..n { + nodes.push(ListNode::new(i)) + } + // 长度为 n 的哈希表占用 O(n) 空间 + let mut map = HashMap::new(); + for i in 0..n { + map.insert(i, i.to_string()); + } + } + ``` + 以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间。 === "Java" @@ -1270,6 +1331,17 @@ $$ } ``` +=== "Rust" + + ```rust title="space_complexity.rs" + /* 线性阶(递归实现) */ + fn linear_recur(n: i32) { + println!("递归 n = {}", n); + if n == 1 {return}; + linear_recur(n - 1); + } + ``` + ![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png)

Fig. 递归函数产生的线性阶空间复杂度

@@ -1471,6 +1543,26 @@ $$ } ``` +=== "Rust" + + ```rust title="space_complexity.rs" + /* 平方阶 */ + #[allow(unused)] + fn quadratic(n: i32) { + // 矩阵占用 O(n^2) 空间 + let num_matrix = vec![vec![0; n as usize]; n as usize]; + // 二维列表占用 O(n^2) 空间 + let mut num_list = Vec::new(); + for i in 0..n { + let mut tmp = Vec::new(); + for j in 0..n { + tmp.push(0); + } + num_list.push(tmp); + } + } + ``` + 在以下递归函数中,同时存在 $n$ 个未返回的 `algorithm()` ,并且每个函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $\frac{n}{2}$ ,因此总体占用 $O(n^2)$ 空间。 === "Java" @@ -1617,6 +1709,19 @@ $$ } ``` +=== "Rust" + + ```rust title="space_complexity.rs" + /* 平方阶(递归实现) */ + fn quadratic_recur(n: i32) -> i32 { + if n <= 0 {return 0}; + // 数组 nums 长度为 n, n-1, ..., 2, 1 + let nums = vec![0; n as usize]; + println!("递归 n = {} 中的 nums 长度 = {}", n, nums.len()); + return quadratic_recur(n - 1); + } + ``` + ![递归函数产生的平方阶空间复杂度](space_complexity.assets/space_complexity_recursive_quadratic.png)

Fig. 递归函数产生的平方阶空间复杂度

@@ -1776,6 +1881,19 @@ $$ } ``` +=== "Rust" + + ```rust title="space_complexity.rs" + /* 指数阶(建立满二叉树) */ + fn build_tree(n: i32) -> Option>> { + if n == 0 {return None}; + let root = TreeNode::new(0); + root.borrow_mut().left = build_tree(n - 1); + root.borrow_mut().right = build_tree(n - 1); + return Some(root); + } + ``` + ![满二叉树产生的指数阶空间复杂度](space_complexity.assets/space_complexity_exponential.png)

Fig. 满二叉树产生的指数阶空间复杂度

diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md index 7f5b15ad9..11d404a61 100755 --- a/chapter_computational_complexity/time_complexity.md +++ b/chapter_computational_complexity/time_complexity.md @@ -172,6 +172,12 @@ $$ } ``` +=== "Rust" + + ```rust title="" + + ``` + 然而实际上,**统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。 ## 2.2.2.   统计时间增长趋势 @@ -398,6 +404,12 @@ $$ } ``` +=== "Rust" + + ```rust title="" + + ``` + ![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png)

Fig. 算法 A, B, C 的时间增长趋势

@@ -562,6 +574,12 @@ $$ } ``` +=== "Rust" + + ```rust title="" + + ``` + $T(n)$ 是一次函数,说明时间增长趋势是线性的,因此可以得出时间复杂度是线性阶。 我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 Big-$O$ Notation」,表示函数 $T(n)$ 的「渐近上界 Asymptotic Upper Bound」。 @@ -803,6 +821,12 @@ $$ } ``` +=== "Rust" + + ```rust title="" + + ``` + ### 第二步:判断渐近上界 **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。 @@ -994,6 +1018,21 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 常数阶 */ + fn constant(n: i32) -> i32 { + _ = n; + let mut count = 0; + let size = 100_000; + for _ in 0..size { + count += 1; + } + count + } + ``` + ### 线性阶 $O(n)$ 线性阶的操作数量相对于输入数据大小以线性级别增长。线性阶通常出现在单层循环中。 @@ -1133,6 +1172,19 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 线性阶 */ + fn linear(n: i32) -> i32 { + let mut count = 0; + for _ in 0..n { + count += 1; + } + count + } + ``` + 遍历数组和遍历链表等操作的时间复杂度均为 $O(n)$ ,其中 $n$ 为数组或链表的长度。 !!! question "如何确定输入数据大小 $n$ ?" @@ -1291,6 +1343,20 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 线性阶(遍历数组) */ + fn array_traversal(nums: &[i32]) -> i32 { + let mut count = 0; + // 循环次数与数组长度成正比 + for _ in nums { + count += 1; + } + count + } + ``` + ### 平方阶 $O(n^2)$ 平方阶的操作数量相对于输入数据大小以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ 。 @@ -1470,6 +1536,22 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 平方阶 */ + fn quadratic(n: i32) -> i32 { + let mut count = 0; + // 循环次数与数组长度成平方关系 + for _ in 0..n { + for _ in 0..n { + count += 1; + } + } + count + } + ``` + ![常数阶、线性阶、平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png)

Fig. 常数阶、线性阶、平方阶的时间复杂度

@@ -1729,6 +1811,29 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 平方阶(冒泡排序) */ + fn bubble_sort(nums: &mut [i32]) -> i32 { + let mut count = 0; // 计数器 + // 外循环:未排序区间为 [0, i] + for i in (1..nums.len()).rev() { + // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 + for j in 0..i { + if nums[j] > nums[j + 1] { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + count + } + ``` + ### 指数阶 $O(2^n)$ !!! note @@ -1940,6 +2045,25 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 指数阶(循环实现) */ + fn exponential(n: i32) -> i32 { + let mut count = 0; + let mut base = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for _ in 0..n { + for _ in 0..base { + count += 1 + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + count + } + ``` + ![指数阶的时间复杂度](time_complexity.assets/time_complexity_exponential.png)

Fig. 指数阶的时间复杂度

@@ -2063,6 +2187,18 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 指数阶(递归实现) */ + fn exp_recur(n: i32) -> i32 { + if n == 1 { + return 1; + } + exp_recur(n - 1) + exp_recur(n - 1) + 1 + } + ``` + ### 对数阶 $O(\log n)$ 与指数阶相反,对数阶反映了“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长缓慢,是理想的时间复杂度。 @@ -2226,6 +2362,20 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 对数阶(循环实现) */ + fn logarithmic(mut n: f32) -> i32 { + let mut count = 0; + while n > 1.0 { + n = n / 2.0; + count += 1; + } + count + } + ``` + ![对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic.png)

Fig. 对数阶的时间复杂度

@@ -2349,6 +2499,18 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 对数阶(递归实现) */ + fn log_recur(n: f32) -> i32 { + if n <= 1.0 { + return 0; + } + log_recur(n / 2.0) + 1 + } + ``` + ### 线性对数阶 $O(n \log n)$ 线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 $O(\log n)$ 和 $O(n)$ 。 @@ -2520,6 +2682,23 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 线性对数阶 */ + fn linear_log_recur(n: f32) -> i32 { + if n <= 1.0 { + return 1; + } + let mut count = linear_log_recur(n / 2.0) + + linear_log_recur(n / 2.0); + for _ in 0 ..n as i32 { + count += 1; + } + return count + } + ``` + ![线性对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic_linear.png)

Fig. 线性对数阶的时间复杂度

@@ -2705,6 +2884,23 @@ $$ } ``` +=== "Rust" + + ```rust title="time_complexity.rs" + /* 阶乘阶(递归实现) */ + fn factorial_recur(n: i32) -> i32 { + if n == 0 { + return 1; + } + let mut count = 0; + // 从 1 个分裂出 n 个 + for _ in 0..n { + count += factorial_recur(n - 1); + } + count + } + ``` + ![阶乘阶的时间复杂度](time_complexity.assets/time_complexity_factorial.png)

Fig. 阶乘阶的时间复杂度

@@ -3043,6 +3239,31 @@ $$ } ``` +=== "Rust" + + ```rust title="worst_best_time_complexity.rs" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + fn random_numbers(n: i32) -> Vec { + // 生成数组 nums = { 1, 2, 3, ..., n } + let mut nums = (1..=n).collect::>(); + // 随机打乱数组元素 + nums.shuffle(&mut thread_rng()); + nums + } + + /* 查找数组 nums 中数字 1 所在索引 */ + fn find_one(nums: &[i32]) -> Option { + for i in 0..nums.len() { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if nums[i] == 1 { + return Some(i); + } + } + None + } + ``` + !!! tip 实际应用中我们很少使用「最佳时间复杂度」,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。相反,「最差时间复杂度」更为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。 diff --git a/chapter_data_structure/basic_data_types.md b/chapter_data_structure/basic_data_types.md index fceec9aa9..a2b2b506e 100644 --- a/chapter_data_structure/basic_data_types.md +++ b/chapter_data_structure/basic_data_types.md @@ -143,3 +143,9 @@ comments: true List characters = List.filled(5, 'a'); List booleans = List.filled(5, false); ``` + +=== "Rust" + + ```rust title="" + + ``` diff --git a/chapter_divide_and_conquer/binary_search_recur.md b/chapter_divide_and_conquer/binary_search_recur.md index adc21c1d0..e2ca3ecf4 100644 --- a/chapter_divide_and_conquer/binary_search_recur.md +++ b/chapter_divide_and_conquer/binary_search_recur.md @@ -247,3 +247,31 @@ status: new [class]{}-[func]{binarySearch} ``` + +=== "Rust" + + ```rust title="binary_search_recur.rs" + /* 二分查找:问题 f(i, j) */ + fn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 { + // 若区间为空,代表无目标元素,则返回 -1 + if i > j { return -1; } + let m: i32 = (i + j) / 2; + if nums[m as usize] < target { + // 递归子问题 f(m+1, j) + return dfs(nums, target, m + 1, j); + } else if nums[m as usize] > target { + // 递归子问题 f(i, m-1) + return dfs(nums, target, i, m - 1); + } else { + // 找到目标元素,返回其索引 + return m; + } + } + + /* 二分查找 */ + fn binary_search(nums: &[i32], target: i32) -> i32 { + let n = nums.len() as i32; + // 求解问题 f(0, n-1) + dfs(nums, target, 0, n - 1) + } + ``` diff --git a/chapter_divide_and_conquer/build_binary_tree_problem.md b/chapter_divide_and_conquer/build_binary_tree_problem.md index e46e2b963..ae5fc814f 100644 --- a/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -282,6 +282,37 @@ status: new [class]{}-[func]{buildTree} ``` +=== "Rust" + + ```rust title="build_tree.rs" + /* 构建二叉树:分治 */ + fn dfs(preorder: &[i32], inorder: &[i32], hmap: &HashMap, i: i32, l: i32, r: i32) -> Option>> { + // 子树区间为空时终止 + if r - l < 0 { return None; } + // 初始化根节点 + let root = TreeNode::new(preorder[i as usize]); + // 查询 m ,从而划分左右子树 + let m = hmap.get(&preorder[i as usize]).unwrap(); + // 子问题:构建左子树 + root.borrow_mut().left = dfs(preorder, inorder, hmap, i + 1, l, m - 1); + // 子问题:构建右子树 + root.borrow_mut().right = dfs(preorder, inorder, hmap, i + 1 + m - l, m + 1, r); + // 返回根节点 + Some(root) + } + + /* 构建二叉树 */ + fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option>> { + // 初始化哈希表,存储 inorder 元素到索引的映射 + let mut hmap: HashMap = HashMap::new(); + for i in 0..inorder.len() { + hmap.insert(inorder[i], i as i32); + } + let root = dfs(preorder, inorder, &hmap, 0, 0, inorder.len() as i32 - 1); + root + } + ``` + 下图展示了构建二叉树的递归过程,各个节点是在向下“递”的过程中建立的,而各条边(即引用)是在向上“归”的过程中建立的。 === "<1>" diff --git a/chapter_divide_and_conquer/hanota_problem.md b/chapter_divide_and_conquer/hanota_problem.md index 82489340d..0750580a5 100644 --- a/chapter_divide_and_conquer/hanota_problem.md +++ b/chapter_divide_and_conquer/hanota_problem.md @@ -321,6 +321,40 @@ status: new [class]{}-[func]{hanota} ``` +=== "Rust" + + ```rust title="hanota.rs" + /* 移动一个圆盘 */ + fn move_pan(src: &mut Vec, tar: &mut Vec) { + // 从 src 顶部拿出一个圆盘 + let pan = src.remove(src.len() - 1); + // 将圆盘放入 tar 顶部 + tar.push(pan); + } + + /* 求解汉诺塔:问题 f(i) */ + fn dfs(i: i32, src: &mut Vec, buf: &mut Vec, tar: &mut Vec) { + // 若 src 只剩下一个圆盘,则直接将其移到 tar + if i == 1 { + move_pan(src, tar); + return; + } + // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf + dfs(i - 1, src, tar, buf); + // 子问题 f(1) :将 src 剩余一个圆盘移到 tar + move_pan(src, tar); + // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar + dfs(i - 1, buf, src, tar); + } + + /* 求解汉诺塔 */ + fn hanota(A: &mut Vec, B: &mut Vec, C: &mut Vec) { + let n = A.len() as i32; + // 将 A 顶部 n 个圆盘借助 B 移到 C + dfs(n, A, B, C); + } + ``` + 如下图所示,汉诺塔问题形成一个高度为 $n$ 的递归树,每个节点代表一个子问题、对应一个开启的 `dfs()` 函数,**因此时间复杂度为 $O(2^n)$ ,空间复杂度为 $O(n)$** 。 ![汉诺塔问题的递归树](hanota_problem.assets/hanota_recursive_tree.png) diff --git a/chapter_dynamic_programming/dp_problem_features.md b/chapter_dynamic_programming/dp_problem_features.md index 2fa251bcd..391a236c8 100644 --- a/chapter_dynamic_programming/dp_problem_features.md +++ b/chapter_dynamic_programming/dp_problem_features.md @@ -212,6 +212,26 @@ $$ [class]{}-[func]{minCostClimbingStairsDP} ``` +=== "Rust" + + ```rust title="min_cost_climbing_stairs_dp.rs" + /* 爬楼梯最小代价:动态规划 */ + fn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 { + let n = cost.len() - 1; + if n == 1 || n == 2 { return cost[n]; } + // 初始化 dp 表,用于存储子问题的解 + let mut dp = vec![-1; n + 1]; + // 初始状态:预设最小子问题的解 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状态转移:从较小子问题逐步求解较大子问题 + for i in 3..=n { + dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i]; + } + dp[n] + } + ``` + ![爬楼梯最小代价的动态规划过程](dp_problem_features.assets/min_cost_cs_dp.png)

Fig. 爬楼梯最小代价的动态规划过程

@@ -369,6 +389,23 @@ $$ [class]{}-[func]{minCostClimbingStairsDPComp} ``` +=== "Rust" + + ```rust title="min_cost_climbing_stairs_dp.rs" + /* 爬楼梯最小代价:状态压缩后的动态规划 */ + fn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 { + let n = cost.len() - 1; + if n == 1 || n == 2 { return cost[n] }; + let (mut a, mut b) = (cost[1], cost[2]); + for i in 3..=n { + let tmp = b; + b = cmp::min(a, tmp) + cost[i]; + a = tmp; + } + b + } + ``` + ## 14.2.2.   无后效性 「无后效性」是动态规划能够有效解决问题的重要特性之一,定义为:**给定一个确定的状态,它的未来发展只与当前状态有关,而与当前状态过去所经历过的所有状态无关**。 @@ -598,6 +635,28 @@ $$ [class]{}-[func]{climbingStairsConstraintDP} ``` +=== "Rust" + + ```rust title="climbing_stairs_constraint_dp.rs" + /* 带约束爬楼梯:动态规划 */ + fn climbing_stairs_constraint_dp(n: usize) -> i32 { + if n == 1 || n == 2 { return n as i32 }; + // 初始化 dp 表,用于存储子问题的解 + let mut dp = vec![vec![-1; 3]; n + 1]; + // 初始状态:预设最小子问题的解 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状态转移:从较小子问题逐步求解较大子问题 + for i in 3..=n { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + dp[n][1] + dp[n][2] + } + ``` + 在上面的案例中,由于仅需多考虑前面一个状态,我们仍然可以通过扩展状态定义,使得问题恢复无后效性。然而,许多问题具有非常严重的“有后效性”,例如: !!! question "爬楼梯与障碍生成" diff --git a/chapter_dynamic_programming/dp_solution_pipeline.md b/chapter_dynamic_programming/dp_solution_pipeline.md index 08eef8d07..d2afa9ec1 100644 --- a/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/chapter_dynamic_programming/dp_solution_pipeline.md @@ -279,6 +279,27 @@ $$ [class]{}-[func]{minPathSumDFS} ``` +=== "Rust" + + ```rust title="min_path_sum.rs" + /* 最小路径和:暴力搜索 */ + fn min_path_sum_dfs(grid: &Vec>, i: i32, j: i32) -> i32 { + // 若为左上角单元格,则终止搜索 + if i == 0 && j == 0 { + return grid[0][0]; + } + // 若行列索引越界,则返回 +∞ 代价 + if i < 0 || j < 0 { + return i32::MAX; + } + // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价 + let left = min_path_sum_dfs(grid, i - 1, j); + let up = min_path_sum_dfs(grid, i, j - 1); + // 返回从左上角到 (i, j) 的最小路径代价 + std::cmp::min(left, up) + grid[i as usize][j as usize] + } + ``` + 下图给出了以 $dp[2, 1]$ 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 `grid` 的尺寸变大而急剧增多。 本质上看,造成重叠子问题的原因为:**存在多条路径可以从左上角到达某一单元格**。 @@ -498,6 +519,32 @@ $$ [class]{}-[func]{minPathSumDFSMem} ``` +=== "Rust" + + ```rust title="min_path_sum.rs" + /* 最小路径和:记忆化搜索 */ + fn min_path_sum_dfs_mem(grid: &Vec>, mem: &mut Vec>, i: i32, j: i32) -> i32 { + // 若为左上角单元格,则终止搜索 + if i == 0 && j == 0 { + return grid[0][0]; + } + // 若行列索引越界,则返回 +∞ 代价 + if i < 0 || j < 0 { + return i32::MAX; + } + // 若已有记录,则直接返回 + if mem[i as usize][j as usize] != -1 { + return mem[i as usize][j as usize]; + } + // 左边和上边单元格的最小路径代价 + let left = min_path_sum_dfs_mem(grid, mem, i - 1, j); + let up = min_path_sum_dfs_mem(grid, mem, i, j - 1); + // 记录并返回左上角到 (i, j) 的最小路径代价 + mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize]; + mem[i as usize][j as usize] + } + ``` + 引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 $O(nm)$ 。 ![记忆化搜索递归树](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png) @@ -721,6 +768,33 @@ $$ [class]{}-[func]{minPathSumDP} ``` +=== "Rust" + + ```rust title="min_path_sum.rs" + /* 最小路径和:动态规划 */ + fn min_path_sum_dp(grid: &Vec>) -> i32 { + let (n, m) = (grid.len(), grid[0].len()); + // 初始化 dp 表 + let mut dp = vec![vec![0; m]; n]; + dp[0][0] = grid[0][0]; + // 状态转移:首行 + for j in 1..m { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状态转移:首列 + for i in 1..n { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状态转移:其余行列 + for i in 1..n { + for j in 1..m { + dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + dp[n - 1][m - 1] + } + ``` + 下图展示了最小路径和的状态转移过程,其遍历了整个网格,**因此时间复杂度为 $O(nm)$** 。 数组 `dp` 大小为 $n \times m$ ,**因此空间复杂度为 $O(nm)$** 。 @@ -969,3 +1043,29 @@ $$ ```dart title="min_path_sum.dart" [class]{}-[func]{minPathSumDPComp} ``` + +=== "Rust" + + ```rust title="min_path_sum.rs" + /* 最小路径和:状态压缩后的动态规划 */ + fn min_path_sum_dp_comp(grid: &Vec>) -> i32 { + let (n, m) = (grid.len(), grid[0].len()); + // 初始化 dp 表 + let mut dp = vec![0; m]; + // 状态转移:首行 + dp[0] = grid[0][0]; + for j in 1..m { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状态转移:其余行 + for i in 1..n { + // 状态转移:首列 + dp[0] = dp[0] + grid[i][0]; + // 状态转移:其余列 + for j in 1..m { + dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + dp[m - 1] + } + ``` diff --git a/chapter_dynamic_programming/edit_distance_problem.md b/chapter_dynamic_programming/edit_distance_problem.md index 1189346b5..9b46b399a 100644 --- a/chapter_dynamic_programming/edit_distance_problem.md +++ b/chapter_dynamic_programming/edit_distance_problem.md @@ -310,6 +310,36 @@ $$ [class]{}-[func]{editDistanceDP} ``` +=== "Rust" + + ```rust title="edit_distance.rs" + /* 编辑距离:动态规划 */ + fn edit_distance_dp(s: &str, t: &str) -> i32 { + let (n, m) = (s.len(), t.len()); + let mut dp = vec![vec![0; m + 1]; n + 1]; + // 状态转移:首行首列 + for i in 1..= n { + dp[i][0] = i as i32; + } + for j in 1..m { + dp[0][j] = j as i32; + } + // 状态转移:其余行列 + for i in 1..=n { + for j in 1..=m { + if s.chars().nth(i - 1) == t.chars().nth(j - 1) { + // 若两字符相等,则直接跳过此两字符 + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[i][j] = std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; + } + } + } + dp[n][m] + } + ``` + 如下图所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作是填写一个二维网格的过程。 === "<1>" @@ -615,3 +645,36 @@ $$ ```dart title="edit_distance.dart" [class]{}-[func]{editDistanceDPComp} ``` + +=== "Rust" + + ```rust title="edit_distance.rs" + /* 编辑距离:状态压缩后的动态规划 */ + fn edit_distance_dp_comp(s: &str, t: &str) -> i32 { + let (n, m) = (s.len(), t.len()); + let mut dp = vec![0; m + 1]; + // 状态转移:首行 + for j in 1..m { + dp[j] = j as i32; + } + // 状态转移:其余行 + for i in 1..=n { + // 状态转移:首列 + let mut leftup = dp[0]; // 暂存 dp[i-1, j-1] + dp[0] = i as i32; + // 状态转移:其余列 + for j in 1..=m { + let temp = dp[j]; + if s.chars().nth(i - 1) == t.chars().nth(j - 1) { + // 若两字符相等,则直接跳过此两字符 + dp[j] = leftup; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 更新为下一轮的 dp[i-1, j-1] + } + } + dp[m] + } + ``` diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming.md b/chapter_dynamic_programming/intro_to_dynamic_programming.md index d7f90aa70..be3092ca5 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -265,6 +265,34 @@ status: new [class]{}-[func]{climbingStairsBacktrack} ``` +=== "Rust" + + ```rust title="climbing_stairs_backtrack.rs" + /* 回溯 */ + fn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) { + // 当爬到第 n 阶时,方案数量加 1 + if state == n { res[0] = res[0] + 1; } + // 遍历所有选择 + for &choice in choices { + // 剪枝:不允许越过第 n 阶 + if state + choice > n { break; } + // 尝试:做出选择,更新状态 + backtrack(choices, state + choice, n, res); + // 回退 + } + } + + /* 爬楼梯:回溯 */ + fn climbing_stairs_backtrack(n: usize) -> i32 { + let choices = vec![ 1, 2 ]; // 可选择向上爬 1 或 2 阶 + let state = 0; // 从第 0 阶开始爬 + let mut res = Vec::new(); + res.push(0); // 使用 res[0] 记录方案数量 + backtrack(&choices, state, n as i32, &mut res); + res[0] + } + ``` + ## 14.1.1.   方法一:暴力搜索 回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。 @@ -462,6 +490,24 @@ $$ [class]{}-[func]{climbingStairsDFS} ``` +=== "Rust" + + ```rust title="climbing_stairs_dfs.rs" + /* 搜索 */ + fn dfs(i: usize) -> i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 || i == 2 { return i as i32; } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i - 1) + dfs(i - 2); + count + } + + /* 爬楼梯:搜索 */ + fn climbing_stairs_dfs(n: usize) -> i32 { + dfs(n) + } + ``` + 下图展示了暴力搜索形成的递归树。对于问题 $dp[n]$ ,其递归树的深度为 $n$ ,时间复杂度为 $O(2^n)$ 。指数阶属于爆炸式增长,如果我们输入一个比较大的 $n$ ,则会陷入漫长的等待之中。 ![爬楼梯对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png) @@ -702,6 +748,30 @@ $$ [class]{}-[func]{climbingStairsDFSMem} ``` +=== "Rust" + + ```rust title="climbing_stairs_dfs_mem.rs" + /* 记忆化搜索 */ + fn dfs(i: usize, mem: &mut [i32]) -> i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if i == 1 || i == 2 { return i as i32; } + // 若存在记录 dp[i] ,则直接返回之 + if mem[i] != -1 { return mem[i]; } + // dp[i] = dp[i-1] + dp[i-2] + let count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 记录 dp[i] + mem[i] = count; + count + } + + /* 爬楼梯:记忆化搜索 */ + fn climbing_stairs_dfs_mem(n: usize) -> i32 { + // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录 + let mut mem = vec![-1; n + 1]; + dfs(n, &mut mem) + } + ``` + 观察下图,**经过记忆化处理后,所有重叠子问题都只需被计算一次,时间复杂度被优化至 $O(n)$** ,这是一个巨大的飞跃。 ![记忆化搜索对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png) @@ -881,6 +951,26 @@ $$ [class]{}-[func]{climbingStairsDP} ``` +=== "Rust" + + ```rust title="climbing_stairs_dp.rs" + /* 爬楼梯:动态规划 */ + fn climbing_stairs_dp(n: usize) -> i32 { + // 已知 dp[1] 和 dp[2] ,返回之 + if n == 1 || n == 2 { return n as i32; } + // 初始化 dp 表,用于存储子问题的解 + let mut dp = vec![-1; n + 1]; + // 初始状态:预设最小子问题的解 + dp[1] = 1; + dp[2] = 2; + // 状态转移:从较小子问题逐步求解较大子问题 + for i in 3..=n { + dp[i] = dp[i - 1] + dp[i - 2]; + } + dp[n] + } + ``` + 与回溯算法一样,动态规划也使用“状态”概念来表示问题求解的某个特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例如,爬楼梯问题的状态定义为当前所在楼梯阶数 $i$ 。 总结以上,动态规划的常用术语包括: @@ -1038,6 +1128,22 @@ $$ [class]{}-[func]{climbingStairsDPComp} ``` +=== "Rust" + + ```rust title="climbing_stairs_dp.rs" + /* 爬楼梯:状态压缩后的动态规划 */ + fn climbing_stairs_dp_comp(n: usize) -> i32 { + if n == 1 || n == 2 { return n as i32; } + let (mut a, mut b) = (1, 2); + for _ in 3..=n { + let tmp = b; + b = a + b; + a = tmp; + } + b + } + ``` + 观察以上代码,由于省去了数组 `dp` 占用的空间,因此空间复杂度从 $O(n)$ 降低至 $O(1)$ 。 **这种空间优化技巧被称为「状态压缩」**。在常见的动态规划问题中,当前状态仅与前面有限个状态有关,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。 diff --git a/chapter_dynamic_programming/knapsack_problem.md b/chapter_dynamic_programming/knapsack_problem.md index e2f4d1328..778ea1617 100644 --- a/chapter_dynamic_programming/knapsack_problem.md +++ b/chapter_dynamic_programming/knapsack_problem.md @@ -231,6 +231,27 @@ $$ [class]{}-[func]{knapsackDFS} ``` +=== "Rust" + + ```rust title="knapsack.rs" + /* 0-1 背包:暴力搜索 */ + fn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 { + // 若已选完所有物品或背包无容量,则返回价值 0 + if i == 0 || c == 0 { + return 0; + } + // 若超过背包容量,则只能不放入背包 + if wgt[i - 1] > c as i32 { + return knapsack_dfs(wgt, val, i - 1, c); + } + // 计算不放入和放入物品 i 的最大价值 + let no = knapsack_dfs(wgt, val, i - 1, c); + let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1]; + // 返回两种方案中价值更大的那一个 + std::cmp::max(no, yes) + } + ``` + 如下图所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 $O(2^n)$ 。 观察递归树,容易发现其中存在重叠子问题,例如 $dp[1, 10]$ 等。而当物品较多、背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅增多。 @@ -449,6 +470,32 @@ $$ [class]{}-[func]{knapsackDFSMem} ``` +=== "Rust" + + ```rust title="knapsack.rs" + /* 0-1 背包:记忆化搜索 */ + fn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec>, i: usize, c: usize) -> i32 { + // 若已选完所有物品或背包无容量,则返回价值 0 + if i == 0 || c == 0 { + return 0; + } + // 若已有记录,则直接返回 + if mem[i][c] != -1 { + return mem[i][c]; + } + // 若超过背包容量,则只能不放入背包 + if wgt[i - 1] > c as i32 { + return knapsack_dfs_mem(wgt, val, mem, i - 1, c); + } + // 计算不放入和放入物品 i 的最大价值 + let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c); + let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1]; + // 记录并返回两种方案中价值更大的那一个 + mem[i][c] = std::cmp::max(no, yes); + mem[i][c] + } + ``` + ![0-1 背包的记忆化搜索递归树](knapsack_problem.assets/knapsack_dfs_mem.png)

Fig. 0-1 背包的记忆化搜索递归树

@@ -648,6 +695,30 @@ $$ [class]{}-[func]{knapsackDP} ``` +=== "Rust" + + ```rust title="knapsack.rs" + /* 0-1 背包:动态规划 */ + fn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // 初始化 dp 表 + let mut dp = vec![vec![0; cap + 1]; n + 1]; + // 状态转移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // 若超过背包容量,则不选物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + dp[n][cap] + } + ``` + 如下图所示,时间复杂度和空间复杂度都由数组 `dp` 大小决定,即 $O(n \times cap)$ 。 === "<1>" @@ -903,3 +974,25 @@ $$ ```dart title="knapsack.dart" [class]{}-[func]{knapsackDPComp} ``` + +=== "Rust" + + ```rust title="knapsack.rs" + /* 0-1 背包:状态压缩后的动态规划 */ + fn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // 初始化 dp 表 + let mut dp = vec![0; cap + 1]; + // 状态转移 + for i in 1..=n { + // 倒序遍历 + for c in (1..=cap).rev() { + if wgt[i - 1] <= c as i32 { + // 不选和选物品 i 这两种方案的较大值 + dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + dp[cap] + } + ``` diff --git a/chapter_dynamic_programming/unbounded_knapsack_problem.md b/chapter_dynamic_programming/unbounded_knapsack_problem.md index 99023596c..cb8bec09f 100644 --- a/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -228,6 +228,30 @@ $$ [class]{}-[func]{unboundedKnapsackDP} ``` +=== "Rust" + + ```rust title="unbounded_knapsack.rs" + /* 完全背包:动态规划 */ + fn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // 初始化 dp 表 + let mut dp = vec![vec![0; cap + 1]; n + 1]; + // 状态转移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // 若超过背包容量,则不选物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + return dp[n][cap]; + } + ``` + ### 状态压缩 由于当前状态是从左边和上边的状态转移而来,**因此状态压缩后应该对 $dp$ 表中的每一行采取正序遍历**。 @@ -443,6 +467,30 @@ $$ [class]{}-[func]{unboundedKnapsackDPComp} ``` +=== "Rust" + + ```rust title="unbounded_knapsack.rs" + /* 完全背包:状态压缩后的动态规划 */ + fn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // 初始化 dp 表 + let mut dp = vec![0; cap + 1]; + // 状态转移 + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // 若超过背包容量,则不选物品 i + dp[c] = dp[c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]); + } + } + } + dp[cap] + } + ``` + ## 14.5.2.   零钱兑换问题 背包问题是一大类动态规划问题的代表,其拥有很多的变种,例如零钱兑换问题。 @@ -724,6 +772,35 @@ $$ [class]{}-[func]{coinChangeDP} ``` +=== "Rust" + + ```rust title="coin_change.rs" + /* 零钱兑换:动态规划 */ + fn coin_change_dp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + let max = amt + 1; + // 初始化 dp 表 + let mut dp = vec![vec![0; amt + 1]; n + 1]; + // 状态转移:首行首列 + for a in 1..= amt { + dp[0][a] = max; + } + // 状态转移:其余行列 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 若超过背包容量,则不选硬币 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1); + } + } + } + if dp[n][amt] != max { return dp[n][amt] as i32; } else { -1 } + } + ``` + 下图展示了零钱兑换的动态规划过程,和完全背包非常相似。 === "<1>" @@ -991,6 +1068,33 @@ $$ [class]{}-[func]{coinChangeDPComp} ``` +=== "Rust" + + ```rust title="coin_change.rs" + /* 零钱兑换:状态压缩后的动态规划 */ + fn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + let max = amt + 1; + // 初始化 dp 表 + let mut dp = vec![0; amt + 1]; + dp.fill(max); + dp[0] = 0; + // 状态转移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 若超过背包容量,则不选硬币 i + dp[a] = dp[a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1); + } + } + } + if dp[amt] != max { return dp[amt] as i32; } else { -1 } + } + ``` + ## 14.5.3.   零钱兑换问题 II !!! question @@ -1231,6 +1335,34 @@ $$ [class]{}-[func]{coinChangeIIDP} ``` +=== "Rust" + + ```rust title="coin_change_ii.rs" + /* 零钱兑换 II:动态规划 */ + fn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + // 初始化 dp 表 + let mut dp = vec![vec![0; amt + 1]; n + 1]; + // 初始化首列 + for i in 0..= n { + dp[i][0] = 1; + } + // 状态转移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 若超过背包容量,则不选硬币 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize]; + } + } + } + dp[n][amt] + } + ``` + ### 状态压缩 状态压缩处理方式相同,删除硬币维度即可。 @@ -1431,3 +1563,28 @@ $$ ```dart title="coin_change_ii.dart" [class]{}-[func]{coinChangeIIDPComp} ``` + +=== "Rust" + + ```rust title="coin_change_ii.rs" + /* 零钱兑换 II:状态压缩后的动态规划 */ + fn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + // 初始化 dp 表 + let mut dp = vec![0; amt + 1]; + dp[0] = 1; + // 状态转移 + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // 若超过背包容量,则不选硬币 i + dp[a] = dp[a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[a] = dp[a] + dp[a - coins[i - 1] as usize]; + } + } + } + dp[amt] + } + ``` diff --git a/chapter_graph/graph_operations.md b/chapter_graph/graph_operations.md index 76f34c1a5..4651de22e 100644 --- a/chapter_graph/graph_operations.md +++ b/chapter_graph/graph_operations.md @@ -1021,6 +1021,107 @@ comments: true } ``` +=== "Rust" + + ```rust title="graph_adjacency_matrix.rs" + /* 基于邻接矩阵实现的无向图类型 */ + pub struct GraphAdjMat { + // 顶点列表,元素代表“顶点值”,索引代表“顶点索引” + pub vertices: Vec, + // 邻接矩阵,行列索引对应“顶点索引” + pub adj_mat: Vec>, + } + + impl GraphAdjMat { + /* 构造方法 */ + pub fn new(vertices: Vec, edges: Vec<[usize; 2]>) -> Self { + let mut graph = GraphAdjMat { + vertices: vec![], + adj_mat: vec![], + }; + // 添加顶点 + for val in vertices { + graph.add_vertex(val); + } + // 添加边 + // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引 + for edge in edges { + graph.add_edge(edge[0], edge[1]) + } + + graph + } + + /* 获取顶点数量 */ + pub fn size(&self) -> usize { + self.vertices.len() + } + + /* 添加顶点 */ + pub fn add_vertex(&mut self, val: i32) { + let n = self.size(); + // 向顶点列表中添加新顶点的值 + self.vertices.push(val); + // 在邻接矩阵中添加一行 + self.adj_mat.push(vec![0; n]); + // 在邻接矩阵中添加一列 + for row in &mut self.adj_mat { + row.push(0); + } + } + + /* 删除顶点 */ + pub fn remove_vertex(&mut self, index: usize) { + if index >= self.size() { + panic!("index error") + } + // 在顶点列表中移除索引 index 的顶点 + self.vertices.remove(index); + // 在邻接矩阵中删除索引 index 的行 + self.adj_mat.remove(index); + // 在邻接矩阵中删除索引 index 的列 + for row in &mut self.adj_mat { + row.remove(index); + } + } + + /* 添加边 */ + pub fn add_edge(&mut self, i: usize, j: usize) { + // 参数 i, j 对应 vertices 元素索引 + // 索引越界与相等处理 + if i >= self.size() || j >= self.size() || i == j { + panic!("index error") + } + // 在无向图中,邻接矩阵沿主对角线对称,即满足 (i, j) == (j, i) + self.adj_mat[i][j] = 1; + self.adj_mat[j][i] = 1; + } + + /* 删除边 */ + // 参数 i, j 对应 vertices 元素索引 + pub fn remove_edge(&mut self, i: usize, j: usize) { + // 参数 i, j 对应 vertices 元素索引 + // 索引越界与相等处理 + if i >= self.size() || j >= self.size() || i == j { + panic!("index error") + } + self.adj_mat[i][j] = 0; + self.adj_mat[j][i] = 0; + } + + /* 打印邻接矩阵 */ + pub fn print(&self) { + println!("顶点列表 = {:?}", self.vertices); + println!("邻接矩阵 ="); + println!("["); + for row in &self.adj_mat { + println!(" {:?},", row); + } + println!("]") + } + } + ``` + ## 9.2.2.   基于邻接表的实现 设无向图的顶点总数为 $n$ 、边总数为 $m$ ,则有: @@ -1913,6 +2014,100 @@ comments: true } ``` +=== "Rust" + + ```rust title="graph_adjacency_list.rs" + /* 基于邻接表实现的无向图类型 */ + pub struct GraphAdjList { + // 邻接表,key: 顶点,value:该顶点的所有邻接顶点 + pub adj_list: HashMap>, + } + + impl GraphAdjList { + /* 构造方法 */ + pub fn new(edges: Vec<[Vertex; 2]>) -> Self { + let mut graph = GraphAdjList { + adj_list: HashMap::new(), + }; + // 添加所有顶点和边 + for edge in edges { + graph.add_vertex(edge[0]); + graph.add_vertex(edge[1]); + graph.add_edge(edge[0], edge[1]); + } + + graph + } + + /* 获取顶点数量 */ + #[allow(unused)] + pub fn size(&self) -> usize { + self.adj_list.len() + } + + /* 添加边 */ + pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) { + if !self.adj_list.contains_key(&vet1) || !self.adj_list.contains_key(&vet2) || vet1 == vet2 + { + panic!("value error"); + } + // 添加边 vet1 - vet2 + self.adj_list.get_mut(&vet1).unwrap().push(vet2); + self.adj_list.get_mut(&vet2).unwrap().push(vet1); + } + + /* 删除边 */ + #[allow(unused)] + pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) { + if !self.adj_list.contains_key(&vet1) || !self.adj_list.contains_key(&vet2) || vet1 == vet2 + { + panic!("value error"); + } + // 删除边 vet1 - vet2 + self.adj_list + .get_mut(&vet1) + .unwrap() + .retain(|&vet| vet != vet2); + self.adj_list + .get_mut(&vet2) + .unwrap() + .retain(|&vet| vet != vet1); + } + + /* 添加顶点 */ + pub fn add_vertex(&mut self, vet: Vertex) { + if self.adj_list.contains_key(&vet) { + return; + } + // 在邻接表中添加一个新链表 + self.adj_list.insert(vet, vec![]); + } + + /* 删除顶点 */ + #[allow(unused)] + pub fn remove_vertex(&mut self, vet: Vertex) { + if !self.adj_list.contains_key(&vet) { + panic!("value error"); + } + // 在邻接表中删除顶点 vet 对应的链表 + self.adj_list.remove(&vet); + // 遍历其他顶点的链表,删除所有包含 vet 的边 + for list in self.adj_list.values_mut() { + list.retain(|&v| v != vet); + } + } + + /* 打印邻接表 */ + pub fn print(&self) { + println!("邻接表 ="); + for (vertex, list) in &self.adj_list { + let list = list.iter().map(|vertex| vertex.val).collect::>(); + println!("{}: {:?},", vertex.val, list); + } + } + } + ``` + ## 9.2.3.   效率对比 设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。 diff --git a/chapter_graph/graph_traversal.md b/chapter_graph/graph_traversal.md index 54fa37577..82abb8986 100644 --- a/chapter_graph/graph_traversal.md +++ b/chapter_graph/graph_traversal.md @@ -358,6 +358,40 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 } ``` +=== "Rust" + + ```rust title="graph_bfs.rs" + /* 广度优先遍历 BFS */ + // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 + fn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec { + // 顶点遍历序列 + let mut res = vec![]; + // 哈希表,用于记录已被访问过的顶点 + let mut visited = HashSet::new(); + visited.insert(start_vet); + // 队列用于实现 BFS + let mut que = VecDeque::new(); + que.push_back(start_vet); + // 以顶点 vet 为起点,循环直至访问完所有顶点 + while !que.is_empty() { + let vet = que.pop_front().unwrap(); // 队首顶点出队 + res.push(vet); // 记录访问顶点 + // 遍历该顶点的所有邻接顶点 + if let Some(adj_vets) = graph.adj_list.get(&vet) { + for &adj_vet in adj_vets { + if visited.contains(&adj_vet) { + continue; // 跳过已被访问过的顶点 + } + que.push_back(adj_vet); // 只入队未访问的顶点 + visited.insert(adj_vet); // 标记该顶点已被访问 + } + } + } + // 返回顶点遍历序列 + res + } + ``` + 代码相对抽象,建议对照以下动画图示来加深理解。 === "<1>" @@ -729,6 +763,38 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 } ``` +=== "Rust" + + ```rust title="graph_dfs.rs" + /* 深度优先遍历 DFS 辅助函数 */ + fn dfs(graph: &GraphAdjList, visited: &mut HashSet, res: &mut Vec, vet: Vertex) { + res.push(vet); // 记录访问顶点 + visited.insert(vet); // 标记该顶点已被访问 + // 遍历该顶点的所有邻接顶点 + if let Some(adj_vets) = graph.adj_list.get(&vet) { + for &adj_vet in adj_vets { + if visited.contains(&adj_vet) { + continue; // 跳过已被访问过的顶点 + } + // 递归访问邻接顶点 + dfs(graph, visited, res, adj_vet); + } + } + } + + /* 深度优先遍历 DFS */ + // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 + fn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec { + // 顶点遍历序列 + let mut res = vec![]; + // 哈希表,用于记录已被访问过的顶点 + let mut visited = HashSet::new(); + dfs(&graph, &mut visited, &mut res, start_vet); + + res + } + ``` + 深度优先遍历的算法流程如下图所示,其中: - **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点。 diff --git a/chapter_greedy/fractional_knapsack_problem.md b/chapter_greedy/fractional_knapsack_problem.md index 4cb97c617..b50fedbf2 100644 --- a/chapter_greedy/fractional_knapsack_problem.md +++ b/chapter_greedy/fractional_knapsack_problem.md @@ -281,6 +281,53 @@ status: new [class]{}-[func]{fractionalKnapsack} ``` +=== "Rust" + + ```rust title="fractional_knapsack.rs" + /* 物品 */ + struct Item { + w: i32, // 物品重量 + v: i32, // 物品价值 + } + + impl Item { + fn new(w: i32, v: i32) -> Self { + Self { w, v } + } + } + + /* 分数背包:贪心 */ + fn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 { + // 创建物品列表,包含两个属性:重量、价值 + let mut items = wgt + .iter() + .zip(val.iter()) + .map(|(&w, &v)| Item::new(w, v)) + .collect::>(); + // 按照单位价值 item.v / item.w 从高到低进行排序 + items.sort_by(|a, b| { + (b.v as f64 / b.w as f64) + .partial_cmp(&(a.v as f64 / a.w as f64)) + .unwrap() + }); + // 循环贪心选择 + let mut res = 0.0; + for item in &items { + if item.w <= cap { + // 若剩余容量充足,则将当前物品整个装进背包 + res += item.v as f64; + cap -= item.w; + } else { + // 若剩余容量不足,则将当前物品的一部分装进背包 + res += item.v as f64 / item.w as f64 * cap as f64; + // 已无剩余容量,因此跳出循环 + break; + } + } + res + } + ``` + 最差情况下,需要遍历整个物品列表,**因此时间复杂度为 $O(n)$** ,其中 $n$ 为物品数量。 由于初始化了一个 `Item` 对象列表,**因此空间复杂度为 $O(n)$** 。 diff --git a/chapter_greedy/greedy_algorithm.md b/chapter_greedy/greedy_algorithm.md index 5b5699496..6f9e22459 100644 --- a/chapter_greedy/greedy_algorithm.md +++ b/chapter_greedy/greedy_algorithm.md @@ -177,6 +177,33 @@ status: new [class]{}-[func]{coinChangeGreedy} ``` +=== "Rust" + + ```rust title="coin_change_greedy.rs" + /* 零钱兑换:贪心 */ + fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 { + // 假设 coins 列表有序 + let mut i = coins.len() - 1; + let mut count = 0; + // 循环进行贪心选择,直到无剩余金额 + while amt > 0 { + // 找到小于且最接近剩余金额的硬币 + while coins[i] > amt { + i -= 1; + } + // 选择 coins[i] + amt -= coins[i]; + count += 1; + } + // 若未找到可行方案,则返回 -1 + if amt == 0 { + count + } else { + -1 + } + } + ``` + ## 15.1.1.   贪心优点与局限性 **贪心算法不仅操作直接、实现简单,而且通常效率也很高**。在以上代码中,记硬币最小面值为 $\min(coins)$ ,则贪心选择最多循环 $amt / \min(coins)$ 次,时间复杂度为 $O(amt / \min(coins))$ 。这比动态规划解法的时间复杂度 $O(n \times amt)$ 提升了一个数量级。 diff --git a/chapter_greedy/max_capacity_problem.md b/chapter_greedy/max_capacity_problem.md index 8e0fd59ee..78513635a 100644 --- a/chapter_greedy/max_capacity_problem.md +++ b/chapter_greedy/max_capacity_problem.md @@ -248,6 +248,32 @@ $$ [class]{}-[func]{maxCapacity} ``` +=== "Rust" + + ```rust title="max_capacity.rs" + /* 最大容量:贪心 */ + fn max_capacity(ht: &[i32]) -> i32 { + // 初始化 i, j 分列数组两端 + let mut i = 0; + let mut j = ht.len() - 1; + // 初始最大容量为 0 + let mut res = 0; + // 循环贪心选择,直至两板相遇 + while i < j { + // 更新最大容量 + let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32; + res = std::cmp::max(res, cap); + // 向内移动短板 + if ht[i] < ht[j] { + i += 1; + } else { + j -= 1; + } + } + res + } + ``` + ### 正确性证明 之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。 diff --git a/chapter_greedy/max_product_cutting_problem.md b/chapter_greedy/max_product_cutting_problem.md index 04aa7c3d3..3539f94df 100644 --- a/chapter_greedy/max_product_cutting_problem.md +++ b/chapter_greedy/max_product_cutting_problem.md @@ -230,6 +230,31 @@ $$ [class]{}-[func]{maxProductCutting} ``` +=== "Rust" + + ```rust title="max_product_cutting.rs" + /* 最大切分乘积:贪心 */ + fn max_product_cutting(n: i32) -> i32 { + // 当 n <= 3 时,必须切分出一个 1 + if n <= 3 { + return 1 * (n - 1); + } + // 贪心地切分出 3 ,a 为 3 的个数,b 为余数 + let a = n / 3; + let b = n % 3; + if b == 1 { + // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2 + 3_i32.pow(a as u32 - 1) * 2 * 2 + } else if b == 2 { + // 当余数为 2 时,不做处理 + 3_i32.pow(a as u32) * 2 + } else { + // 当余数为 0 时,不做处理 + 3_i32.pow(a as u32) + } + } + ``` + ![最大切分乘积的计算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png)

Fig. 最大切分乘积的计算方法

diff --git a/chapter_hashing/hash_algorithm.md b/chapter_hashing/hash_algorithm.md index cd2247460..2b6e57f16 100644 --- a/chapter_hashing/hash_algorithm.md +++ b/chapter_hashing/hash_algorithm.md @@ -384,6 +384,18 @@ index = hash(key) % capacity [class]{}-[func]{rot_hash} ``` +=== "Rust" + + ```rust title="simple_hash.rs" + [class]{}-[func]{add_hash} + + [class]{}-[func]{mul_hash} + + [class]{}-[func]{xor_hash} + + [class]{}-[func]{rot_hash} + ``` + 观察发现,每种哈希算法的最后一步都是对大质数 $1000000007$ 取模,以确保哈希值在合适的范围内。值得思考的是,为什么要强调对质数取模,或者说对合数取模的弊端是什么?这是一个有趣的问题。 先抛出结论:**当我们使用大质数作为模数时,可以最大化地保证哈希值的均匀分布**。因为质数不会与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。 @@ -638,6 +650,12 @@ $$ // 节点对象 Instance of 'ListNode' 的哈希值为 1033450432 ``` +=== "Rust" + + ```rust title="built_in_hash.rs" + + ``` + 在许多编程语言中,**只有不可变对象才可作为哈希表的 `key`** 。假如我们将列表(动态数组)作为 `key` ,当列表的内容发生变化时,它的哈希值也随之改变,我们就无法在哈希表中查询到原先的 `value` 了。 虽然自定义对象(比如链表节点)的成员变量是可变的,但它是可哈希的。**这是因为对象的哈希值通常是基于内存地址生成的**,即使对象的内容发生了变化,但它的内存地址不变,哈希值仍然是不变的。 diff --git a/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md index 538094f31..63d40c37d 100644 --- a/chapter_hashing/hash_collision.md +++ b/chapter_hashing/hash_collision.md @@ -807,6 +807,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="hash_map_chaining.rs" + [class]{HashMapChaining}-[func]{} + ``` + !!! tip 当链表很长时,查询效率 $O(n)$ 很差,**此时可以将链表转换为「AVL 树」或「红黑树」**,从而将查询操作的时间复杂度优化至 $O(\log n)$ 。 @@ -1709,6 +1715,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="hash_map_open_addressing.rs" + [class]{HashMapOpenAddressing}-[func]{} + ``` + ### 多次哈希 顾名思义,多次哈希方法是使用多个哈希函数 $f_1(x)$ , $f_2(x)$ , $f_3(x)$ , $\cdots$ 进行探测。 diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md index 318eab196..4553b3eb1 100755 --- a/chapter_hashing/hash_map.md +++ b/chapter_hashing/hash_map.md @@ -256,6 +256,12 @@ comments: true map.remove(10583); ``` +=== "Rust" + + ```rust title="hash_map.rs" + + ``` + 哈希表有三种常用遍历方式:遍历键值对、遍历键和遍历值。 === "Java" @@ -431,6 +437,12 @@ comments: true }); ``` +=== "Rust" + + ```rust title="hash_map.rs" + + ``` + ## 6.1.2.   哈希表简单实现 我们先考虑最简单的情况,**仅用一个数组来实现哈希表**。在哈希表中,我们将数组中的每个空位称为「桶 Bucket」,每个桶可存储一个键值对。因此,查询操作就是找到 `key` 对应的桶,并在桶中获取 `value` 。 @@ -1403,6 +1415,77 @@ index = hash(key) % capacity } ``` +=== "Rust" + + ```rust title="array_hash_map.rs" + /* 键值对 */ + pub struct Pair { + pub key: i32, + pub val: String, + } + + /* 基于数组简易实现的哈希表 */ + pub struct ArrayHashMap { + buckets: Vec> + } + + impl ArrayHashMap { + pub fn new() -> ArrayHashMap { + // 初始化数组,包含 100 个桶 + Self { buckets: vec![None; 100] } + } + + /* 哈希函数 */ + fn hash_func(&self, key: i32) -> usize { + key as usize % 100 + } + + /* 查询操作 */ + pub fn get(&self, key: i32) -> Option<&String> { + let index = self.hash_func(key); + self.buckets[index].as_ref().map(|pair| &pair.val) + } + + /* 添加操作 */ + pub fn put(&mut self, key: i32, val: &str) { + let index = self.hash_func(key); + self.buckets[index] = Some(Pair { + key, + val: val.to_string(), + }); + } + + /* 删除操作 */ + pub fn remove(&mut self, key: i32) { + let index = self.hash_func(key); + // 置为 None ,代表删除 + self.buckets[index] = None; + } + + /* 获取所有键值对 */ + pub fn entry_set(&self) -> Vec<&Pair> { + self.buckets.iter().filter_map(|pair| pair.as_ref()).collect() + } + + /* 获取所有键 */ + pub fn key_set(&self) -> Vec<&i32> { + self.buckets.iter().filter_map(|pair| pair.as_ref().map(|pair| &pair.key)).collect() + } + + /* 获取所有值 */ + pub fn value_set(&self) -> Vec<&String> { + self.buckets.iter().filter_map(|pair| pair.as_ref().map(|pair| &pair.val)).collect() + } + + /* 打印哈希表 */ + pub fn print(&self) { + for pair in self.entry_set() { + println!("{} -> {}", pair.key, pair.val); + } + } + } + ``` + ## 6.1.3.   哈希冲突与扩容 本质上看,哈希函数的作用是将所有 `key` 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,**理论上一定存在“多个输入对应相同输出”的情况**。 diff --git a/chapter_heap/build_heap.md b/chapter_heap/build_heap.md index c5277520d..48eec419f 100644 --- a/chapter_heap/build_heap.md +++ b/chapter_heap/build_heap.md @@ -176,6 +176,21 @@ comments: true } ``` +=== "Rust" + + ```rust title="my_heap.rs" + /* 构造方法,根据输入列表建堆 */ + fn new(nums: Vec) -> Self { + // 将列表元素原封不动添加进堆 + let mut heap = MaxHeap { max_heap: nums }; + // 堆化除叶节点以外的其他所有节点 + for i in (0..=Self::parent(heap.size() - 1)).rev() { + heap.sift_down(i); + } + heap + } + ``` + ## 8.2.3.   复杂度分析 为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。 diff --git a/chapter_heap/heap.md b/chapter_heap/heap.md index 2983e84a9..7c10098f4 100644 --- a/chapter_heap/heap.md +++ b/chapter_heap/heap.md @@ -313,6 +313,12 @@ comments: true // Dart 未提供内置 Heap 类 ``` +=== "Rust" + + ```rust title="heap.rs" + + ``` + ## 8.1.2.   堆的实现 下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。 @@ -539,6 +545,25 @@ comments: true } ``` +=== "Rust" + + ```rust title="my_heap.rs" + /* 获取左子节点索引 */ + fn left(i: usize) -> usize { + 2 * i + 1 + } + + /* 获取右子节点索引 */ + fn right(i: usize) -> usize { + 2 * i + 2 + } + + /* 获取父节点索引 */ + fn parent(i: usize) -> usize { + (i - 1) / 2 // 向下整除 + } + ``` + ### 访问堆顶元素 堆顶元素即为二叉树的根节点,也就是列表的首个元素。 @@ -641,6 +666,15 @@ comments: true } ``` +=== "Rust" + + ```rust title="my_heap.rs" + /* 访问堆顶元素 */ + fn peek(&self) -> Option { + self.max_heap.first().copied() + } + ``` + ### 元素入堆 给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 Heapify」。 @@ -966,6 +1000,38 @@ comments: true [class]{MaxHeap}-[func]{siftUp} ``` +=== "Rust" + + ```rust title="my_heap.rs" + /* 元素入堆 */ + fn push(&mut self, val: i32) { + // 添加节点 + self.max_heap.push(val); + // 从底至顶堆化 + self.sift_up(self.size() - 1); + } + + /* 从节点 i 开始,从底至顶堆化 */ + fn sift_up(&mut self, mut i: usize) { + loop { + // 节点 i 已经是堆顶节点了,结束堆化 + if i == 0 { + break; + } + // 获取节点 i 的父节点 + let p = Self::parent(i); + // 当“节点无需修复”时,结束堆化 + if self.max_heap[i] <= self.max_heap[p] { + break; + } + // 交换两节点 + self.swap(i, p); + // 循环向上堆化 + i = p; + } + } + ``` + ### 堆顶元素出堆 堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采取以下操作步骤: @@ -1422,6 +1488,48 @@ comments: true [class]{MaxHeap}-[func]{siftDown} ``` +=== "Rust" + + ```rust title="my_heap.rs" + /* 元素出堆 */ + fn pop(&mut self) -> i32 { + // 判空处理 + if self.is_empty() { + panic!("index out of bounds"); + } + // 交换根节点与最右叶节点(即交换首元素与尾元素) + self.swap(0, self.size() - 1); + // 删除节点 + let val = self.max_heap.remove(self.size() - 1); + // 从顶至底堆化 + self.sift_down(0); + // 返回堆顶元素 + val + } + + /* 从节点 i 开始,从顶至底堆化 */ + fn sift_down(&mut self, mut i: usize) { + loop { + // 判断节点 i, l, r 中值最大的节点,记为 ma + let (l, r, mut ma) = (Self::left(i), Self::right(i), i); + if l < self.size() && self.max_heap[l] > self.max_heap[ma] { + ma = l; + } + if r < self.size() && self.max_heap[r] > self.max_heap[ma] { + ma = r; + } + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + if ma == i { + break; + } + // 交换两节点 + self.swap(i, ma); + // 循环向下堆化 + i = ma; + } + } + ``` + ## 8.1.3.   堆常见应用 - **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。 diff --git a/chapter_heap/top_k.md b/chapter_heap/top_k.md index ab2933d97..a0d8bfdf2 100644 --- a/chapter_heap/top_k.md +++ b/chapter_heap/top_k.md @@ -213,3 +213,26 @@ comments: true ```dart title="top_k.dart" [class]{}-[func]{top_k_heap} ``` + +=== "Rust" + + ```rust title="top_k.rs" + /* 基于堆查找数组中最大的 k 个元素 */ + fn top_k_heap(nums: Vec, k: usize) -> BinaryHeap> { + // Rust 的 BinaryHeap 是大顶堆,使用 Reverse 将元素大小反转 + let mut heap = BinaryHeap::>::new(); + // 将数组的前 k 个元素入堆 + for &num in nums.iter().take(k) { + heap.push(Reverse(num)); + } + // 从第 k+1 个元素开始,保持堆的长度为 k + for &num in nums.iter().skip(k) { + // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆 + if num > heap.peek().unwrap().0 { + heap.pop(); + heap.push(Reverse(num)); + } + } + heap + } + ``` diff --git a/chapter_preface/suggestions.md b/chapter_preface/suggestions.md index efafaeab8..3924d7896 100644 --- a/chapter_preface/suggestions.md +++ b/chapter_preface/suggestions.md @@ -158,6 +158,12 @@ comments: true */ ``` +=== "Rust" + + ```rust title="" + + ``` + ## 0.2.2.   在动画图解中高效学习 相较于文字,视频和图片具有更高的信息密度和结构化程度,因此更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。 diff --git a/chapter_searching/binary_search.md b/chapter_searching/binary_search.md index 759b9c546..7543d5f7f 100755 --- a/chapter_searching/binary_search.md +++ b/chapter_searching/binary_search.md @@ -305,6 +305,30 @@ comments: true } ``` +=== "Rust" + + ```rust title="binary_search.rs" + /* 二分查找(双闭区间) */ + fn binary_search(nums: &[i32], target: i32) -> i32 { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + let mut i = 0; + let mut j = nums.len() - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while i <= j { + let m = i + (j - i) / 2; // 计算中点索引 m + if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + } else if nums[m] > target { // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + } else { // 找到目标元素,返回其索引 + return m as i32; + } + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + 时间复杂度为 $O(\log n)$ 。每轮缩小一半区间,因此二分循环次数为 $\log_2 n$ 。 空间复杂度为 $O(1)$ 。指针 `i` , `j` 使用常数大小空间。 @@ -572,6 +596,30 @@ comments: true } ``` +=== "Rust" + + ```rust title="binary_search.rs" + /* 二分查找(左闭右开) */ + fn binary_search_lcro(nums: &[i32], target: i32) -> i32 { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + let mut i = 0; + let mut j = nums.len(); + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while i < j { + let m = i + (j - i) / 2; // 计算中点索引 m + if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1; + } else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中 + j = m - 1; + } else { // 找到目标元素,返回其索引 + return m as i32; + } + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + 如下图所示,在两种区间表示下,二分查找算法的初始化、循环条件和缩小区间操作皆有所不同。 在“双闭区间”表示法中,由于左右边界都被定义为闭区间,因此指针 $i$ 和 $j$ 缩小区间操作也是对称的。这样更不容易出错。因此,**我们通常采用“双闭区间”的写法**。 diff --git a/chapter_searching/binary_search_edge.md b/chapter_searching/binary_search_edge.md index 587fd1c04..7c8c12fea 100644 --- a/chapter_searching/binary_search_edge.md +++ b/chapter_searching/binary_search_edge.md @@ -288,6 +288,30 @@ comments: true } ``` +=== "Rust" + + ```rust title="binary_search_edge.rs" + /* 二分查找最左一个元素 */ + fn binary_search_left_edge(nums: &[i32], target: i32) -> i32 { + let mut i = 0; + let mut j = nums.len() as i32 - 1; // 初始化双闭区间 [0, n-1] + while i <= j { + let m = i + (j - i) / 2; // 计算中点索引 m + if nums[m as usize] < target { + i = m + 1; // target 在区间 [m+1, j] 中 + } else if nums[m as usize] > target { + j = m - 1; // target 在区间 [i, m-1] 中 + } else { + j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中 + } + } + if i == nums.len() as i32 || nums[i as usize] != target { + return -1; // 未找到目标元素,返回 -1 + } + i + } + ``` + ## 10.2.3.   查找右边界 类似地,我们也可以二分查找最右边的 `target` 。当 `nums[m] == target` 时,说明大于 `target` 的元素在区间 $[m + 1, j]$ 中,因此执行 `i = m + 1` ,**使得指针 $i$ 向大于 `target` 的元素靠近**。 @@ -524,6 +548,30 @@ comments: true } ``` +=== "Rust" + + ```rust title="binary_search_edge.rs" + /* 二分查找最右一个元素 */ + fn binary_search_right_edge(nums: &[i32], target: i32) -> i32 { + let mut i = 0; + let mut j = nums.len() as i32 - 1; // 初始化双闭区间 [0, n-1] + while i <= j { + let m = i + (j - i) / 2; // 计算中点索引 m + if nums[m as usize] < target { + i = m + 1; // target 在区间 [m+1, j] 中 + } else if nums[m as usize] > target { + j = m - 1; // target 在区间 [i, m-1] 中 + } else { + i = m + 1; // 首个大于 target 的元素在区间 [m+1, j] 中 + } + } + if j < 0 || nums[j as usize] != target { + return -1; // 未找到目标元素,返回 -1 + } + j + } + ``` + 观察下图,搜索最右边元素时指针 $j$ 的作用与搜索最左边元素时指针 $i$ 的作用一致,反之亦然。也就是说,**搜索最左边元素和最右边元素的实现是镜像对称的**。 ![查找最左边和最右边元素的对称性](binary_search_edge.assets/binary_search_left_right_edge.png) diff --git a/chapter_searching/replace_linear_by_hashing.md b/chapter_searching/replace_linear_by_hashing.md index 2f40a0093..c8cf1148a 100755 --- a/chapter_searching/replace_linear_by_hashing.md +++ b/chapter_searching/replace_linear_by_hashing.md @@ -208,6 +208,24 @@ comments: true } ``` +=== "Rust" + + ```rust title="two_sum.rs" + /* 方法一:暴力枚举 */ + pub fn two_sum_brute_force(nums: &Vec, target: i32) -> Option> { + let size = nums.len(); + // 两层循环,时间复杂度 O(n^2) + for i in 0..size - 1 { + for j in i + 1..size { + if nums[i] + nums[j] == target { + return Some(vec![i as i32, j as i32]); + } + } + } + None + } + ``` + 此方法的时间复杂度为 $O(n^2)$ ,空间复杂度为 $O(1)$ ,在大数据量下非常耗时。 ## 10.3.2.   哈希查找:以空间换时间 @@ -462,6 +480,24 @@ comments: true } ``` +=== "Rust" + + ```rust title="two_sum.rs" + /* 方法二:辅助哈希表 */ + pub fn two_sum_hash_table(nums: &Vec, target: i32) -> Option> { + // 辅助哈希表,空间复杂度 O(n) + let mut dic = HashMap::new(); + // 单层循环,时间复杂度 O(n) + for (i, num) in nums.iter().enumerate() { + match dic.get(&(target - num)) { + Some(v) => return Some(vec![*v as i32, i as i32]), + None => dic.insert(num, i as i32) + }; + } + None + } + ``` + 此方法通过哈希查找将时间复杂度从 $O(n^2)$ 降低至 $O(n)$ ,大幅提升运行效率。 由于需要维护一个额外的哈希表,因此空间复杂度为 $O(n)$ 。**尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法**。 diff --git a/chapter_sorting/bubble_sort.md b/chapter_sorting/bubble_sort.md index f55a3065a..6af05fcca 100755 --- a/chapter_sorting/bubble_sort.md +++ b/chapter_sorting/bubble_sort.md @@ -255,6 +255,26 @@ comments: true } ``` +=== "Rust" + + ```rust title="bubble_sort.rs" + /* 冒泡排序 */ + fn bubble_sort(nums: &mut [i32]) { + // 外循环:未排序区间为 [0, i] + for i in (1..nums.len()).rev() { + // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 + for j in 0..i { + if nums[j] > nums[j + 1] { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + ## 11.3.2.   效率优化 我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。 @@ -514,6 +534,29 @@ comments: true } ``` +=== "Rust" + + ```rust title="bubble_sort.rs" + /* 冒泡排序(标志优化) */ + fn bubble_sort_with_flag(nums: &mut [i32]) { + // 外循环:未排序区间为 [0, i] + for i in (1..nums.len()).rev() { + let mut flag = false; // 初始化标志位 + // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 + for j in 0..i { + if nums[j] > nums[j + 1] { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 记录交换元素 + } + } + if !flag {break}; // 此轮冒泡未交换任何元素,直接跳出 + } + } + ``` + ## 11.3.3.   算法特性 - **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。 diff --git a/chapter_sorting/bucket_sort.md b/chapter_sorting/bucket_sort.md index 37d511a26..7f0386c37 100644 --- a/chapter_sorting/bucket_sort.md +++ b/chapter_sorting/bucket_sort.md @@ -361,6 +361,37 @@ comments: true } ``` +=== "Rust" + + ```rust title="bucket_sort.rs" + /* 桶排序 */ + fn bucket_sort(nums: &mut [f64]) { + // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素 + let k = nums.len() / 2; + let mut buckets = vec![vec![]; k]; + // 1. 将数组元素分配到各个桶中 + for &mut num in &mut *nums { + // 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1] + let i = (num * k as f64) as usize; + // 将 num 添加进桶 i + buckets[i].push(num); + } + // 2. 对各个桶执行排序 + for bucket in &mut buckets { + // 使用内置排序函数,也可以替换成其他排序算法 + bucket.sort_by(|a, b| a.partial_cmp(b).unwrap()); + } + // 3. 遍历桶合并结果 + let mut i = 0; + for bucket in &mut buckets { + for &mut num in bucket { + nums[i] = num; + i += 1; + } + } + } + ``` + !!! question "桶排序的适用场景是什么?" 桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。 diff --git a/chapter_sorting/counting_sort.md b/chapter_sorting/counting_sort.md index 17a073106..d0bff8009 100644 --- a/chapter_sorting/counting_sort.md +++ b/chapter_sorting/counting_sort.md @@ -292,6 +292,31 @@ comments: true } ``` +=== "Rust" + + ```rust title="counting_sort.rs" + /* 计数排序 */ + // 简单实现,无法用于排序对象 + fn counting_sort_naive(nums: &mut [i32]) { + // 1. 统计数组最大元素 m + let m = *nums.into_iter().max().unwrap(); + // 2. 统计各数字的出现次数 + // counter[num] 代表 num 的出现次数 + let mut counter = vec![0; m as usize + 1]; + for &num in &*nums { + counter[num as usize] += 1; + } + // 3. 遍历 counter ,将各元素填入原数组 nums + let mut i = 0; + for num in 0..m + 1 { + for _ in 0..counter[num as usize] { + nums[i] = num; + i += 1; + } + } + } + ``` + !!! note "计数排序与桶排序的联系" 从桶排序的角度看,我们可以将计数排序中的计数数组 `counter` 的每个索引视为一个桶,将统计数量的过程看作是将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。 @@ -710,6 +735,41 @@ $$ } ``` +=== "Rust" + + ```rust title="counting_sort.rs" + /* 计数排序 */ + // 完整实现,可排序对象,并且是稳定排序 + fn counting_sort(nums: &mut [i32]) { + // 1. 统计数组最大元素 m + let m = *nums.into_iter().max().unwrap(); + // 2. 统计各数字的出现次数 + // counter[num] 代表 num 的出现次数 + let mut counter = vec![0; m as usize + 1]; + for &num in &*nums { + counter[num as usize] += 1; + } + // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引” + // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引 + for i in 0..m as usize { + counter[i + 1] += counter[i]; + } + // 4. 倒序遍历 nums ,将各元素填入结果数组 res + // 初始化数组 res 用于记录结果 + let n = nums.len(); + let mut res = vec![0; n]; + for i in (0..n).rev() { + let num = nums[i]; + res[counter[num as usize] - 1] = num; // 将 num 放置到对应索引处 + counter[num as usize] -= 1; // 令前缀和自减 1 ,得到下次放置 num 的索引 + } + // 使用结果数组 res 覆盖原数组 nums + for i in 0..n { + nums[i] = res[i]; + } + } + ``` + ## 11.9.3.   算法特性 - **时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。 diff --git a/chapter_sorting/heap_sort.md b/chapter_sorting/heap_sort.md index 23d074a43..1b120b6d3 100644 --- a/chapter_sorting/heap_sort.md +++ b/chapter_sorting/heap_sort.md @@ -491,6 +491,53 @@ comments: true } ``` +=== "Rust" + + ```rust title="heap_sort.rs" + /* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */ + fn sift_down(nums: &mut [i32], n: usize, mut i: usize) { + loop { + // 判断节点 i, l, r 中值最大的节点,记为 ma + let l = 2 * i + 1; + let r = 2 * i + 2; + let mut ma = i; + if l < n && nums[l] > nums[ma] { + ma = l; + } + if r < n && nums[r] > nums[ma] { + ma = r; + } + // 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + if ma == i { + break; + } + // 交换两节点 + let temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // 循环向下堆化 + i = ma; + } + } + + /* 堆排序 */ + fn heap_sort(nums: &mut [i32]) { + // 建堆操作:堆化除叶节点以外的其他所有节点 + for i in (0..=nums.len() / 2 - 1).rev() { + sift_down(nums, nums.len(), i); + } + // 从堆中提取最大元素,循环 n-1 轮 + for i in (1..=nums.len() - 1).rev() { + // 交换根节点与最右叶节点(即交换首元素与尾元素) + let tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // 以根节点为起点,从顶至底进行堆化 + sift_down(nums, i, 0); + } + } + ``` + ## 11.7.2.   算法特性 - **时间复杂度 $O(n \log n)$ 、非自适应排序** :建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。 diff --git a/chapter_sorting/insertion_sort.md b/chapter_sorting/insertion_sort.md index b351d030e..15dfaee2b 100755 --- a/chapter_sorting/insertion_sort.md +++ b/chapter_sorting/insertion_sort.md @@ -230,6 +230,24 @@ comments: true } ``` +=== "Rust" + + ```rust title="insertion_sort.rs" + /* 插入排序 */ + fn insertion_sort(nums: &mut [i32]) { + // 外循环:已排序元素数量为 1, 2, ..., n + for i in 1..nums.len() { + let (base, mut j) = (nums[i], (i - 1) as i32); + // 内循环:将 base 插入到已排序部分的正确位置 + while j >= 0 && nums[j as usize] > base { + nums[(j + 1) as usize] = nums[j as usize]; // 将 nums[j] 向右移动一位 + j -= 1; + } + nums[(j + 1) as usize] = base; // 将 base 赋值到正确位置 + } + } + ``` + ## 11.4.2.   算法特性 - **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。 diff --git a/chapter_sorting/merge_sort.md b/chapter_sorting/merge_sort.md index 2c624084d..744e4eaa4 100755 --- a/chapter_sorting/merge_sort.md +++ b/chapter_sorting/merge_sort.md @@ -570,6 +570,54 @@ comments: true } ``` +=== "Rust" + + ```rust title="merge_sort.rs" + /* 合并左子数组和右子数组 */ + // 左子数组区间 [left, mid] + // 右子数组区间 [mid + 1, right] + fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) { + // 初始化辅助数组 + let tmp: Vec = nums[left..right + 1].to_vec(); + // 左子数组的起始索引和结束索引 + let (left_start, left_end) = (left - left, mid - left); + // 右子数组的起始索引和结束索引 + let (right_start, right_end) = (mid + 1 - left, right-left); + // i, j 分别指向左子数组、右子数组的首元素 + let (mut l_corrent, mut r_corrent) = (left_start, right_start); + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for k in left..right + 1 { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if l_corrent > left_end { + nums[k] = tmp[r_corrent]; + r_corrent += 1; + } + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + else if r_corrent > right_end || tmp[l_corrent] <= tmp[r_corrent] { + nums[k] = tmp[l_corrent]; + l_corrent += 1; + } + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else { + nums[k] = tmp[r_corrent]; + r_corrent += 1; + } + } + } + + /* 归并排序 */ + fn merge_sort(left: usize, right: usize, nums: &mut [i32]) { + // 终止条件 + if left >= right { return; } // 当子数组长度为 1 时终止递归 + // 划分阶段 + let mid = (left + right) / 2; // 计算中点 + merge_sort(left, mid, nums); // 递归左子数组 + merge_sort(mid + 1, right, nums); // 递归右子数组 + // 合并阶段 + merge(nums, left, mid, right); + } + ``` + 合并方法 `merge()` 代码中的难点包括: - **在阅读代码时,需要特别注意各个变量的含义**。`nums` 的待合并区间为 `[left, right]` ,但由于 `tmp` 仅复制了 `nums` 该区间的元素,因此 `tmp` 对应区间为 `[0, right - left]` 。 diff --git a/chapter_sorting/quick_sort.md b/chapter_sorting/quick_sort.md index c4697d9e8..eb0aabb25 100755 --- a/chapter_sorting/quick_sort.md +++ b/chapter_sorting/quick_sort.md @@ -337,6 +337,27 @@ comments: true } ``` +=== "Rust" + + ```rust title="quick_sort.rs" + /* 哨兵划分 */ + fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { + // 以 nums[left] 作为基准数 + let (mut i, mut j) = (left, right); + while i < j { + while i < j && nums[j] >= nums[left] { + j -= 1; // 从右向左找首个小于基准数的元素 + } + while i < j && nums[i] <= nums[left] { + i += 1; // 从左向右找首个大于基准数的元素 + } + nums.swap(i, j); // 交换这两个元素 + } + nums.swap(i, left); // 将基准数交换至两子数组的分界线 + i // 返回基准数的索引 + } + ``` + ## 11.5.1.   算法流程 1. 首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组。 @@ -546,6 +567,23 @@ comments: true } ``` +=== "Rust" + + ```rust title="quick_sort.rs" + /* 快速排序 */ + pub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) { + // 子数组长度为 1 时终止递归 + if left >= right { + return; + } + // 哨兵划分 + let pivot = Self::partition(nums, left as usize, right as usize) as i32; + // 递归左子数组、右子数组 + Self::quick_sort(left, pivot - 1, nums); + Self::quick_sort(pivot + 1, right, nums); + } + ``` + ## 11.5.2.   算法特性 - **时间复杂度 $O(n \log n)$ 、自适应排序** :在平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。在最差情况下,每轮哨兵划分操作都将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。 @@ -976,6 +1014,43 @@ comments: true } ``` +=== "Rust" + + ```rust title="quick_sort.rs" + /* 选取三个元素的中位数 */ + fn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize { + // 此处使用异或运算来简化代码 + // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if (nums[left] < nums[mid]) ^ (nums[left] < nums[right]) { + return left; + } else if (nums[mid] < nums[left]) ^ (nums[mid] < nums[right]) { + return mid; + } + right + } + + /* 哨兵划分(三数取中值) */ + fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { + // 选取三个候选元素的中位数 + let med = Self::median_three(nums, left, (left + right) / 2, right); + // 将中位数交换至数组最左端 + nums.swap(left, med); + // 以 nums[left] 作为基准数 + let (mut i, mut j) = (left, right); + while i < j { + while i < j && nums[j] >= nums[left] { + j -= 1; // 从右向左找首个小于基准数的元素 + } + while i < j && nums[i] <= nums[left] { + i += 1; // 从左向右找首个大于基准数的元素 + } + nums.swap(i, j); // 交换这两个元素 + } + nums.swap(i, left); // 将基准数交换至两子数组的分界线 + i // 返回基准数的索引 + } + ``` + ## 11.5.5.   尾递归优化 **在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。 @@ -1214,3 +1289,24 @@ comments: true } } ``` + +=== "Rust" + + ```rust title="quick_sort.rs" + /* 快速排序(尾递归优化) */ + pub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) { + // 子数组长度为 1 时终止 + while left < right { + // 哨兵划分操作 + let pivot = Self::partition(nums, left as usize, right as usize) as i32; + // 对两个子数组中较短的那个执行快排 + if pivot - left < right - pivot { + Self::quick_sort(left, pivot - 1, nums); // 递归排序左子数组 + left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right] + } else { + Self::quick_sort(pivot + 1, right, nums); // 递归排序右子数组 + right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1] + } + } + } + ``` diff --git a/chapter_sorting/radix_sort.md b/chapter_sorting/radix_sort.md index 4a5ec4ef6..cf6d84177 100644 --- a/chapter_sorting/radix_sort.md +++ b/chapter_sorting/radix_sort.md @@ -634,6 +634,56 @@ $$ } ``` +=== "Rust" + + ```rust title="radix_sort.rs" + /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + fn digit(num: i32, exp: i32) -> usize { + // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算 + return ((num / exp) % 10) as usize; + } + + /* 计数排序(根据 nums 第 k 位排序) */ + fn counting_sort_digit(nums: &mut [i32], exp: i32) { + // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶 + let mut counter = [0; 10]; + let n = nums.len(); + // 统计 0~9 各数字的出现次数 + for i in 0..n { + let d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d + counter[d] += 1; // 统计数字 d 的出现次数 + } + // 求前缀和,将“出现个数”转换为“数组索引” + for i in 1..10 { + counter[i] += counter[i - 1]; + } + // 倒序遍历,根据桶内统计结果,将各元素填入 res + let mut res = vec![0; n]; + for i in (0..n).rev() { + let d = digit(nums[i], exp); + let j = counter[d] - 1; // 获取 d 在数组中的索引 j + res[j] = nums[i]; // 将当前元素填入索引 j + counter[d] -= 1; // 将 d 的数量减 1 + } + // 使用结果覆盖原数组 nums + for i in 0..n { + nums[i] = res[i]; + } + } + + /* 基数排序 */ + fn radix_sort(nums: &mut [i32]) { + // 获取数组的最大元素,用于判断最大位数 + let m = *nums.into_iter().max().unwrap(); + // 按照从低位到高位的顺序遍历 + let mut exp = 1; + while exp <= m { + counting_sort_digit(nums, exp); + exp *= 10; + } + } + ``` + !!! question "为什么从最低位开始排序?" 在连续的排序轮次中,后一轮排序会覆盖前一轮排序的结果。举例来说,如果第一轮排序结果 $a < b$ ,而第二轮排序结果 $a > b$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。 diff --git a/chapter_sorting/selection_sort.md b/chapter_sorting/selection_sort.md index 895922af8..5a1063158 100644 --- a/chapter_sorting/selection_sort.md +++ b/chapter_sorting/selection_sort.md @@ -261,6 +261,27 @@ comments: true } ``` +=== "Rust" + + ```rust title="selection_sort.rs" + /* 选择排序 */ + fn selection_sort(nums: &mut [i32]) { + let n = nums.len(); + // 外循环:未排序区间为 [i, n-1] + for i in 0..n-1 { + // 内循环:找到未排序区间内的最小元素 + let mut k = i; + for j in i+1..n { + if nums[j] < nums[k] { + k = j; // 记录最小元素的索引 + } + } + // 将该最小元素与未排序区间的首个元素交换 + nums.swap(i, k); + } + } + ``` + ## 11.2.1.   算法特性 - **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。 diff --git a/chapter_stack_and_queue/deque.md b/chapter_stack_and_queue/deque.md index 09845e342..28c1698f4 100644 --- a/chapter_stack_and_queue/deque.md +++ b/chapter_stack_and_queue/deque.md @@ -318,6 +318,12 @@ comments: true bool isEmpty = deque.isEmpty;W ``` +=== "Rust" + + ```rust title="deque.rs" + + ``` + ## 5.3.2.   双向队列实现 * 双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。 @@ -1795,6 +1801,174 @@ comments: true } ``` +=== "Rust" + + ```rust title="linkedlist_deque.rs" + /* 双向链表节点 */ + pub struct ListNode { + pub val: T, // 节点值 + pub next: Option>>>, // 后继节点引用(指针) + pub prev: Option>>>, // 前驱节点引用(指针) + } + + impl ListNode { + pub fn new(val: T) -> Rc>> { + Rc::new(RefCell::new(ListNode { + val, + next: None, + prev: None, + })) + } + } + + /* 基于双向链表实现的双向队列 */ + #[allow(dead_code)] + pub struct LinkedListDeque { + front: Option>>>, // 头节点 front + rear: Option>>>, // 尾节点 rear + que_size: usize, // 双向队列的长度 + } + + impl LinkedListDeque { + pub fn new() -> Self { + Self { + front: None, + rear: None, + que_size: 0, + } + } + + /* 获取双向队列的长度 */ + pub fn size(&self) -> usize { + return self.que_size; + } + + /* 判断双向队列是否为空 */ + pub fn is_empty(&self) -> bool { + return self.size() == 0; + } + + /* 入队操作 */ + pub fn push(&mut self, num: T, is_front: bool) { + let node = ListNode::new(num); + // 队首入队操作 + if is_front { + match self.front.take() { + // 若链表为空,则令 front, rear 都指向 node + None => { + self.rear = Some(node.clone()); + self.front = Some(node); + } + // 将 node 添加至链表头部 + Some(old_front) => { + old_front.borrow_mut().prev = Some(node.clone()); + node.borrow_mut().next = Some(old_front); + self.front = Some(node); // 更新头节点 + } + } + } + // 队尾入队操作 + else { + match self.rear.take() { + // 若链表为空,则令 front, rear 都指向 node + None => { + self.front = Some(node.clone()); + self.rear = Some(node); + } + // 将 node 添加至链表尾部 + Some(old_rear) => { + old_rear.borrow_mut().next = Some(node.clone()); + node.borrow_mut().prev = Some(old_rear); + self.rear = Some(node); // 更新尾节点 + } + } + } + self.que_size += 1; // 更新队列长度 + } + + /* 队首入队 */ + pub fn push_first(&mut self, num: T) { + self.push(num, true); + } + + /* 队尾入队 */ + pub fn push_last(&mut self, num: T) { + self.push(num, false); + } + + /* 出队操作 */ + pub fn pop(&mut self, is_front: bool) -> Option { + // 若队列为空,直接返回 None + if self.is_empty() { + return None + }; + // 队首出队操作 + if is_front { + self.front.take().map(|old_front| { + match old_front.borrow_mut().next.take() { + Some(new_front) => { + new_front.borrow_mut().prev.take(); + self.front = Some(new_front); // 更新头节点 + } + None => { + self.rear.take(); + } + } + self.que_size -= 1; // 更新队列长度 + Rc::try_unwrap(old_front).ok().unwrap().into_inner().val + }) + + } + // 队尾出队操作 + else { + self.rear.take().map(|old_rear| { + match old_rear.borrow_mut().prev.take() { + Some(new_rear) => { + new_rear.borrow_mut().next.take(); + self.rear = Some(new_rear); // 更新尾节点 + } + None => { + self.front.take(); + } + } + self.que_size -= 1; // 更新队列长度 + Rc::try_unwrap(old_rear).ok().unwrap().into_inner().val + }) + } + } + + /* 队首出队 */ + pub fn pop_first(&mut self) -> Option { + return self.pop(true); + } + + /* 队尾出队 */ + pub fn pop_last(&mut self) -> Option { + return self.pop(false); + } + + /* 访问队首元素 */ + pub fn peek_first(&self) -> Option<&Rc>>> { + self.front.as_ref() + } + + /* 访问队尾元素 */ + pub fn peek_last(&self) -> Option<&Rc>>> { + self.rear.as_ref() + } + + /* 返回数组用于打印 */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + if let Some(node) = head { + let mut nums = self.to_array(node.borrow().next.as_ref()); + nums.insert(0, node.borrow().val); + return nums; + } + return Vec::new(); + } + } + ``` + ### 基于数组的实现 与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列。在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法。 @@ -2914,6 +3088,120 @@ comments: true } ``` +=== "Rust" + + ```rust title="array_deque.rs" + /* 基于环形数组实现的双向队列 */ + struct ArrayDeque { + nums: Vec, // 用于存储双向队列元素的数组 + front: usize, // 队首指针,指向队首元素 + que_size: usize, // 双向队列长度 + } + + impl ArrayDeque { + /* 构造方法 */ + pub fn new(capacity: usize) -> Self { + Self { + nums: vec![0; capacity], + front: 0, + que_size: 0, + } + } + + /* 获取双向队列的容量 */ + pub fn capacity(&self) -> usize { + self.nums.len() + } + + /* 获取双向队列的长度 */ + pub fn size(&self) -> usize { + self.que_size + } + + /* 判断双向队列是否为空 */ + pub fn is_empty(&self) -> bool { + self.que_size == 0 + } + + /* 计算环形数组索引 */ + fn index(&self, i: i32) -> usize { + // 通过取余操作实现数组首尾相连 + // 当 i 越过数组尾部后,回到头部 + // 当 i 越过数组头部后,回到尾部 + return ((i + self.capacity() as i32) % self.capacity() as i32) as usize; + } + + /* 队首入队 */ + pub fn push_first(&mut self, num: i32) { + if self.que_size == self.capacity() { + println!("双向队列已满"); + return + } + // 队首指针向左移动一位 + // 通过取余操作,实现 front 越过数组头部后回到尾部 + self.front = self.index(self.front as i32 - 1); + // 将 num 添加至队首 + self.nums[self.front] = num; + self.que_size += 1; + } + + /* 队尾入队 */ + pub fn push_last(&mut self, num: i32) { + if self.que_size == self.capacity() { + println!("双向队列已满"); + return + } + // 计算尾指针,指向队尾索引 + 1 + let rear = self.index(self.front as i32 + self.que_size as i32); + // 将 num 添加至队尾 + self.nums[rear] = num; + self.que_size += 1; + } + + /* 队首出队 */ + fn pop_first(&mut self) -> i32 { + let num = self.peek_first(); + // 队首指针向后移动一位 + self.front = self.index(self.front as i32 + 1); + self.que_size -= 1; + num + } + + /* 队尾出队 */ + fn pop_last(&mut self) -> i32 { + let num = self.peek_last(); + self.que_size -= 1; + num + } + + /* 访问队首元素 */ + fn peek_first(&self) -> i32 { + if self.is_empty() { panic!("双向队列为空") }; + self.nums[self.front] + } + + /* 访问队尾元素 */ + fn peek_last(&self) -> i32 { + if self.is_empty() { panic!("双向队列为空") }; + // 计算尾元素索引 + let last = self.index(self.front as i32 + self.que_size as i32 - 1); + self.nums[last] + } + + /* 返回数组用于打印 */ + fn to_array(&self) -> Vec { + // 仅转换有效长度范围内的列表元素 + let mut res = vec![0; self.que_size]; + let mut j = self.front; + for i in 0..self.que_size { + res[i] = self.nums[self.index(j as i32)]; + j += 1; + } + res + } + } + ``` + ## 5.3.3.   双向队列应用 双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。 diff --git a/chapter_stack_and_queue/queue.md b/chapter_stack_and_queue/queue.md index 6810c9025..79fe7a6b9 100755 --- a/chapter_stack_and_queue/queue.md +++ b/chapter_stack_and_queue/queue.md @@ -285,6 +285,12 @@ comments: true bool isEmpty = queue.isEmpty; ``` +=== "Rust" + + ```rust title="queue.rs" + + ``` + ## 5.2.2.   队列实现 为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列。 @@ -1090,6 +1096,88 @@ comments: true } ``` +=== "Rust" + + ```rust title="linkedlist_queue.rs" + /* 基于链表实现的队列 */ + #[allow(dead_code)] + pub struct LinkedListQueue { + front: Option>>>, // 头节点 front + rear: Option>>>, // 尾节点 rear + que_size: usize, // 队列的长度 + } + + impl LinkedListQueue { + pub fn new() -> Self { + Self { + front: None, + rear: None, + que_size: 0, + } + } + + /* 获取队列的长度 */ + pub fn size(&self) -> usize { + return self.que_size; + } + + /* 判断队列是否为空 */ + pub fn is_empty(&self) -> bool { + return self.size() == 0; + } + + /* 入队 */ + pub fn push(&mut self, num: T) { + // 尾节点后添加 num + let new_rear = ListNode::new(num); + match self.rear.take() { + // 如果队列不为空,则将该节点添加到尾节点后 + Some(old_rear) => { + old_rear.borrow_mut().next = Some(new_rear.clone()); + self.rear = Some(new_rear); + } + // 如果队列为空,则令头、尾节点都指向该节点 + None => { + self.front = Some(new_rear.clone()); + self.rear = Some(new_rear); + } + } + self.que_size += 1; + } + + /* 出队 */ + pub fn pop(&mut self) -> Option { + self.front.take().map(|old_front| { + match old_front.borrow_mut().next.take() { + Some(new_front) => { + self.front = Some(new_front); + } + None => { + self.rear.take(); + } + } + self.que_size -= 1; + Rc::try_unwrap(old_front).ok().unwrap().into_inner().val + }) + } + + /* 访问队首元素 */ + pub fn peek(&self) -> Option<&Rc>>> { + self.front.as_ref() + } + + /* 将链表转化为 Array 并返回 */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + if let Some(node) = head { + let mut nums = self.to_array(node.borrow().next.as_ref()); + nums.insert(0, node.borrow().val); + return nums; + } + return Vec::new(); + } + } + ``` + ### 基于数组的实现 由于数组删除首元素的时间复杂度为 $O(n)$ ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。 @@ -1927,6 +2015,88 @@ comments: true } ``` +=== "Rust" + + ```rust title="array_queue.rs" + /* 基于环形数组实现的队列 */ + struct ArrayQueue { + nums: Vec, // 用于存储队列元素的数组 + front: i32, // 队首指针,指向队首元素 + que_size: i32, // 队列长度 + que_capacity: i32, // 队列容量 + } + + impl ArrayQueue { + /* 构造方法 */ + fn new(capacity: i32) -> ArrayQueue { + ArrayQueue { + nums: vec![0; capacity as usize], + front: 0, + que_size: 0, + que_capacity: capacity, + } + } + + /* 获取队列的容量 */ + fn capacity(&self) -> i32 { + self.que_capacity + } + + /* 获取队列的长度 */ + fn size(&self) -> i32 { + self.que_size + } + + /* 判断队列是否为空 */ + fn is_empty(&self) -> bool { + self.que_size == 0 + } + + /* 入队 */ + fn push(&mut self, num: i32) { + if self.que_size == self.capacity() { + println!("队列已满"); + return; + } + // 计算尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + let rear = (self.front + self.que_size) % self.que_capacity; + // 将 num 添加至队尾 + self.nums[rear as usize] = num; + self.que_size += 1; + } + + /* 出队 */ + fn pop(&mut self) -> i32 { + let num = self.peek(); + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + self.front = (self.front + 1) % self.que_capacity; + self.que_size -= 1; + num + } + + /* 访问队首元素 */ + fn peek(&self) -> i32 { + if self.is_empty() { + panic!("index out of bounds"); + } + self.nums[self.front as usize] + } + + /* 返回数组 */ + fn to_vector(&self) -> Vec { + let cap = self.que_capacity; + let mut j = self.front; + let mut arr = vec![0; self.que_size as usize]; + for i in 0..self.que_size { + arr[i as usize] = self.nums[(j % cap) as usize]; + j += 1; + } + arr + } + } + ``` + 以上实现的队列仍然具有局限性,即其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的同学可以尝试自行实现。 两种实现的对比结论与栈一致,在此不再赘述。 diff --git a/chapter_stack_and_queue/stack.md b/chapter_stack_and_queue/stack.md index 62f3a539b..6bad46658 100755 --- a/chapter_stack_and_queue/stack.md +++ b/chapter_stack_and_queue/stack.md @@ -283,6 +283,12 @@ comments: true bool isEmpty = stack.isEmpty; ``` +=== "Rust" + + ```rust title="stack.rs" + + ``` + ## 5.1.2.   栈的实现 为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。 @@ -989,6 +995,75 @@ comments: true } ``` +=== "Rust" + + ```rust title="linkedlist_stack.rs" + /* 基于链表实现的栈 */ + #[allow(dead_code)] + pub struct LinkedListStack { + stack_peek: Option>>>, // 将头节点作为栈顶 + stk_size: usize, // 栈的长度 + } + + impl LinkedListStack { + pub fn new() -> Self { + Self { + stack_peek: None, + stk_size: 0, + } + } + + /* 获取栈的长度 */ + pub fn size(&self) -> usize { + return self.stk_size; + } + + /* 判断栈是否为空 */ + pub fn is_empty(&self) -> bool { + return self.size() == 0; + } + + /* 入栈 */ + pub fn push(&mut self, num: T) { + let node = ListNode::new(num); + node.borrow_mut().next = self.stack_peek.take(); + self.stack_peek = Some(node); + self.stk_size += 1; + } + + /* 出栈 */ + pub fn pop(&mut self) -> Option { + self.stack_peek.take().map(|old_head| { + match old_head.borrow_mut().next.take() { + Some(new_head) => { + self.stack_peek = Some(new_head); + } + None => { + self.stack_peek = None; + } + } + self.stk_size -= 1; + Rc::try_unwrap(old_head).ok().unwrap().into_inner().val + }) + } + + /* 访问栈顶元素 */ + pub fn peek(&self) -> Option<&Rc>>> { + self.stack_peek.as_ref() + } + + /* 将 List 转化为 Array 并返回 */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + if let Some(node) = head { + let mut nums = self.to_array(node.borrow().next.as_ref()); + nums.push(node.borrow().val); + return nums; + } + return Vec::new(); + } + } + ``` + ### 基于数组的实现 在基于「数组」实现栈时,我们可以将数组的尾部作为栈顶。在这样的设计下,入栈与出栈操作就分别对应在数组尾部添加元素与删除元素,时间复杂度都为 $O(1)$ 。 @@ -1542,6 +1617,56 @@ comments: true } ``` +=== "Rust" + + ```rust title="array_stack.rs" + /* 基于数组实现的栈 */ + struct ArrayStack { + stack: Vec, + } + + impl ArrayStack { + /* 初始化栈 */ + fn new() -> ArrayStack { + ArrayStack:: { stack: Vec::::new() } + } + + /* 获取栈的长度 */ + fn size(&self) -> usize { + self.stack.len() + } + + /* 判断栈是否为空 */ + fn is_empty(&self) -> bool { + self.size() == 0 + } + + /* 入栈 */ + fn push(&mut self, num: T) { + self.stack.push(num); + } + + /* 出栈 */ + fn pop(&mut self) -> Option { + match self.stack.pop() { + Some(num) => Some(num), + None => None, + } + } + + /* 访问栈顶元素 */ + fn peek(&self) -> Option<&T> { + if self.is_empty() { panic!("栈为空") }; + self.stack.last() + } + + /* 返回 &Vec */ + fn to_array(&self) -> &Vec { + &self.stack + } + } + ``` + ## 5.1.3.   两种实现对比 ### 支持操作 diff --git a/chapter_tree/array_representation_of_tree.md b/chapter_tree/array_representation_of_tree.md index 1a72f2958..2d7541e2f 100644 --- a/chapter_tree/array_representation_of_tree.md +++ b/chapter_tree/array_representation_of_tree.md @@ -116,6 +116,12 @@ comments: true List tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; ``` +=== "Rust" + + ```rust title="" + + ``` + ![任意类型二叉树的数组表示](array_representation_of_tree.assets/array_representation_with_empty.png)

Fig. 任意类型二叉树的数组表示

@@ -620,6 +626,107 @@ comments: true [class]{ArrayBinaryTree}-[func]{} ``` +=== "Rust" + + ```rust title="array_binary_tree.rs" + /* 数组表示下的二叉树类 */ + struct ArrayBinaryTree { + tree: Vec>, + } + + impl ArrayBinaryTree { + /* 构造方法 */ + fn new(arr: Vec>) -> Self { + Self { tree: arr } + } + + /* 节点数量 */ + fn size(&self) -> i32 { + self.tree.len() as i32 + } + + /* 获取索引为 i 节点的值 */ + fn val(&self, i: i32) -> Option { + // 若索引越界,则返回 None ,代表空位 + if i < 0 || i >= self.size() { + None + } else { + self.tree[i as usize] + } + } + + /* 获取索引为 i 节点的左子节点的索引 */ + fn left(&self, i: i32) -> i32 { + 2 * i + 1 + } + + /* 获取索引为 i 节点的右子节点的索引 */ + fn right(&self, i: i32) -> i32 { + 2 * i + 2 + } + + /* 获取索引为 i 节点的父节点的索引 */ + fn parent(&self, i: i32) -> i32 { + (i - 1) / 2 + } + + /* 层序遍历 */ + fn level_order(&self) -> Vec { + let mut res = vec![]; + // 直接遍历数组 + for i in 0..self.size() { + if let Some(val) = self.val(i) { + res.push(val) + } + } + res + } + + /* 深度优先遍历 */ + fn dfs(&self, i: i32, order: &str, res: &mut Vec) { + if self.val(i).is_none() { + return; + } + let val = self.val(i).unwrap(); + // 前序遍历 + if order == "pre" { + res.push(val); + } + self.dfs(self.left(i), order, res); + // 中序遍历 + if order == "in" { + res.push(val); + } + self.dfs(self.right(i), order, res); + // 后序遍历 + if order == "post" { + res.push(val); + } + } + + /* 前序遍历 */ + fn pre_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "pre", &mut res); + res + } + + /* 中序遍历 */ + fn in_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "in", &mut res); + res + } + + /* 后序遍历 */ + fn post_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "post", &mut res); + res + } + } + ``` + ## 7.3.3.   优势与局限性 二叉树的数组表示的优点包括: diff --git a/chapter_tree/avl_tree.md b/chapter_tree/avl_tree.md index 0efb8fb42..9c23e35d5 100644 --- a/chapter_tree/avl_tree.md +++ b/chapter_tree/avl_tree.md @@ -190,6 +190,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Rust" + + ```rust title="" + + ``` + 「节点高度」是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。 === "Java" @@ -388,6 +394,29 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Rust" + + ```rust title="avl_tree.rs" + /* 获取节点高度 */ + fn height(node: OptionTreeNodeRc) -> i32 { + // 空节点高度为 -1 ,叶节点高度为 0 + match node { + Some(node) => node.borrow().height, + None => -1, + } + } + + /* 更新节点高度 */ + fn update_height(node: OptionTreeNodeRc) { + if let Some(node) = node { + let left = node.borrow().left.clone(); + let right = node.borrow().right.clone(); + // 节点高度等于最高子树高度 + 1 + node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1; + } + } + ``` + ### 节点平衡因子 节点的「平衡因子 Balance Factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。 @@ -530,6 +559,22 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Rust" + + ```rust title="avl_tree.rs" + /* 获取平衡因子 */ + fn balance_factor(node: OptionTreeNodeRc) -> i32 { + match node { + // 空节点平衡因子为 0 + None => 0, + // 节点平衡因子 = 左子树高度 - 右子树高度 + Some(node) => { + Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone()) + } + } + } + ``` + !!! note 设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。 @@ -762,6 +807,29 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Rust" + + ```rust title="avl_tree.rs" + /* 右旋操作 */ + fn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + match node { + Some(node) => { + let child = node.borrow().left.clone().unwrap(); + let grand_child = child.borrow().right.clone(); + // 以 child 为原点,将 node 向右旋转 + child.borrow_mut().right = Some(node.clone()); + node.borrow_mut().left = grand_child; + // 更新节点高度 + Self::update_height(Some(node)); + Self::update_height(Some(child.clone())); + // 返回旋转后子树的根节点 + Some(child) + } + None => None, + } + } + ``` + ### 左旋 相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。 @@ -976,6 +1044,29 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Rust" + + ```rust title="avl_tree.rs" + /* 左旋操作 */ + fn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + match node { + Some(node) => { + let child = node.borrow().right.clone().unwrap(); + let grand_child = child.borrow().left.clone(); + // 以 child 为原点,将 node 向左旋转 + child.borrow_mut().left = Some(node.clone()); + node.borrow_mut().right = grand_child; + // 更新节点高度 + Self::update_height(Some(node)); + Self::update_height(Some(child.clone())); + // 返回旋转后子树的根节点 + Some(child) + } + None => None, + } + } + ``` + ### 先左旋后右旋 对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 @@ -1385,6 +1476,45 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Rust" + + ```rust title="avl_tree.rs" + /* 执行旋转操作,使该子树重新恢复平衡 */ + fn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + // 获取节点 node 的平衡因子 + let balance_factor = Self::balance_factor(node.clone()); + // 左偏树 + if balance_factor > 1 { + let node = node.unwrap(); + if Self::balance_factor(node.borrow().left.clone()) >= 0 { + // 右旋 + Self::right_rotate(Some(node)) + } else { + // 先左旋后右旋 + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::left_rotate(left); + Self::right_rotate(Some(node)) + } + } + // 右偏树 + else if balance_factor < -1 { + let node = node.unwrap(); + if Self::balance_factor(node.borrow().right.clone()) <= 0 { + // 左旋 + Self::left_rotate(Some(node)) + } else { + // 先右旋后左旋 + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::right_rotate(right); + Self::left_rotate(Some(node)) + } + } else { + // 平衡树,无需旋转,直接返回 + node + } + } + ``` + ## 7.5.3.   AVL 树常用操作 ### 插入节点 @@ -1697,6 +1827,48 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Rust" + + ```rust title="avl_tree.rs" + /* 插入节点 */ + fn insert(&mut self, val: i32) { + self.root = Self::insert_helper(self.root.clone(), val); + } + + /* 递归插入节点(辅助方法) */ + fn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc { + match node { + Some(mut node) => { + /* 1. 查找插入位置,并插入节点 */ + match { + let node_val = node.borrow().val; + node_val + } + .cmp(&val) + { + Ordering::Greater => { + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::insert_helper(left, val); + } + Ordering::Less => { + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::insert_helper(right, val); + } + Ordering::Equal => { + return Some(node); // 重复节点不插入,直接返回 + } + } + Self::update_height(Some(node.clone())); // 更新节点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = Self::rotate(Some(node)).unwrap(); + // 返回子树的根节点 + Some(node) + } + None => Some(TreeNode::new(val)), + } + } + ``` + ### 删除节点 类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡。 @@ -2198,6 +2370,64 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Rust" + + ```rust title="avl_tree.rs" + /* 删除节点 */ + fn remove(&self, val: i32) { + Self::remove_helper(self.root.clone(), val); + } + + /* 递归删除节点(辅助方法) */ + fn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc { + match node { + Some(mut node) => { + /* 1. 查找节点,并删除之 */ + if val < node.borrow().val { + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::remove_helper(left, val); + } else if val > node.borrow().val { + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::remove_helper(right, val); + } else if node.borrow().left.is_none() || node.borrow().right.is_none() { + let child = if node.borrow().left.is_some() { + node.borrow().left.clone() + } else { + node.borrow().right.clone() + }; + match child { + // 子节点数量 = 0 ,直接删除 node 并返回 + None => { + return None; + } + // 子节点数量 = 1 ,直接删除 node + Some(child) => node = child, + } + } else { + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 + let mut temp = node.borrow().right.clone().unwrap(); + loop { + let temp_left = temp.borrow().left.clone(); + if temp_left.is_none() { + break; + } + temp = temp_left.unwrap(); + } + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val); + node.borrow_mut().val = temp.borrow().val; + } + Self::update_height(Some(node.clone())); // 更新节点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = Self::rotate(Some(node)).unwrap(); + // 返回子树的根节点 + Some(node) + } + None => None, + } + } + ``` + ### 查找节点 AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。 diff --git a/chapter_tree/binary_search_tree.md b/chapter_tree/binary_search_tree.md index a65702b5a..c8fa08d00 100755 --- a/chapter_tree/binary_search_tree.md +++ b/chapter_tree/binary_search_tree.md @@ -271,6 +271,33 @@ comments: true [class]{BinarySearchTree}-[func]{search} ``` +=== "Rust" + + ```rust title="binary_search_tree.rs" + /* 查找节点 */ + pub fn search(&self, num: i32) -> Option { + let mut cur = self.root.clone(); + + // 循环查找,越过叶节点后跳出 + while let Some(node) = cur.clone() { + // 目标节点在 cur 的右子树中 + if node.borrow().val < num { + cur = node.borrow().right.clone(); + } + // 目标节点在 cur 的左子树中 + else if node.borrow().val > num { + cur = node.borrow().left.clone(); + } + // 找到目标节点,跳出循环 + else { + break; + } + } + // 返回目标节点 + cur + } + ``` + ### 插入节点 给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作分为两步: @@ -614,6 +641,44 @@ comments: true [class]{BinarySearchTree}-[func]{insert} ``` +=== "Rust" + + ```rust title="binary_search_tree.rs" + /* 插入节点 */ + pub fn insert(&mut self, num: i32) { + // 若树为空,直接提前返回 + if self.root.is_none() { + return; + } + let mut cur = self.root.clone(); + let mut pre = None; + // 循环查找,越过叶节点后跳出 + while let Some(node) = cur.clone() { + // 找到重复节点,直接返回 + if node.borrow().val == num { + return; + } + // 插入位置在 cur 的右子树中 + pre = cur.clone(); + if node.borrow().val < num { + cur = node.borrow().right.clone(); + } + // 插入位置在 cur 的左子树中 + else { + cur = node.borrow().left.clone(); + } + } + // 插入节点 + let node = TreeNode::new(num); + let pre = pre.unwrap(); + if pre.borrow().val < num { + pre.borrow_mut().right = Some(Rc::clone(&node)); + } else { + pre.borrow_mut().left = Some(Rc::clone(&node)); + } + } + ``` + 为了插入节点,我们需要利用辅助节点 `pre` 保存上一轮循环的节点,这样在遍历至 $\text{None}$ 时,我们可以获取到其父节点,从而完成节点插入操作。 与查找节点相同,插入节点使用 $O(\log n)$ 时间。 @@ -1217,6 +1282,76 @@ comments: true [class]{BinarySearchTree}-[func]{remove} ``` +=== "Rust" + + ```rust title="binary_search_tree.rs" + /* 删除节点 */ + pub fn remove(&mut self, num: i32) { + // 若树为空,直接提前返回 + if self.root.is_none() { + return; + } + let mut cur = self.root.clone(); + let mut pre = None; + // 循环查找,越过叶节点后跳出 + while let Some(node) = cur.clone() { + // 找到待删除节点,跳出循环 + if node.borrow().val == num { + break; + } + // 待删除节点在 cur 的右子树中 + pre = cur.clone(); + if node.borrow().val < num { + cur = node.borrow().right.clone(); + } + // 待删除节点在 cur 的左子树中 + else { + cur = node.borrow().left.clone(); + } + } + // 若无待删除节点,则直接返回 + if cur.is_none() { + return; + } + let cur = cur.unwrap(); + // 子节点数量 = 0 or 1 + if cur.borrow().left.is_none() || cur.borrow().right.is_none() { + // 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点 + let child = cur.borrow().left.clone().or_else(|| cur.borrow().right.clone()); + let pre = pre.unwrap(); + let left = pre.borrow().left.clone().unwrap(); + // 删除节点 cur + if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) { + if Rc::ptr_eq(&left, &cur) { + pre.borrow_mut().left = child; + } else { + pre.borrow_mut().right = child; + } + } else { + // 若删除节点为根节点,则重新指定根节点 + self.root = child; + } + } + // 子节点数量 = 2 + else { + // 获取中序遍历中 cur 的下一个节点 + let mut tmp = cur.borrow().right.clone(); + while let Some(node) = tmp.clone() { + if node.borrow().left.is_some() { + tmp = node.borrow().left.clone(); + } else { + break; + } + } + let tmpval = tmp.unwrap().borrow().val; + // 递归删除节点 tmp + self.remove(tmpval); + // 用 tmp 覆盖 cur + cur.borrow_mut().val = tmpval; + } + } + ``` + ### 排序 我们知道,二叉树的中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。 diff --git a/chapter_tree/binary_tree.md b/chapter_tree/binary_tree.md index 6d7543ee2..4758ba217 100644 --- a/chapter_tree/binary_tree.md +++ b/chapter_tree/binary_tree.md @@ -159,6 +159,12 @@ comments: true } ``` +=== "Rust" + + ```rust title="" + + ``` + 节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树」。 **在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。例如,在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。 @@ -366,6 +372,12 @@ comments: true n2.right = n5; ``` +=== "Rust" + + ```rust title="binary_tree.rs" + + ``` + **插入与删除节点**。与链表类似,通过修改指针来实现插入与删除节点。 ![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png) @@ -496,6 +508,12 @@ comments: true n1.left = n2; ``` +=== "Rust" + + ```rust title="binary_tree.rs" + + ``` + !!! note 需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。 diff --git a/chapter_tree/binary_tree_traversal.md b/chapter_tree/binary_tree_traversal.md index 87f4158ae..7e1e439b7 100755 --- a/chapter_tree/binary_tree_traversal.md +++ b/chapter_tree/binary_tree_traversal.md @@ -298,6 +298,30 @@ comments: true } ``` +=== "Rust" + + ```rust title="binary_tree_bfs.rs" + /* 层序遍历 */ + fn level_order(root: &Rc>) -> Vec { + // 初始化队列,加入根结点 + let mut que = VecDeque::new(); + que.push_back(Rc::clone(&root)); + // 初始化一个列表,用于保存遍历序列 + let mut vec = Vec::new(); + + while let Some(node) = que.pop_front() { // 队列出队 + vec.push(node.borrow().val); // 保存结点值 + if let Some(left) = node.borrow().left.as_ref() { + que.push_back(Rc::clone(left)); // 左子结点入队 + } + if let Some(right) = node.borrow().right.as_ref() { + que.push_back(Rc::clone(right)); // 右子结点入队 + }; + } + vec + } + ``` + **时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 **空间复杂度**:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $\frac{n + 1}{2}$ 个节点,占用 $O(n)$ 空间。 @@ -682,6 +706,49 @@ comments: true } ``` +=== "Rust" + + ```rust title="binary_tree_dfs.rs" + /* 前序遍历 */ + fn pre_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + if let Some(node) = root { + // 访问优先级:根结点 -> 左子树 -> 右子树 + result.push(node.borrow().val); + result.append(&mut pre_order(node.borrow().left.as_ref())); + result.append(&mut pre_order(node.borrow().right.as_ref())); + } + result + } + + /* 中序遍历 */ + fn in_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + if let Some(node) = root { + // 访问优先级:左子树 -> 根结点 -> 右子树 + result.append(&mut in_order(node.borrow().left.as_ref())); + result.push(node.borrow().val); + result.append(&mut in_order(node.borrow().right.as_ref())); + } + result + } + + /* 后序遍历 */ + fn post_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + if let Some(node) = root { + // 访问优先级:左子树 -> 右子树 -> 根结点 + result.append(&mut post_order(node.borrow().left.as_ref())); + result.append(&mut post_order(node.borrow().right.as_ref())); + result.push(node.borrow().val); + } + result + } + ``` + **时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 **空间复杂度**:在最差情况下,即树退化为链表时,递归深度达到 $n$ ,系统占用 $O(n)$ 栈帧空间。