From 7ce7386babbe3c0da539f8f7c99a5cb711c1a9ad Mon Sep 17 00:00:00 2001 From: Yudong Jin Date: Wed, 1 Feb 2023 22:03:04 +0800 Subject: [PATCH] Add Zig code blocks. --- docs/chapter_array_and_linkedlist/array.md | 36 +++++++ .../linked_list.md | 36 +++++++ docs/chapter_array_and_linkedlist/list.md | 42 ++++++++ .../space_complexity.md | 54 +++++++++++ .../space_time_tradeoff.md | 12 +++ .../time_complexity.md | 96 +++++++++++++++++++ .../chapter_data_structure/data_and_memory.md | 6 ++ .../chapter_graph/basic_operation_of_graph.md | 12 +++ docs/chapter_hashing/hash_map.md | 18 ++++ docs/chapter_heap/heap.md | 36 +++++++ docs/chapter_preface/about_the_book.md | 11 +++ docs/chapter_searching/binary_search.md | 18 ++++ docs/chapter_searching/hashing_search.md | 12 +++ docs/chapter_searching/linear_search.md | 12 +++ docs/chapter_sorting/bubble_sort.md | 12 +++ docs/chapter_sorting/insertion_sort.md | 6 ++ docs/chapter_sorting/merge_sort.md | 6 ++ docs/chapter_sorting/quick_sort.md | 24 +++++ docs/chapter_stack_and_queue/deque.md | 12 +++ docs/chapter_stack_and_queue/queue.md | 18 ++++ docs/chapter_stack_and_queue/stack.md | 18 ++++ docs/chapter_tree/avl_tree.md | 48 ++++++++++ docs/chapter_tree/binary_search_tree.md | 18 ++++ docs/chapter_tree/binary_tree.md | 24 +++++ docs/chapter_tree/binary_tree_traversal.md | 12 +++ 25 files changed, 599 insertions(+) diff --git a/docs/chapter_array_and_linkedlist/array.md b/docs/chapter_array_and_linkedlist/array.md index 050a6b395..01b13840a 100644 --- a/docs/chapter_array_and_linkedlist/array.md +++ b/docs/chapter_array_and_linkedlist/array.md @@ -89,6 +89,12 @@ comments: true let nums = [1, 3, 2, 5, 4] ``` +=== "Zig" + + ```zig title="array.zig" + + ``` + ## 4.1.1. 数组优点 **在数组中访问元素非常高效**。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。 @@ -217,6 +223,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Zig" + + ```zig title="array.zig" + + ``` + ## 4.1.2. 数组缺点 **数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。 @@ -359,6 +371,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Zig" + + ```zig title="array.zig" + + ``` + **数组中插入或删除元素效率低下**。假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点: - **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。 @@ -551,6 +569,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Zig" + + ```zig title="array.zig" + + ``` + ## 4.1.3. 数组常用操作 **数组遍历**。以下介绍两种常用的遍历方法。 @@ -693,6 +717,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Zig" + + ```zig title="array.zig" + + ``` + **数组查找**。通过遍历数组,查找数组内的指定元素,并输出对应索引。 === "Java" @@ -809,6 +839,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Zig" + + ```zig title="array.zig" + + ``` + ## 4.1.4. 数组典型应用 **随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 diff --git a/docs/chapter_array_and_linkedlist/linked_list.md b/docs/chapter_array_and_linkedlist/linked_list.md index 15c94284e..d3cacd6b4 100644 --- a/docs/chapter_array_and_linkedlist/linked_list.md +++ b/docs/chapter_array_and_linkedlist/linked_list.md @@ -126,6 +126,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="" + + ``` + **尾结点指向什么?** 我们一般将链表的最后一个结点称为「尾结点」,其指向的是「空」,在 Java / C++ / Python 中分别记为 `null` / `nullptr` / `None` 。在不引起歧义下,本书都使用 `null` 来表示空。 **链表初始化方法**。建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的首个结点(即头结点)出发,访问其余所有的结点。 @@ -277,6 +283,12 @@ comments: true n3.next = n4 ``` +=== "Zig" + + ```zig title="linked_list.zig" + + ``` + ## 4.2.1. 链表优点 **在链表中,插入与删除结点的操作效率高**。例如,如果想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。在链表中删除某个结点也很方便,只需要改变一个结点指针即可。 @@ -465,6 +477,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="linked_list.zig" + + ``` + ## 4.2.2. 链表缺点 **链表访问结点效率低**。上节提到,数组可以在 $O(1)$ 时间下访问任意元素,但链表无法直接访问任意结点。这是因为计算机需要从头结点出发,一个一个地向后遍历到目标结点。例如,倘若想要访问链表索引为 `index` (即第 `index + 1` 个)的结点,那么需要 `index` 次访问操作。 @@ -591,6 +609,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="linked_list.zig" + + ``` + **链表的内存占用多**。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着同样数据量下,链表比数组需要占用更多内存空间。 ## 4.2.3. 链表常用操作 @@ -736,6 +760,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="linked_list.zig" + + ``` + ## 4.2.4. 常见链表类型 **单向链表**。即上述介绍的普通链表。单向链表的结点有「值」和指向下一结点的「指针(引用)」两项数据。我们将首个结点称为头结点,尾结点指向 `null` 。 @@ -864,6 +894,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="" + + ``` + ![linkedlist_common_types](linked_list.assets/linkedlist_common_types.png)

Fig. 常见链表类型

diff --git a/docs/chapter_array_and_linkedlist/list.md b/docs/chapter_array_and_linkedlist/list.md index 76ea55dfb..9ffa6fce9 100644 --- a/docs/chapter_array_and_linkedlist/list.md +++ b/docs/chapter_array_and_linkedlist/list.md @@ -101,6 +101,12 @@ comments: true var list = [1, 3, 2, 5, 4] ``` +=== "Zig" + + ```zig title="list.zig" + + ``` + **访问与更新元素**。列表的底层数据结构是数组,因此可以在 $O(1)$ 时间内访问与更新元素,效率很高。 === "Java" @@ -189,6 +195,12 @@ comments: true list[1] = 0 // 将索引 1 处的元素更新为 0 ``` +=== "Zig" + + ```zig title="list.zig" + + ``` + **在列表中添加、插入、删除元素**。相对于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但是插入与删除元素的效率仍与数组一样低,时间复杂度为 $O(N)$ 。 === "Java" @@ -357,6 +369,12 @@ comments: true list.remove(at: 3) // 删除索引 3 处的元素 ``` +=== "Zig" + + ```zig title="list.zig" + + ``` + **遍历列表**。与数组一样,列表可以使用索引遍历,也可以使用 `for-each` 直接遍历。 === "Java" @@ -493,6 +511,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="list.zig" + + ``` + **拼接两个列表**。再创建一个新列表 `list1` ,我们可以将其中一个列表拼接到另一个的尾部。 === "Java" @@ -566,6 +590,12 @@ comments: true list.append(contentsOf: list1) // 将列表 list1 拼接到 list 之后 ``` +=== "Zig" + + ```zig title="list.zig" + + ``` + **排序列表**。排序也是常用的方法之一,完成列表排序后,我们就可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法了。 === "Java" @@ -630,6 +660,12 @@ comments: true list.sort() // 排序后,列表元素从小到大排列 ``` +=== "Zig" + + ```zig title="list.zig" + + ``` + ## 4.3.2. 列表简易实现 * 为了帮助加深对列表的理解,我们在此提供一个列表的简易版本的实现。需要关注三个核心点: @@ -1399,3 +1435,9 @@ comments: true } } ``` + +=== "Zig" + + ```zig title="my_list.zig" + + ``` diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index 479839647..5d998c63f 100644 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -202,6 +202,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="" + + ``` + ## 2.3.2. 推算方法 空间复杂度的推算方法和时间复杂度总体类似,只是从统计“计算操作数量”变为统计“使用空间大小”。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。 @@ -301,6 +307,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="" + + ``` + **在递归函数中,需要注意统计栈帧空间**。例如函数 `loop()`,在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行中会同时存在 $n$ 个未返回的 `recur()` ,从而使用 $O(n)$ 的栈帧空间。 === "Java" @@ -452,6 +464,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="" + + ``` + ## 2.3.3. 常见类型 设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列) @@ -622,6 +640,12 @@ $$ } ``` +=== "Zig" + + ```zig title="space_complexity.zig" + + ``` + ### 线性阶 $O(n)$ 线性阶常见于元素数量与 $n$ 成正比的数组、链表、栈、队列等。 @@ -754,6 +778,12 @@ $$ } ``` +=== "Zig" + + ```zig title="space_complexity.zig" + + ``` + 以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间。 === "Java" @@ -844,6 +874,12 @@ $$ } ``` +=== "Zig" + + ```zig title="space_complexity.zig" + + ``` + ![space_complexity_recursive_linear](space_complexity.assets/space_complexity_recursive_linear.png)

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

@@ -961,6 +997,12 @@ $$ } ``` +=== "Zig" + + ```zig title="space_complexity.zig" + + ``` + 在以下递归函数中,同时存在 $n$ 个未返回的 `algorithm()` ,并且每个函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $\frac{n}{2}$ ,因此总体使用 $O(n^2)$ 空间。 === "Java" @@ -1058,6 +1100,12 @@ $$ } ``` +=== "Zig" + + ```zig title="space_complexity.zig" + + ``` + ![space_complexity_recursive_quadratic](space_complexity.assets/space_complexity_recursive_quadratic.png)

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

@@ -1166,6 +1214,12 @@ $$ } ``` +=== "Zig" + + ```zig title="space_complexity.zig" + + ``` + ![space_complexity_exponential](space_complexity.assets/space_complexity_exponential.png)

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

diff --git a/docs/chapter_computational_complexity/space_time_tradeoff.md b/docs/chapter_computational_complexity/space_time_tradeoff.md index af865f2ad..ddf178730 100644 --- a/docs/chapter_computational_complexity/space_time_tradeoff.md +++ b/docs/chapter_computational_complexity/space_time_tradeoff.md @@ -175,6 +175,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="leetcode_two_sum.zig" + + ``` + ### 方法二:辅助哈希表 时间复杂度 $O(N)$ ,空间复杂度 $O(N)$ ,属于「空间换时间」。 @@ -337,3 +343,9 @@ comments: true return [0] } ``` + +=== "Zig" + + ```zig title="leetcode_two_sum.zig" + + ``` diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index 97c9120c8..6d10e400c 100644 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -153,6 +153,12 @@ $$ } ``` +=== "Zig" + + ```zig title="" + + ``` + 但实际上, **统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,毕竟算法需要跑在各式各样的平台之上。其次,我们很难获知每一种操作的运行时间,这为预估过程带来了极大的难度。 ## 2.2.2. 统计时间增长趋势 @@ -357,6 +363,12 @@ $$ } ``` +=== "Zig" + + ```zig title="" + + ``` + ![time_complexity_first_example](time_complexity.assets/time_complexity_first_example.png)

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

@@ -503,6 +515,12 @@ $$ } ``` +=== "Zig" + + ```zig title="" + + ``` + $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得时间复杂度是线性阶。 我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号被称为「大 $O$ 记号 Big-$O$ Notation」,代表函数 $T(n)$ 的「渐近上界 asymptotic upper bound」。 @@ -725,6 +743,12 @@ $$ } ``` +=== "Zig" + + ```zig title="" + + ``` + ### 2) 判断渐近上界 **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将处于主导作用,其它项的影响都可以被忽略。 @@ -887,6 +911,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ### 线性阶 $O(n)$ 线性阶的操作数量相对输入数据大小成线性级别增长。线性阶常出现于单层循环。 @@ -1000,6 +1030,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + 「遍历数组」和「遍历链表」等操作,时间复杂度都为 $O(n)$ ,其中 $n$ 为数组或链表的长度。 !!! tip @@ -1132,6 +1168,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ### 平方阶 $O(n^2)$ 平方阶的操作数量相对输入数据大小成平方级别增长。平方阶常出现于嵌套循环,外层循环和内层循环都为 $O(n)$ ,总体为 $O(n^2)$ 。 @@ -1280,6 +1322,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ![time_complexity_constant_linear_quadratic](time_complexity.assets/time_complexity_constant_linear_quadratic.png)

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

@@ -1500,6 +1548,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ### 指数阶 $O(2^n)$ !!! note @@ -1675,6 +1729,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ![time_complexity_exponential](time_complexity.assets/time_complexity_exponential.png)

Fig. 指数阶的时间复杂度

@@ -1776,6 +1836,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ### 对数阶 $O(\log n)$ 对数阶与指数阶正好相反,后者反映“每轮增加到两倍的情况”,而前者反映“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长得很慢,是理想的时间复杂度。 @@ -1911,6 +1977,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ![time_complexity_logarithmic](time_complexity.assets/time_complexity_logarithmic.png)

Fig. 对数阶的时间复杂度

@@ -2011,6 +2083,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ### 线性对数阶 $O(n \log n)$ 线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 $O(\log n)$ 和 $O(n)$ 。 @@ -2153,6 +2231,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ![time_complexity_logarithmic_linear](time_complexity.assets/time_complexity_logarithmic_linear.png)

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

@@ -2305,6 +2389,12 @@ $$ } ``` +=== "Zig" + + ```zig title="time_complexity.zig" + + ``` + ![time_complexity_factorial](time_complexity.assets/time_complexity_factorial.png)

Fig. 阶乘阶的时间复杂度

@@ -2692,6 +2782,12 @@ $$ } ``` +=== "Zig" + + ```zig title="worst_best_time_complexity.zig" + + ``` + !!! tip 我们在实际应用中很少使用「最佳时间复杂度」,因为往往只有很小概率下才能达到,会带来一定的误导性。反之,「最差时间复杂度」最为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。 diff --git a/docs/chapter_data_structure/data_and_memory.md b/docs/chapter_data_structure/data_and_memory.md index 531744fb3..c10980616 100644 --- a/docs/chapter_data_structure/data_and_memory.md +++ b/docs/chapter_data_structure/data_and_memory.md @@ -128,6 +128,12 @@ comments: true let booleans = Array(repeating: Bool(), count: 5) ``` +=== "Zig" + + ```zig title="" + + ``` + ## 3.1.2. 计算机内存 在计算机中,内存和硬盘是两种主要的存储硬件设备。「硬盘」主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。「内存」用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。 diff --git a/docs/chapter_graph/basic_operation_of_graph.md b/docs/chapter_graph/basic_operation_of_graph.md index 920663b54..8152e21a6 100644 --- a/docs/chapter_graph/basic_operation_of_graph.md +++ b/docs/chapter_graph/basic_operation_of_graph.md @@ -162,6 +162,12 @@ comments: true ``` +=== "Zig" + + ```zig title="graph_adjacency_matrix.zig" + + ``` + ## 9.2.2. 基于邻接表的实现 设图的顶点总数为 $n$ 、边总数为 $m$ ,则有: @@ -309,6 +315,12 @@ comments: true ``` +=== "Zig" + + ```zig title="graph_adjacency_list.zig" + + ``` + ## 9.2.3. 效率对比 设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。 diff --git a/docs/chapter_hashing/hash_map.md b/docs/chapter_hashing/hash_map.md index 313b085a7..673727e66 100644 --- a/docs/chapter_hashing/hash_map.md +++ b/docs/chapter_hashing/hash_map.md @@ -230,6 +230,12 @@ comments: true map.removeValue(forKey: 10583) ``` +=== "Zig" + + ```zig title="hash_map.zig" + + ``` + 遍历哈希表有三种方式,即 **遍历键值对、遍历键、遍历值**。 === "Java" @@ -380,6 +386,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="hash_map.zig" + + ``` + ## 6.1.3. 哈希函数 哈希表中存储元素的数据结构被称为「桶 Bucket」,底层实现可能是数组、链表、二叉树(红黑树),或是它们的组合。 @@ -851,6 +863,12 @@ $$ } ``` +=== "Zig" + + ```zig title="array_hash_map.zig" + + ``` + ## 6.1.4. 哈希冲突 细心的同学可能会发现,**哈希函数 $f(x) = x \% 100$ 会在某些情况下失效**。具体地,当输入的 key 后两位相同时,哈希函数的计算结果也相同,指向同一个 value 。例如,分别查询两个学号 $12836$ 和 $20336$ ,则有 diff --git a/docs/chapter_heap/heap.md b/docs/chapter_heap/heap.md index f1b73e8c1..62d093c29 100644 --- a/docs/chapter_heap/heap.md +++ b/docs/chapter_heap/heap.md @@ -203,6 +203,12 @@ comments: true // Swift 未提供内置 heap 类 ``` +=== "Zig" + + ```zig title="heap.zig" + + ``` + ## 8.1.3. 堆的实现 下文实现的是「大顶堆」,若想转换为「小顶堆」,将所有大小逻辑判断取逆(例如将 $\geq$ 替换为 $\leq$ )即可,有兴趣的同学可自行实现。 @@ -340,6 +346,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + ### 访问堆顶元素 堆顶元素是二叉树的根结点,即列表首元素。 @@ -407,6 +419,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + ### 元素入堆 给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,**因此需要修复从插入结点到根结点这条路径上的各个结点**,该操作被称为「堆化 Heapify」。 @@ -553,6 +571,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + ### 堆顶元素出堆 堆顶元素是二叉树根结点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有结点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤: @@ -758,6 +782,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + ### 输入数据并建堆 * 如果我们想要直接输入一个列表并将其建堆,那么该怎么做呢?最直接地,考虑使用「元素入堆」方法,将列表元素依次入堆。元素入堆的时间复杂度为 $O(n)$ ,而平均长度为 $\frac{n}{2}$ ,因此该方法的总体时间复杂度为 $O(n \log n)$ 。 @@ -843,6 +873,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + 那么,第二种建堆方法的时间复杂度时多少呢?我们来做一下简单推算。 - 完全二叉树中,设结点总数为 $n$ ,则叶结点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此在排除叶结点后,需要堆化结点数量为 $(n - 1)/2$ ,即为 $O(n)$ ; diff --git a/docs/chapter_preface/about_the_book.md b/docs/chapter_preface/about_the_book.md index 3d2973f8c..12e5b7e93 100644 --- a/docs/chapter_preface/about_the_book.md +++ b/docs/chapter_preface/about_the_book.md @@ -205,6 +205,17 @@ comments: true */ ``` +=== "Zig" + + ```zig title="" + // 标题注释,用于标注函数、类、测试样例等 + + // 内容注释,用于详解代码 + + // 多行 + // 注释 + ``` + ## 0.1.5. 本书特点 * ??? abstract "默认折叠,可以跳过" diff --git a/docs/chapter_searching/binary_search.md b/docs/chapter_searching/binary_search.md index 58718fc35..9ac254cec 100644 --- a/docs/chapter_searching/binary_search.md +++ b/docs/chapter_searching/binary_search.md @@ -234,6 +234,12 @@ $$ } ``` +=== "Zig" + + ```zig title="binary_search.zig" + + ``` + ### “左闭右开”实现 当然,我们也可以使用“左闭右开”的表示方法,写出相同功能的二分查找代码。 @@ -422,6 +428,12 @@ $$ } ``` +=== "Zig" + + ```zig title="binary_search.zig" + + ``` + ### 两种表示对比 对比下来,两种表示的代码写法有以下不同点: @@ -517,6 +529,12 @@ $$ let m = i + (j - 1) / 2 ``` +=== "Zig" + + ```zig title="" + + ``` + ## 10.2.2. 复杂度分析 **时间复杂度 $O(\log n)$** :其中 $n$ 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 $\log_2 n$ ,使用 $O(\log n)$ 时间。 diff --git a/docs/chapter_searching/hashing_search.md b/docs/chapter_searching/hashing_search.md index a6b98bd7d..6604deaba 100644 --- a/docs/chapter_searching/hashing_search.md +++ b/docs/chapter_searching/hashing_search.md @@ -116,6 +116,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="hashing_search.zig" + + ``` + 再比如,如果我们想要给定一个目标结点值 `target` ,获取对应的链表结点对象,那么也可以使用哈希查找实现。 ![hash_search_listnode](hashing_search.assets/hash_search_listnode.png) @@ -221,6 +227,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="hashing_search.zig" + + ``` + ## 10.3.2. 复杂度分析 **时间复杂度 $O(1)$** :哈希表的查找操作使用 $O(1)$ 时间。 diff --git a/docs/chapter_searching/linear_search.md b/docs/chapter_searching/linear_search.md index aa6e6b4ec..73da9fe70 100644 --- a/docs/chapter_searching/linear_search.md +++ b/docs/chapter_searching/linear_search.md @@ -150,6 +150,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="linear_search.zig" + + ``` + 再比如,我们想要在给定一个目标结点值 `target` ,返回此结点对象,也可以在链表中进行线性查找。 === "Java" @@ -297,6 +303,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="linear_search.zig" + + ``` + ## 10.1.2. 复杂度分析 **时间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index 8f44d7219..c9063d754 100644 --- a/docs/chapter_sorting/bubble_sort.md +++ b/docs/chapter_sorting/bubble_sort.md @@ -232,6 +232,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="bubble_sort.zig" + + ``` + ## 11.2.2. 算法特性 **时间复杂度 $O(n^2)$** :各轮「冒泡」遍历的数组长度为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。 @@ -458,3 +464,9 @@ comments: true } } ``` + +=== "Zig" + + ```zig title="bubble_sort.zig" + + ``` diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index c44dc9254..ec1fd4d8e 100644 --- a/docs/chapter_sorting/insertion_sort.md +++ b/docs/chapter_sorting/insertion_sort.md @@ -194,6 +194,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="insertion_sort.zig" + + ``` + ## 11.3.2. 算法特性 **时间复杂度 $O(n^2)$** :最差情况下,各轮插入操作循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,使用 $O(n^2)$ 时间。 diff --git a/docs/chapter_sorting/merge_sort.md b/docs/chapter_sorting/merge_sort.md index c0c0fd76d..969bd65ca 100644 --- a/docs/chapter_sorting/merge_sort.md +++ b/docs/chapter_sorting/merge_sort.md @@ -442,6 +442,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="merge_sort.zig" + + ``` + 下面重点解释一下合并方法 `merge()` 的流程: 1. 初始化一个辅助数组 `tmp` 暂存待合并区间 `[left, right]` 内的元素,后续通过覆盖原数组 `nums` 的元素来实现合并; diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index f804c2105..63a876271 100644 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -259,6 +259,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="quick_sort.zig" + + ``` + !!! note "快速排序的分治思想" 哨兵划分的实质是将 **一个长数组的排序问题** 简化为 **两个短数组的排序问题**。 @@ -412,6 +418,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="quick_sort.zig" + + ``` + ## 11.4.2. 算法特性 **平均时间复杂度 $O(n \log n)$** :平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。 @@ -652,6 +664,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="quick_sort.zig" + + ``` + ## 11.4.5. 尾递归优化 **普通快速排序在某些输入下的空间效率变差**。仍然以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 0 ,那么将形成一个高度为 $n - 1$ 的递归树,此时使用的栈帧空间大小劣化至 $O(n)$ 。 @@ -835,3 +853,9 @@ comments: true } } ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + + ``` diff --git a/docs/chapter_stack_and_queue/deque.md b/docs/chapter_stack_and_queue/deque.md index 955593cbc..0a1bdce07 100644 --- a/docs/chapter_stack_and_queue/deque.md +++ b/docs/chapter_stack_and_queue/deque.md @@ -223,6 +223,12 @@ comments: true let isEmpty = deque.isEmpty ``` +=== "Zig" + + ```zig title="deque.zig" + + ``` + ## 5.3.2. 双向队列实现 双向队列需要一种可以在两端添加、两端删除的数据结构。与队列的实现方法类似,双向队列也可以使用双向链表和循环数组来实现。 @@ -413,3 +419,9 @@ comments: true ```swift title="linkedlist_deque.swift" ``` + +=== "Zig" + + ```zig title="linkedlist_deque.zig" + + ``` diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index ee0d052ba..ca3ed24ad 100644 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -256,6 +256,12 @@ comments: true let isEmpty = queue.isEmpty ``` +=== "Zig" + + ```zig title="queue.zig" + + ``` + ## 5.2.2. 队列实现 队列需要一种可以在一端添加,并在另一端删除的数据结构,也可以使用链表或数组来实现。 @@ -719,6 +725,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="linkedlist_queue.zig" + + ``` + ### 基于数组的实现 数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。 @@ -1241,6 +1253,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="array_queue.zig" + + ``` + 以上代码仍存在局限性,即长度不可变。然而,我们可以通过将数组替换为列表(即动态数组)来引入扩容机制,有兴趣的同学可以尝试实现。 ## 5.2.3. 两种实现对比 diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index d19c5760b..c97af130e 100644 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -255,6 +255,12 @@ comments: true let isEmpty = stack.isEmpty ``` +=== "Zig" + + ```zig title="stack.zig" + + ``` + ## 5.1.2. 栈的实现 为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。 @@ -683,6 +689,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="linkedlist_stack.zig" + + ``` + ### 基于数组的实现 使用「数组」实现栈时,考虑将数组的尾部当作栈顶。这样设计下,「入栈」与「出栈」操作就对应在数组尾部「添加元素」与「删除元素」,时间复杂度都为 $O(1)$ 。 @@ -1023,6 +1035,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="array_stack.zig" + + ``` + ## 5.1.3. 两种实现对比 ### 支持操作 diff --git a/docs/chapter_tree/avl_tree.md b/docs/chapter_tree/avl_tree.md index bec639d68..20b632959 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -117,6 +117,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + 「结点高度」是最远叶结点到该结点的距离,即走过的「边」的数量。需要特别注意,**叶结点的高度为 0 ,空结点的高度为 -1**。我们封装两个工具函数,分别用于获取与更新结点的高度。 === "Java" @@ -234,6 +240,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + ### 结点平衡因子 结点的「平衡因子 Balance Factor」是 **结点的左子树高度减去右子树高度**,并定义空结点的平衡因子为 0 。同样地,我们将获取结点平衡因子封装成函数,以便后续使用。 @@ -325,6 +337,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + !!! note 设平衡因子为 $f$ ,则一棵 AVL 树的任意结点的平衡因子皆满足 $-1 \le f \le 1$ 。 @@ -469,6 +487,12 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } ``` +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + ### Case 2 - 左旋 类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。 @@ -595,6 +619,12 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } ``` +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + ### Case 3 - 先左后右 对于下图的失衡结点 3 ,**单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 @@ -827,6 +857,12 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } ``` +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + ## 7.4.3. AVL 树常用操作 ### 插入结点 @@ -1002,6 +1038,12 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } ``` +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + ### 删除结点 「AVL 树」删除结点操作与「二叉搜索树」删除结点操作总体相同。类似地,**在删除结点后,也需要从底至顶地执行旋转操作,使所有失衡结点恢复平衡**。 @@ -1249,6 +1291,12 @@ AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影 } ``` +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + ### 查找结点 「AVL 树」的结点查找操作与「二叉搜索树」一致,在此不再赘述。 diff --git a/docs/chapter_tree/binary_search_tree.md b/docs/chapter_tree/binary_search_tree.md index e9fb1d8c9..222c8c2dc 100644 --- a/docs/chapter_tree/binary_search_tree.md +++ b/docs/chapter_tree/binary_search_tree.md @@ -214,6 +214,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="binary_search_tree.zig" + + ``` + ### 插入结点 给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根结点 < 右子树”的性质,插入操作分为两步: @@ -483,6 +489,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="binary_search_tree.zig" + + ``` + 为了插入结点,需要借助 **辅助结点 `prev`** 保存上一轮循环的结点,这样在遍历到 $\text{null}$ 时,我们也可以获取到其父结点,从而完成结点插入操作。 与查找结点相同,插入结点使用 $O(\log n)$ 时间。 @@ -934,6 +946,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="binary_search_tree.zig" + + ``` + ## 7.3.2. 二叉搜索树的效率 假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为: diff --git a/docs/chapter_tree/binary_tree.md b/docs/chapter_tree/binary_tree.md index 66a80a791..2e876d539 100644 --- a/docs/chapter_tree/binary_tree.md +++ b/docs/chapter_tree/binary_tree.md @@ -121,6 +121,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="" + + ``` + 结点的两个指针分别指向「左子结点 Left Child Node」和「右子结点 Right Child Node」,并且称该结点为两个子结点的「父结点 Parent Node」。给定二叉树某结点,将左子结点以下的树称为该结点的「左子树 Left Subtree」,右子树同理。 除了叶结点外,每个结点都有子结点和子树。例如,若将下图的「结点 2」看作父结点,那么其左子结点和右子结点分别为「结点 4」和「结点 5」,左子树和右子树分别为「结点 4 及其以下结点形成的树」和「结点 5 及其以下结点形成的树」。 @@ -294,6 +300,12 @@ comments: true n2.right = n5 ``` +=== "Zig" + + ```zig title="binary_tree.zig" + + ``` + **插入与删除结点**。与链表类似,插入与删除结点都可以通过修改指针实现。 ![binary_tree_add_remove](binary_tree.assets/binary_tree_add_remove.png) @@ -400,6 +412,12 @@ comments: true n1.left = n2 ``` +=== "Zig" + + ```zig title="binary_tree.zig" + + ``` + !!! note 插入结点会改变二叉树的原有逻辑结构,删除结点往往意味着删除了该结点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。 @@ -545,6 +563,12 @@ comments: true let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] ``` +=== "Zig" + + ```zig title="" + + ``` + ![array_representation_with_empty](binary_tree.assets/array_representation_with_empty.png) 回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。 diff --git a/docs/chapter_tree/binary_tree_traversal.md b/docs/chapter_tree/binary_tree_traversal.md index aa727c431..197cb0bb5 100644 --- a/docs/chapter_tree/binary_tree_traversal.md +++ b/docs/chapter_tree/binary_tree_traversal.md @@ -208,6 +208,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="binary_tree_bfs.zig" + + ``` + ## 7.2.2. 前序、中序、后序遍历 相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」,其体现着一种“先走到尽头,再回头继续”的回溯遍历方式。 @@ -503,6 +509,12 @@ comments: true } ``` +=== "Zig" + + ```zig title="binary_tree_dfs.zig" + + ``` + !!! note 使用循环一样可以实现前、中、后序遍历,但代码相对繁琐,有兴趣的同学可以自行实现。