mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-26 14:16:28 +08:00
build
This commit is contained in:
parent
6381f16506
commit
f8f7086196
52 changed files with 4032 additions and 0 deletions
|
@ -106,6 +106,12 @@ comments: true
|
|||
List<int> nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="array.rs"
|
||||
|
||||
```
|
||||
|
||||
## 4.1.1. 数组优点
|
||||
|
||||
**在数组中访问元素非常高效**。由于数组元素被存储在连续的内存空间中,因此计算数组元素的内存地址非常容易。给定数组首个元素的地址和某个元素的索引,我们可以使用以下公式计算得到该元素的内存地址,从而直接访问此元素。
|
||||
|
@ -270,6 +276,19 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="array.rs"
|
||||
/* 随机返回一个数组元素 */
|
||||
fn random_access(nums: &[i32]) -> i32 {
|
||||
// 在区间 [0, nums.len()) 中随机抽取一个数字
|
||||
let random_index = rand::thread_rng().gen_range(0..nums.len());
|
||||
// 获取并返回随机元素
|
||||
let random_num = nums[random_index];
|
||||
random_num
|
||||
}
|
||||
```
|
||||
|
||||
## 4.1.2. 数组缺点
|
||||
|
||||
**数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。
|
||||
|
@ -457,6 +476,22 @@ elementAddr = firtstElementAddr + elementLength * elementIndex
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="array.rs"
|
||||
/* 扩展数组长度 */
|
||||
fn extend(nums: Vec<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. 数组典型应用
|
||||
|
||||
数组是最基础的数据结构,在各类数据结构和算法中都有广泛应用。
|
||||
|
|
|
@ -169,6 +169,12 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
!!! question "尾节点指向什么?"
|
||||
|
||||
我们将链表的最后一个节点称为「尾节点」,其指向的是“空”,在 Java, C++, Python 中分别记为 $\text{null}$ , $\text{nullptr}$ , $\text{None}$ 。在不引起歧义的前提下,本书都使用 $\text{None}$ 来表示空。
|
||||
|
@ -366,6 +372,12 @@ comments: true
|
|||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="linked_list.rs"
|
||||
|
||||
```
|
||||
|
||||
## 4.2.1. 链表优点
|
||||
|
||||
**链表中插入与删除节点的操作效率高**。例如,如果我们想在链表中间的两个节点 `A` , `B` 之间插入一个新节点 `P` ,我们只需要改变两个节点指针即可,时间复杂度为 $O(1)$ ;相比之下,数组的插入操作效率要低得多。
|
||||
|
@ -494,6 +506,18 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="linked_list.rs"
|
||||
/* 在链表的节点 n0 之后插入节点 P */
|
||||
#[allow(non_snake_case)]
|
||||
pub fn insert<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. 链表缺点
|
||||
|
||||
**链表访问节点效率较低**。如上节所述,数组可以在 $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. 链表常用操作
|
||||
|
@ -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. 常见链表类型
|
||||
|
||||
**单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的指针(引用)两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向空 $\text{None}$ 。
|
||||
|
@ -1184,6 +1250,12 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
![常见链表种类](linked_list.assets/linkedlist_common_types.png)
|
||||
|
||||
<p align="center"> Fig. 常见链表种类 </p>
|
||||
|
|
|
@ -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. 列表实现 *
|
||||
|
||||
为了帮助加深对列表的理解,我们在此提供一个简易版列表实现。需要关注三个核心点:
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)$** 。
|
||||
|
|
|
@ -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. 考虑相等元素的情况
|
||||
|
||||
!!! 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)$** 。
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -286,6 +286,12 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
## 2.3.2. 推算方法
|
||||
|
||||
空间复杂度的推算方法与时间复杂度大致相同,只是将统计对象从“计算操作数量”转为“使用空间大小”。与时间复杂度不同的是,**我们通常只关注「最差空间复杂度」**,这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。
|
||||
|
@ -418,6 +424,12 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
**在递归函数中,需要注意统计栈帧空间**。例如,函数 `loop()` 在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行过程中会同时存在 $n$ 个未返回的 `recur()` ,从而占用 $O(n)$ 的栈帧空间。
|
||||
|
||||
=== "Java"
|
||||
|
@ -633,6 +645,12 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
## 2.3.3. 常见类型
|
||||
|
||||
设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列)
|
||||
|
@ -895,6 +913,28 @@ $$
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="space_complexity.rs"
|
||||
/* 常数阶 */
|
||||
#[allow(unused)]
|
||||
fn constant(n: i32) {
|
||||
// 常量、变量、对象占用 O(1) 空间
|
||||
const A: i32 = 0;
|
||||
let b = 0;
|
||||
let nums = vec![0; 10000];
|
||||
let node = ListNode::new(0);
|
||||
// 循环中的变量占用 O(1) 空间
|
||||
for i in 0..n {
|
||||
let c = 0;
|
||||
}
|
||||
// 循环中的函数占用 O(1) 空间
|
||||
for i in 0..n {
|
||||
function();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 线性阶 $O(n)$
|
||||
|
||||
线性阶常见于元素数量与 $n$ 成正比的数组、链表、栈、队列等。
|
||||
|
@ -1140,6 +1180,27 @@ $$
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="space_complexity.rs"
|
||||
/* 线性阶 */
|
||||
#[allow(unused)]
|
||||
fn linear(n: i32) {
|
||||
// 长度为 n 的数组占用 O(n) 空间
|
||||
let mut nums = vec![0; n as usize];
|
||||
// 长度为 n 的列表占用 O(n) 空间
|
||||
let mut nodes = Vec::new();
|
||||
for i in 0..n {
|
||||
nodes.push(ListNode::new(i))
|
||||
}
|
||||
// 长度为 n 的哈希表占用 O(n) 空间
|
||||
let mut map = HashMap::new();
|
||||
for i in 0..n {
|
||||
map.insert(i, i.to_string());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间。
|
||||
|
||||
=== "Java"
|
||||
|
@ -1270,6 +1331,17 @@ $$
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="space_complexity.rs"
|
||||
/* 线性阶(递归实现) */
|
||||
fn linear_recur(n: i32) {
|
||||
println!("递归 n = {}", n);
|
||||
if n == 1 {return};
|
||||
linear_recur(n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png)
|
||||
|
||||
<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>
|
||||
|
|
|
@ -172,6 +172,12 @@ $$
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
然而实际上,**统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。
|
||||
|
||||
## 2.2.2. 统计时间增长趋势
|
||||
|
@ -398,6 +404,12 @@ $$
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png)
|
||||
|
||||
<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
|
||||
|
||||
实际应用中我们很少使用「最佳时间复杂度」,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。相反,「最差时间复杂度」更为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。
|
||||
|
|
|
@ -143,3 +143,9 @@ comments: true
|
|||
List<String> characters = List.filled(5, 'a');
|
||||
List<bool> booleans = List.filled(5, false);
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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>"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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. 无后效性
|
||||
|
||||
「无后效性」是动态规划能够有效解决问题的重要特性之一,定义为:**给定一个确定的状态,它的未来发展只与当前状态有关,而与当前状态过去所经历过的所有状态无关**。
|
||||
|
@ -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 "爬楼梯与障碍生成"
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -265,6 +265,34 @@ status: new
|
|||
[class]{}-[func]{climbingStairsBacktrack}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="climbing_stairs_backtrack.rs"
|
||||
/* 回溯 */
|
||||
fn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {
|
||||
// 当爬到第 n 阶时,方案数量加 1
|
||||
if state == n { res[0] = res[0] + 1; }
|
||||
// 遍历所有选择
|
||||
for &choice in choices {
|
||||
// 剪枝:不允许越过第 n 阶
|
||||
if state + choice > n { break; }
|
||||
// 尝试:做出选择,更新状态
|
||||
backtrack(choices, state + choice, n, res);
|
||||
// 回退
|
||||
}
|
||||
}
|
||||
|
||||
/* 爬楼梯:回溯 */
|
||||
fn climbing_stairs_backtrack(n: usize) -> i32 {
|
||||
let choices = vec![ 1, 2 ]; // 可选择向上爬 1 或 2 阶
|
||||
let state = 0; // 从第 0 阶开始爬
|
||||
let mut res = Vec::new();
|
||||
res.push(0); // 使用 res[0] 记录方案数量
|
||||
backtrack(&choices, state, n as i32, &mut res);
|
||||
res[0]
|
||||
}
|
||||
```
|
||||
|
||||
## 14.1.1. 方法一:暴力搜索
|
||||
|
||||
回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。
|
||||
|
@ -462,6 +490,24 @@ $$
|
|||
[class]{}-[func]{climbingStairsDFS}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="climbing_stairs_dfs.rs"
|
||||
/* 搜索 */
|
||||
fn dfs(i: usize) -> i32 {
|
||||
// 已知 dp[1] 和 dp[2] ,返回之
|
||||
if i == 1 || i == 2 { return i as i32; }
|
||||
// dp[i] = dp[i-1] + dp[i-2]
|
||||
let count = dfs(i - 1) + dfs(i - 2);
|
||||
count
|
||||
}
|
||||
|
||||
/* 爬楼梯:搜索 */
|
||||
fn climbing_stairs_dfs(n: usize) -> i32 {
|
||||
dfs(n)
|
||||
}
|
||||
```
|
||||
|
||||
下图展示了暴力搜索形成的递归树。对于问题 $dp[n]$ ,其递归树的深度为 $n$ ,时间复杂度为 $O(2^n)$ 。指数阶属于爆炸式增长,如果我们输入一个比较大的 $n$ ,则会陷入漫长的等待之中。
|
||||
|
||||
![爬楼梯对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png)
|
||||
|
@ -702,6 +748,30 @@ $$
|
|||
[class]{}-[func]{climbingStairsDFSMem}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="climbing_stairs_dfs_mem.rs"
|
||||
/* 记忆化搜索 */
|
||||
fn dfs(i: usize, mem: &mut [i32]) -> i32 {
|
||||
// 已知 dp[1] 和 dp[2] ,返回之
|
||||
if i == 1 || i == 2 { return i as i32; }
|
||||
// 若存在记录 dp[i] ,则直接返回之
|
||||
if mem[i] != -1 { return mem[i]; }
|
||||
// dp[i] = dp[i-1] + dp[i-2]
|
||||
let count = dfs(i - 1, mem) + dfs(i - 2, mem);
|
||||
// 记录 dp[i]
|
||||
mem[i] = count;
|
||||
count
|
||||
}
|
||||
|
||||
/* 爬楼梯:记忆化搜索 */
|
||||
fn climbing_stairs_dfs_mem(n: usize) -> i32 {
|
||||
// mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录
|
||||
let mut mem = vec![-1; n + 1];
|
||||
dfs(n, &mut mem)
|
||||
}
|
||||
```
|
||||
|
||||
观察下图,**经过记忆化处理后,所有重叠子问题都只需被计算一次,时间复杂度被优化至 $O(n)$** ,这是一个巨大的飞跃。
|
||||
|
||||
![记忆化搜索对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png)
|
||||
|
@ -881,6 +951,26 @@ $$
|
|||
[class]{}-[func]{climbingStairsDP}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="climbing_stairs_dp.rs"
|
||||
/* 爬楼梯:动态规划 */
|
||||
fn climbing_stairs_dp(n: usize) -> i32 {
|
||||
// 已知 dp[1] 和 dp[2] ,返回之
|
||||
if n == 1 || n == 2 { return n as i32; }
|
||||
// 初始化 dp 表,用于存储子问题的解
|
||||
let mut dp = vec![-1; n + 1];
|
||||
// 初始状态:预设最小子问题的解
|
||||
dp[1] = 1;
|
||||
dp[2] = 2;
|
||||
// 状态转移:从较小子问题逐步求解较大子问题
|
||||
for i in 3..=n {
|
||||
dp[i] = dp[i - 1] + dp[i - 2];
|
||||
}
|
||||
dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
与回溯算法一样,动态规划也使用“状态”概念来表示问题求解的某个特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例如,爬楼梯问题的状态定义为当前所在楼梯阶数 $i$ 。
|
||||
|
||||
总结以上,动态规划的常用术语包括:
|
||||
|
@ -1038,6 +1128,22 @@ $$
|
|||
[class]{}-[func]{climbingStairsDPComp}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="climbing_stairs_dp.rs"
|
||||
/* 爬楼梯:状态压缩后的动态规划 */
|
||||
fn climbing_stairs_dp_comp(n: usize) -> i32 {
|
||||
if n == 1 || n == 2 { return n as i32; }
|
||||
let (mut a, mut b) = (1, 2);
|
||||
for _ in 3..=n {
|
||||
let tmp = b;
|
||||
b = a + b;
|
||||
a = tmp;
|
||||
}
|
||||
b
|
||||
}
|
||||
```
|
||||
|
||||
观察以上代码,由于省去了数组 `dp` 占用的空间,因此空间复杂度从 $O(n)$ 降低至 $O(1)$ 。
|
||||
|
||||
**这种空间优化技巧被称为「状态压缩」**。在常见的动态规划问题中,当前状态仅与前面有限个状态有关,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -228,6 +228,30 @@ $$
|
|||
[class]{}-[func]{unboundedKnapsackDP}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="unbounded_knapsack.rs"
|
||||
/* 完全背包:动态规划 */
|
||||
fn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {
|
||||
let n = wgt.len();
|
||||
// 初始化 dp 表
|
||||
let mut dp = vec![vec![0; cap + 1]; n + 1];
|
||||
// 状态转移
|
||||
for i in 1..=n {
|
||||
for c in 1..=cap {
|
||||
if wgt[i - 1] > c as i32 {
|
||||
// 若超过背包容量,则不选物品 i
|
||||
dp[i][c] = dp[i - 1][c];
|
||||
} else {
|
||||
// 不选和选物品 i 这两种方案的较大值
|
||||
dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][cap];
|
||||
}
|
||||
```
|
||||
|
||||
### 状态压缩
|
||||
|
||||
由于当前状态是从左边和上边的状态转移而来,**因此状态压缩后应该对 $dp$ 表中的每一行采取正序遍历**。
|
||||
|
@ -443,6 +467,30 @@ $$
|
|||
[class]{}-[func]{unboundedKnapsackDPComp}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="unbounded_knapsack.rs"
|
||||
/* 完全背包:状态压缩后的动态规划 */
|
||||
fn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {
|
||||
let n = wgt.len();
|
||||
// 初始化 dp 表
|
||||
let mut dp = vec![0; cap + 1];
|
||||
// 状态转移
|
||||
for i in 1..=n {
|
||||
for c in 1..=cap {
|
||||
if wgt[i - 1] > c as i32 {
|
||||
// 若超过背包容量,则不选物品 i
|
||||
dp[c] = dp[c];
|
||||
} else {
|
||||
// 不选和选物品 i 这两种方案的较大值
|
||||
dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
dp[cap]
|
||||
}
|
||||
```
|
||||
|
||||
## 14.5.2. 零钱兑换问题
|
||||
|
||||
背包问题是一大类动态规划问题的代表,其拥有很多的变种,例如零钱兑换问题。
|
||||
|
@ -724,6 +772,35 @@ $$
|
|||
[class]{}-[func]{coinChangeDP}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="coin_change.rs"
|
||||
/* 零钱兑换:动态规划 */
|
||||
fn coin_change_dp(coins: &[i32], amt: usize) -> i32 {
|
||||
let n = coins.len();
|
||||
let max = amt + 1;
|
||||
// 初始化 dp 表
|
||||
let mut dp = vec![vec![0; amt + 1]; n + 1];
|
||||
// 状态转移:首行首列
|
||||
for a in 1..= amt {
|
||||
dp[0][a] = max;
|
||||
}
|
||||
// 状态转移:其余行列
|
||||
for i in 1..=n {
|
||||
for a in 1..=amt {
|
||||
if coins[i - 1] > a as i32 {
|
||||
// 若超过背包容量,则不选硬币 i
|
||||
dp[i][a] = dp[i - 1][a];
|
||||
} else {
|
||||
// 不选和选硬币 i 这两种方案的较小值
|
||||
dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if dp[n][amt] != max { return dp[n][amt] as i32; } else { -1 }
|
||||
}
|
||||
```
|
||||
|
||||
下图展示了零钱兑换的动态规划过程,和完全背包非常相似。
|
||||
|
||||
=== "<1>"
|
||||
|
@ -991,6 +1068,33 @@ $$
|
|||
[class]{}-[func]{coinChangeDPComp}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="coin_change.rs"
|
||||
/* 零钱兑换:状态压缩后的动态规划 */
|
||||
fn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {
|
||||
let n = coins.len();
|
||||
let max = amt + 1;
|
||||
// 初始化 dp 表
|
||||
let mut dp = vec![0; amt + 1];
|
||||
dp.fill(max);
|
||||
dp[0] = 0;
|
||||
// 状态转移
|
||||
for i in 1..=n {
|
||||
for a in 1..=amt {
|
||||
if coins[i - 1] > a as i32 {
|
||||
// 若超过背包容量,则不选硬币 i
|
||||
dp[a] = dp[a];
|
||||
} else {
|
||||
// 不选和选硬币 i 这两种方案的较小值
|
||||
dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if dp[amt] != max { return dp[amt] as i32; } else { -1 }
|
||||
}
|
||||
```
|
||||
|
||||
## 14.5.3. 零钱兑换问题 II
|
||||
|
||||
!!! question
|
||||
|
@ -1231,6 +1335,34 @@ $$
|
|||
[class]{}-[func]{coinChangeIIDP}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="coin_change_ii.rs"
|
||||
/* 零钱兑换 II:动态规划 */
|
||||
fn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {
|
||||
let n = coins.len();
|
||||
// 初始化 dp 表
|
||||
let mut dp = vec![vec![0; amt + 1]; n + 1];
|
||||
// 初始化首列
|
||||
for i in 0..= n {
|
||||
dp[i][0] = 1;
|
||||
}
|
||||
// 状态转移
|
||||
for i in 1..=n {
|
||||
for a in 1..=amt {
|
||||
if coins[i - 1] > a as i32 {
|
||||
// 若超过背包容量,则不选硬币 i
|
||||
dp[i][a] = dp[i - 1][a];
|
||||
} else {
|
||||
// 不选和选硬币 i 这两种方案的较小值
|
||||
dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];
|
||||
}
|
||||
}
|
||||
}
|
||||
dp[n][amt]
|
||||
}
|
||||
```
|
||||
|
||||
### 状态压缩
|
||||
|
||||
状态压缩处理方式相同,删除硬币维度即可。
|
||||
|
@ -1431,3 +1563,28 @@ $$
|
|||
```dart title="coin_change_ii.dart"
|
||||
[class]{}-[func]{coinChangeIIDPComp}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="coin_change_ii.rs"
|
||||
/* 零钱兑换 II:状态压缩后的动态规划 */
|
||||
fn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {
|
||||
let n = coins.len();
|
||||
// 初始化 dp 表
|
||||
let mut dp = vec![0; amt + 1];
|
||||
dp[0] = 1;
|
||||
// 状态转移
|
||||
for i in 1..=n {
|
||||
for a in 1..=amt {
|
||||
if coins[i - 1] > a as i32 {
|
||||
// 若超过背包容量,则不选硬币 i
|
||||
dp[a] = dp[a];
|
||||
} else {
|
||||
// 不选和选硬币 i 这两种方案的较小值
|
||||
dp[a] = dp[a] + dp[a - coins[i - 1] as usize];
|
||||
}
|
||||
}
|
||||
}
|
||||
dp[amt]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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. 基于邻接表的实现
|
||||
|
||||
设无向图的顶点总数为 $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. 效率对比
|
||||
|
||||
设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。
|
||||
|
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
深度优先遍历的算法流程如下图所示,其中:
|
||||
|
||||
- **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点。
|
||||
|
|
|
@ -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)$** 。
|
||||
|
|
|
@ -177,6 +177,33 @@ status: new
|
|||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="coin_change_greedy.rs"
|
||||
/* 零钱兑换:贪心 */
|
||||
fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {
|
||||
// 假设 coins 列表有序
|
||||
let mut i = coins.len() - 1;
|
||||
let mut count = 0;
|
||||
// 循环进行贪心选择,直到无剩余金额
|
||||
while amt > 0 {
|
||||
// 找到小于且最接近剩余金额的硬币
|
||||
while coins[i] > amt {
|
||||
i -= 1;
|
||||
}
|
||||
// 选择 coins[i]
|
||||
amt -= coins[i];
|
||||
count += 1;
|
||||
}
|
||||
// 若未找到可行方案,则返回 -1
|
||||
if amt == 0 {
|
||||
count
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 15.1.1. 贪心优点与局限性
|
||||
|
||||
**贪心算法不仅操作直接、实现简单,而且通常效率也很高**。在以上代码中,记硬币最小面值为 $\min(coins)$ ,则贪心选择最多循环 $amt / \min(coins)$ 次,时间复杂度为 $O(amt / \min(coins))$ 。这比动态规划解法的时间复杂度 $O(n \times amt)$ 提升了一个数量级。
|
||||
|
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
### 正确性证明
|
||||
|
||||
之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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` 了。
|
||||
|
||||
虽然自定义对象(比如链表节点)的成员变量是可变的,但它是可哈希的。**这是因为对象的哈希值通常是基于内存地址生成的**,即使对象的内容发生了变化,但它的内存地址不变,哈希值仍然是不变的。
|
||||
|
|
|
@ -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$ 进行探测。
|
||||
|
|
|
@ -256,6 +256,12 @@ comments: true
|
|||
map.remove(10583);
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="hash_map.rs"
|
||||
|
||||
```
|
||||
|
||||
哈希表有三种常用遍历方式:遍历键值对、遍历键和遍历值。
|
||||
|
||||
=== "Java"
|
||||
|
@ -431,6 +437,12 @@ comments: true
|
|||
});
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="hash_map.rs"
|
||||
|
||||
```
|
||||
|
||||
## 6.1.2. 哈希表简单实现
|
||||
|
||||
我们先考虑最简单的情况,**仅用一个数组来实现哈希表**。在哈希表中,我们将数组中的每个空位称为「桶 Bucket」,每个桶可存储一个键值对。因此,查询操作就是找到 `key` 对应的桶,并在桶中获取 `value` 。
|
||||
|
@ -1403,6 +1415,77 @@ index = hash(key) % capacity
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="array_hash_map.rs"
|
||||
/* 键值对 */
|
||||
pub struct Pair {
|
||||
pub key: i32,
|
||||
pub val: String,
|
||||
}
|
||||
|
||||
/* 基于数组简易实现的哈希表 */
|
||||
pub struct ArrayHashMap {
|
||||
buckets: Vec<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. 哈希冲突与扩容
|
||||
|
||||
本质上看,哈希函数的作用是将所有 `key` 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,**理论上一定存在“多个输入对应相同输出”的情况**。
|
||||
|
|
|
@ -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. 复杂度分析
|
||||
|
||||
为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。
|
||||
|
|
|
@ -313,6 +313,12 @@ comments: true
|
|||
// Dart 未提供内置 Heap 类
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="heap.rs"
|
||||
|
||||
```
|
||||
|
||||
## 8.1.2. 堆的实现
|
||||
|
||||
下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。
|
||||
|
@ -539,6 +545,25 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="my_heap.rs"
|
||||
/* 获取左子节点索引 */
|
||||
fn left(i: usize) -> usize {
|
||||
2 * i + 1
|
||||
}
|
||||
|
||||
/* 获取右子节点索引 */
|
||||
fn right(i: usize) -> usize {
|
||||
2 * i + 2
|
||||
}
|
||||
|
||||
/* 获取父节点索引 */
|
||||
fn parent(i: usize) -> usize {
|
||||
(i - 1) / 2 // 向下整除
|
||||
}
|
||||
```
|
||||
|
||||
### 访问堆顶元素
|
||||
|
||||
堆顶元素即为二叉树的根节点,也就是列表的首个元素。
|
||||
|
@ -641,6 +666,15 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="my_heap.rs"
|
||||
/* 访问堆顶元素 */
|
||||
fn peek(&self) -> Option<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. 堆常见应用
|
||||
|
||||
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。
|
||||
|
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
|
@ -158,6 +158,12 @@ comments: true
|
|||
*/
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
## 0.2.2. 在动画图解中高效学习
|
||||
|
||||
相较于文字,视频和图片具有更高的信息密度和结构化程度,因此更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。
|
||||
|
|
|
@ -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$ 缩小区间操作也是对称的。这样更不容易出错。因此,**我们通常采用“双闭区间”的写法**。
|
||||
|
|
|
@ -288,6 +288,30 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search_edge.rs"
|
||||
/* 二分查找最左一个元素 */
|
||||
fn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {
|
||||
let mut i = 0;
|
||||
let mut j = nums.len() as i32 - 1; // 初始化双闭区间 [0, n-1]
|
||||
while i <= j {
|
||||
let m = i + (j - i) / 2; // 计算中点索引 m
|
||||
if nums[m as usize] < target {
|
||||
i = m + 1; // target 在区间 [m+1, j] 中
|
||||
} else if nums[m as usize] > target {
|
||||
j = m - 1; // target 在区间 [i, m-1] 中
|
||||
} else {
|
||||
j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中
|
||||
}
|
||||
}
|
||||
if i == nums.len() as i32 || nums[i as usize] != target {
|
||||
return -1; // 未找到目标元素,返回 -1
|
||||
}
|
||||
i
|
||||
}
|
||||
```
|
||||
|
||||
## 10.2.3. 查找右边界
|
||||
|
||||
类似地,我们也可以二分查找最右边的 `target` 。当 `nums[m] == target` 时,说明大于 `target` 的元素在区间 $[m + 1, j]$ 中,因此执行 `i = m + 1` ,**使得指针 $i$ 向大于 `target` 的元素靠近**。
|
||||
|
@ -524,6 +548,30 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search_edge.rs"
|
||||
/* 二分查找最右一个元素 */
|
||||
fn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {
|
||||
let mut i = 0;
|
||||
let mut j = nums.len() as i32 - 1; // 初始化双闭区间 [0, n-1]
|
||||
while i <= j {
|
||||
let m = i + (j - i) / 2; // 计算中点索引 m
|
||||
if nums[m as usize] < target {
|
||||
i = m + 1; // target 在区间 [m+1, j] 中
|
||||
} else if nums[m as usize] > target {
|
||||
j = m - 1; // target 在区间 [i, m-1] 中
|
||||
} else {
|
||||
i = m + 1; // 首个大于 target 的元素在区间 [m+1, j] 中
|
||||
}
|
||||
}
|
||||
if j < 0 || nums[j as usize] != target {
|
||||
return -1; // 未找到目标元素,返回 -1
|
||||
}
|
||||
j
|
||||
}
|
||||
```
|
||||
|
||||
观察下图,搜索最右边元素时指针 $j$ 的作用与搜索最左边元素时指针 $i$ 的作用一致,反之亦然。也就是说,**搜索最左边元素和最右边元素的实现是镜像对称的**。
|
||||
|
||||
![查找最左边和最右边元素的对称性](binary_search_edge.assets/binary_search_left_right_edge.png)
|
||||
|
|
|
@ -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. 哈希查找:以空间换时间
|
||||
|
@ -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)$ 。**尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法**。
|
||||
|
|
|
@ -255,6 +255,26 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="bubble_sort.rs"
|
||||
/* 冒泡排序 */
|
||||
fn bubble_sort(nums: &mut [i32]) {
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i in (1..nums.len()).rev() {
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j in 0..i {
|
||||
if nums[j] > nums[j + 1] {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
let tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.3.2. 效率优化
|
||||
|
||||
我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。
|
||||
|
@ -514,6 +534,29 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="bubble_sort.rs"
|
||||
/* 冒泡排序(标志优化) */
|
||||
fn bubble_sort_with_flag(nums: &mut [i32]) {
|
||||
// 外循环:未排序区间为 [0, i]
|
||||
for i in (1..nums.len()).rev() {
|
||||
let mut flag = false; // 初始化标志位
|
||||
// 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
|
||||
for j in 0..i {
|
||||
if nums[j] > nums[j + 1] {
|
||||
// 交换 nums[j] 与 nums[j + 1]
|
||||
let tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
flag = true; // 记录交换元素
|
||||
}
|
||||
}
|
||||
if !flag {break}; // 此轮冒泡未交换任何元素,直接跳出
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.3.3. 算法特性
|
||||
|
||||
- **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。
|
||||
|
|
|
@ -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 个桶,然后分别对每个桶进行排序,最后将结果合并。
|
||||
|
|
|
@ -292,6 +292,31 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="counting_sort.rs"
|
||||
/* 计数排序 */
|
||||
// 简单实现,无法用于排序对象
|
||||
fn counting_sort_naive(nums: &mut [i32]) {
|
||||
// 1. 统计数组最大元素 m
|
||||
let m = *nums.into_iter().max().unwrap();
|
||||
// 2. 统计各数字的出现次数
|
||||
// counter[num] 代表 num 的出现次数
|
||||
let mut counter = vec![0; m as usize + 1];
|
||||
for &num in &*nums {
|
||||
counter[num as usize] += 1;
|
||||
}
|
||||
// 3. 遍历 counter ,将各元素填入原数组 nums
|
||||
let mut i = 0;
|
||||
for num in 0..m + 1 {
|
||||
for _ in 0..counter[num as usize] {
|
||||
nums[i] = num;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note "计数排序与桶排序的联系"
|
||||
|
||||
从桶排序的角度看,我们可以将计数排序中的计数数组 `counter` 的每个索引视为一个桶,将统计数量的过程看作是将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。
|
||||
|
@ -710,6 +735,41 @@ $$
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="counting_sort.rs"
|
||||
/* 计数排序 */
|
||||
// 完整实现,可排序对象,并且是稳定排序
|
||||
fn counting_sort(nums: &mut [i32]) {
|
||||
// 1. 统计数组最大元素 m
|
||||
let m = *nums.into_iter().max().unwrap();
|
||||
// 2. 统计各数字的出现次数
|
||||
// counter[num] 代表 num 的出现次数
|
||||
let mut counter = vec![0; m as usize + 1];
|
||||
for &num in &*nums {
|
||||
counter[num as usize] += 1;
|
||||
}
|
||||
// 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”
|
||||
// 即 counter[num]-1 是 num 在 res 中最后一次出现的索引
|
||||
for i in 0..m as usize {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. 倒序遍历 nums ,将各元素填入结果数组 res
|
||||
// 初始化数组 res 用于记录结果
|
||||
let n = nums.len();
|
||||
let mut res = vec![0; n];
|
||||
for i in (0..n).rev() {
|
||||
let num = nums[i];
|
||||
res[counter[num as usize] - 1] = num; // 将 num 放置到对应索引处
|
||||
counter[num as usize] -= 1; // 令前缀和自减 1 ,得到下次放置 num 的索引
|
||||
}
|
||||
// 使用结果数组 res 覆盖原数组 nums
|
||||
for i in 0..n {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.9.3. 算法特性
|
||||
|
||||
- **时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。
|
||||
|
|
|
@ -491,6 +491,53 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="heap_sort.rs"
|
||||
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */
|
||||
fn sift_down(nums: &mut [i32], n: usize, mut i: usize) {
|
||||
loop {
|
||||
// 判断节点 i, l, r 中值最大的节点,记为 ma
|
||||
let l = 2 * i + 1;
|
||||
let r = 2 * i + 2;
|
||||
let mut ma = i;
|
||||
if l < n && nums[l] > nums[ma] {
|
||||
ma = l;
|
||||
}
|
||||
if r < n && nums[r] > nums[ma] {
|
||||
ma = r;
|
||||
}
|
||||
// 若节点 i 最大或索引 l, r 越界,则无需继续堆化,跳出
|
||||
if ma == i {
|
||||
break;
|
||||
}
|
||||
// 交换两节点
|
||||
let temp = nums[i];
|
||||
nums[i] = nums[ma];
|
||||
nums[ma] = temp;
|
||||
// 循环向下堆化
|
||||
i = ma;
|
||||
}
|
||||
}
|
||||
|
||||
/* 堆排序 */
|
||||
fn heap_sort(nums: &mut [i32]) {
|
||||
// 建堆操作:堆化除叶节点以外的其他所有节点
|
||||
for i in (0..=nums.len() / 2 - 1).rev() {
|
||||
sift_down(nums, nums.len(), i);
|
||||
}
|
||||
// 从堆中提取最大元素,循环 n-1 轮
|
||||
for i in (1..=nums.len() - 1).rev() {
|
||||
// 交换根节点与最右叶节点(即交换首元素与尾元素)
|
||||
let tmp = nums[0];
|
||||
nums[0] = nums[i];
|
||||
nums[i] = tmp;
|
||||
// 以根节点为起点,从顶至底进行堆化
|
||||
sift_down(nums, i, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.7.2. 算法特性
|
||||
|
||||
- **时间复杂度 $O(n \log n)$ 、非自适应排序** :建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。
|
||||
|
|
|
@ -230,6 +230,24 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="insertion_sort.rs"
|
||||
/* 插入排序 */
|
||||
fn insertion_sort(nums: &mut [i32]) {
|
||||
// 外循环:已排序元素数量为 1, 2, ..., n
|
||||
for i in 1..nums.len() {
|
||||
let (base, mut j) = (nums[i], (i - 1) as i32);
|
||||
// 内循环:将 base 插入到已排序部分的正确位置
|
||||
while j >= 0 && nums[j as usize] > base {
|
||||
nums[(j + 1) as usize] = nums[j as usize]; // 将 nums[j] 向右移动一位
|
||||
j -= 1;
|
||||
}
|
||||
nums[(j + 1) as usize] = base; // 将 base 赋值到正确位置
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.4.2. 算法特性
|
||||
|
||||
- **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。
|
||||
|
|
|
@ -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]` 。
|
||||
|
|
|
@ -337,6 +337,27 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="quick_sort.rs"
|
||||
/* 哨兵划分 */
|
||||
fn partition(nums: &mut [i32], left: usize, right: usize) -> usize {
|
||||
// 以 nums[left] 作为基准数
|
||||
let (mut i, mut j) = (left, right);
|
||||
while i < j {
|
||||
while i < j && nums[j] >= nums[left] {
|
||||
j -= 1; // 从右向左找首个小于基准数的元素
|
||||
}
|
||||
while i < j && nums[i] <= nums[left] {
|
||||
i += 1; // 从左向右找首个大于基准数的元素
|
||||
}
|
||||
nums.swap(i, j); // 交换这两个元素
|
||||
}
|
||||
nums.swap(i, left); // 将基准数交换至两子数组的分界线
|
||||
i // 返回基准数的索引
|
||||
}
|
||||
```
|
||||
|
||||
## 11.5.1. 算法流程
|
||||
|
||||
1. 首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组。
|
||||
|
@ -546,6 +567,23 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="quick_sort.rs"
|
||||
/* 快速排序 */
|
||||
pub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {
|
||||
// 子数组长度为 1 时终止递归
|
||||
if left >= right {
|
||||
return;
|
||||
}
|
||||
// 哨兵划分
|
||||
let pivot = Self::partition(nums, left as usize, right as usize) as i32;
|
||||
// 递归左子数组、右子数组
|
||||
Self::quick_sort(left, pivot - 1, nums);
|
||||
Self::quick_sort(pivot + 1, right, nums);
|
||||
}
|
||||
```
|
||||
|
||||
## 11.5.2. 算法特性
|
||||
|
||||
- **时间复杂度 $O(n \log n)$ 、自适应排序** :在平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。在最差情况下,每轮哨兵划分操作都将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。
|
||||
|
@ -976,6 +1014,43 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="quick_sort.rs"
|
||||
/* 选取三个元素的中位数 */
|
||||
fn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize {
|
||||
// 此处使用异或运算来简化代码
|
||||
// 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1
|
||||
if (nums[left] < nums[mid]) ^ (nums[left] < nums[right]) {
|
||||
return left;
|
||||
} else if (nums[mid] < nums[left]) ^ (nums[mid] < nums[right]) {
|
||||
return mid;
|
||||
}
|
||||
right
|
||||
}
|
||||
|
||||
/* 哨兵划分(三数取中值) */
|
||||
fn partition(nums: &mut [i32], left: usize, right: usize) -> usize {
|
||||
// 选取三个候选元素的中位数
|
||||
let med = Self::median_three(nums, left, (left + right) / 2, right);
|
||||
// 将中位数交换至数组最左端
|
||||
nums.swap(left, med);
|
||||
// 以 nums[left] 作为基准数
|
||||
let (mut i, mut j) = (left, right);
|
||||
while i < j {
|
||||
while i < j && nums[j] >= nums[left] {
|
||||
j -= 1; // 从右向左找首个小于基准数的元素
|
||||
}
|
||||
while i < j && nums[i] <= nums[left] {
|
||||
i += 1; // 从左向右找首个大于基准数的元素
|
||||
}
|
||||
nums.swap(i, j); // 交换这两个元素
|
||||
}
|
||||
nums.swap(i, left); // 将基准数交换至两子数组的分界线
|
||||
i // 返回基准数的索引
|
||||
}
|
||||
```
|
||||
|
||||
## 11.5.5. 尾递归优化
|
||||
|
||||
**在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。
|
||||
|
@ -1214,3 +1289,24 @@ comments: true
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="quick_sort.rs"
|
||||
/* 快速排序(尾递归优化) */
|
||||
pub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {
|
||||
// 子数组长度为 1 时终止
|
||||
while left < right {
|
||||
// 哨兵划分操作
|
||||
let pivot = Self::partition(nums, left as usize, right as usize) as i32;
|
||||
// 对两个子数组中较短的那个执行快排
|
||||
if pivot - left < right - pivot {
|
||||
Self::quick_sort(left, pivot - 1, nums); // 递归排序左子数组
|
||||
left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]
|
||||
} else {
|
||||
Self::quick_sort(pivot + 1, right, nums); // 递归排序右子数组
|
||||
right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。
|
||||
|
|
|
@ -261,6 +261,27 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="selection_sort.rs"
|
||||
/* 选择排序 */
|
||||
fn selection_sort(nums: &mut [i32]) {
|
||||
let n = nums.len();
|
||||
// 外循环:未排序区间为 [i, n-1]
|
||||
for i in 0..n-1 {
|
||||
// 内循环:找到未排序区间内的最小元素
|
||||
let mut k = i;
|
||||
for j in i+1..n {
|
||||
if nums[j] < nums[k] {
|
||||
k = j; // 记录最小元素的索引
|
||||
}
|
||||
}
|
||||
// 将该最小元素与未排序区间的首个元素交换
|
||||
nums.swap(i, k);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 11.2.1. 算法特性
|
||||
|
||||
- **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。
|
||||
|
|
|
@ -318,6 +318,12 @@ comments: true
|
|||
bool isEmpty = deque.isEmpty;W
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="deque.rs"
|
||||
|
||||
```
|
||||
|
||||
## 5.3.2. 双向队列实现 *
|
||||
|
||||
双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。
|
||||
|
@ -1795,6 +1801,174 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="linkedlist_deque.rs"
|
||||
/* 双向链表节点 */
|
||||
pub struct ListNode<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. 双向队列应用
|
||||
|
||||
双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。
|
||||
|
|
|
@ -285,6 +285,12 @@ comments: true
|
|||
bool isEmpty = queue.isEmpty;
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="queue.rs"
|
||||
|
||||
```
|
||||
|
||||
## 5.2.2. 队列实现
|
||||
|
||||
为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列。
|
||||
|
@ -1090,6 +1096,88 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="linkedlist_queue.rs"
|
||||
/* 基于链表实现的队列 */
|
||||
#[allow(dead_code)]
|
||||
pub struct LinkedListQueue<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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以上实现的队列仍然具有局限性,即其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的同学可以尝试自行实现。
|
||||
|
||||
两种实现的对比结论与栈一致,在此不再赘述。
|
||||
|
|
|
@ -283,6 +283,12 @@ comments: true
|
|||
bool isEmpty = stack.isEmpty;
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="stack.rs"
|
||||
|
||||
```
|
||||
|
||||
## 5.1.2. 栈的实现
|
||||
|
||||
为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。
|
||||
|
@ -989,6 +995,75 @@ comments: true
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="linkedlist_stack.rs"
|
||||
/* 基于链表实现的栈 */
|
||||
#[allow(dead_code)]
|
||||
pub struct LinkedListStack<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. 两种实现对比
|
||||
|
||||
### 支持操作
|
||||
|
|
|
@ -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. 优势与局限性
|
||||
|
||||
二叉树的数组表示的优点包括:
|
||||
|
|
|
@ -190,6 +190,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
|
||||
```
|
||||
|
||||
「节点高度」是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。
|
||||
|
||||
=== "Java"
|
||||
|
@ -388,6 +394,29 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="avl_tree.rs"
|
||||
/* 获取节点高度 */
|
||||
fn height(node: OptionTreeNodeRc) -> i32 {
|
||||
// 空节点高度为 -1 ,叶节点高度为 0
|
||||
match node {
|
||||
Some(node) => node.borrow().height,
|
||||
None => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/* 更新节点高度 */
|
||||
fn update_height(node: OptionTreeNodeRc) {
|
||||
if let Some(node) = node {
|
||||
let left = node.borrow().left.clone();
|
||||
let right = node.borrow().right.clone();
|
||||
// 节点高度等于最高子树高度 + 1
|
||||
node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 节点平衡因子
|
||||
|
||||
节点的「平衡因子 Balance Factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。
|
||||
|
@ -530,6 +559,22 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="avl_tree.rs"
|
||||
/* 获取平衡因子 */
|
||||
fn balance_factor(node: OptionTreeNodeRc) -> i32 {
|
||||
match node {
|
||||
// 空节点平衡因子为 0
|
||||
None => 0,
|
||||
// 节点平衡因子 = 左子树高度 - 右子树高度
|
||||
Some(node) => {
|
||||
Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。
|
||||
|
@ -762,6 +807,29 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="avl_tree.rs"
|
||||
/* 右旋操作 */
|
||||
fn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {
|
||||
match node {
|
||||
Some(node) => {
|
||||
let child = node.borrow().left.clone().unwrap();
|
||||
let grand_child = child.borrow().right.clone();
|
||||
// 以 child 为原点,将 node 向右旋转
|
||||
child.borrow_mut().right = Some(node.clone());
|
||||
node.borrow_mut().left = grand_child;
|
||||
// 更新节点高度
|
||||
Self::update_height(Some(node));
|
||||
Self::update_height(Some(child.clone()));
|
||||
// 返回旋转后子树的根节点
|
||||
Some(child)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 左旋
|
||||
|
||||
相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。
|
||||
|
@ -976,6 +1044,29 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="avl_tree.rs"
|
||||
/* 左旋操作 */
|
||||
fn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {
|
||||
match node {
|
||||
Some(node) => {
|
||||
let child = node.borrow().right.clone().unwrap();
|
||||
let grand_child = child.borrow().left.clone();
|
||||
// 以 child 为原点,将 node 向左旋转
|
||||
child.borrow_mut().left = Some(node.clone());
|
||||
node.borrow_mut().right = grand_child;
|
||||
// 更新节点高度
|
||||
Self::update_height(Some(node));
|
||||
Self::update_height(Some(child.clone()));
|
||||
// 返回旋转后子树的根节点
|
||||
Some(child)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 先左旋后右旋
|
||||
|
||||
对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。
|
||||
|
@ -1385,6 +1476,45 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="avl_tree.rs"
|
||||
/* 执行旋转操作,使该子树重新恢复平衡 */
|
||||
fn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {
|
||||
// 获取节点 node 的平衡因子
|
||||
let balance_factor = Self::balance_factor(node.clone());
|
||||
// 左偏树
|
||||
if balance_factor > 1 {
|
||||
let node = node.unwrap();
|
||||
if Self::balance_factor(node.borrow().left.clone()) >= 0 {
|
||||
// 右旋
|
||||
Self::right_rotate(Some(node))
|
||||
} else {
|
||||
// 先左旋后右旋
|
||||
let left = node.borrow().left.clone();
|
||||
node.borrow_mut().left = Self::left_rotate(left);
|
||||
Self::right_rotate(Some(node))
|
||||
}
|
||||
}
|
||||
// 右偏树
|
||||
else if balance_factor < -1 {
|
||||
let node = node.unwrap();
|
||||
if Self::balance_factor(node.borrow().right.clone()) <= 0 {
|
||||
// 左旋
|
||||
Self::left_rotate(Some(node))
|
||||
} else {
|
||||
// 先右旋后左旋
|
||||
let right = node.borrow().right.clone();
|
||||
node.borrow_mut().right = Self::right_rotate(right);
|
||||
Self::left_rotate(Some(node))
|
||||
}
|
||||
} else {
|
||||
// 平衡树,无需旋转,直接返回
|
||||
node
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7.5.3. AVL 树常用操作
|
||||
|
||||
### 插入节点
|
||||
|
@ -1697,6 +1827,48 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="avl_tree.rs"
|
||||
/* 插入节点 */
|
||||
fn insert(&mut self, val: i32) {
|
||||
self.root = Self::insert_helper(self.root.clone(), val);
|
||||
}
|
||||
|
||||
/* 递归插入节点(辅助方法) */
|
||||
fn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {
|
||||
match node {
|
||||
Some(mut node) => {
|
||||
/* 1. 查找插入位置,并插入节点 */
|
||||
match {
|
||||
let node_val = node.borrow().val;
|
||||
node_val
|
||||
}
|
||||
.cmp(&val)
|
||||
{
|
||||
Ordering::Greater => {
|
||||
let left = node.borrow().left.clone();
|
||||
node.borrow_mut().left = Self::insert_helper(left, val);
|
||||
}
|
||||
Ordering::Less => {
|
||||
let right = node.borrow().right.clone();
|
||||
node.borrow_mut().right = Self::insert_helper(right, val);
|
||||
}
|
||||
Ordering::Equal => {
|
||||
return Some(node); // 重复节点不插入,直接返回
|
||||
}
|
||||
}
|
||||
Self::update_height(Some(node.clone())); // 更新节点高度
|
||||
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
|
||||
node = Self::rotate(Some(node)).unwrap();
|
||||
// 返回子树的根节点
|
||||
Some(node)
|
||||
}
|
||||
None => Some(TreeNode::new(val)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 删除节点
|
||||
|
||||
类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡。
|
||||
|
@ -2198,6 +2370,64 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
|
|||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="avl_tree.rs"
|
||||
/* 删除节点 */
|
||||
fn remove(&self, val: i32) {
|
||||
Self::remove_helper(self.root.clone(), val);
|
||||
}
|
||||
|
||||
/* 递归删除节点(辅助方法) */
|
||||
fn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {
|
||||
match node {
|
||||
Some(mut node) => {
|
||||
/* 1. 查找节点,并删除之 */
|
||||
if val < node.borrow().val {
|
||||
let left = node.borrow().left.clone();
|
||||
node.borrow_mut().left = Self::remove_helper(left, val);
|
||||
} else if val > node.borrow().val {
|
||||
let right = node.borrow().right.clone();
|
||||
node.borrow_mut().right = Self::remove_helper(right, val);
|
||||
} else if node.borrow().left.is_none() || node.borrow().right.is_none() {
|
||||
let child = if node.borrow().left.is_some() {
|
||||
node.borrow().left.clone()
|
||||
} else {
|
||||
node.borrow().right.clone()
|
||||
};
|
||||
match child {
|
||||
// 子节点数量 = 0 ,直接删除 node 并返回
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
// 子节点数量 = 1 ,直接删除 node
|
||||
Some(child) => node = child,
|
||||
}
|
||||
} else {
|
||||
// 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点
|
||||
let mut temp = node.borrow().right.clone().unwrap();
|
||||
loop {
|
||||
let temp_left = temp.borrow().left.clone();
|
||||
if temp_left.is_none() {
|
||||
break;
|
||||
}
|
||||
temp = temp_left.unwrap();
|
||||
}
|
||||
let right = node.borrow().right.clone();
|
||||
node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);
|
||||
node.borrow_mut().val = temp.borrow().val;
|
||||
}
|
||||
Self::update_height(Some(node.clone())); // 更新节点高度
|
||||
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
|
||||
node = Self::rotate(Some(node)).unwrap();
|
||||
// 返回子树的根节点
|
||||
Some(node)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查找节点
|
||||
|
||||
AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。
|
||||
|
|
|
@ -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$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。
|
||||
|
|
|
@ -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
|
||||
|
||||
需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。
|
||||
|
|
|
@ -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)$ 栈帧空间。
|
||||
|
|
Loading…
Reference in a new issue