This commit is contained in:
krahets 2023-07-26 10:57:40 +08:00
parent 6381f16506
commit f8f7086196
52 changed files with 4032 additions and 0 deletions

View file

@ -106,6 +106,12 @@ comments: true
List<int> nums = [1, 3, 2, 5, 4];
```
=== "Rust"
```rust title="array.rs"
```
## 4.1.1. &nbsp; 数组优点
**在数组中访问元素非常高效**。由于数组元素被存储在连续的内存空间中,因此计算数组元素的内存地址非常容易。给定数组首个元素的地址和某个元素的索引,我们可以使用以下公式计算得到该元素的内存地址,从而直接访问此元素。
@ -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. &nbsp; 数组缺点
**数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。
@ -457,6 +476,22 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
}
```
=== "Rust"
```rust title="array.rs"
/* 扩展数组长度 */
fn extend(nums: Vec<i32>, enlarge: usize) -> Vec<i32> {
// 初始化一个扩展长度后的数组
let mut res: Vec<i32> = 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<i32>, 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<i32>, 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<usize> {
for i in 0..nums.len() {
if nums[i] == target {
return Some(i);
}
}
None
}
```
## 4.1.4. &nbsp; 数组典型应用
数组是最基础的数据结构,在各类数据结构和算法中都有广泛应用。

View file

@ -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. &nbsp; 链表优点
**链表中插入与删除节点的操作效率高**。例如,如果我们想在链表中间的两个节点 `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<T>(n0: &Rc<RefCell<ListNode<T>>>, P: Rc<RefCell<ListNode<T>>>) {
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<T>(n0: &Rc<RefCell<ListNode<T>>>) {
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. &nbsp; 链表缺点
**链表访问节点效率较低**。如上节所述,数组可以在 $O(1)$ 时间下访问任意元素。然而,链表无法直接访问任意节点,这是因为系统需要从头节点出发,逐个向后遍历直至找到目标节点。例如,若要访问链表索引为 `index`(即第 `index + 1` 个)的节点,则需要向后遍历 `index` 轮。
@ -819,6 +859,19 @@ comments: true
}
```
=== "Rust"
```rust title="linked_list.rs"
/* 访问链表中索引为 index 的节点 */
pub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Rc<RefCell<ListNode<T>>> {
if index <= 0 {return head};
if let Some(node) = &head.borrow_mut().next {
return access(node.clone(), index - 1);
}
return head;
}
```
**链表的内存占用较大**。链表以节点为单位,每个节点除了保存值之外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多的内存空间。
## 4.2.3. &nbsp; 链表常用操作
@ -1005,6 +1058,19 @@ comments: true
}
```
=== "Rust"
```rust title="linked_list.rs"
/* 在链表中查找值为 target 的首个节点 */
pub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, 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. &nbsp; 常见链表类型
**单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的指针(引用)两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向空 $\text{None}$ 。
@ -1184,6 +1250,12 @@ comments: true
}
```
=== "Rust"
```rust title=""
```
![常见链表种类](linked_list.assets/linkedlist_common_types.png)
<p align="center"> Fig. 常见链表种类 </p>

View file

@ -120,6 +120,12 @@ comments: true
List<int> 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. &nbsp; 列表实现 *
为了帮助加深对列表的理解,我们在此提供一个简易版列表实现。需要关注三个核心点:
@ -1918,3 +1954,114 @@ comments: true
}
}
```
=== "Rust"
```rust title="my_list.rs"
/* 列表类简易实现 */
#[allow(dead_code)]
struct MyList {
nums: Vec<i32>, // 数组(存储列表元素)
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<i32> {
// 仅转换有效长度范围内的列表元素
let mut nums = Vec::new();
for i in 0..self.size {
nums.push(self.get(i));
}
nums
}
}
```

View file

@ -165,6 +165,25 @@ comments: true
[class]{}-[func]{preOrder}
```
=== "Rust"
```rust title="preorder_traversal_i_compact.rs"
/* 前序遍历:例题一 */
fn pre_order(res: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<Rc<RefCell<TreeNode>>>) {
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)
<p align="center"> Fig. 在前序遍历中搜索节点 </p>
@ -370,6 +389,29 @@ comments: true
[class]{}-[func]{preOrder}
```
=== "Rust"
```rust title="preorder_traversal_ii_compact.rs"
/* 前序遍历:例题二 */
fn pre_order(res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>, path: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<Rc<RefCell<TreeNode>>>) {
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<Vec<Rc<RefCell<TreeNode>>>>, path: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<Rc<RefCell<TreeNode>>>) {
// 剪枝
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<Rc<RefCell<TreeNode>>>) -> bool {
return !state.is_empty() && state.get(state.len() - 1).unwrap().borrow().val == 7;
}
/* 记录解 */
fn record_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>, res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>) {
res.push(state.clone());
}
/* 判断在当前状态下,该选择是否合法 */
fn is_valid(_: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) -> bool {
return choice.borrow().val != 3;
}
/* 更新状态 */
fn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {
state.push(choice);
}
/* 恢复状态 */
fn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {
state.remove(state.len() - 1);
}
/* 回溯算法:例题三 */
fn backtrack(state: &mut Vec<Rc<RefCell<TreeNode>>>, choices: &mut Vec<Rc<RefCell<TreeNode>>>, res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>) {
// 检查是否为解
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)

View file

@ -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<Vec<String>>, res: &mut Vec<Vec<Vec<String>>>,
cols: &mut [bool], diags1: &mut [bool], diags2: &mut [bool]) {
// 当放置完所有行时,记录解
if row == n {
let mut copy_state: Vec<Vec<String>> = 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<Vec<Vec<String>>> {
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
let mut state: Vec<Vec<String>> = Vec::new();
for _ in 0..n {
let mut row: Vec<String> = 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<Vec<String>>> = 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)$** 。

View file

@ -361,6 +361,41 @@ comments: true
[class]{}-[func]{permutationsI}
```
=== "Rust"
```rust title="permutations_i.rs"
/* 回溯算法:全排列 I */
fn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {
// 当状态长度等于元素数量时,记录解
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<Vec<i32>> {
let mut res = Vec::new(); // 状态(子集)
backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);
res
}
```
## 13.2.2. &nbsp; 考虑相等元素的情况
!!! question
@ -718,6 +753,43 @@ comments: true
[class]{}-[func]{permutationsII}
```
=== "Rust"
```rust title="permutations_ii.rs"
/* 回溯算法:全排列 II */
fn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {
// 当状态长度等于元素数量时,记录解
if state.len() == choices.len() {
res.push(state);
return;
}
// 遍历所有选择
let mut duplicated = HashSet::<i32>::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<Vec<i32>> {
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)$** 。

View file

@ -273,6 +273,41 @@ comments: true
[class]{}-[func]{subsetSumINaive}
```
=== "Rust"
```rust title="subset_sum_i_naive.rs"
/* 回溯算法:子集和 I */
fn backtrack(mut state: Vec<i32>, target: i32, total: i32, choices: &[i32], res: &mut Vec<Vec<i32>>) {
// 子集和等于 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<Vec<i32>> {
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<i32>, target: i32, choices: &[i32], start: usize, res: &mut Vec<Vec<i32>>) {
// 子集和等于 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<Vec<i32>> {
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<i32>, target: i32, choices: &[i32], start: usize, res: &mut Vec<Vec<i32>>) {
// 子集和等于 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<Vec<i32>> {
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)

View file

@ -286,6 +286,12 @@ comments: true
}
```
=== "Rust"
```rust title=""
```
## 2.3.2. &nbsp; 推算方法
空间复杂度的推算方法与时间复杂度大致相同,只是将统计对象从“计算操作数量”转为“使用空间大小”。与时间复杂度不同的是,**我们通常只关注「最差空间复杂度」**,这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。
@ -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. &nbsp; 常见类型
设输入数据大小为 $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)
<p align="center"> Fig. 递归函数产生的线性阶空间复杂度 </p>
@ -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)
<p align="center"> Fig. 递归函数产生的平方阶空间复杂度 </p>
@ -1776,6 +1881,19 @@ $$
}
```
=== "Rust"
```rust title="space_complexity.rs"
/* 指数阶(建立满二叉树) */
fn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {
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)
<p align="center"> Fig. 满二叉树产生的指数阶空间复杂度 </p>

View file

@ -172,6 +172,12 @@ $$
}
```
=== "Rust"
```rust title=""
```
然而实际上,**统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。
## 2.2.2. &nbsp; 统计时间增长趋势
@ -398,6 +404,12 @@ $$
}
```
=== "Rust"
```rust title=""
```
![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png)
<p align="center"> Fig. 算法 A, B, C 的时间增长趋势 </p>
@ -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)
<p align="center"> Fig. 常数阶、线性阶、平方阶的时间复杂度 </p>
@ -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)
<p align="center"> Fig. 指数阶的时间复杂度 </p>
@ -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)
<p align="center"> Fig. 对数阶的时间复杂度 </p>
@ -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)
<p align="center"> Fig. 线性对数阶的时间复杂度 </p>
@ -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)
<p align="center"> Fig. 阶乘阶的时间复杂度 </p>
@ -3043,6 +3239,31 @@ $$
}
```
=== "Rust"
```rust title="worst_best_time_complexity.rs"
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */
fn random_numbers(n: i32) -> Vec<i32> {
// 生成数组 nums = { 1, 2, 3, ..., n }
let mut nums = (1..=n).collect::<Vec<i32>>();
// 随机打乱数组元素
nums.shuffle(&mut thread_rng());
nums
}
/* 查找数组 nums 中数字 1 所在索引 */
fn find_one(nums: &[i32]) -> Option<usize> {
for i in 0..nums.len() {
// 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)
// 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)
if nums[i] == 1 {
return Some(i);
}
}
None
}
```
!!! tip
实际应用中我们很少使用「最佳时间复杂度」,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。相反,「最差时间复杂度」更为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。

View file

@ -143,3 +143,9 @@ comments: true
List<String> characters = List.filled(5, 'a');
List<bool> booleans = List.filled(5, false);
```
=== "Rust"
```rust title=""
```

View file

@ -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)
}
```

View file

@ -282,6 +282,37 @@ status: new
[class]{}-[func]{buildTree}
```
=== "Rust"
```rust title="build_tree.rs"
/* 构建二叉树:分治 */
fn dfs(preorder: &[i32], inorder: &[i32], hmap: &HashMap<i32, i32>, i: i32, l: i32, r: i32) -> Option<Rc<RefCell<TreeNode>>> {
// 子树区间为空时终止
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<Rc<RefCell<TreeNode>>> {
// 初始化哈希表,存储 inorder 元素到索引的映射
let mut hmap: HashMap<i32, i32> = 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>"

View file

@ -321,6 +321,40 @@ status: new
[class]{}-[func]{hanota}
```
=== "Rust"
```rust title="hanota.rs"
/* 移动一个圆盘 */
fn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {
// 从 src 顶部拿出一个圆盘
let pan = src.remove(src.len() - 1);
// 将圆盘放入 tar 顶部
tar.push(pan);
}
/* 求解汉诺塔:问题 f(i) */
fn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {
// 若 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<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {
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)

View file

@ -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)
<p align="center"> Fig. 爬楼梯最小代价的动态规划过程 </p>
@ -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. &nbsp; 无后效性
「无后效性」是动态规划能够有效解决问题的重要特性之一,定义为:**给定一个确定的状态,它的未来发展只与当前状态有关,而与当前状态过去所经历过的所有状态无关**。
@ -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 "爬楼梯与障碍生成"

View file

@ -279,6 +279,27 @@ $$
[class]{}-[func]{minPathSumDFS}
```
=== "Rust"
```rust title="min_path_sum.rs"
/* 最小路径和:暴力搜索 */
fn min_path_sum_dfs(grid: &Vec<Vec<i32>>, 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<Vec<i32>>, mem: &mut Vec<Vec<i32>>, 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<Vec<i32>>) -> 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<Vec<i32>>) -> 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]
}
```

View file

@ -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]
}
```

View file

@ -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. &nbsp; 方法一:暴力搜索
回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。
@ -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)$ 。
**这种空间优化技巧被称为「状态压缩」**。在常见的动态规划问题中,当前状态仅与前面有限个状态有关,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。

View file

@ -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<Vec<i32>>, 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)
<p align="center"> Fig. 0-1 背包的记忆化搜索递归树 </p>
@ -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]
}
```

View file

@ -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. &nbsp; 零钱兑换问题
背包问题是一大类动态规划问题的代表,其拥有很多的变种,例如零钱兑换问题。
@ -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. &nbsp; 零钱兑换问题 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]
}
```

View file

@ -1021,6 +1021,107 @@ comments: true
}
```
=== "Rust"
```rust title="graph_adjacency_matrix.rs"
/* 基于邻接矩阵实现的无向图类型 */
pub struct GraphAdjMat {
// 顶点列表,元素代表“顶点值”,索引代表“顶点索引”
pub vertices: Vec<i32>,
// 邻接矩阵,行列索引对应“顶点索引”
pub adj_mat: Vec<Vec<i32>>,
}
impl GraphAdjMat {
/* 构造方法 */
pub fn new(vertices: Vec<i32>, 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. &nbsp; 基于邻接表的实现
设无向图的顶点总数为 $n$ 、边总数为 $m$ ,则有:
@ -1913,6 +2014,100 @@ comments: true
}
```
=== "Rust"
```rust title="graph_adjacency_list.rs"
/* 基于邻接表实现的无向图类型 */
pub struct GraphAdjList {
// 邻接表key: 顶点value该顶点的所有邻接顶点
pub adj_list: HashMap<Vertex, Vec<Vertex>>,
}
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::<Vec<i32>>();
println!("{}: {:?},", vertex.val, list);
}
}
}
```
## 9.2.3. &nbsp; 效率对比
设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。

View file

@ -358,6 +358,40 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质
}
```
=== "Rust"
```rust title="graph_bfs.rs"
/* 广度优先遍历 BFS */
// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
fn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {
// 顶点遍历序列
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<Vertex>, res: &mut Vec<Vertex>, 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<Vertex> {
// 顶点遍历序列
let mut res = vec![];
// 哈希表,用于记录已被访问过的顶点
let mut visited = HashSet::new();
dfs(&graph, &mut visited, &mut res, start_vet);
res
}
```
深度优先遍历的算法流程如下图所示,其中:
- **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点。

View file

@ -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::<Vec<Item>>();
// 按照单位价值 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)$** 。

View file

@ -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. &nbsp; 贪心优点与局限性
**贪心算法不仅操作直接、实现简单,而且通常效率也很高**。在以上代码中,记硬币最小面值为 $\min(coins)$ ,则贪心选择最多循环 $amt / \min(coins)$ 次,时间复杂度为 $O(amt / \min(coins))$ 。这比动态规划解法的时间复杂度 $O(n \times amt)$ 提升了一个数量级。

View file

@ -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
}
```
### 正确性证明
之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。

View file

@ -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)
<p align="center"> Fig. 最大切分乘积的计算方法 </p>

View file

@ -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` 了。
虽然自定义对象(比如链表节点)的成员变量是可变的,但它是可哈希的。**这是因为对象的哈希值通常是基于内存地址生成的**,即使对象的内容发生了变化,但它的内存地址不变,哈希值仍然是不变的。

View file

@ -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$ 进行探测。

View file

@ -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. &nbsp; 哈希表简单实现
我们先考虑最简单的情况,**仅用一个数组来实现哈希表**。在哈希表中,我们将数组中的每个空位称为「桶 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<Option<Pair>>
}
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. &nbsp; 哈希冲突与扩容
本质上看,哈希函数的作用是将所有 `key` 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,**理论上一定存在“多个输入对应相同输出”的情况**。

View file

@ -176,6 +176,21 @@ comments: true
}
```
=== "Rust"
```rust title="my_heap.rs"
/* 构造方法,根据输入列表建堆 */
fn new(nums: Vec<i32>) -> 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. &nbsp; 复杂度分析
为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。

View file

@ -313,6 +313,12 @@ comments: true
// Dart 未提供内置 Heap 类
```
=== "Rust"
```rust title="heap.rs"
```
## 8.1.2. &nbsp; 堆的实现
下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\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<i32> {
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. &nbsp; 堆常见应用
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。

View file

@ -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<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {
// Rust 的 BinaryHeap 是大顶堆,使用 Reverse 将元素大小反转
let mut heap = BinaryHeap::<Reverse<i32>>::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
}
```

View file

@ -158,6 +158,12 @@ comments: true
*/
```
=== "Rust"
```rust title=""
```
## 0.2.2. &nbsp; 在动画图解中高效学习
相较于文字,视频和图片具有更高的信息密度和结构化程度,因此更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。

View file

@ -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$ 缩小区间操作也是对称的。这样更不容易出错。因此,**我们通常采用“双闭区间”的写法**。

View file

@ -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. &nbsp; 查找右边界
类似地,我们也可以二分查找最右边的 `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)

View file

@ -208,6 +208,24 @@ comments: true
}
```
=== "Rust"
```rust title="two_sum.rs"
/* 方法一:暴力枚举 */
pub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
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. &nbsp; 哈希查找:以空间换时间
@ -462,6 +480,24 @@ comments: true
}
```
=== "Rust"
```rust title="two_sum.rs"
/* 方法二:辅助哈希表 */
pub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
// 辅助哈希表,空间复杂度 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)$ 。**尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法**。

View file

@ -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. &nbsp; 效率优化
我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `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. &nbsp; 算法特性
- **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。

View file

@ -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 个桶,然后分别对每个桶进行排序,最后将结果合并。

View file

@ -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. &nbsp; 算法特性
- **时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。

View file

@ -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. &nbsp; 算法特性
- **时间复杂度 $O(n \log n)$ 、非自适应排序** :建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。

View file

@ -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. &nbsp; 算法特性
- **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。

View file

@ -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<i32> = 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]`

View file

@ -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. &nbsp; 算法流程
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. &nbsp; 算法特性
- **时间复杂度 $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. &nbsp; 尾递归优化
**在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $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]
}
}
}
```

View file

@ -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$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。

View file

@ -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. &nbsp; 算法特性
- **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。

View file

@ -318,6 +318,12 @@ comments: true
bool isEmpty = deque.isEmpty;W
```
=== "Rust"
```rust title="deque.rs"
```
## 5.3.2. &nbsp; 双向队列实现 *
双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。
@ -1795,6 +1801,174 @@ comments: true
}
```
=== "Rust"
```rust title="linkedlist_deque.rs"
/* 双向链表节点 */
pub struct ListNode<T> {
pub val: T, // 节点值
pub next: Option<Rc<RefCell<ListNode<T>>>>, // 后继节点引用(指针)
pub prev: Option<Rc<RefCell<ListNode<T>>>>, // 前驱节点引用(指针)
}
impl<T> ListNode<T> {
pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {
Rc::new(RefCell::new(ListNode {
val,
next: None,
prev: None,
}))
}
}
/* 基于双向链表实现的双向队列 */
#[allow(dead_code)]
pub struct LinkedListDeque<T> {
front: Option<Rc<RefCell<ListNode<T>>>>, // 头节点 front
rear: Option<Rc<RefCell<ListNode<T>>>>, // 尾节点 rear
que_size: usize, // 双向队列的长度
}
impl<T: Copy> LinkedListDeque<T> {
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<T> {
// 若队列为空,直接返回 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<T> {
return self.pop(true);
}
/* 队尾出队 */
pub fn pop_last(&mut self) -> Option<T> {
return self.pop(false);
}
/* 访问队首元素 */
pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {
self.front.as_ref()
}
/* 访问队尾元素 */
pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {
self.rear.as_ref()
}
/* 返回数组用于打印 */
pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {
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<i32>, // 用于存储双向队列元素的数组
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<i32> {
// 仅转换有效长度范围内的列表元素
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. &nbsp; 双向队列应用
双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。

View file

@ -285,6 +285,12 @@ comments: true
bool isEmpty = queue.isEmpty;
```
=== "Rust"
```rust title="queue.rs"
```
## 5.2.2. &nbsp; 队列实现
为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列。
@ -1090,6 +1096,88 @@ comments: true
}
```
=== "Rust"
```rust title="linkedlist_queue.rs"
/* 基于链表实现的队列 */
#[allow(dead_code)]
pub struct LinkedListQueue<T> {
front: Option<Rc<RefCell<ListNode<T>>>>, // 头节点 front
rear: Option<Rc<RefCell<ListNode<T>>>>, // 尾节点 rear
que_size: usize, // 队列的长度
}
impl<T: Copy> LinkedListQueue<T> {
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<T> {
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<RefCell<ListNode<T>>>> {
self.front.as_ref()
}
/* 将链表转化为 Array 并返回 */
pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {
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<i32>, // 用于存储队列元素的数组
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<i32> {
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
}
}
```
以上实现的队列仍然具有局限性,即其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的同学可以尝试自行实现。
两种实现的对比结论与栈一致,在此不再赘述。

View file

@ -283,6 +283,12 @@ comments: true
bool isEmpty = stack.isEmpty;
```
=== "Rust"
```rust title="stack.rs"
```
## 5.1.2. &nbsp; 栈的实现
为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。
@ -989,6 +995,75 @@ comments: true
}
```
=== "Rust"
```rust title="linkedlist_stack.rs"
/* 基于链表实现的栈 */
#[allow(dead_code)]
pub struct LinkedListStack<T> {
stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // 将头节点作为栈顶
stk_size: usize, // 栈的长度
}
impl<T: Copy> LinkedListStack<T> {
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<T> {
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<RefCell<ListNode<T>>>> {
self.stack_peek.as_ref()
}
/* 将 List 转化为 Array 并返回 */
pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {
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<T> {
stack: Vec<T>,
}
impl<T> ArrayStack<T> {
/* 初始化栈 */
fn new() -> ArrayStack<T> {
ArrayStack::<T> { stack: Vec::<T>::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<T> {
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<T> {
&self.stack
}
}
```
## 5.1.3. &nbsp; 两种实现对比
### 支持操作

View file

@ -116,6 +116,12 @@ comments: true
List<int?> 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)
<p align="center"> Fig. 任意类型二叉树的数组表示 </p>
@ -620,6 +626,107 @@ comments: true
[class]{ArrayBinaryTree}-[func]{}
```
=== "Rust"
```rust title="array_binary_tree.rs"
/* 数组表示下的二叉树类 */
struct ArrayBinaryTree {
tree: Vec<Option<i32>>,
}
impl ArrayBinaryTree {
/* 构造方法 */
fn new(arr: Vec<Option<i32>>) -> Self {
Self { tree: arr }
}
/* 节点数量 */
fn size(&self) -> i32 {
self.tree.len() as i32
}
/* 获取索引为 i 节点的值 */
fn val(&self, i: i32) -> Option<i32> {
// 若索引越界,则返回 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<i32> {
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<i32>) {
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<i32> {
let mut res = vec![];
self.dfs(0, "pre", &mut res);
res
}
/* 中序遍历 */
fn in_order(&self) -> Vec<i32> {
let mut res = vec![];
self.dfs(0, "in", &mut res);
res
}
/* 后序遍历 */
fn post_order(&self) -> Vec<i32> {
let mut res = vec![];
self.dfs(0, "post", &mut res);
res
}
}
```
## 7.3.3. &nbsp; 优势与局限性
二叉树的数组表示的优点包括:

View file

@ -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. &nbsp; 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 树的节点查找操作与二叉搜索树一致,在此不再赘述。

View file

@ -271,6 +271,33 @@ comments: true
[class]{BinarySearchTree}-[func]{search}
```
=== "Rust"
```rust title="binary_search_tree.rs"
/* 查找节点 */
pub fn search(&self, num: i32) -> Option<TreeNodeRc> {
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$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。

View file

@ -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
需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。

View file

@ -298,6 +298,30 @@ comments: true
}
```
=== "Rust"
```rust title="binary_tree_bfs.rs"
/* 层序遍历 */
fn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {
// 初始化队列,加入根结点
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<RefCell<TreeNode>>>) -> Vec<i32> {
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<RefCell<TreeNode>>>) -> Vec<i32> {
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<RefCell<TreeNode>>>) -> Vec<i32> {
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)$ 栈帧空间。