diff --git a/codes/rust/chapter_array_and_linkedlist/linked_list.rs b/codes/rust/chapter_array_and_linkedlist/linked_list.rs index c6c732e69a..7a8dce70f6 100644 --- a/codes/rust/chapter_array_and_linkedlist/linked_list.rs +++ b/codes/rust/chapter_array_and_linkedlist/linked_list.rs @@ -13,9 +13,9 @@ use list_node::ListNode; /* 在链表的节点 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); + let n1 = n0.borrow_mut().next.take(); + P.borrow_mut().next = n1; + n0.borrow_mut().next = Some(P); } /* 删除链表的节点 n0 之后的首个节点 */ @@ -28,7 +28,7 @@ pub fn remove(n0: &Rc>>) { let n1 = node.borrow_mut().next.take(); n0.borrow_mut().next = n1; } - } +} /* 访问链表中索引为 index 的节点 */ pub fn access(head: Rc>>, index: i32) -> Rc>> { @@ -37,7 +37,7 @@ pub fn access(head: Rc>>, index: i32) -> Rc(head: Rc>>, target: T, index: i32) -> i32 { @@ -46,7 +46,7 @@ pub fn find(head: Rc>>, target: T, index: i32) return find(node.clone(), target, index + 1); } return -1; - } +} /* Driver Code */ fn main() { @@ -74,7 +74,7 @@ fn main() { remove(&n0); print!("删除节点后的链表为 "); print_util::print_linked_list(&n0); - + /* 访问节点 */ let node = access(n0.clone(), 3); println!("链表中索引 3 处的节点的值 = {}", node.borrow().val); diff --git a/codes/rust/chapter_array_and_linkedlist/list.rs b/codes/rust/chapter_array_and_linkedlist/list.rs index eb38412120..63ef409d88 100644 --- a/codes/rust/chapter_array_and_linkedlist/list.rs +++ b/codes/rust/chapter_array_and_linkedlist/list.rs @@ -4,72 +4,72 @@ * Author: xBLACICEx (xBLACKICEx@outlook.com), sjinzh (sjinzh@gmail.com) */ - include!("../include/include.rs"); +include!("../include/include.rs"); /* Driver Code */ - fn main() { - // 初始化列表 - let mut list: Vec = vec![ 1, 3, 2, 5, 4 ]; - print!("列表 list = "); - print_util::print_array(&list); - - // 访问元素 - let num = list[1]; - println!("\n访问索引 1 处的元素,得到 num = {num}"); - - // 更新元素 - list[1] = 0; - print!("将索引 1 处的元素更新为 0 ,得到 list = "); - print_util::print_array(&list); - - // 清空列表 - list.clear(); - print!("\n清空列表后 list = "); - print_util::print_array(&list); - - // 尾部添加元素 - list.push(1); - list.push(3); - list.push(2); - list.push(5); - list.push(4); - print!("\n添加元素后 list = "); - print_util::print_array(&list); - - // 中间插入元素 - list.insert(3, 6); - print!("\n在索引 3 处插入数字 6 ,得到 list = "); - print_util::print_array(&list); - - // 删除元素 - list.remove(3); - print!("\n删除索引 3 处的元素,得到 list = "); - print_util::print_array(&list); - - // 通过索引遍历列表 - let mut _count = 0; - for _ in 0..list.len() { - _count += 1; - } - - // 直接遍历列表元素 - _count = 0; - for _n in &list { - _count += 1; - } - // 或者 - // list.iter().for_each(|_| _count += 1); - // let _count = list.iter().fold(0, |_count, _| _count + 1); - - // 拼接两个列表 - let mut list1 = vec![ 6, 8, 7, 10, 9 ]; - list.append(&mut list1); // append(移动) 之后 list1 为空! - // list.extend(&list1); // extend(借用) list1 能继续使用 - print!("\n将列表 list1 拼接到 list 之后,得到 list = "); - print_util::print_array(&list); - - // 排序列表 - list.sort(); - print!("\n排序列表后 list = "); - print_util::print_array(&list); - } \ No newline at end of file +fn main() { + // 初始化列表 + let mut list: Vec = vec![ 1, 3, 2, 5, 4 ]; + print!("列表 list = "); + print_util::print_array(&list); + + // 访问元素 + let num = list[1]; + println!("\n访问索引 1 处的元素,得到 num = {num}"); + + // 更新元素 + list[1] = 0; + print!("将索引 1 处的元素更新为 0 ,得到 list = "); + print_util::print_array(&list); + + // 清空列表 + list.clear(); + print!("\n清空列表后 list = "); + print_util::print_array(&list); + + // 尾部添加元素 + list.push(1); + list.push(3); + list.push(2); + list.push(5); + list.push(4); + print!("\n添加元素后 list = "); + print_util::print_array(&list); + + // 中间插入元素 + list.insert(3, 6); + print!("\n在索引 3 处插入数字 6 ,得到 list = "); + print_util::print_array(&list); + + // 删除元素 + list.remove(3); + print!("\n删除索引 3 处的元素,得到 list = "); + print_util::print_array(&list); + + // 通过索引遍历列表 + let mut _count = 0; + for _ in 0..list.len() { + _count += 1; + } + + // 直接遍历列表元素 + _count = 0; + for _n in &list { + _count += 1; + } + // 或者 + // list.iter().for_each(|_| _count += 1); + // let _count = list.iter().fold(0, |_count, _| _count + 1); + + // 拼接两个列表 + let mut list1 = vec![ 6, 8, 7, 10, 9 ]; + list.append(&mut list1); // append(移动) 之后 list1 为空! + // list.extend(&list1); // extend(借用) list1 能继续使用 + print!("\n将列表 list1 拼接到 list 之后,得到 list = "); + print_util::print_array(&list); + + // 排序列表 + list.sort(); + print!("\n排序列表后 list = "); + print_util::print_array(&list); +} diff --git a/codes/rust/chapter_array_and_linkedlist/my_list.rs b/codes/rust/chapter_array_and_linkedlist/my_list.rs index c2f63dd709..6d22d9fb72 100644 --- a/codes/rust/chapter_array_and_linkedlist/my_list.rs +++ b/codes/rust/chapter_array_and_linkedlist/my_list.rs @@ -6,8 +6,8 @@ include!("../include/include.rs"); -#[allow(dead_code)] /* 列表类简易实现 */ +#[allow(dead_code)] struct MyList { nums: Vec, // 数组(存储列表元素) capacity: usize, // 列表容量 @@ -154,4 +154,4 @@ fn main() { print!("\n扩容后的列表 list = "); print_util::print_array(&list.to_array()); print!(" ,容量 = {} ,长度 = {}", list.capacity(), list.size()); -} \ No newline at end of file +} diff --git a/codes/rust/chapter_computational_complexity/worst_best_time_complexity.rs b/codes/rust/chapter_computational_complexity/worst_best_time_complexity.rs index 52c44f73f9..d5b3641fb5 100644 --- a/codes/rust/chapter_computational_complexity/worst_best_time_complexity.rs +++ b/codes/rust/chapter_computational_complexity/worst_best_time_complexity.rs @@ -4,40 +4,40 @@ * Author: xBLACICEx (xBLACKICEx@outlook.com), sjinzh (sjinzh@gmail.com) */ - include!("../include/include.rs"); +include!("../include/include.rs"); - use rand::seq::SliceRandom; - use rand::thread_rng; - - /* 生成一个数组,元素为 { 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 - } - - /* Driver Code */ - fn main() { - for _ in 0..10 { - let n = 100; - let nums = random_numbers(n); - let index = find_one(&nums).unwrap(); - print!("\n数组 [ 1, 2, ..., n ] 被打乱后 = "); - print_util::print_array(&nums); - println!("\n数字 1 的索引为 {}", index); - } - } \ No newline at end of file +use rand::seq::SliceRandom; +use rand::thread_rng; + +/* 生成一个数组,元素为 { 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 +} + +/* Driver Code */ +fn main() { + for _ in 0..10 { + let n = 100; + let nums = random_numbers(n); + let index = find_one(&nums).unwrap(); + print!("\n数组 [ 1, 2, ..., n ] 被打乱后 = "); + print_util::print_array(&nums); + println!("\n数字 1 的索引为 {}", index); + } +} \ No newline at end of file diff --git a/codes/rust/chapter_dynamic_programming/coin_change_ii.rs b/codes/rust/chapter_dynamic_programming/coin_change_ii.rs index 6e80905153..15f9563df1 100644 --- a/codes/rust/chapter_dynamic_programming/coin_change_ii.rs +++ b/codes/rust/chapter_dynamic_programming/coin_change_ii.rs @@ -29,7 +29,7 @@ fn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 { } /* 零钱兑换 II:状态压缩后的动态规划 */ -fn coin_change_dp_ii_comp(coins: &[i32], amt: usize) -> i32 { +fn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 { let n = coins.len(); // 初始化 dp 表 let mut dp = vec![0; amt + 1]; diff --git a/codes/rust/chapter_sorting/bubble_sort.rs b/codes/rust/chapter_sorting/bubble_sort.rs index d0afc20a41..97fb6d520c 100644 --- a/codes/rust/chapter_sorting/bubble_sort.rs +++ b/codes/rust/chapter_sorting/bubble_sort.rs @@ -26,7 +26,7 @@ fn bubble_sort(nums: &mut [i32]) { fn bubble_sort_with_flag(nums: &mut [i32]) { // 外循环:未排序区间为 [0, i] for i in (1..nums.len()).rev() { - let mut flag = false; // 初始化标志位 + let mut flag = false; // 初始化标志位 // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 for j in 0..i { if nums[j] > nums[j + 1] { @@ -34,10 +34,10 @@ fn bubble_sort_with_flag(nums: &mut [i32]) { let tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; - flag = true; // 记录交换元素 + flag = true; // 记录交换元素 } } - if !flag {break}; // 此轮冒泡未交换任何元素,直接跳出 + if !flag {break}; // 此轮冒泡未交换任何元素,直接跳出 } } diff --git a/codes/rust/chapter_sorting/insertion_sort.rs b/codes/rust/chapter_sorting/insertion_sort.rs index a8354731ee..a0d3d140fc 100644 --- a/codes/rust/chapter_sorting/insertion_sort.rs +++ b/codes/rust/chapter_sorting/insertion_sort.rs @@ -6,7 +6,7 @@ include!("../include/include.rs"); -/*插入排序 */ +/* 插入排序 */ fn insertion_sort(nums: &mut [i32]) { // 外循环:已排序元素数量为 1, 2, ..., n for i in 1..nums.len() { diff --git a/codes/rust/chapter_sorting/quick_sort.rs b/codes/rust/chapter_sorting/quick_sort.rs index 963de8b9f2..314e0bac3f 100644 --- a/codes/rust/chapter_sorting/quick_sort.rs +++ b/codes/rust/chapter_sorting/quick_sort.rs @@ -4,14 +4,10 @@ * Author: xBLACKICEx (xBLACKICE@outlook.com) */ -// 快速排序 -struct QuickSort; -// 快速排序(中位基准数优化) -struct QuickSortMedian; -// 快速排序(尾递归优化) -struct QuickSortTailCall; /* 快速排序 */ +struct QuickSort; + impl QuickSort { /* 哨兵划分 */ fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { @@ -19,15 +15,15 @@ impl QuickSort { let (mut i, mut j) = (left, right); while i < j { while i < j && nums[j] >= nums[left] { - j -= 1; // 从右向左找首个小于基准数的元素 + j -= 1; // 从右向左找首个小于基准数的元素 } while i < j && nums[i] <= nums[left] { - i += 1; // 从左向右找首个大于基准数的元素 + i += 1; // 从左向右找首个大于基准数的元素 } nums.swap(i, j); // 交换这两个元素 } - nums.swap(i, left); // 将基准数交换至两子数组的分界线 - i // 返回基准数的索引 + nums.swap(i, left); // 将基准数交换至两子数组的分界线 + i // 返回基准数的索引 } /* 快速排序 */ @@ -45,6 +41,8 @@ impl QuickSort { } /* 快速排序(中位基准数优化) */ +struct QuickSortMedian; + impl QuickSortMedian { /* 选取三个元素的中位数 */ fn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize { @@ -66,17 +64,17 @@ impl QuickSortMedian { 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 // 返回基准数的索引 + 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 // 返回基准数的索引 } /* 快速排序 */ @@ -94,23 +92,24 @@ impl QuickSortMedian { } /* 快速排序(尾递归优化) */ +struct QuickSortTailCall; + impl QuickSortTailCall { /* 哨兵划分 */ 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; // 从右向左找首个小于基准数的元素 + j -= 1; // 从右向左找首个小于基准数的元素 } while i < j && nums[i] <= nums[left] { - i += 1; // 从左向右找首个大于基准数的元素 + i += 1; // 从左向右找首个大于基准数的元素 } nums.swap(i, j); // 交换这两个元素 } - nums.swap(i, left); // 将基准数交换至两子数组的分界线 - i // 返回基准数的索引 + nums.swap(i, left); // 将基准数交换至两子数组的分界线 + i // 返回基准数的索引 } /* 快速排序(尾递归优化) */ @@ -131,6 +130,7 @@ impl QuickSortTailCall { } } +/* Driver Code */ fn main() { /* 快速排序 */ let mut nums = [2, 4, 1, 0, 3, 5]; @@ -146,4 +146,4 @@ fn main() { let mut nums = [2, 4, 1, 0, 3, 5]; QuickSortTailCall::quick_sort(0, (nums.len() - 1) as i32, &mut nums); println!("快速排序(尾递归优化)完成后 nums = {:?}", nums); -} \ No newline at end of file +} diff --git a/codes/rust/chapter_stack_and_queue/linkedlist_deque.rs b/codes/rust/chapter_stack_and_queue/linkedlist_deque.rs index 4e9c6b8b0e..a6e6d4ec49 100644 --- a/codes/rust/chapter_stack_and_queue/linkedlist_deque.rs +++ b/codes/rust/chapter_stack_and_queue/linkedlist_deque.rs @@ -212,4 +212,4 @@ fn main() { /* 判断双向队列是否为空 */ let is_empty = deque.is_empty(); print!("\n双向队列是否为空 = {}", is_empty); -} \ No newline at end of file +} diff --git a/docs/chapter_array_and_linkedlist/array.md b/docs/chapter_array_and_linkedlist/array.md index 11d572fd85..aae6a57f5b 100755 --- a/docs/chapter_array_and_linkedlist/array.md +++ b/docs/chapter_array_and_linkedlist/array.md @@ -100,6 +100,12 @@ List nums = [1, 3, 2, 5, 4]; ``` +=== "Rust" + + ```rust title="array.rs" + + ``` + ## 数组优点 **在数组中访问元素非常高效**。由于数组元素被存储在连续的内存空间中,因此计算数组元素的内存地址非常容易。给定数组首个元素的地址和某个元素的索引,我们可以使用以下公式计算得到该元素的内存地址,从而直接访问此元素。 @@ -185,6 +191,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{randomAccess} ``` +=== "Rust" + + ```rust title="array.rs" + [class]{}-[func]{random_access} + ``` + ## 数组缺点 **数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。 @@ -255,6 +267,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{extend} ``` +=== "Rust" + + ```rust title="array.rs" + [class]{}-[func]{extend} + ``` + **数组中插入或删除元素效率低下**。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。 ![数组插入元素](array.assets/array_insert_element.png) @@ -325,6 +343,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{insert} ``` +=== "Rust" + + ```rust title="array.rs" + [class]{}-[func]{insert} + ``` + 删除元素也类似,如果我们想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。 ![数组删除元素](array.assets/array_remove_element.png) @@ -395,6 +419,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{remove} ``` +=== "Rust" + + ```rust title="array.rs" + [class]{}-[func]{remove} + ``` + 总结来看,数组的插入与删除操作有以下缺点: - **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(n)$ ,其中 $n$ 为数组长度。 @@ -471,6 +501,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{traverse} ``` +=== "Rust" + + ```rust title="array.rs" + [class]{}-[func]{traverse} + ``` + **数组查找**。通过遍历数组,查找数组内的指定元素,并输出对应索引。 === "Java" @@ -539,6 +575,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{find} ``` +=== "Rust" + + ```rust title="array.rs" + [class]{}-[func]{find} + ``` + ## 数组典型应用 数组是最基础的数据结构,在各类数据结构和算法中都有广泛应用。 diff --git a/docs/chapter_array_and_linkedlist/linked_list.md b/docs/chapter_array_and_linkedlist/linked_list.md index e701421dbe..380ced58da 100755 --- a/docs/chapter_array_and_linkedlist/linked_list.md +++ b/docs/chapter_array_and_linkedlist/linked_list.md @@ -163,6 +163,12 @@ } ``` +=== "Rust" + + ```rust title="" + + ``` + !!! question "尾节点指向什么?" 我们将链表的最后一个节点称为「尾节点」,其指向的是“空”,在 Java, C++, Python 中分别记为 $\text{null}$ , $\text{nullptr}$ , $\text{None}$ 。在不引起歧义的前提下,本书都使用 $\text{None}$ 来表示空。 @@ -360,6 +366,12 @@ n3.next = n4; ``` +=== "Rust" + + ```rust title="linked_list.rs" + + ``` + ## 链表优点 **链表中插入与删除节点的操作效率高**。例如,如果我们想在链表中间的两个节点 `A` , `B` 之间插入一个新节点 `P` ,我们只需要改变两个节点指针即可,时间复杂度为 $O(1)$ ;相比之下,数组的插入操作效率要低得多。 @@ -432,6 +444,12 @@ [class]{}-[func]{insert} ``` +=== "Rust" + + ```rust title="linked_list.rs" + [class]{}-[func]{insert} + ``` + 在链表中删除节点也非常方便,只需改变一个节点的指针即可。如下图所示,尽管在删除操作完成后,节点 `P` 仍然指向 `n1` ,但实际上 `P` 已经不再属于此链表,因为遍历此链表时无法访问到 `P` 。 ![链表删除节点](linked_list.assets/linkedlist_remove_node.png) @@ -502,6 +520,12 @@ [class]{}-[func]{remove} ``` +=== "Rust" + + ```rust title="linked_list.rs" + [class]{}-[func]{remove} + ``` + ## 链表缺点 **链表访问节点效率较低**。如上节所述,数组可以在 $O(1)$ 时间下访问任意元素。然而,链表无法直接访问任意节点,这是因为系统需要从头节点出发,逐个向后遍历直至找到目标节点。例如,若要访问链表索引为 `index`(即第 `index + 1` 个)的节点,则需要向后遍历 `index` 轮。 @@ -572,6 +596,12 @@ [class]{}-[func]{access} ``` +=== "Rust" + + ```rust title="linked_list.rs" + [class]{}-[func]{access} + ``` + **链表的内存占用较大**。链表以节点为单位,每个节点除了保存值之外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多的内存空间。 ## 链表常用操作 @@ -644,6 +674,12 @@ [class]{}-[func]{find} ``` +=== "Rust" + + ```rust title="linked_list.rs" + [class]{}-[func]{find} + ``` + ## 常见链表类型 **单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的指针(引用)两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向空 $\text{None}$ 。 @@ -823,6 +859,12 @@ } ``` +=== "Rust" + + ```rust title="" + + ``` + ![常见链表种类](linked_list.assets/linkedlist_common_types.png) ## 链表典型应用 diff --git a/docs/chapter_array_and_linkedlist/list.md b/docs/chapter_array_and_linkedlist/list.md index 021011e548..d3d65ef45f 100755 --- a/docs/chapter_array_and_linkedlist/list.md +++ b/docs/chapter_array_and_linkedlist/list.md @@ -116,6 +116,12 @@ List list = [1, 3, 2, 5, 4]; ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **访问与更新元素**。由于列表的底层数据结构是数组,因此可以在 $O(1)$ 时间内访问和更新元素,效率很高。 === "Java" @@ -224,6 +230,12 @@ list[1] = 0; // 将索引 1 处的元素更新为 0 ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **在列表中添加、插入、删除元素**。相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但插入和删除元素的效率仍与数组相同,时间复杂度为 $O(N)$ 。 === "Java" @@ -432,6 +444,12 @@ list.removeAt(3); // 删除索引 3 处的元素 ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **遍历列表**。与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。 === "Java" @@ -599,6 +617,12 @@ } ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **拼接两个列表**。给定一个新列表 `list1` ,我们可以将该列表拼接到原列表的尾部。 === "Java" @@ -690,6 +714,12 @@ list.addAll(list1); // 将列表 list1 拼接到 list 之后 ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + **排序列表**。排序也是常用的方法之一。完成列表排序后,我们便可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法。 === "Java" @@ -768,6 +798,12 @@ list.sort(); // 排序后,列表元素从小到大排列 ``` +=== "Rust" + + ```rust title="list.rs" + + ``` + ## 列表实现 * 为了帮助加深对列表的理解,我们在此提供一个简易版列表实现。需要关注三个核心点: @@ -843,3 +879,9 @@ ```dart title="my_list.dart" [class]{MyList}-[func]{} ``` + +=== "Rust" + + ```rust title="my_list.rs" + [class]{MyList}-[func]{} + ``` diff --git a/docs/chapter_backtracking/backtracking_algorithm.md b/docs/chapter_backtracking/backtracking_algorithm.md index 5d68aa1838..31ff1cb04c 100644 --- a/docs/chapter_backtracking/backtracking_algorithm.md +++ b/docs/chapter_backtracking/backtracking_algorithm.md @@ -76,6 +76,12 @@ [class]{}-[func]{preOrder} ``` +=== "Rust" + + ```rust title="preorder_traversal_i_compact.rs" + [class]{}-[func]{pre_order} + ``` + ![在前序遍历中搜索节点](backtracking_algorithm.assets/preorder_find_nodes.png) ## 尝试与回退 @@ -158,6 +164,12 @@ [class]{}-[func]{preOrder} ``` +=== "Rust" + + ```rust title="preorder_traversal_ii_compact.rs" + [class]{}-[func]{pre_order} + ``` + 在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。 观察该过程,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为逆向的。 @@ -271,6 +283,12 @@ [class]{}-[func]{preOrder} ``` +=== "Rust" + + ```rust title="preorder_traversal_iii_compact.rs" + [class]{}-[func]{pre_order} + ``` + 剪枝是一个非常形象的名词。在搜索过程中,**我们“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而实现搜索效率的提高。 ![根据约束条件剪枝](backtracking_algorithm.assets/preorder_find_constrained_paths.png) @@ -543,6 +561,12 @@ } ``` +=== "Rust" + + ```rust title="" + + ``` + 接下来,我们基于框架代码来解决例题三。状态 `state` 为节点遍历路径,选择 `choices` 为当前节点的左子节点和右子节点,结果 `res` 是路径列表。 === "Java" @@ -721,6 +745,22 @@ [class]{}-[func]{backtrack} ``` +=== "Rust" + + ```rust title="preorder_traversal_iii_template.rs" + [class]{}-[func]{is_solution} + + [class]{}-[func]{record_solution} + + [class]{}-[func]{is_valid} + + [class]{}-[func]{make_choice} + + [class]{}-[func]{undo_choice} + + [class]{}-[func]{backtrack} + ``` + 根据题意,当找到值为 7 的节点后应该继续搜索,**因此我们需要将记录解之后的 `return` 语句删除**。下图对比了保留或删除 `return` 语句的搜索过程。 ![保留与删除 return 的搜索过程对比](backtracking_algorithm.assets/backtrack_remove_return_or_not.png) diff --git a/docs/chapter_backtracking/n_queens_problem.md b/docs/chapter_backtracking/n_queens_problem.md index 1a58069ad8..42f1b1c174 100644 --- a/docs/chapter_backtracking/n_queens_problem.md +++ b/docs/chapter_backtracking/n_queens_problem.md @@ -128,6 +128,14 @@ [class]{}-[func]{nQueens} ``` +=== "Rust" + + ```rust title="n_queens.rs" + [class]{}-[func]{backtrack} + + [class]{}-[func]{n_queens} + ``` + 逐行放置 $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/docs/chapter_backtracking/permutations_problem.md b/docs/chapter_backtracking/permutations_problem.md index a3f5f768b6..62e470b41f 100644 --- a/docs/chapter_backtracking/permutations_problem.md +++ b/docs/chapter_backtracking/permutations_problem.md @@ -133,6 +133,14 @@ [class]{}-[func]{permutationsI} ``` +=== "Rust" + + ```rust title="permutations_i.rs" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutations_i} + ``` + ## 考虑相等元素的情况 !!! question @@ -249,6 +257,14 @@ [class]{}-[func]{permutationsII} ``` +=== "Rust" + + ```rust title="permutations_ii.rs" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutations_ii} + ``` + 假设元素两两之间互不相同,则 $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/docs/chapter_backtracking/subset_sum_problem.md b/docs/chapter_backtracking/subset_sum_problem.md index 589e2e954e..a2a609fdb1 100644 --- a/docs/chapter_backtracking/subset_sum_problem.md +++ b/docs/chapter_backtracking/subset_sum_problem.md @@ -105,6 +105,14 @@ [class]{}-[func]{subsetSumINaive} ``` +=== "Rust" + + ```rust title="subset_sum_i_naive.rs" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subset_sum_i_naive} + ``` + 向以上代码输入数组 $[3, 4, 5]$ 和目标元素 $9$ ,输出结果为 $[3, 3, 3], [4, 5], [5, 4]$ 。**虽然成功找出了所有和为 $9$ 的子集,但其中存在重复的子集 $[4, 5]$ 和 $[5, 4]$** 。 这是因为搜索过程是区分选择顺序的,然而子集不区分选择顺序。如下图所示,先选 $4$ 后选 $5$ 与先选 $5$ 后选 $4$ 是两个不同的分支,但两者对应同一个子集。 @@ -230,6 +238,14 @@ [class]{}-[func]{subsetSumI} ``` +=== "Rust" + + ```rust title="subset_sum_i.rs" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subset_sum_i} + ``` + 如下图所示,为将数组 $[3, 4, 5]$ 和目标元素 $9$ 输入到以上代码后的整体回溯过程。 ![子集和 I 回溯过程](subset_sum_problem.assets/subset_sum_i.png) @@ -342,6 +358,14 @@ [class]{}-[func]{subsetSumII} ``` +=== "Rust" + + ```rust title="subset_sum_ii.rs" + [class]{}-[func]{backtrack} + + [class]{}-[func]{subset_sum_ii} + ``` + 下图展示了数组 $[4, 4, 5]$ 和目标元素 $9$ 的回溯过程,共包含四种剪枝操作。请你将图示与代码注释相结合,理解整个搜索过程,以及每种剪枝操作是如何工作的。 ![子集和 II 回溯过程](subset_sum_problem.assets/subset_sum_ii.png) diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index a637b1371b..01ae7bd01a 100755 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -280,6 +280,12 @@ } ``` +=== "Rust" + + ```rust title="" + + ``` + ## 推算方法 空间复杂度的推算方法与时间复杂度大致相同,只是将统计对象从“计算操作数量”转为“使用空间大小”。与时间复杂度不同的是,**我们通常只关注「最差空间复杂度」**,这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。 @@ -412,6 +418,12 @@ } ``` +=== "Rust" + + ```rust title="" + + ``` + **在递归函数中,需要注意统计栈帧空间**。例如,函数 `loop()` 在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行过程中会同时存在 $n$ 个未返回的 `recur()` ,从而占用 $O(n)$ 的栈帧空间。 === "Java" @@ -627,6 +639,12 @@ } ``` +=== "Rust" + + ```rust title="" + + ``` + ## 常见类型 设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列) @@ -716,6 +734,12 @@ $$ [class]{}-[func]{constant} ``` +=== "Rust" + + ```rust title="space_complexity.rs" + [class]{}-[func]{constant} + ``` + ### 线性阶 $O(n)$ 线性阶常见于元素数量与 $n$ 成正比的数组、链表、栈、队列等。 @@ -788,6 +812,12 @@ $$ [class]{}-[func]{linear} ``` +=== "Rust" + + ```rust title="space_complexity.rs" + [class]{}-[func]{linear} + ``` + 以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间。 === "Java" @@ -856,6 +886,12 @@ $$ [class]{}-[func]{linearRecur} ``` +=== "Rust" + + ```rust title="space_complexity.rs" + [class]{}-[func]{linear_recur} + ``` + ![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png) ### 平方阶 $O(n^2)$ @@ -928,6 +964,12 @@ $$ [class]{}-[func]{quadratic} ``` +=== "Rust" + + ```rust title="space_complexity.rs" + [class]{}-[func]{quadratic} + ``` + 在以下递归函数中,同时存在 $n$ 个未返回的 `algorithm()` ,并且每个函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $\frac{n}{2}$ ,因此总体占用 $O(n^2)$ 空间。 === "Java" @@ -996,6 +1038,12 @@ $$ [class]{}-[func]{quadraticRecur} ``` +=== "Rust" + + ```rust title="space_complexity.rs" + [class]{}-[func]{quadratic_recur} + ``` + ![递归函数产生的平方阶空间复杂度](space_complexity.assets/space_complexity_recursive_quadratic.png) ### 指数阶 $O(2^n)$ @@ -1068,6 +1116,12 @@ $$ [class]{}-[func]{buildTree} ``` +=== "Rust" + + ```rust title="space_complexity.rs" + [class]{}-[func]{build_tree} + ``` + ![满二叉树产生的指数阶空间复杂度](space_complexity.assets/space_complexity_exponential.png) ### 对数阶 $O(\log n)$ diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index ba9e5cf9c4..254e6bc3d1 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -168,6 +168,12 @@ $$ } ``` +=== "Rust" + + ```rust title="" + + ``` + 然而实际上,**统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。 ## 统计时间增长趋势 @@ -394,6 +400,12 @@ $$ } ``` +=== "Rust" + + ```rust title="" + + ``` + ![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png) 相较于直接统计算法运行时间,时间复杂度分析有哪些优势和局限性呢? @@ -556,6 +568,12 @@ $$ } ``` +=== "Rust" + + ```rust title="" + + ``` + $T(n)$ 是一次函数,说明时间增长趋势是线性的,因此可以得出时间复杂度是线性阶。 我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 Big-$O$ Notation」,表示函数 $T(n)$ 的「渐近上界 Asymptotic Upper Bound」。 @@ -795,6 +813,12 @@ $$ } ``` +=== "Rust" + + ```rust title="" + + ``` + ### 第二步:判断渐近上界 **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。 @@ -902,6 +926,12 @@ $$ [class]{}-[func]{constant} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{constant} + ``` + ### 线性阶 $O(n)$ 线性阶的操作数量相对于输入数据大小以线性级别增长。线性阶通常出现在单层循环中。 @@ -972,6 +1002,12 @@ $$ [class]{}-[func]{linear} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{linear} + ``` + 遍历数组和遍历链表等操作的时间复杂度均为 $O(n)$ ,其中 $n$ 为数组或链表的长度。 !!! question "如何确定输入数据大小 $n$ ?" @@ -1044,6 +1080,12 @@ $$ [class]{}-[func]{arrayTraversal} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{array_traversal} + ``` + ### 平方阶 $O(n^2)$ 平方阶的操作数量相对于输入数据大小以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ 。 @@ -1114,6 +1156,12 @@ $$ [class]{}-[func]{quadratic} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{quadratic} + ``` + ![常数阶、线性阶、平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png) 以「冒泡排序」为例,外层循环执行 $n - 1$ 次,内层循环执行 $n-1, n-2, \cdots, 2, 1$ 次,平均为 $\frac{n}{2}$ 次,因此时间复杂度为 $O(n^2)$ 。 @@ -1188,6 +1236,12 @@ $$ [class]{}-[func]{bubbleSort} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{bubble_sort} + ``` + ### 指数阶 $O(2^n)$ !!! note @@ -1262,6 +1316,12 @@ $$ [class]{}-[func]{exponential} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{exponential} + ``` + ![指数阶的时间复杂度](time_complexity.assets/time_complexity_exponential.png) 在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,经过 $n$ 次分裂后停止。 @@ -1332,6 +1392,12 @@ $$ [class]{}-[func]{expRecur} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{exp_recur} + ``` + ### 对数阶 $O(\log n)$ 与指数阶相反,对数阶反映了“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长缓慢,是理想的时间复杂度。 @@ -1406,6 +1472,12 @@ $$ [class]{}-[func]{logarithmic} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{logarithmic} + ``` + ![对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic.png) 与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 $\log_2 n$ 的递归树。 @@ -1476,6 +1548,12 @@ $$ [class]{}-[func]{logRecur} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{log_recur} + ``` + ### 线性对数阶 $O(n \log n)$ 线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 $O(\log n)$ 和 $O(n)$ 。 @@ -1548,6 +1626,12 @@ $$ [class]{}-[func]{linearLogRecur} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{linear_log_recur} + ``` + ![线性对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic_linear.png) ### 阶乘阶 $O(n!)$ @@ -1626,6 +1710,12 @@ $$ [class]{}-[func]{factorialRecur} ``` +=== "Rust" + + ```rust title="time_complexity.rs" + [class]{}-[func]{factorial_recur} + ``` + ![阶乘阶的时间复杂度](time_complexity.assets/time_complexity_factorial.png) ## 最差、最佳、平均时间复杂度 @@ -1744,6 +1834,14 @@ $$ [class]{}-[func]{findOne} ``` +=== "Rust" + + ```rust title="worst_best_time_complexity.rs" + [class]{}-[func]{random_numbers} + + [class]{}-[func]{find_one} + ``` + !!! tip 实际应用中我们很少使用「最佳时间复杂度」,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。相反,「最差时间复杂度」更为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。 diff --git a/docs/chapter_data_structure/basic_data_types.md b/docs/chapter_data_structure/basic_data_types.md index 96b4567ab4..01e501948b 100644 --- a/docs/chapter_data_structure/basic_data_types.md +++ b/docs/chapter_data_structure/basic_data_types.md @@ -139,3 +139,9 @@ List characters = List.filled(5, 'a'); List booleans = List.filled(5, false); ``` + +=== "Rust" + + ```rust title="" + + ``` diff --git a/docs/chapter_divide_and_conquer/binary_search_recur.md b/docs/chapter_divide_and_conquer/binary_search_recur.md index 1a27e2ca1a..f89cc171e1 100644 --- a/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -127,3 +127,11 @@ [class]{}-[func]{binarySearch} ``` + +=== "Rust" + + ```rust title="binary_search_recur.rs" + [class]{}-[func]{dfs} + + [class]{}-[func]{binary_search} + ``` diff --git a/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index 4fd1b6e13b..7997fb27ae 100644 --- a/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -147,6 +147,14 @@ [class]{}-[func]{buildTree} ``` +=== "Rust" + + ```rust title="build_tree.rs" + [class]{}-[func]{dfs} + + [class]{}-[func]{build_tree} + ``` + 下图展示了构建二叉树的递归过程,各个节点是在向下“递”的过程中建立的,而各条边(即引用)是在向上“归”的过程中建立的。 === "<1>" diff --git a/docs/chapter_divide_and_conquer/hanota_problem.md b/docs/chapter_divide_and_conquer/hanota_problem.md index ceac401058..fe0d1931d7 100644 --- a/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/docs/chapter_divide_and_conquer/hanota_problem.md @@ -192,6 +192,16 @@ [class]{}-[func]{hanota} ``` +=== "Rust" + + ```rust title="hanota.rs" + [class]{}-[func]{move_pan} + + [class]{}-[func]{dfs} + + [class]{}-[func]{hanota} + ``` + 如下图所示,汉诺塔问题形成一个高度为 $n$ 的递归树,每个节点代表一个子问题、对应一个开启的 `dfs()` 函数,**因此时间复杂度为 $O(2^n)$ ,空间复杂度为 $O(n)$** 。 ![汉诺塔问题的递归树](hanota_problem.assets/hanota_recursive_tree.png) diff --git a/docs/chapter_dynamic_programming/dp_problem_features.md b/docs/chapter_dynamic_programming/dp_problem_features.md index ac19dada10..6fdb68b34f 100644 --- a/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/docs/chapter_dynamic_programming/dp_problem_features.md @@ -100,6 +100,12 @@ $$ [class]{}-[func]{minCostClimbingStairsDP} ``` +=== "Rust" + + ```rust title="min_cost_climbing_stairs_dp.rs" + [class]{}-[func]{min_cost_climbing_stairs_dp} + ``` + ![爬楼梯最小代价的动态规划过程](dp_problem_features.assets/min_cost_cs_dp.png) 本题也可以进行状态压缩,将一维压缩至零维,使得空间复杂度从 $O(n)$ 降低至 $O(1)$ 。 @@ -170,6 +176,12 @@ $$ [class]{}-[func]{minCostClimbingStairsDPComp} ``` +=== "Rust" + + ```rust title="min_cost_climbing_stairs_dp.rs" + [class]{}-[func]{min_cost_climbing_stairs_dp_comp} + ``` + ## 无后效性 「无后效性」是动态规划能够有效解决问题的重要特性之一,定义为:**给定一个确定的状态,它的未来发展只与当前状态有关,而与当前状态过去所经历过的所有状态无关**。 @@ -274,6 +286,12 @@ $$ [class]{}-[func]{climbingStairsConstraintDP} ``` +=== "Rust" + + ```rust title="climbing_stairs_constraint_dp.rs" + [class]{}-[func]{climbing_stairs_constraint_dp} + ``` + 在上面的案例中,由于仅需多考虑前面一个状态,我们仍然可以通过扩展状态定义,使得问题恢复无后效性。然而,许多问题具有非常严重的“有后效性”,例如: !!! question "爬楼梯与障碍生成" diff --git a/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/docs/chapter_dynamic_programming/dp_solution_pipeline.md index e71df5044f..7a9f90039e 100644 --- a/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -164,6 +164,12 @@ $$ [class]{}-[func]{minPathSumDFS} ``` +=== "Rust" + + ```rust title="min_path_sum.rs" + [class]{}-[func]{min_path_sum_dfs} + ``` + 下图给出了以 $dp[2, 1]$ 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 `grid` 的尺寸变大而急剧增多。 本质上看,造成重叠子问题的原因为:**存在多条路径可以从左上角到达某一单元格**。 @@ -242,6 +248,12 @@ $$ [class]{}-[func]{minPathSumDFSMem} ``` +=== "Rust" + + ```rust title="min_path_sum.rs" + [class]{}-[func]{min_path_sum_dfs_mem} + ``` + 引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 $O(nm)$ 。 ![记忆化搜索递归树](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png) @@ -316,6 +328,12 @@ $$ [class]{}-[func]{minPathSumDP} ``` +=== "Rust" + + ```rust title="min_path_sum.rs" + [class]{}-[func]{min_path_sum_dp} + ``` + 下图展示了最小路径和的状态转移过程,其遍历了整个网格,**因此时间复杂度为 $O(nm)$** 。 数组 `dp` 大小为 $n \times m$ ,**因此空间复杂度为 $O(nm)$** 。 @@ -427,3 +445,9 @@ $$ ```dart title="min_path_sum.dart" [class]{}-[func]{minPathSumDPComp} ``` + +=== "Rust" + + ```rust title="min_path_sum.rs" + [class]{}-[func]{min_path_sum_dp_comp} + ``` diff --git a/docs/chapter_dynamic_programming/edit_distance_problem.md b/docs/chapter_dynamic_programming/edit_distance_problem.md index ef57687687..fad1c08654 100644 --- a/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -131,6 +131,12 @@ $$ [class]{}-[func]{editDistanceDP} ``` +=== "Rust" + + ```rust title="edit_distance.rs" + [class]{}-[func]{edit_distance_dp} + ``` + 如下图所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作是填写一个二维网格的过程。 === "<1>" @@ -249,3 +255,9 @@ $$ ```dart title="edit_distance.dart" [class]{}-[func]{editDistanceDPComp} ``` + +=== "Rust" + + ```rust title="edit_distance.rs" + [class]{}-[func]{edit_distance_dp_comp} + ``` diff --git a/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index e3c73ca819..5e74fb111a 100644 --- a/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -102,6 +102,14 @@ [class]{}-[func]{climbingStairsBacktrack} ``` +=== "Rust" + + ```rust title="climbing_stairs_backtrack.rs" + [class]{}-[func]{backtrack} + + [class]{}-[func]{climbing_stairs_backtrack} + ``` + ## 方法一:暴力搜索 回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。 @@ -219,6 +227,14 @@ $$ [class]{}-[func]{climbingStairsDFS} ``` +=== "Rust" + + ```rust title="climbing_stairs_dfs.rs" + [class]{}-[func]{dfs} + + [class]{}-[func]{climbing_stairs_dfs} + ``` + 下图展示了暴力搜索形成的递归树。对于问题 $dp[n]$ ,其递归树的深度为 $n$ ,时间复杂度为 $O(2^n)$ 。指数阶属于爆炸式增长,如果我们输入一个比较大的 $n$ ,则会陷入漫长的等待之中。 ![爬楼梯对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png) @@ -322,6 +338,14 @@ $$ [class]{}-[func]{climbingStairsDFSMem} ``` +=== "Rust" + + ```rust title="climbing_stairs_dfs_mem.rs" + [class]{}-[func]{dfs} + + [class]{}-[func]{climbing_stairs_dfs_mem} + ``` + 观察下图,**经过记忆化处理后,所有重叠子问题都只需被计算一次,时间复杂度被优化至 $O(n)$** ,这是一个巨大的飞跃。 ![记忆化搜索对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png) @@ -400,6 +424,12 @@ $$ [class]{}-[func]{climbingStairsDP} ``` +=== "Rust" + + ```rust title="climbing_stairs_dp.rs" + [class]{}-[func]{climbing_stairs_dp} + ``` + 与回溯算法一样,动态规划也使用“状态”概念来表示问题求解的某个特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例如,爬楼梯问题的状态定义为当前所在楼梯阶数 $i$ 。 总结以上,动态规划的常用术语包括: @@ -480,6 +510,12 @@ $$ [class]{}-[func]{climbingStairsDPComp} ``` +=== "Rust" + + ```rust title="climbing_stairs_dp.rs" + [class]{}-[func]{climbing_stairs_dp_comp} + ``` + 观察以上代码,由于省去了数组 `dp` 占用的空间,因此空间复杂度从 $O(n)$ 降低至 $O(1)$ 。 **这种空间优化技巧被称为「状态压缩」**。在常见的动态规划问题中,当前状态仅与前面有限个状态有关,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。 diff --git a/docs/chapter_dynamic_programming/knapsack_problem.md b/docs/chapter_dynamic_programming/knapsack_problem.md index d74b2d4098..9d678956d6 100644 --- a/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/docs/chapter_dynamic_programming/knapsack_problem.md @@ -122,6 +122,12 @@ $$ [class]{}-[func]{knapsackDFS} ``` +=== "Rust" + + ```rust title="knapsack.rs" + [class]{}-[func]{knapsack_dfs} + ``` + 如下图所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 $O(2^n)$ 。 观察递归树,容易发现其中存在重叠子问题,例如 $dp[1, 10]$ 等。而当物品较多、背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅增多。 @@ -200,6 +206,12 @@ $$ [class]{}-[func]{knapsackDFSMem} ``` +=== "Rust" + + ```rust title="knapsack.rs" + [class]{}-[func]{knapsack_dfs_mem} + ``` + ![0-1 背包的记忆化搜索递归树](knapsack_problem.assets/knapsack_dfs_mem.png) ### 方法三:动态规划 @@ -272,6 +284,12 @@ $$ [class]{}-[func]{knapsackDP} ``` +=== "Rust" + + ```rust title="knapsack.rs" + [class]{}-[func]{knapsack_dp} + ``` + 如下图所示,时间复杂度和空间复杂度都由数组 `dp` 大小决定,即 $O(n \times cap)$ 。 === "<1>" @@ -412,3 +430,9 @@ $$ ```dart title="knapsack.dart" [class]{}-[func]{knapsackDPComp} ``` + +=== "Rust" + + ```rust title="knapsack.rs" + [class]{}-[func]{knapsack_dp_comp} + ``` diff --git a/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index e2da6919be..19bbd993d4 100644 --- a/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -96,6 +96,12 @@ $$ [class]{}-[func]{unboundedKnapsackDP} ``` +=== "Rust" + + ```rust title="unbounded_knapsack.rs" + [class]{}-[func]{unbounded_knapsack_dp} + ``` + ### 状态压缩 由于当前状态是从左边和上边的状态转移而来,**因此状态压缩后应该对 $dp$ 表中的每一行采取正序遍历**。 @@ -188,6 +194,12 @@ $$ [class]{}-[func]{unboundedKnapsackDPComp} ``` +=== "Rust" + + ```rust title="unbounded_knapsack.rs" + [class]{}-[func]{unbounded_knapsack_dp_comp} + ``` + ## 零钱兑换问题 背包问题是一大类动态规划问题的代表,其拥有很多的变种,例如零钱兑换问题。 @@ -301,6 +313,12 @@ $$ [class]{}-[func]{coinChangeDP} ``` +=== "Rust" + + ```rust title="coin_change.rs" + [class]{}-[func]{coin_change_dp} + ``` + 下图展示了零钱兑换的动态规划过程,和完全背包非常相似。 === "<1>" @@ -418,6 +436,12 @@ $$ [class]{}-[func]{coinChangeDPComp} ``` +=== "Rust" + + ```rust title="coin_change.rs" + [class]{}-[func]{coin_change_dp_comp} + ``` + ## 零钱兑换问题 II !!! question @@ -504,6 +528,12 @@ $$ [class]{}-[func]{coinChangeIIDP} ``` +=== "Rust" + + ```rust title="coin_change_ii.rs" + [class]{}-[func]{coin_change_ii_dp} + ``` + ### 状态压缩 状态压缩处理方式相同,删除硬币维度即可。 @@ -573,3 +603,9 @@ $$ ```dart title="coin_change_ii.dart" [class]{}-[func]{coinChangeIIDPComp} ``` + +=== "Rust" + + ```rust title="coin_change_ii.rs" + [class]{}-[func]{coin_change_ii_dp_comp} + ``` diff --git a/docs/chapter_graph/graph_operations.md b/docs/chapter_graph/graph_operations.md index c5d4c92e40..a6d2175f27 100644 --- a/docs/chapter_graph/graph_operations.md +++ b/docs/chapter_graph/graph_operations.md @@ -94,6 +94,12 @@ [class]{GraphAdjMat}-[func]{} ``` +=== "Rust" + + ```rust title="graph_adjacency_matrix.rs" + [class]{GraphAdjMat}-[func]{} + ``` + ## 基于邻接表的实现 设无向图的顶点总数为 $n$ 、边总数为 $m$ ,则有: @@ -191,6 +197,12 @@ [class]{GraphAdjList}-[func]{} ``` +=== "Rust" + + ```rust title="graph_adjacency_list.rs" + [class]{GraphAdjList}-[func]{} + ``` + ## 效率对比 设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。 diff --git a/docs/chapter_graph/graph_traversal.md b/docs/chapter_graph/graph_traversal.md index 4358005cdc..936cf5c059 100644 --- a/docs/chapter_graph/graph_traversal.md +++ b/docs/chapter_graph/graph_traversal.md @@ -90,6 +90,12 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 [class]{}-[func]{graphBFS} ``` +=== "Rust" + + ```rust title="graph_bfs.rs" + [class]{}-[func]{graph_bfs} + ``` + 代码相对抽象,建议对照以下动画图示来加深理解。 === "<1>" @@ -233,6 +239,14 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 [class]{}-[func]{graphDFS} ``` +=== "Rust" + + ```rust title="graph_dfs.rs" + [class]{}-[func]{dfs} + + [class]{}-[func]{graph_dfs} + ``` + 深度优先遍历的算法流程如下图所示,其中: - **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点。 diff --git a/docs/chapter_greedy/fractional_knapsack_problem.md b/docs/chapter_greedy/fractional_knapsack_problem.md index 2d6b732e99..3c44ee1804 100644 --- a/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/docs/chapter_greedy/fractional_knapsack_problem.md @@ -119,6 +119,14 @@ [class]{}-[func]{fractionalKnapsack} ``` +=== "Rust" + + ```rust title="fractional_knapsack.rs" + [class]{Item}-[func]{} + + [class]{}-[func]{fractional_knapsack} + ``` + 最差情况下,需要遍历整个物品列表,**因此时间复杂度为 $O(n)$** ,其中 $n$ 为物品数量。 由于初始化了一个 `Item` 对象列表,**因此空间复杂度为 $O(n)$** 。 diff --git a/docs/chapter_greedy/greedy_algorithm.md b/docs/chapter_greedy/greedy_algorithm.md index 4ee97a4d35..c6393e300d 100644 --- a/docs/chapter_greedy/greedy_algorithm.md +++ b/docs/chapter_greedy/greedy_algorithm.md @@ -85,6 +85,12 @@ [class]{}-[func]{coinChangeGreedy} ``` +=== "Rust" + + ```rust title="coin_change_greedy.rs" + [class]{}-[func]{coin_change_greedy} + ``` + ## 贪心优点与局限性 **贪心算法不仅操作直接、实现简单,而且通常效率也很高**。在以上代码中,记硬币最小面值为 $\min(coins)$ ,则贪心选择最多循环 $amt / \min(coins)$ 次,时间复杂度为 $O(amt / \min(coins))$ 。这比动态规划解法的时间复杂度 $O(n \times amt)$ 提升了一个数量级。 diff --git a/docs/chapter_greedy/max_capacity_problem.md b/docs/chapter_greedy/max_capacity_problem.md index 7850f00d19..40596ff140 100644 --- a/docs/chapter_greedy/max_capacity_problem.md +++ b/docs/chapter_greedy/max_capacity_problem.md @@ -143,6 +143,12 @@ $$ [class]{}-[func]{maxCapacity} ``` +=== "Rust" + + ```rust title="max_capacity.rs" + [class]{}-[func]{max_capacity} + ``` + ### 正确性证明 之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。 diff --git a/docs/chapter_greedy/max_product_cutting_problem.md b/docs/chapter_greedy/max_product_cutting_problem.md index 97a1609b82..713062c991 100644 --- a/docs/chapter_greedy/max_product_cutting_problem.md +++ b/docs/chapter_greedy/max_product_cutting_problem.md @@ -129,6 +129,12 @@ $$ [class]{}-[func]{maxProductCutting} ``` +=== "Rust" + + ```rust title="max_product_cutting.rs" + [class]{}-[func]{max_product_cutting} + ``` + ![最大切分乘积的计算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png) **时间复杂度取决于编程语言的幂运算的实现方法**。以 Python 为例,常用的幂计算函数有三种: diff --git a/docs/chapter_hashing/hash_algorithm.md b/docs/chapter_hashing/hash_algorithm.md index f2bbcc4b18..17d986c06d 100644 --- a/docs/chapter_hashing/hash_algorithm.md +++ b/docs/chapter_hashing/hash_algorithm.md @@ -177,6 +177,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$ 取模,以确保哈希值在合适的范围内。值得思考的是,为什么要强调对质数取模,或者说对合数取模的弊端是什么?这是一个有趣的问题。 先抛出结论:**当我们使用大质数作为模数时,可以最大化地保证哈希值的均匀分布**。因为质数不会与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。 @@ -431,6 +443,12 @@ $$ // 节点对象 Instance of 'ListNode' 的哈希值为 1033450432 ``` +=== "Rust" + + ```rust title="built_in_hash.rs" + + ``` + 在许多编程语言中,**只有不可变对象才可作为哈希表的 `key`** 。假如我们将列表(动态数组)作为 `key` ,当列表的内容发生变化时,它的哈希值也随之改变,我们就无法在哈希表中查询到原先的 `value` 了。 虽然自定义对象(比如链表节点)的成员变量是可变的,但它是可哈希的。**这是因为对象的哈希值通常是基于内存地址生成的**,即使对象的内容发生了变化,但它的内存地址不变,哈希值仍然是不变的。 diff --git a/docs/chapter_hashing/hash_collision.md b/docs/chapter_hashing/hash_collision.md index c82c5331c3..60f0913b81 100644 --- a/docs/chapter_hashing/hash_collision.md +++ b/docs/chapter_hashing/hash_collision.md @@ -97,6 +97,12 @@ [class]{HashMapChaining}-[func]{} ``` +=== "Rust" + + ```rust title="hash_map_chaining.rs" + [class]{HashMapChaining}-[func]{} + ``` + !!! tip 当链表很长时,查询效率 $O(n)$ 很差,**此时可以将链表转换为「AVL 树」或「红黑树」**,从而将查询操作的时间复杂度优化至 $O(\log n)$ 。 @@ -190,6 +196,12 @@ [class]{HashMapOpenAddressing}-[func]{} ``` +=== "Rust" + + ```rust title="hash_map_open_addressing.rs" + [class]{HashMapOpenAddressing}-[func]{} + ``` + ### 多次哈希 顾名思义,多次哈希方法是使用多个哈希函数 $f_1(x)$ , $f_2(x)$ , $f_3(x)$ , $\cdots$ 进行探测。 diff --git a/docs/chapter_hashing/hash_map.md b/docs/chapter_hashing/hash_map.md index 8fa2cef152..e5219bc942 100755 --- a/docs/chapter_hashing/hash_map.md +++ b/docs/chapter_hashing/hash_map.md @@ -250,6 +250,12 @@ map.remove(10583); ``` +=== "Rust" + + ```rust title="hash_map.rs" + + ``` + 哈希表有三种常用遍历方式:遍历键值对、遍历键和遍历值。 === "Java" @@ -425,6 +431,12 @@ }); ``` +=== "Rust" + + ```rust title="hash_map.rs" + + ``` + ## 哈希表简单实现 我们先考虑最简单的情况,**仅用一个数组来实现哈希表**。在哈希表中,我们将数组中的每个空位称为「桶 Bucket」,每个桶可存储一个键值对。因此,查询操作就是找到 `key` 对应的桶,并在桶中获取 `value` 。 @@ -545,6 +557,14 @@ index = hash(key) % capacity [class]{ArrayHashMap}-[func]{} ``` +=== "Rust" + + ```rust title="array_hash_map.rs" + [class]{Pair}-[func]{} + + [class]{ArrayHashMap}-[func]{} + ``` + ## 哈希冲突与扩容 本质上看,哈希函数的作用是将所有 `key` 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,**理论上一定存在“多个输入对应相同输出”的情况**。 diff --git a/docs/chapter_heap/build_heap.md b/docs/chapter_heap/build_heap.md index 4d3dd123fb..908f8df19c 100644 --- a/docs/chapter_heap/build_heap.md +++ b/docs/chapter_heap/build_heap.md @@ -78,6 +78,12 @@ [class]{MaxHeap}-[func]{MaxHeap} ``` +=== "Rust" + + ```rust title="my_heap.rs" + [class]{MaxHeap}-[func]{new} + ``` + ## 复杂度分析 为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。 diff --git a/docs/chapter_heap/heap.md b/docs/chapter_heap/heap.md index 0e27601df5..6320ee39d0 100644 --- a/docs/chapter_heap/heap.md +++ b/docs/chapter_heap/heap.md @@ -307,6 +307,12 @@ // Dart 未提供内置 Heap 类 ``` +=== "Rust" + + ```rust title="heap.rs" + + ``` + ## 堆的实现 下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。 @@ -433,6 +439,16 @@ [class]{MaxHeap}-[func]{_parent} ``` +=== "Rust" + + ```rust title="my_heap.rs" + [class]{MaxHeap}-[func]{left} + + [class]{MaxHeap}-[func]{right} + + [class]{MaxHeap}-[func]{parent} + ``` + ### 访问堆顶元素 堆顶元素即为二叉树的根节点,也就是列表的首个元素。 @@ -503,6 +519,12 @@ [class]{MaxHeap}-[func]{peek} ``` +=== "Rust" + + ```rust title="my_heap.rs" + [class]{MaxHeap}-[func]{peek} + ``` + ### 元素入堆 给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 Heapify」。 @@ -626,6 +648,14 @@ [class]{MaxHeap}-[func]{siftUp} ``` +=== "Rust" + + ```rust title="my_heap.rs" + [class]{MaxHeap}-[func]{push} + + [class]{MaxHeap}-[func]{sift_up} + ``` + ### 堆顶元素出堆 堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采取以下操作步骤: @@ -756,6 +786,14 @@ [class]{MaxHeap}-[func]{siftDown} ``` +=== "Rust" + + ```rust title="my_heap.rs" + [class]{MaxHeap}-[func]{pop} + + [class]{MaxHeap}-[func]{sift_down} + ``` + ## 堆常见应用 - **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。 diff --git a/docs/chapter_heap/top_k.md b/docs/chapter_heap/top_k.md index 78182cd86a..328c1a609f 100644 --- a/docs/chapter_heap/top_k.md +++ b/docs/chapter_heap/top_k.md @@ -131,3 +131,9 @@ ```dart title="top_k.dart" [class]{}-[func]{top_k_heap} ``` + +=== "Rust" + + ```rust title="top_k.rs" + [class]{}-[func]{top_k_heap} + ``` diff --git a/docs/chapter_preface/suggestions.md b/docs/chapter_preface/suggestions.md index 30f76d9eb7..4bb7f0118a 100644 --- a/docs/chapter_preface/suggestions.md +++ b/docs/chapter_preface/suggestions.md @@ -154,6 +154,12 @@ */ ``` +=== "Rust" + + ```rust title="" + + ``` + ## 在动画图解中高效学习 相较于文字,视频和图片具有更高的信息密度和结构化程度,因此更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。 diff --git a/docs/chapter_searching/binary_search.md b/docs/chapter_searching/binary_search.md index f87673f311..2111067941 100755 --- a/docs/chapter_searching/binary_search.md +++ b/docs/chapter_searching/binary_search.md @@ -110,6 +110,12 @@ [class]{}-[func]{binarySearch} ``` +=== "Rust" + + ```rust title="binary_search.rs" + [class]{}-[func]{binary_search} + ``` + 时间复杂度为 $O(\log n)$ 。每轮缩小一半区间,因此二分循环次数为 $\log_2 n$ 。 空间复杂度为 $O(1)$ 。指针 `i` , `j` 使用常数大小空间。 @@ -186,6 +192,12 @@ [class]{}-[func]{binarySearchLCRO} ``` +=== "Rust" + + ```rust title="binary_search.rs" + [class]{}-[func]{binary_search_lcro} + ``` + 如下图所示,在两种区间表示下,二分查找算法的初始化、循环条件和缩小区间操作皆有所不同。 在“双闭区间”表示法中,由于左右边界都被定义为闭区间,因此指针 $i$ 和 $j$ 缩小区间操作也是对称的。这样更不容易出错。因此,**我们通常采用“双闭区间”的写法**。 diff --git a/docs/chapter_searching/binary_search_edge.md b/docs/chapter_searching/binary_search_edge.md index 8b0273b340..58cc89f2e1 100644 --- a/docs/chapter_searching/binary_search_edge.md +++ b/docs/chapter_searching/binary_search_edge.md @@ -118,6 +118,12 @@ [class]{}-[func]{binarySearchLeftEdge} ``` +=== "Rust" + + ```rust title="binary_search_edge.rs" + [class]{}-[func]{binary_search_left_edge} + ``` + ## 查找右边界 类似地,我们也可以二分查找最右边的 `target` 。当 `nums[m] == target` 时,说明大于 `target` 的元素在区间 $[m + 1, j]$ 中,因此执行 `i = m + 1` ,**使得指针 $i$ 向大于 `target` 的元素靠近**。 @@ -190,6 +196,12 @@ [class]{}-[func]{binarySearchRightEdge} ``` +=== "Rust" + + ```rust title="binary_search_edge.rs" + [class]{}-[func]{binary_search_right_edge} + ``` + 观察下图,搜索最右边元素时指针 $j$ 的作用与搜索最左边元素时指针 $i$ 的作用一致,反之亦然。也就是说,**搜索最左边元素和最右边元素的实现是镜像对称的**。 ![查找最左边和最右边元素的对称性](binary_search_edge.assets/binary_search_left_right_edge.png) diff --git a/docs/chapter_searching/replace_linear_by_hashing.md b/docs/chapter_searching/replace_linear_by_hashing.md index 5692f8dbb0..f3e6bb22b5 100755 --- a/docs/chapter_searching/replace_linear_by_hashing.md +++ b/docs/chapter_searching/replace_linear_by_hashing.md @@ -78,6 +78,12 @@ [class]{}-[func]{twoSumBruteForce} ``` +=== "Rust" + + ```rust title="two_sum.rs" + [class]{}-[func]{two_sum_brute_force} + ``` + 此方法的时间复杂度为 $O(n^2)$ ,空间复杂度为 $O(1)$ ,在大数据量下非常耗时。 ## 哈希查找:以空间换时间 @@ -166,6 +172,12 @@ [class]{}-[func]{twoSumHashTable} ``` +=== "Rust" + + ```rust title="two_sum.rs" + [class]{}-[func]{two_sum_hash_table} + ``` + 此方法通过哈希查找将时间复杂度从 $O(n^2)$ 降低至 $O(n)$ ,大幅提升运行效率。 由于需要维护一个额外的哈希表,因此空间复杂度为 $O(n)$ 。**尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法**。 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index cfbe82f864..78693f15c1 100755 --- a/docs/chapter_sorting/bubble_sort.md +++ b/docs/chapter_sorting/bubble_sort.md @@ -102,6 +102,12 @@ [class]{}-[func]{bubbleSort} ``` +=== "Rust" + + ```rust title="bubble_sort.rs" + [class]{}-[func]{bubble_sort} + ``` + ## 效率优化 我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。 @@ -174,6 +180,12 @@ [class]{}-[func]{bubbleSortWithFlag} ``` +=== "Rust" + + ```rust title="bubble_sort.rs" + [class]{}-[func]{bubble_sort_with_flag} + ``` + ## 算法特性 - **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。 diff --git a/docs/chapter_sorting/bucket_sort.md b/docs/chapter_sorting/bucket_sort.md index ab48b9710f..37a8509562 100644 --- a/docs/chapter_sorting/bucket_sort.md +++ b/docs/chapter_sorting/bucket_sort.md @@ -80,6 +80,12 @@ [class]{}-[func]{bucketSort} ``` +=== "Rust" + + ```rust title="bucket_sort.rs" + [class]{}-[func]{bucket_sort} + ``` + !!! question "桶排序的适用场景是什么?" 桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。 diff --git a/docs/chapter_sorting/counting_sort.md b/docs/chapter_sorting/counting_sort.md index cebc7ac050..5b3c54a3ea 100644 --- a/docs/chapter_sorting/counting_sort.md +++ b/docs/chapter_sorting/counting_sort.md @@ -78,6 +78,12 @@ [class]{}-[func]{countingSortNaive} ``` +=== "Rust" + + ```rust title="counting_sort.rs" + [class]{}-[func]{counting_sort_naive} + ``` + !!! note "计数排序与桶排序的联系" 从桶排序的角度看,我们可以将计数排序中的计数数组 `counter` 的每个索引视为一个桶,将统计数量的过程看作是将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。 @@ -191,6 +197,12 @@ $$ [class]{}-[func]{countingSort} ``` +=== "Rust" + + ```rust title="counting_sort.rs" + [class]{}-[func]{counting_sort} + ``` + ## 算法特性 - **时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。 diff --git a/docs/chapter_sorting/heap_sort.md b/docs/chapter_sorting/heap_sort.md index ed60e3fa21..797c61d91a 100644 --- a/docs/chapter_sorting/heap_sort.md +++ b/docs/chapter_sorting/heap_sort.md @@ -148,6 +148,14 @@ [class]{}-[func]{heapSort} ``` +=== "Rust" + + ```rust title="heap_sort.rs" + [class]{}-[func]{sift_down} + + [class]{}-[func]{heap_sort} + ``` + ## 算法特性 - **时间复杂度 $O(n \log n)$ 、非自适应排序** :建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。 diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index 21f746f7d4..f6290768ae 100755 --- a/docs/chapter_sorting/insertion_sort.md +++ b/docs/chapter_sorting/insertion_sort.md @@ -85,6 +85,12 @@ [class]{}-[func]{insertionSort} ``` +=== "Rust" + + ```rust title="insertion_sort.rs" + [class]{}-[func]{insertion_sort} + ``` + ## 算法特性 - **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。 diff --git a/docs/chapter_sorting/merge_sort.md b/docs/chapter_sorting/merge_sort.md index 9c451943be..d836438b55 100755 --- a/docs/chapter_sorting/merge_sort.md +++ b/docs/chapter_sorting/merge_sort.md @@ -139,6 +139,14 @@ [class]{}-[func]{mergeSort} ``` +=== "Rust" + + ```rust title="merge_sort.rs" + [class]{}-[func]{merge} + + [class]{}-[func]{merge_sort} + ``` + 合并方法 `merge()` 代码中的难点包括: - **在阅读代码时,需要特别注意各个变量的含义**。`nums` 的待合并区间为 `[left, right]` ,但由于 `tmp` 仅复制了 `nums` 该区间的元素,因此 `tmp` 对应区间为 `[0, right - left]` 。 diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index ea5ddc9f41..553b7c52a7 100755 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -125,6 +125,12 @@ [class]{QuickSort}-[func]{_partition} ``` +=== "Rust" + + ```rust title="quick_sort.rs" + [class]{QuickSort}-[func]{partition} + ``` + ## 算法流程 1. 首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组。 @@ -199,6 +205,12 @@ [class]{QuickSort}-[func]{quickSort} ``` +=== "Rust" + + ```rust title="quick_sort.rs" + [class]{QuickSort}-[func]{quick_sort} + ``` + ## 算法特性 - **时间复杂度 $O(n \log n)$ 、自适应排序** :在平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。在最差情况下,每轮哨兵划分操作都将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。 @@ -311,6 +323,14 @@ [class]{QuickSortMedian}-[func]{_partition} ``` +=== "Rust" + + ```rust title="quick_sort.rs" + [class]{QuickSortMedian}-[func]{median_three} + + [class]{QuickSortMedian}-[func]{partition} + ``` + ## 尾递归优化 **在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。 @@ -382,3 +402,9 @@ ```dart title="quick_sort.dart" [class]{QuickSortTailCall}-[func]{quickSort} ``` + +=== "Rust" + + ```rust title="quick_sort.rs" + [class]{QuickSortTailCall}-[func]{quick_sort} + ``` diff --git a/docs/chapter_sorting/radix_sort.md b/docs/chapter_sorting/radix_sort.md index 0fc00cb629..cb8fdb10c6 100644 --- a/docs/chapter_sorting/radix_sort.md +++ b/docs/chapter_sorting/radix_sort.md @@ -134,6 +134,16 @@ $$ [class]{}-[func]{radixSort} ``` +=== "Rust" + + ```rust title="radix_sort.rs" + [class]{}-[func]{digit} + + [class]{}-[func]{counting_sort_digit} + + [class]{}-[func]{radix_sort} + ``` + !!! question "为什么从最低位开始排序?" 在连续的排序轮次中,后一轮排序会覆盖前一轮排序的结果。举例来说,如果第一轮排序结果 $a < b$ ,而第二轮排序结果 $a > b$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。 diff --git a/docs/chapter_sorting/selection_sort.md b/docs/chapter_sorting/selection_sort.md index daaffd86d9..62af930abd 100644 --- a/docs/chapter_sorting/selection_sort.md +++ b/docs/chapter_sorting/selection_sort.md @@ -111,6 +111,12 @@ [class]{}-[func]{selectionSort} ``` +=== "Rust" + + ```rust title="selection_sort.rs" + [class]{}-[func]{selection_sort} + ``` + ## 算法特性 - **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。 diff --git a/docs/chapter_stack_and_queue/deque.md b/docs/chapter_stack_and_queue/deque.md index 9583f2a1ec..d67374d073 100644 --- a/docs/chapter_stack_and_queue/deque.md +++ b/docs/chapter_stack_and_queue/deque.md @@ -312,6 +312,12 @@ bool isEmpty = deque.isEmpty;W ``` +=== "Rust" + + ```rust title="deque.rs" + + ``` + ## 双向队列实现 * 双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。 @@ -427,6 +433,14 @@ [class]{LinkedListDeque}-[func]{} ``` +=== "Rust" + + ```rust title="linkedlist_deque.rs" + [class]{ListNode}-[func]{} + + [class]{LinkedListDeque}-[func]{} + ``` + ### 基于数组的实现 与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列。在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法。 @@ -514,6 +528,12 @@ [class]{ArrayDeque}-[func]{} ``` +=== "Rust" + + ```rust title="array_deque.rs" + [class]{ArrayDeque}-[func]{} + ``` + ## 双向队列应用 双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。 diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index e1dcd364ed..046579179f 100755 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -279,6 +279,12 @@ bool isEmpty = queue.isEmpty; ``` +=== "Rust" + + ```rust title="queue.rs" + + ``` + ## 队列实现 为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列。 @@ -364,6 +370,12 @@ [class]{LinkedListQueue}-[func]{} ``` +=== "Rust" + + ```rust title="linkedlist_queue.rs" + [class]{LinkedListQueue}-[func]{} + ``` + ### 基于数组的实现 由于数组删除首元素的时间复杂度为 $O(n)$ ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。 @@ -456,6 +468,12 @@ [class]{ArrayQueue}-[func]{} ``` +=== "Rust" + + ```rust title="array_queue.rs" + [class]{ArrayQueue}-[func]{} + ``` + 以上实现的队列仍然具有局限性,即其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的同学可以尝试自行实现。 两种实现的对比结论与栈一致,在此不再赘述。 diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index 0a1fdcf01b..4fdb47c95a 100755 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -277,6 +277,12 @@ bool isEmpty = stack.isEmpty; ``` +=== "Rust" + + ```rust title="stack.rs" + + ``` + ## 栈的实现 为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。 @@ -366,6 +372,12 @@ [class]{LinkedListStack}-[func]{} ``` +=== "Rust" + + ```rust title="linkedlist_stack.rs" + [class]{LinkedListStack}-[func]{} + ``` + ### 基于数组的实现 在基于「数组」实现栈时,我们可以将数组的尾部作为栈顶。在这样的设计下,入栈与出栈操作就分别对应在数组尾部添加元素与删除元素,时间复杂度都为 $O(1)$ 。 @@ -447,6 +459,12 @@ [class]{ArrayStack}-[func]{} ``` +=== "Rust" + + ```rust title="array_stack.rs" + [class]{ArrayStack}-[func]{} + ``` + ## 两种实现对比 ### 支持操作 diff --git a/docs/chapter_tree/array_representation_of_tree.md b/docs/chapter_tree/array_representation_of_tree.md index 600eea78fb..0d0100092c 100644 --- a/docs/chapter_tree/array_representation_of_tree.md +++ b/docs/chapter_tree/array_representation_of_tree.md @@ -108,6 +108,12 @@ 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) 值得说明的是,**完全二叉树非常适合使用数组来表示**。回顾完全二叉树的定义,$\text{None}$ 只出现在最底层且靠右的位置,**因此所有 $\text{None}$ 一定出现在层序遍历序列的末尾**。这意味着使用数组表示完全二叉树时,可以省略存储所有 $\text{None}$ ,非常方便。 @@ -185,6 +191,12 @@ [class]{ArrayBinaryTree}-[func]{} ``` +=== "Rust" + + ```rust title="array_binary_tree.rs" + [class]{ArrayBinaryTree}-[func]{} + ``` + ## 优势与局限性 二叉树的数组表示的优点包括: diff --git a/docs/chapter_tree/avl_tree.md b/docs/chapter_tree/avl_tree.md index 1453dd615e..c662142e55 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -182,6 +182,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Rust" + + ```rust title="" + + ``` + 「节点高度」是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。 === "Java" @@ -272,6 +278,14 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit [class]{AVLTree}-[func]{updateHeight} ``` +=== "Rust" + + ```rust title="avl_tree.rs" + [class]{AVLTree}-[func]{height} + + [class]{AVLTree}-[func]{update_height} + ``` + ### 节点平衡因子 节点的「平衡因子 Balance Factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。 @@ -342,6 +356,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit [class]{AVLTree}-[func]{balanceFactor} ``` +=== "Rust" + + ```rust title="avl_tree.rs" + [class]{AVLTree}-[func]{balance_factor} + ``` + !!! note 设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。 @@ -440,6 +460,12 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{rightRotate} ``` +=== "Rust" + + ```rust title="avl_tree.rs" + [class]{AVLTree}-[func]{right_rotate} + ``` + ### 左旋 相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。 @@ -518,6 +544,12 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{leftRotate} ``` +=== "Rust" + + ```rust title="avl_tree.rs" + [class]{AVLTree}-[func]{left_rotate} + ``` + ### 先左旋后右旋 对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 @@ -617,6 +649,12 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{rotate} ``` +=== "Rust" + + ```rust title="avl_tree.rs" + [class]{AVLTree}-[func]{rotate} + ``` + ## AVL 树常用操作 ### 插入节点 @@ -711,6 +749,14 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{insertHelper} ``` +=== "Rust" + + ```rust title="avl_tree.rs" + [class]{AVLTree}-[func]{insert} + + [class]{AVLTree}-[func]{insert_helper} + ``` + ### 删除节点 类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡。 @@ -803,6 +849,14 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{removeHelper} ``` +=== "Rust" + + ```rust title="avl_tree.rs" + [class]{AVLTree}-[func]{remove} + + [class]{AVLTree}-[func]{remove_helper} + ``` + ### 查找节点 AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。 diff --git a/docs/chapter_tree/binary_search_tree.md b/docs/chapter_tree/binary_search_tree.md index bdfd321ebb..c94c1e1a80 100755 --- a/docs/chapter_tree/binary_search_tree.md +++ b/docs/chapter_tree/binary_search_tree.md @@ -97,6 +97,12 @@ [class]{BinarySearchTree}-[func]{search} ``` +=== "Rust" + + ```rust title="binary_search_tree.rs" + [class]{BinarySearchTree}-[func]{search} + ``` + ### 插入节点 给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作分为两步: @@ -174,6 +180,12 @@ [class]{BinarySearchTree}-[func]{insert} ``` +=== "Rust" + + ```rust title="binary_search_tree.rs" + [class]{BinarySearchTree}-[func]{insert} + ``` + 为了插入节点,我们需要利用辅助节点 `pre` 保存上一轮循环的节点,这样在遍历至 $\text{None}$ 时,我们可以获取到其父节点,从而完成节点插入操作。 与查找节点相同,插入节点使用 $O(\log n)$ 时间。 @@ -275,6 +287,12 @@ [class]{BinarySearchTree}-[func]{remove} ``` +=== "Rust" + + ```rust title="binary_search_tree.rs" + [class]{BinarySearchTree}-[func]{remove} + ``` + ### 排序 我们知道,二叉树的中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。 diff --git a/docs/chapter_tree/binary_tree.md b/docs/chapter_tree/binary_tree.md index c1974b6a97..c99a5552dd 100644 --- a/docs/chapter_tree/binary_tree.md +++ b/docs/chapter_tree/binary_tree.md @@ -155,6 +155,12 @@ } ``` +=== "Rust" + + ```rust title="" + + ``` + 节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树」。 **在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。例如,在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。 @@ -358,6 +364,12 @@ n2.right = n5; ``` +=== "Rust" + + ```rust title="binary_tree.rs" + + ``` + **插入与删除节点**。与链表类似,通过修改指针来实现插入与删除节点。 ![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png) @@ -486,6 +498,12 @@ n1.left = n2; ``` +=== "Rust" + + ```rust title="binary_tree.rs" + + ``` + !!! note 需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。 diff --git a/docs/chapter_tree/binary_tree_traversal.md b/docs/chapter_tree/binary_tree_traversal.md index c48e552b8c..7568ca18e0 100755 --- a/docs/chapter_tree/binary_tree_traversal.md +++ b/docs/chapter_tree/binary_tree_traversal.md @@ -80,6 +80,12 @@ [class]{}-[func]{levelOrder} ``` +=== "Rust" + + ```rust title="binary_tree_bfs.rs" + [class]{}-[func]{level_order} + ``` + **时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 **空间复杂度**:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $\frac{n + 1}{2}$ 个节点,占用 $O(n)$ 空间。 @@ -204,6 +210,16 @@ [class]{}-[func]{postOrder} ``` +=== "Rust" + + ```rust title="binary_tree_dfs.rs" + [class]{}-[func]{pre_order} + + [class]{}-[func]{in_order} + + [class]{}-[func]{post_order} + ``` + **时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 **空间复杂度**:在最差情况下,即树退化为链表时,递归深度达到 $n$ ,系统占用 $O(n)$ 栈帧空间。