krahets 7 months ago
parent f986ae3c8c
commit f748af6aa4

@ -56,6 +56,7 @@ comments: true
| front of the queue | 队首 | 佇列首 |
| rear of the queue | 队尾 | 佇列尾 |
| hash table | 哈希表 | 雜湊表 |
| hash set | 哈希集合 | 雜湊集合 |
| bucket | 桶 | 桶 |
| hash function | 哈希函数 | 雜湊函式 |
| hash collision | 哈希冲突 | 雜湊衝突 |

@ -1555,7 +1555,7 @@ comments: true
单向链表通常用于实现栈、队列、哈希表和图等数据结构。
- **栈与队列**:当插入和删除操作都在链表的一端进行时,它表现出先进后出的特性,对应栈;当插入操作在链表的一端进行,删除操作在链表的另一端进行,它表现出先进先出的特性,对应队列。
- **栈与队列**:当插入和删除操作都在链表的一端进行时,它表现的特性为先进后出,对应栈;当插入操作在链表的一端进行,删除操作在链表的另一端进行,它表现的特性为先进先出,对应队列。
- **哈希表**:链式地址是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表中。
- **图**:邻接表是表示图的一种常用方式,其中图的每个顶点都与一个链表相关联,链表中的每个元素都代表与该顶点相连的其他顶点。

@ -36,7 +36,7 @@ status: new
<p align="center"> 图 4-9 &nbsp; 计算机存储系统 </p>
!!! note
!!! tip
计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳平衡点。

@ -538,7 +538,7 @@ comments: true
<p align="center"> 图 13-7 &nbsp; 重复排列 </p>
那么如何去除重复的排列呢?最直接地,考虑借助一个哈希,直接对排列结果进行去重。然而这样做不够优雅,**因为生成重复排列的搜索分支没有必要,应当提前识别并剪枝**,这样可以进一步提升算法效率。
那么如何去除重复的排列呢?最直接地,考虑借助一个哈希集合,直接对排列结果进行去重。然而这样做不够优雅,**因为生成重复排列的搜索分支没有必要,应当提前识别并剪枝**,这样可以进一步提升算法效率。
### 1. &nbsp; 相等元素剪枝
@ -554,7 +554,7 @@ comments: true
### 2. &nbsp; 代码实现
在上一题的代码的基础上,我们考虑在每一轮选择中开启一个哈希 `duplicated` ,用于记录该轮中已经尝试过的元素,并将重复元素剪枝:
在上一题的代码的基础上,我们考虑在每一轮选择中开启一个哈希集合 `duplicated` ,用于记录该轮中已经尝试过的元素,并将重复元素剪枝:
=== "Python"

@ -11,7 +11,7 @@ comments: true
- 回溯问题通常包含多个约束条件,它们可用于实现剪枝操作。剪枝可以提前结束不必要的搜索分支,大幅提升搜索效率。
- 回溯算法主要可用于解决搜索问题和约束满足问题。组合优化问题虽然可以用回溯算法解决,但往往存在效率更高或效果更好的解法。
- 全排列问题旨在搜索给定集合元素的所有可能的排列。我们借助一个数组来记录每个元素是否被选择,剪掉重复选择同一元素的搜索分支,确保每个元素只被选择一次。
- 在全排列问题中,如果集合中存在重复元素,则最终结果会出现重复排列。我们需要约束相等元素在每轮中只能被选择一次,这通常借助一个哈希来实现。
- 在全排列问题中,如果集合中存在重复元素,则最终结果会出现重复排列。我们需要约束相等元素在每轮中只能被选择一次,这通常借助一个哈希集合来实现。
- 子集和问题的目标是在给定集合中找到和为目标值的所有子集。集合不区分元素顺序,而搜索过程会输出所有顺序的结果,产生重复子集。我们在回溯前将数据进行排序,并设置一个变量来指示每一轮的遍历起始点,从而将生成重复子集的搜索分支进行剪枝。
- 对于子集和问题,数组中的相等元素会产生重复集合。我们利用数组已排序的前置条件,通过判断相邻元素是否相等实现剪枝,从而确保相等元素在每轮中只能被选中一次。
- $n$ 皇后问题旨在寻找将 $n$ 个皇后放置到 $n \times n$ 尺寸棋盘上的方案,要求所有皇后两两之间无法攻击对方。该问题的约束条件有行约束、列约束、主对角线和次对角线约束。为满足行约束,我们采用按行放置的策略,保证每一行放置一个皇后。

@ -753,7 +753,7 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因
时间复杂度分析本质上是计算“操作数量 $T(n)$”的渐近上界,它具有明确的数学定义。
!!! abstract "函数渐近上界"
!!! note "函数渐近上界"
若存在正实数 $c$ 和实数 $n_0$ ,使得对于所有的 $n > n_0$ ,均有 $T(n) \leq c \cdot f(n)$ ,则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为 $T(n) = O(f(n))$ 。

@ -4,7 +4,7 @@ comments: true
# 3.3 &nbsp; 数字编码 *
!!! note
!!! tip
在本书中,标题带有 * 符号的是选读章节。如果你时间有限或感到理解困难,可以先跳过,等学完必读章节后再单独攻克。

@ -24,7 +24,11 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
2. 在循环的每轮迭代中,弹出队首顶点并记录访问,然后将该顶点的所有邻接顶点加入到队列尾部。
3. 循环步骤 `2.` ,直到所有顶点被访问完毕后结束。
为了防止重复遍历顶点,我们需要借助一个哈希表 `visited` 来记录哪些节点已被访问。
为了防止重复遍历顶点,我们需要借助一个哈希集合 `visited` 来记录哪些节点已被访问。
!!! tip
哈希集合可以看作一个只存储 `key` 而不存储 `value` 的哈希表,它可以在 $O(1)$ 时间复杂度下进行 `key` 的增删查改操作。根据 `key` 的唯一性,哈希集合通常用于数据去重等场景。
=== "Python"
@ -34,7 +38,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希,用于记录已被访问过的顶点
# 哈希集合,用于记录已被访问过的顶点
visited = set[Vertex]([start_vet])
# 队列用于实现 BFS
que = deque[Vertex]([start_vet])
@ -60,7 +64,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
vector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {
// 顶点遍历序列
vector<Vertex *> res;
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
unordered_set<Vertex *> visited = {startVet};
// 队列用于实现 BFS
queue<Vertex *> que;
@ -91,7 +95,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
List<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = new ArrayList<>();
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
Set<Vertex> visited = new HashSet<>();
visited.add(startVet);
// 队列用于实现 BFS
@ -122,7 +126,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
List<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
HashSet<Vertex> visited = [startVet];
// 队列用于实现 BFS
Queue<Vertex> que = new();
@ -153,7 +157,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
func graphBFS(g *graphAdjList, startVet Vertex) []Vertex {
// 顶点遍历序列
res := make([]Vertex, 0)
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
visited := make(map[Vertex]struct{})
visited[startVet] = struct{}{}
// 队列用于实现 BFS, 使用切片模拟队列
@ -189,7 +193,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
func graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {
// 顶点遍历序列
var res: [Vertex] = []
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
var visited: Set<Vertex> = [startVet]
// 队列用于实现 BFS
var que: [Vertex] = [startVet]
@ -219,7 +223,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
function graphBFS(graph, startVet) {
// 顶点遍历序列
const res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
const visited = new Set();
visited.add(startVet);
// 队列用于实现 BFS
@ -250,7 +254,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
function graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {
// 顶点遍历序列
const res: Vertex[] = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
const visited: Set<Vertex> = new Set();
visited.add(startVet);
// 队列用于实现 BFS
@ -281,7 +285,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
// 顶点遍历序列
List<Vertex> res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
Set<Vertex> visited = {};
visited.add(startVet);
// 队列用于实现 BFS
@ -313,7 +317,7 @@ 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
@ -421,7 +425,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
fun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {
// 顶点遍历序列
val res = mutableListOf<Vertex?>()
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
val visited = HashSet<Vertex>()
visited.add(startVet)
// 队列用于实现 BFS
@ -452,7 +456,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希,用于记录已被访问过的顶点
# 哈希集合,用于记录已被访问过的顶点
visited = Set.new([start_vet])
# 队列用于实现 BFS
que = [start_vet]
@ -528,7 +532,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
**时间复杂度**:所有顶点都会入队并出队一次,使用 $O(|V|)$ 时间;在遍历邻接顶点的过程中,由于是无向图,因此所有边都会被访问 $2$ 次,使用 $O(2|E|)$ 时间;总体使用 $O(|V| + |E|)$ 时间。
**空间复杂度**:列表 `res` ,哈希 `visited` ,队列 `que` 中的顶点数量最多为 $|V|$ ,使用 $O(|V|)$ 空间。
**空间复杂度**:列表 `res` ,哈希集合 `visited` ,队列 `que` 中的顶点数量最多为 $|V|$ ,使用 $O(|V|)$ 空间。
## 9.3.2 &nbsp; 深度优先遍历
@ -540,7 +544,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
### 1. &nbsp; 算法实现
这种“走到尽头再返回”的算法范式通常基于递归来实现。与广度优先遍历类似,在深度优先遍历中,我们也需要借助一个哈希 `visited` 来记录已被访问的顶点,以避免重复访问顶点。
这种“走到尽头再返回”的算法范式通常基于递归来实现。与广度优先遍历类似,在深度优先遍历中,我们也需要借助一个哈希集合 `visited` 来记录已被访问的顶点,以避免重复访问顶点。
=== "Python"
@ -561,7 +565,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希,用于记录已被访问过的顶点
# 哈希集合,用于记录已被访问过的顶点
visited = set[Vertex]()
dfs(graph, visited, res, start_vet)
return res
@ -588,7 +592,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
vector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {
// 顶点遍历序列
vector<Vertex *> res;
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
unordered_set<Vertex *> visited;
dfs(graph, visited, res, startVet);
return res;
@ -616,7 +620,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
List<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = new ArrayList<>();
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
Set<Vertex> visited = new HashSet<>();
dfs(graph, visited, res, startVet);
return res;
@ -645,7 +649,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
List<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
HashSet<Vertex> visited = [];
DFS(graph, visited, res, startVet);
return res;
@ -675,7 +679,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
func graphDFS(g *graphAdjList, startVet Vertex) []Vertex {
// 顶点遍历序列
res := make([]Vertex, 0)
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
visited := make(map[Vertex]struct{})
dfs(g, visited, &res, startVet)
// 返回顶点遍历序列
@ -705,7 +709,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
func graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {
// 顶点遍历序列
var res: [Vertex] = []
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
var visited: Set<Vertex> = []
dfs(graph: graph, visited: &visited, res: &res, vet: startVet)
return res
@ -735,7 +739,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
function graphDFS(graph, startVet) {
// 顶点遍历序列
const res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
const visited = new Set();
dfs(graph, visited, res, startVet);
return res;
@ -769,7 +773,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
function graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {
// 顶点遍历序列
const res: Vertex[] = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
const visited: Set<Vertex> = new Set();
dfs(graph, visited, res, startVet);
return res;
@ -802,7 +806,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
List<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
Set<Vertex> visited = {};
dfs(graph, visited, res, startVet);
return res;
@ -833,7 +837,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
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);
@ -904,7 +908,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
fun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {
// 顶点遍历序列
val res = mutableListOf<Vertex?>()
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
val visited = HashSet<Vertex?>()
dfs(graph, visited, res, startVet)
return res
@ -931,7 +935,7 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希,用于记录已被访问过的顶点
# 哈希集合,用于记录已被访问过的顶点
visited = Set.new
dfs(graph, visited, res, start_vet)
res
@ -1003,4 +1007,4 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先
**时间复杂度**:所有顶点都会被访问 $1$ 次,使用 $O(|V|)$ 时间;所有边都会被访问 $2$ 次,使用 $O(2|E|)$ 时间;总体使用 $O(|V| + |E|)$ 时间。
**空间复杂度**:列表 `res` ,哈希 `visited` 顶点数量最多为 $|V|$ ,递归深度最大为 $|V|$ ,因此使用 $O(|V|)$ 空间。
**空间复杂度**:列表 `res` ,哈希集合 `visited` 顶点数量最多为 $|V|$ ,递归深度最大为 $|V|$ ,因此使用 $O(|V|)$ 空间。

@ -676,11 +676,20 @@ comments: true
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{left}
### 获取左子节点的索引 ###
def left(i)
2 * i + 1
end
[class]{MaxHeap}-[func]{right}
### 获取右子节点的索引 ###
def right(i)
2 * i + 2
end
[class]{MaxHeap}-[func]{parent}
### 获取父节点的索引 ###
def parent(i)
(i - 1) / 2 # 向下整除
end
```
=== "Zig"
@ -817,7 +826,10 @@ comments: true
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{peek}
### 访问堆顶元素 ###
def peek
@max_heap[0]
end
```
=== "Zig"
@ -1211,9 +1223,27 @@ comments: true
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{sift_up}
### 元素入堆 ###
def push(val)
# 添加节点
@max_heap << val
# 从底至顶堆化
sift_up(size - 1)
end
### 从节点 i 开始,从底至顶堆化 ###
def sift_up(i)
loop do
# 获取节点 i 的父节点
p = parent(i)
# 当“越过根节点”或“节点无须修复”时,结束堆化
break if p < 0 || @max_heap[i] <= @max_heap[p]
# 交换两节点
swap(i, p)
# 循环向上堆化
i = p
end
end
```
=== "Zig"
@ -1649,7 +1679,7 @@ comments: true
// 交换根节点与最右叶节点(交换首元素与尾元素)
self.swap(0, self.size() - 1);
// 删除节点
let val = self.max_heap.remove(self.size() - 1);
let val = self.max_heap.pop().unwrap();
// 从顶至底堆化
self.sift_down(0);
// 返回堆顶元素
@ -1767,9 +1797,37 @@ comments: true
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{sift_down}
### 元素出堆 ###
def pop
# 判空处理
raise IndexError, "堆为空" if is_empty?
# 交换根节点与最右叶节点(交换首元素与尾元素)
swap(0, size - 1)
# 删除节点
val = @max_heap.pop
# 从顶至底堆化
sift_down(0)
# 返回堆顶元素
val
end
### 从节点 i 开始,从顶至底堆化 ###
def sift_down(i)
loop do
# 判断节点 i, l, r 中值最大的节点,记为 ma
l, r, ma = left(i), right(i), i
ma = l if l < size && @max_heap[l] > @max_heap[ma]
ma = r if r < size && @max_heap[r] > @max_heap[ma]
# 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
break if ma == i
# 交换两节点
swap(i, ma)
# 循环向下堆化
i = ma
end
end
```
=== "Zig"

@ -437,7 +437,28 @@ comments: true
=== "Ruby"
```ruby title="top_k.rb"
[class]{}-[func]{top_k_heap}
### 基于堆查找数组中最大的 k 个元素 ###
def top_k_heap(nums, k)
# 初始化小顶堆
# 请注意:我们将堆中所有元素取反,从而用大顶堆来模拟小顶堆
max_heap = MaxHeap.new([])
# 将数组的前 k 个元素入堆
for i in 0...k
push_min_heap(max_heap, nums[i])
end
# 从第 k+1 个元素开始,保持堆的长度为 k
for i in k...nums.length
# 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆
if nums[i] > peek_min_heap(max_heap)
pop_min_heap(max_heap)
push_min_heap(max_heap, nums[i])
end
end
get_min_heap(max_heap)
end
```
=== "Zig"

@ -11,3 +11,16 @@ comments: true
- 算法是在有限时间内解决特定问题的一组指令或操作步骤,而数据结构是计算机中组织和存储数据的方式。
- 数据结构与算法紧密相连。数据结构是算法的基石,而算法是数据结构发挥作用的舞台。
- 我们可以将数据结构与算法类比为拼装积木,积木代表数据,积木的形状和连接方式等代表数据结构,拼装积木的步骤则对应算法。
### 1. &nbsp; Q & A
**Q**:作为一名程序员,我在日常工作中从未用算法解决过问题,常用算法都被编程语言封装好了,直接用就可以了;这是否意味着我们工作中的问题还没有到达需要算法的程度?
如果把具体的工作技能比作是武功的“招式”的话,那么基础科目应该更像是“内功”。
我认为学算法(以及其他基础科目)的意义不是在于在工作中从零实现它,而是基于学到的知识,在解决问题时能够作出专业的反应和判断,从而提升工作的整体质量。举一个简单例子,每种编程语言都内置了排序函数:
- 如果我们没有学过数据结构与算法,那么给定任何数据,我们可能都塞给这个排序函数去做了。运行顺畅、性能不错,看上去并没有什么问题。
- 但如果学过算法,我们就会知道内置排序函数的时间复杂度是 $O(n \log n)$ ;而如果给定的数据是固定位数的整数(例如学号),那么我们就可以用效率更高的“基数排序”来做,将时间复杂度降为 $O(nk)$ ,其中 $k$ 为位数。当数据体量很大时,节省出来的运行时间就能创造较大价值(成本降低、体验变好等)。
在工程领域中,大量问题是难以达到最优解的,许多问题只是被“差不多”地解决了。问题的难易程度一方面取决于问题本身的性质,另一方面也取决于观测问题的人的知识储备。人的知识越完备、经验越多,分析问题就会越深入,问题就能被解决得更优雅。

@ -681,7 +681,7 @@ AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二
}
```
!!! note
!!! tip
设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。

@ -645,7 +645,7 @@ comments: true
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=37&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=37&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全屏观看 ></a></div>
!!! note
!!! tip
需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除通常是由一套操作配合完成的,以实现有实际意义的操作。

@ -35,7 +35,7 @@ We can imagine the computer storage system as a pyramid structure shown in the F
<p align="center"> Figure 4-9 &nbsp; Computer storage system </p>
!!! note
!!! tip
The storage hierarchy of computers reflects a delicate balance between speed, capacity, and cost. In fact, this kind of trade-off is common in all industrial fields, requiring us to find the best balance between different advantages and limitations.

@ -671,7 +671,7 @@ Since $T(n)$ is a linear function, its growth trend is linear, and therefore, it
In essence, time complexity analysis is about finding the asymptotic upper bound of the "number of operations $T(n)$". It has a precise mathematical definition.
!!! abstract "Asymptotic Upper Bound"
!!! note "Asymptotic Upper Bound"
If there exist positive real numbers $c$ and $n_0$ such that for all $n > n_0$, $T(n) \leq c \cdot f(n)$, then $f(n)$ is considered an asymptotic upper bound of $T(n)$, denoted as $T(n) = O(f(n))$.

@ -4,7 +4,7 @@ comments: true
# 3.3 &nbsp; Number encoding *
!!! note
!!! tip
In this book, chapters marked with an asterisk '*' are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters.

@ -34,7 +34,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希,用于记录已被访问过的顶点
# 哈希集合,用于记录已被访问过的顶点
visited = set[Vertex]([start_vet])
# 队列用于实现 BFS
que = deque[Vertex]([start_vet])
@ -60,7 +60,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
vector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {
// 顶点遍历序列
vector<Vertex *> res;
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
unordered_set<Vertex *> visited = {startVet};
// 队列用于实现 BFS
queue<Vertex *> que;
@ -91,7 +91,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
List<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = new ArrayList<>();
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
Set<Vertex> visited = new HashSet<>();
visited.add(startVet);
// 队列用于实现 BFS
@ -122,7 +122,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
List<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
HashSet<Vertex> visited = [startVet];
// 队列用于实现 BFS
Queue<Vertex> que = new();
@ -153,7 +153,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
func graphBFS(g *graphAdjList, startVet Vertex) []Vertex {
// 顶点遍历序列
res := make([]Vertex, 0)
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
visited := make(map[Vertex]struct{})
visited[startVet] = struct{}{}
// 队列用于实现 BFS, 使用切片模拟队列
@ -189,7 +189,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
func graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {
// 顶点遍历序列
var res: [Vertex] = []
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
var visited: Set<Vertex> = [startVet]
// 队列用于实现 BFS
var que: [Vertex] = [startVet]
@ -219,7 +219,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
function graphBFS(graph, startVet) {
// 顶点遍历序列
const res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
const visited = new Set();
visited.add(startVet);
// 队列用于实现 BFS
@ -250,7 +250,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
function graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {
// 顶点遍历序列
const res: Vertex[] = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
const visited: Set<Vertex> = new Set();
visited.add(startVet);
// 队列用于实现 BFS
@ -281,7 +281,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
// 顶点遍历序列
List<Vertex> res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
Set<Vertex> visited = {};
visited.add(startVet);
// 队列用于实现 BFS
@ -313,7 +313,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
fn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {
// 顶点遍历序列
let mut res = vec![];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
let mut visited = HashSet::new();
visited.insert(start_vet);
// 队列用于实现 BFS
@ -421,7 +421,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
fun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {
// 顶点遍历序列
val res = mutableListOf<Vertex?>()
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
val visited = HashSet<Vertex>()
visited.add(startVet)
// 队列用于实现 BFS
@ -452,7 +452,7 @@ To prevent revisiting vertices, we use a hash table `visited` to record which no
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希,用于记录已被访问过的顶点
# 哈希集合,用于记录已被访问过的顶点
visited = Set.new([start_vet])
# 队列用于实现 BFS
que = [start_vet]
@ -561,7 +561,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希,用于记录已被访问过的顶点
# 哈希集合,用于记录已被访问过的顶点
visited = set[Vertex]()
dfs(graph, visited, res, start_vet)
return res
@ -588,7 +588,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
vector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {
// 顶点遍历序列
vector<Vertex *> res;
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
unordered_set<Vertex *> visited;
dfs(graph, visited, res, startVet);
return res;
@ -616,7 +616,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
List<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = new ArrayList<>();
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
Set<Vertex> visited = new HashSet<>();
dfs(graph, visited, res, startVet);
return res;
@ -645,7 +645,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
List<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
HashSet<Vertex> visited = [];
DFS(graph, visited, res, startVet);
return res;
@ -675,7 +675,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
func graphDFS(g *graphAdjList, startVet Vertex) []Vertex {
// 顶点遍历序列
res := make([]Vertex, 0)
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
visited := make(map[Vertex]struct{})
dfs(g, visited, &res, startVet)
// 返回顶点遍历序列
@ -705,7 +705,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
func graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {
// 顶点遍历序列
var res: [Vertex] = []
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
var visited: Set<Vertex> = []
dfs(graph: graph, visited: &visited, res: &res, vet: startVet)
return res
@ -735,7 +735,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
function graphDFS(graph, startVet) {
// 顶点遍历序列
const res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
const visited = new Set();
dfs(graph, visited, res, startVet);
return res;
@ -769,7 +769,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
function graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {
// 顶点遍历序列
const res: Vertex[] = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
const visited: Set<Vertex> = new Set();
dfs(graph, visited, res, startVet);
return res;
@ -802,7 +802,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
List<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {
// 顶点遍历序列
List<Vertex> res = [];
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
Set<Vertex> visited = {};
dfs(graph, visited, res, startVet);
return res;
@ -833,7 +833,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
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);
@ -904,7 +904,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
fun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {
// 顶点遍历序列
val res = mutableListOf<Vertex?>()
// 哈希,用于记录已被访问过的顶点
// 哈希集合,用于记录已被访问过的顶点
val visited = HashSet<Vertex?>()
dfs(graph, visited, res, startVet)
return res
@ -931,7 +931,7 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
# 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
# 顶点遍历序列
res = []
# 哈希,用于记录已被访问过的顶点
# 哈希集合,用于记录已被访问过的顶点
visited = Set.new
dfs(graph, visited, res, start_vet)
res

@ -675,11 +675,20 @@ We can encapsulate the index mapping formula into functions for convenient later
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{left}
### 获取左子节点的索引 ###
def left(i)
2 * i + 1
end
[class]{MaxHeap}-[func]{right}
### 获取右子节点的索引 ###
def right(i)
2 * i + 2
end
[class]{MaxHeap}-[func]{parent}
### 获取父节点的索引 ###
def parent(i)
(i - 1) / 2 # 向下整除
end
```
=== "Zig"
@ -816,7 +825,10 @@ The top element of the heap is the root node of the binary tree, which is also t
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{peek}
### 访问堆顶元素 ###
def peek
@max_heap[0]
end
```
=== "Zig"
@ -1210,9 +1222,27 @@ Given a total of $n$ nodes, the height of the tree is $O(\log n)$. Hence, the lo
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{sift_up}
### 元素入堆 ###
def push(val)
# 添加节点
@max_heap << val
# 从底至顶堆化
sift_up(size - 1)
end
### 从节点 i 开始,从底至顶堆化 ###
def sift_up(i)
loop do
# 获取节点 i 的父节点
p = parent(i)
# 当“越过根节点”或“节点无须修复”时,结束堆化
break if p < 0 || @max_heap[i] <= @max_heap[p]
# 交换两节点
swap(i, p)
# 循环向上堆化
i = p
end
end
```
=== "Zig"
@ -1648,7 +1678,7 @@ Similar to the element insertion operation, the time complexity of the top eleme
// 交换根节点与最右叶节点(交换首元素与尾元素)
self.swap(0, self.size() - 1);
// 删除节点
let val = self.max_heap.remove(self.size() - 1);
let val = self.max_heap.pop().unwrap();
// 从顶至底堆化
self.sift_down(0);
// 返回堆顶元素
@ -1766,9 +1796,37 @@ Similar to the element insertion operation, the time complexity of the top eleme
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{sift_down}
### 元素出堆 ###
def pop
# 判空处理
raise IndexError, "堆为空" if is_empty?
# 交换根节点与最右叶节点(交换首元素与尾元素)
swap(0, size - 1)
# 删除节点
val = @max_heap.pop
# 从顶至底堆化
sift_down(0)
# 返回堆顶元素
val
end
### 从节点 i 开始,从顶至底堆化 ###
def sift_down(i)
loop do
# 判断节点 i, l, r 中值最大的节点,记为 ma
l, r, ma = left(i), right(i), i
ma = l if l < size && @max_heap[l] > @max_heap[ma]
ma = r if r < size && @max_heap[r] > @max_heap[ma]
# 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
break if ma == i
# 交换两节点
swap(i, ma)
# 循环向下堆化
i = ma
end
end
```
=== "Zig"

@ -437,7 +437,28 @@ Example code is as follows:
=== "Ruby"
```ruby title="top_k.rb"
[class]{}-[func]{top_k_heap}
### 基于堆查找数组中最大的 k 个元素 ###
def top_k_heap(nums, k)
# 初始化小顶堆
# 请注意:我们将堆中所有元素取反,从而用大顶堆来模拟小顶堆
max_heap = MaxHeap.new([])
# 将数组的前 k 个元素入堆
for i in 0...k
push_min_heap(max_heap, nums[i])
end
# 从第 k+1 个元素开始,保持堆的长度为 k
for i in k...nums.length
# 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆
if nums[i] > peek_min_heap(max_heap)
pop_min_heap(max_heap)
push_min_heap(max_heap, nums[i])
end
end
get_min_heap(max_heap)
end
```
=== "Zig"

@ -670,7 +670,7 @@ The "balance factor" of a node is defined as the height of the node's left subtr
}
```
!!! note
!!! tip
Let the balance factor be $f$, then the balance factor of any node in an AVL tree satisfies $-1 \le f \le 1$.

@ -617,7 +617,7 @@ Similar to a linked list, inserting and removing nodes in a binary tree can be a
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
!!! note
!!! tip
It's important to note that inserting nodes may change the original logical structure of the binary tree, while removing nodes usually means removing the node and all its subtrees. Therefore, in a binary tree, insertion and removal are usually performed through a set of operations to achieve meaningful actions.

@ -1555,7 +1555,7 @@ comments: true
單向鏈結串列通常用於實現堆疊、佇列、雜湊表和圖等資料結構。
- **堆疊與佇列**:當插入和刪除操作都在鏈結串列的一端進行時,它表現出先進後出的特性,對應堆疊;當插入操作在鏈結串列的一端進行,刪除操作在鏈結串列的另一端進行,它表現出先進先出的特性,對應佇列。
- **堆疊與佇列**:當插入和刪除操作都在鏈結串列的一端進行時,它表現的特性為先進後出,對應堆疊;當插入操作在鏈結串列的一端進行,刪除操作在鏈結串列的另一端進行,它表現的特性為先進先出,對應佇列。
- **雜湊表**:鏈式位址是解決雜湊衝突的主流方案之一,在該方案中,所有衝突的元素都會被放到一個鏈結串列中。
- **圖**:鄰接表是表示圖的一種常用方式,其中圖的每個頂點都與一個鏈結串列相關聯,鏈結串列中的每個元素都代表與該頂點相連的其他頂點。

@ -36,7 +36,7 @@ status: new
<p align="center"> 圖 4-9 &nbsp; 計算機儲存系統 </p>
!!! note
!!! tip
計算機的儲存層次結構體現了速度、容量和成本三者之間的精妙平衡。實際上,這種權衡普遍存在於所有工業領域,它要求我們在不同的優勢和限制之間找到最佳平衡點。

@ -538,7 +538,7 @@ comments: true
<p align="center"> 圖 13-7 &nbsp; 重複排列 </p>
那麼如何去除重複的排列呢?最直接地,考慮藉助一個雜湊,直接對排列結果進行去重。然而這樣做不夠優雅,**因為生成重複排列的搜尋分支沒有必要,應當提前識別並剪枝**,這樣可以進一步提升演算法效率。
那麼如何去除重複的排列呢?最直接地,考慮藉助一個雜湊集合,直接對排列結果進行去重。然而這樣做不夠優雅,**因為生成重複排列的搜尋分支沒有必要,應當提前識別並剪枝**,這樣可以進一步提升演算法效率。
### 1. &nbsp; 相等元素剪枝
@ -554,7 +554,7 @@ comments: true
### 2. &nbsp; 程式碼實現
在上一題的程式碼的基礎上,我們考慮在每一輪選擇中開啟一個雜湊 `duplicated` ,用於記錄該輪中已經嘗試過的元素,並將重複元素剪枝:
在上一題的程式碼的基礎上,我們考慮在每一輪選擇中開啟一個雜湊集合 `duplicated` ,用於記錄該輪中已經嘗試過的元素,並將重複元素剪枝:
=== "Python"

@ -11,7 +11,7 @@ comments: true
- 回溯問題通常包含多個約束條件,它們可用於實現剪枝操作。剪枝可以提前結束不必要的搜尋分支,大幅提升搜尋效率。
- 回溯演算法主要可用於解決搜尋問題和約束滿足問題。組合最佳化問題雖然可以用回溯演算法解決,但往往存在效率更高或效果更好的解法。
- 全排列問題旨在搜尋給定集合元素的所有可能的排列。我們藉助一個陣列來記錄每個元素是否被選擇,剪掉重複選擇同一元素的搜尋分支,確保每個元素只被選擇一次。
- 在全排列問題中,如果集合中存在重複元素,則最終結果會出現重複排列。我們需要約束相等元素在每輪中只能被選擇一次,這通常藉助一個雜湊來實現。
- 在全排列問題中,如果集合中存在重複元素,則最終結果會出現重複排列。我們需要約束相等元素在每輪中只能被選擇一次,這通常藉助一個雜湊集合來實現。
- 子集和問題的目標是在給定集合中找到和為目標值的所有子集。集合不區分元素順序,而搜尋過程會輸出所有順序的結果,產生重複子集。我們在回溯前將資料進行排序,並設定一個變數來指示每一輪的走訪起始點,從而將生成重複子集的搜尋分支進行剪枝。
- 對於子集和問題,陣列中的相等元素會產生重複集合。我們利用陣列已排序的前置條件,透過判斷相鄰元素是否相等實現剪枝,從而確保相等元素在每輪中只能被選中一次。
- $n$ 皇后問題旨在尋找將 $n$ 個皇后放置到 $n \times n$ 尺寸棋盤上的方案,要求所有皇后兩兩之間無法攻擊對方。該問題的約束條件有行約束、列約束、主對角線和次對角線約束。為滿足行約束,我們採用按行放置的策略,保證每一行放置一個皇后。

@ -753,7 +753,7 @@ $T(n)$ 是一次函式,說明其執行時間的增長趨勢是線性的,因
時間複雜度分析本質上是計算“操作數量 $T(n)$”的漸近上界,它具有明確的數學定義。
!!! abstract "函式漸近上界"
!!! note "函式漸近上界"
若存在正實數 $c$ 和實數 $n_0$ ,使得對於所有的 $n > n_0$ ,均有 $T(n) \leq c \cdot f(n)$ ,則可認為 $f(n)$ 給出了 $T(n)$ 的一個漸近上界,記為 $T(n) = O(f(n))$ 。

@ -4,7 +4,7 @@ comments: true
# 3.3 &nbsp; 數字編碼 *
!!! note
!!! tip
在本書中,標題帶有 * 符號的是選讀章節。如果你時間有限或感到理解困難,可以先跳過,等學完必讀章節後再單獨攻克。

@ -1130,7 +1130,80 @@ comments: true
=== "Ruby"
```ruby title="graph_adjacency_matrix.rb"
[class]{GraphAdjMat}-[func]{}
### 基於鄰接矩陣實現的無向圖類別 ###
class GraphAdjMat
def initialize(vertices, edges)
### 建構子 ###
# 頂點串列,元素代表“頂點值”,索引代表“頂點索引”
@vertices = []
# 鄰接矩陣,行列索引對應“頂點索引”
@adj_mat = []
# 新增頂點
vertices.each { |val| add_vertex(val) }
# 新增邊
# 請注意edges 元素代表頂點索引,即對應 vertices 元素索引
edges.each { |e| add_edge(e[0], e[1]) }
end
### 獲取頂點數量 ###
def size
@vertices.length
end
### 新增頂點 ###
def add_vertex(val)
n = size
# 向頂點串列中新增新頂點的值
@vertices << val
# 在鄰接矩陣中新增一行
new_row = Array.new(n, 0)
@adj_mat << new_row
# 在鄰接矩陣中新增一列
@adj_mat.each { |row| row << 0 }
end
### 刪除頂點 ###
def remove_vertex(index)
raise IndexError if index >= size
# 在頂點串列中移除索引 index 的頂點
@vertices.delete_at(index)
# 在鄰接矩陣中刪除索引 index 的行
@adj_mat.delete_at(index)
# 在鄰接矩陣中刪除索引 index 的列
@adj_mat.each { |row| row.delete_at(index) }
end
### 新增邊 ###
def add_edge(i, j)
# 參數 i, j 對應 vertices 元素索引
# 索引越界與相等處理
if i < 0 || j < 0 || i >= size || j >= size || i == j
raise IndexError
end
# 在無向圖中,鄰接矩陣關於主對角線對稱,即滿足 (i, j) == (j, i)
@adj_mat[i][j] = 1
@adj_mat[j][i] = 1
end
### 刪除邊 ###
def remove_edge(i, j)
# 參數 i, j 對應 vertices 元素索引
# 索引越界與相等處理
if i < 0 || j < 0 || i >= size || j >= size || i == j
raise IndexError
end
@adj_mat[i][j] = 0
@adj_mat[j][i] = 0
end
### 列印鄰接矩陣 ###
def __print__
puts "頂點串列 = #{@vertices}"
puts '鄰接矩陣 ='
print_matrix(@adj_mat)
end
end
```
=== "Zig"
@ -2233,7 +2306,73 @@ comments: true
=== "Ruby"
```ruby title="graph_adjacency_list.rb"
[class]{GraphAdjList}-[func]{}
### 基於鄰接表實現的無向圖類別 ###
class GraphAdjList
attr_reader :adj_list
### 建構子 ###
def initialize(edges)
# 鄰接表key頂點value該頂點的所有鄰接頂點
@adj_list = {}
# 新增所有頂點和邊
for edge in edges
add_vertex(edge[0])
add_vertex(edge[1])
add_edge(edge[0], edge[1])
end
end
### 獲取頂點數量 ###
def size
@adj_list.length
end
### 新增邊 ###
def add_edge(vet1, vet2)
raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)
@adj_list[vet1] << vet2
@adj_list[vet2] << vet1
end
### 刪除邊 ###
def remove_edge(vet1, vet2)
raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)
# 刪除邊 vet1 - vet2
@adj_list[vet1].delete(vet2)
@adj_list[vet2].delete(vet1)
end
### 新增頂點 ###
def add_vertex(vet)
return if @adj_list.include?(vet)
# 在鄰接表中新增一個新鏈結串列
@adj_list[vet] = []
end
### 刪除頂點 ###
def remove_vertex(vet)
raise ArgumentError unless @adj_list.include?(vet)
# 在鄰接表中刪除頂點 vet 對應的鏈結串列
@adj_list.delete(vet)
# 走訪其他頂點的鏈結串列,刪除所有包含 vet 的邊
for vertex in @adj_list
@adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)
end
end
### 列印鄰接表 ###
def __print__
puts '鄰接表 ='
for vertex in @adj_list
tmp = @adj_list[vertex.first].map { |v| v.val }
puts "#{vertex.first.val}: #{tmp},"
end
end
end
```
=== "Zig"

@ -24,7 +24,11 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
2. 在迴圈的每輪迭代中,彈出佇列首頂點並記錄訪問,然後將該頂點的所有鄰接頂點加入到佇列尾部。
3. 迴圈步驟 `2.` ,直到所有頂點被訪問完畢後結束。
為了防止重複走訪頂點,我們需要藉助一個雜湊表 `visited` 來記錄哪些節點已被訪問。
為了防止重複走訪頂點,我們需要藉助一個雜湊集合 `visited` 來記錄哪些節點已被訪問。
!!! tip
雜湊集合可以看作一個只儲存 `key` 而不儲存 `value` 的雜湊表,它可以在 $O(1)$ 時間複雜度下進行 `key` 的增刪查改操作。根據 `key` 的唯一性,雜湊集合通常用於資料去重等場景。
=== "Python"
@ -34,7 +38,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
# 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點
# 頂點走訪序列
res = []
# 雜湊,用於記錄已被訪問過的頂點
# 雜湊集合,用於記錄已被訪問過的頂點
visited = set[Vertex]([start_vet])
# 佇列用於實現 BFS
que = deque[Vertex]([start_vet])
@ -60,7 +64,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
vector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {
// 頂點走訪序列
vector<Vertex *> res;
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
unordered_set<Vertex *> visited = {startVet};
// 佇列用於實現 BFS
queue<Vertex *> que;
@ -91,7 +95,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
List<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {
// 頂點走訪序列
List<Vertex> res = new ArrayList<>();
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
Set<Vertex> visited = new HashSet<>();
visited.add(startVet);
// 佇列用於實現 BFS
@ -122,7 +126,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
List<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {
// 頂點走訪序列
List<Vertex> res = [];
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
HashSet<Vertex> visited = [startVet];
// 佇列用於實現 BFS
Queue<Vertex> que = new();
@ -153,7 +157,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
func graphBFS(g *graphAdjList, startVet Vertex) []Vertex {
// 頂點走訪序列
res := make([]Vertex, 0)
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
visited := make(map[Vertex]struct{})
visited[startVet] = struct{}{}
// 佇列用於實現 BFS, 使用切片模擬佇列
@ -189,7 +193,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
func graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {
// 頂點走訪序列
var res: [Vertex] = []
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
var visited: Set<Vertex> = [startVet]
// 佇列用於實現 BFS
var que: [Vertex] = [startVet]
@ -219,7 +223,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
function graphBFS(graph, startVet) {
// 頂點走訪序列
const res = [];
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
const visited = new Set();
visited.add(startVet);
// 佇列用於實現 BFS
@ -250,7 +254,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
function graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {
// 頂點走訪序列
const res: Vertex[] = [];
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
const visited: Set<Vertex> = new Set();
visited.add(startVet);
// 佇列用於實現 BFS
@ -281,7 +285,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
// 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點
// 頂點走訪序列
List<Vertex> res = [];
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
Set<Vertex> visited = {};
visited.add(startVet);
// 佇列用於實現 BFS
@ -313,7 +317,7 @@ 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
@ -421,7 +425,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
fun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {
// 頂點走訪序列
val res = mutableListOf<Vertex?>()
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
val visited = HashSet<Vertex>()
visited.add(startVet)
// 佇列用於實現 BFS
@ -447,7 +451,29 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
=== "Ruby"
```ruby title="graph_bfs.rb"
[class]{}-[func]{graph_bfs}
### 廣度優先走訪 ###
def graph_bfs(graph, start_vet)
# 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點
# 頂點走訪序列
res = []
# 雜湊集合,用於記錄已被訪問過的頂點
visited = Set.new([start_vet])
# 佇列用於實現 BFS
que = [start_vet]
# 以頂點 vet 為起點,迴圈直至訪問完所有頂點
while que.length > 0
vet = que.shift # 佇列首頂點出隊
res << vet #
# 走訪該頂點的所有鄰接頂點
for adj_vet in graph.adj_list[vet]
next if visited.include?(adj_vet) # 跳過已被訪問的頂點
que << adj_vet #
visited.add(adj_vet) # 標記該頂點已被訪問
end
end
# 返回頂點走訪序列
res
end
```
=== "Zig"
@ -506,7 +532,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
**時間複雜度**:所有頂點都會入列並出隊一次,使用 $O(|V|)$ 時間;在走訪鄰接頂點的過程中,由於是無向圖,因此所有邊都會被訪問 $2$ 次,使用 $O(2|E|)$ 時間;總體使用 $O(|V| + |E|)$ 時間。
**空間複雜度**:串列 `res` ,雜湊 `visited` ,佇列 `que` 中的頂點數量最多為 $|V|$ ,使用 $O(|V|)$ 空間。
**空間複雜度**:串列 `res` ,雜湊集合 `visited` ,佇列 `que` 中的頂點數量最多為 $|V|$ ,使用 $O(|V|)$ 空間。
## 9.3.2 &nbsp; 深度優先走訪
@ -518,7 +544,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
### 1. &nbsp; 演算法實現
這種“走到盡頭再返回”的演算法範式通常基於遞迴來實現。與廣度優先走訪類似,在深度優先走訪中,我們也需要藉助一個雜湊 `visited` 來記錄已被訪問的頂點,以避免重複訪問頂點。
這種“走到盡頭再返回”的演算法範式通常基於遞迴來實現。與廣度優先走訪類似,在深度優先走訪中,我們也需要藉助一個雜湊集合 `visited` 來記錄已被訪問的頂點,以避免重複訪問頂點。
=== "Python"
@ -539,7 +565,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
# 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點
# 頂點走訪序列
res = []
# 雜湊,用於記錄已被訪問過的頂點
# 雜湊集合,用於記錄已被訪問過的頂點
visited = set[Vertex]()
dfs(graph, visited, res, start_vet)
return res
@ -566,7 +592,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
vector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {
// 頂點走訪序列
vector<Vertex *> res;
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
unordered_set<Vertex *> visited;
dfs(graph, visited, res, startVet);
return res;
@ -594,7 +620,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
List<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {
// 頂點走訪序列
List<Vertex> res = new ArrayList<>();
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
Set<Vertex> visited = new HashSet<>();
dfs(graph, visited, res, startVet);
return res;
@ -623,7 +649,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
List<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {
// 頂點走訪序列
List<Vertex> res = [];
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
HashSet<Vertex> visited = [];
DFS(graph, visited, res, startVet);
return res;
@ -653,7 +679,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
func graphDFS(g *graphAdjList, startVet Vertex) []Vertex {
// 頂點走訪序列
res := make([]Vertex, 0)
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
visited := make(map[Vertex]struct{})
dfs(g, visited, &res, startVet)
// 返回頂點走訪序列
@ -683,7 +709,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
func graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {
// 頂點走訪序列
var res: [Vertex] = []
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
var visited: Set<Vertex> = []
dfs(graph: graph, visited: &visited, res: &res, vet: startVet)
return res
@ -713,7 +739,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
function graphDFS(graph, startVet) {
// 頂點走訪序列
const res = [];
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
const visited = new Set();
dfs(graph, visited, res, startVet);
return res;
@ -747,7 +773,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
function graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {
// 頂點走訪序列
const res: Vertex[] = [];
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
const visited: Set<Vertex> = new Set();
dfs(graph, visited, res, startVet);
return res;
@ -780,7 +806,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
List<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {
// 頂點走訪序列
List<Vertex> res = [];
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
Set<Vertex> visited = {};
dfs(graph, visited, res, startVet);
return res;
@ -811,7 +837,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
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);
@ -882,7 +908,7 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
fun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {
// 頂點走訪序列
val res = mutableListOf<Vertex?>()
// 雜湊,用於記錄已被訪問過的頂點
// 雜湊集合,用於記錄已被訪問過的頂點
val visited = HashSet<Vertex?>()
dfs(graph, visited, res, startVet)
return res
@ -892,9 +918,28 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
=== "Ruby"
```ruby title="graph_dfs.rb"
[class]{}-[func]{dfs}
[class]{}-[func]{graph_dfs}
### 深度優先走訪輔助函式 ###
def dfs(graph, visited, res, vet)
res << vet #
visited.add(vet) # 標記該頂點已被訪問
# 走訪該頂點的所有鄰接頂點
for adj_vet in graph.adj_list[vet]
next if visited.include?(adj_vet) # 跳過已被訪問的頂點
# 遞迴訪問鄰接頂點
dfs(graph, visited, res, adj_vet)
end
end
### 深度優先走訪 ###
def graph_dfs(graph, start_vet)
# 使用鄰接表來表示圖,以便獲取指定頂點的所有鄰接頂點
# 頂點走訪序列
res = []
# 雜湊集合,用於記錄已被訪問過的頂點
visited = Set.new
dfs(graph, visited, res, start_vet)
res
end
```
=== "Zig"
@ -962,4 +1007,4 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入
**時間複雜度**:所有頂點都會被訪問 $1$ 次,使用 $O(|V|)$ 時間;所有邊都會被訪問 $2$ 次,使用 $O(2|E|)$ 時間;總體使用 $O(|V| + |E|)$ 時間。
**空間複雜度**:串列 `res` ,雜湊 `visited` 頂點數量最多為 $|V|$ ,遞迴深度最大為 $|V|$ ,因此使用 $O(|V|)$ 空間。
**空間複雜度**:串列 `res` ,雜湊集合 `visited` 頂點數量最多為 $|V|$ ,遞迴深度最大為 $|V|$ ,因此使用 $O(|V|)$ 空間。

@ -676,11 +676,20 @@ comments: true
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{left}
### 獲取左子節點的索引 ###
def left(i)
2 * i + 1
end
[class]{MaxHeap}-[func]{right}
### 獲取右子節點的索引 ###
def right(i)
2 * i + 2
end
[class]{MaxHeap}-[func]{parent}
### 獲取父節點的索引 ###
def parent(i)
(i - 1) / 2 # 向下整除
end
```
=== "Zig"
@ -817,7 +826,10 @@ comments: true
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{peek}
### 訪問堆積頂元素 ###
def peek
@max_heap[0]
end
```
=== "Zig"
@ -1211,9 +1223,27 @@ comments: true
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{sift_up}
### 元素入堆積 ###
def push(val)
# 新增節點
@max_heap << val
# 從底至頂堆積化
sift_up(size - 1)
end
### 從節點 i 開始,從底至頂堆積化 ###
def sift_up(i)
loop do
# 獲取節點 i 的父節點
p = parent(i)
# 當“越過根節點”或“節點無須修復”時,結束堆積化
break if p < 0 || @max_heap[i] <= @max_heap[p]
# 交換兩節點
swap(i, p)
# 迴圈向上堆積化
i = p
end
end
```
=== "Zig"
@ -1649,7 +1679,7 @@ comments: true
// 交換根節點與最右葉節點(交換首元素與尾元素)
self.swap(0, self.size() - 1);
// 刪除節點
let val = self.max_heap.remove(self.size() - 1);
let val = self.max_heap.pop().unwrap();
// 從頂至底堆積化
self.sift_down(0);
// 返回堆積頂元素
@ -1767,9 +1797,37 @@ comments: true
=== "Ruby"
```ruby title="my_heap.rb"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{sift_down}
### 元素出堆積 ###
def pop
# 判空處理
raise IndexError, "堆積為空" if is_empty?
# 交換根節點與最右葉節點(交換首元素與尾元素)
swap(0, size - 1)
# 刪除節點
val = @max_heap.pop
# 從頂至底堆積化
sift_down(0)
# 返回堆積頂元素
val
end
### 從節點 i 開始,從頂至底堆積化 ###
def sift_down(i)
loop do
# 判斷節點 i, l, r 中值最大的節點,記為 ma
l, r, ma = left(i), right(i), i
ma = l if l < size && @max_heap[l] > @max_heap[ma]
ma = r if r < size && @max_heap[r] > @max_heap[ma]
# 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出
break if ma == i
# 交換兩節點
swap(i, ma)
# 迴圈向下堆積化
i = ma
end
end
```
=== "Zig"

@ -437,7 +437,28 @@ comments: true
=== "Ruby"
```ruby title="top_k.rb"
[class]{}-[func]{top_k_heap}
### 基於堆積查詢陣列中最大的 k 個元素 ###
def top_k_heap(nums, k)
# 初始化小頂堆積
# 請注意:我們將堆積中所有元素取反,從而用大頂堆積來模擬小頂堆積
max_heap = MaxHeap.new([])
# 將陣列的前 k 個元素入堆積
for i in 0...k
push_min_heap(max_heap, nums[i])
end
# 從第 k+1 個元素開始,保持堆積的長度為 k
for i in k...nums.length
# 若當前元素大於堆積頂元素,則將堆積頂元素出堆積、當前元素入堆積
if nums[i] > peek_min_heap(max_heap)
pop_min_heap(max_heap)
push_min_heap(max_heap, nums[i])
end
end
get_min_heap(max_heap)
end
```
=== "Zig"

@ -11,3 +11,16 @@ comments: true
- 演算法是在有限時間內解決特定問題的一組指令或操作步驟,而資料結構是計算機中組織和儲存資料的方式。
- 資料結構與演算法緊密相連。資料結構是演算法的基石,而演算法是資料結構發揮作用的舞臺。
- 我們可以將資料結構與演算法類比為拼裝積木,積木代表資料,積木的形狀和連線方式等代表資料結構,拼裝積木的步驟則對應演算法。
### 1. &nbsp; Q & A
**Q**:作為一名程式設計師,我在日常工作中從未用演算法解決過問題,常用演算法都被程式語言封裝好了,直接用就可以了;這是否意味著我們工作中的問題還沒有到達需要演算法的程度?
如果把具體的工作技能比作是武功的“招式”的話,那麼基礎科目應該更像是“內功”。
我認為學演算法(以及其他基礎科目)的意義不是在於在工作中從零實現它,而是基於學到的知識,在解決問題時能夠作出專業的反應和判斷,從而提升工作的整體質量。舉一個簡單例子,每種程式語言都內建了排序函式:
- 如果我們沒有學過資料結構與演算法,那麼給定任何資料,我們可能都塞給這個排序函式去做了。執行順暢、效能不錯,看上去並沒有什麼問題。
- 但如果學過演算法,我們就會知道內建排序函式的時間複雜度是 $O(n \log n)$ ;而如果給定的資料是固定位數的整數(例如學號),那麼我們就可以用效率更高的“基數排序”來做,將時間複雜度降為 $O(nk)$ ,其中 $k$ 為位數。當資料體量很大時,節省出來的執行時間就能創造較大價值(成本降低、體驗變好等)。
在工程領域中,大量問題是難以達到最優解的,許多問題只是被“差不多”地解決了。問題的難易程度一方面取決於問題本身的性質,另一方面也取決於觀測問題的人的知識儲備。人的知識越完備、經驗越多,分析問題就會越深入,問題就能被解決得更優雅。

@ -681,7 +681,7 @@ AVL 樹既是二元搜尋樹,也是平衡二元樹,同時滿足這兩類二
}
```
!!! note
!!! tip
設平衡因子為 $f$ ,則一棵 AVL 樹的任意節點的平衡因子皆滿足 $-1 \le f \le 1$ 。

@ -645,7 +645,7 @@ comments: true
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%85%83%E6%A8%B9%E7%AF%80%E9%BB%9E%E9%A1%9E%E5%88%A5%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E7%AF%80%E9%BB%9E%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E7%AF%80%E9%BB%9E%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E7%AF%80%E9%BB%9E%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%85%83%E6%A8%B9%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E7%AF%80%E9%BB%9E%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%A7%8B%E5%BB%BA%E7%AF%80%E9%BB%9E%E4%B9%8B%E9%96%93%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E6%A8%99%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E8%88%87%E5%88%AA%E9%99%A4%E7%AF%80%E9%BB%9E%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%96%93%E6%8F%92%E5%85%A5%E7%AF%80%E9%BB%9E%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%AA%E9%99%A4%E7%AF%80%E9%BB%9E%20P%0A%20%20%20%20n1.left%20%3D%20n2&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=37&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%85%83%E6%A8%B9%E7%AF%80%E9%BB%9E%E9%A1%9E%E5%88%A5%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E7%AF%80%E9%BB%9E%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E7%AF%80%E9%BB%9E%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E7%AF%80%E9%BB%9E%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%85%83%E6%A8%B9%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E7%AF%80%E9%BB%9E%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%A7%8B%E5%BB%BA%E7%AF%80%E9%BB%9E%E4%B9%8B%E9%96%93%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E6%A8%99%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E8%88%87%E5%88%AA%E9%99%A4%E7%AF%80%E9%BB%9E%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%96%93%E6%8F%92%E5%85%A5%E7%AF%80%E9%BB%9E%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%AA%E9%99%A4%E7%AF%80%E9%BB%9E%20P%0A%20%20%20%20n1.left%20%3D%20n2&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=37&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
!!! note
!!! tip
需要注意的是,插入節點可能會改變二元樹的原有邏輯結構,而刪除節點通常意味著刪除該節點及其所有子樹。因此,在二元樹中,插入與刪除通常是由一套操作配合完成的,以實現有實際意義的操作。

Loading…
Cancel
Save