From 2a85d796e690821abddbcae8bcf5eee120a61f27 Mon Sep 17 00:00:00 2001 From: krahets Date: Fri, 2 Jun 2023 02:38:24 +0800 Subject: [PATCH] build --- chapter_array_and_linkedlist/array.md | 109 ++++++++ chapter_array_and_linkedlist/linked_list.md | 71 +++++ chapter_array_and_linkedlist/list.md | 129 +++++++++ .../backtracking_algorithm.md | 40 +++ chapter_backtracking/n_queens_problem.md | 8 + chapter_backtracking/permutations_problem.md | 16 ++ .../space_complexity.md | 115 ++++++++ .../time_complexity.md | 212 +++++++++++++++ chapter_data_structure/basic_data_types.md | 6 + chapter_graph/graph_operations.md | 169 ++++++++++++ chapter_graph/graph_traversal.md | 65 +++++ chapter_hashing/hash_map.md | 102 ++++++++ chapter_heap/build_heap.md | 14 + chapter_heap/heap.md | 68 +++++ chapter_preface/suggestions.md | 11 + chapter_searching/binary_search.md | 52 ++++ chapter_searching/binary_search_edge.md | 12 + .../replace_linear_by_hashing.md | 32 +++ chapter_sorting/bubble_sort.md | 43 +++ chapter_sorting/bucket_sort.md | 30 +++ chapter_sorting/counting_sort.md | 63 +++++ chapter_sorting/heap_sort.md | 8 + chapter_sorting/insertion_sort.md | 18 ++ chapter_sorting/merge_sort.md | 42 +++ chapter_sorting/quick_sort.md | 93 +++++++ chapter_sorting/radix_sort.md | 51 ++++ chapter_sorting/selection_sort.md | 6 + chapter_stack_and_queue/deque.md | 245 ++++++++++++++++++ chapter_stack_and_queue/queue.md | 147 +++++++++++ chapter_stack_and_queue/stack.md | 112 ++++++++ chapter_tree/array_representation_of_tree.md | 6 + chapter_tree/avl_tree.md | 172 ++++++++++++ chapter_tree/binary_search_tree.md | 18 ++ chapter_tree/binary_tree.md | 18 ++ chapter_tree/binary_tree_traversal.md | 51 ++++ 35 files changed, 2354 insertions(+) diff --git a/chapter_array_and_linkedlist/array.md b/chapter_array_and_linkedlist/array.md index 7a40d59cb..b8b9aeade 100755 --- a/chapter_array_and_linkedlist/array.md +++ b/chapter_array_and_linkedlist/array.md @@ -98,6 +98,12 @@ comments: true var nums = [_]i32{ 1, 3, 2, 5, 4 }; ``` +=== "Dart" + + ```dart title="array.dart" + + ``` + ## 4.1.1.   数组优点 **在数组中访问元素非常高效**。由于数组元素被存储在连续的内存空间中,因此计算数组元素的内存地址非常容易。给定数组首个元素的地址和某个元素的索引,我们可以使用以下公式计算得到该元素的内存地址,从而直接访问此元素。 @@ -249,6 +255,19 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Dart" + + ```dart title="array.dart" + /* 随机返回一个 数组元素 */ + int randomAccess(List nums) { + // 在区间[0,size) 中随机抽取一个数字 + int randomIndex = Random().nextInt(nums.length); + // 获取并返回随机元素 + int randomNum = nums[randomIndex]; + return randomNum; + } + ``` + ## 4.1.2.   数组缺点 **数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。 @@ -420,6 +439,23 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Dart" + + ```dart title="array.dart" + /* 扩展数组长度 */ + List extend(List nums, int enlarge) { + // 初始化一个扩展长度后的数组,元素初始值为0 + List res = List.filled(nums.length + enlarge, 0); + + // 将原数组中的所有元素复制到新数组 + for (var i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // 返回扩展后的新数组 + return res; + } + ``` + **数组中插入或删除元素效率低下**。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。 ![数组插入元素](array.assets/array_insert_element.png) @@ -550,6 +586,35 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Zig" + + ```zig title="array.zig" + // 在数组的索引 index 处插入元素 num + fn insert(nums: []i32, num: i32, index: usize) void { + // 把索引 index 以及之后的所有元素向后移动一位 + var i = nums.len - 1; + while (i > index) : (i -= 1) { + nums[i] = nums[i - 1]; + } + // 将 num 赋给 index 处元素 + nums[index] = num; + } + ``` + +=== "Dart" + + ```dart title="array.dart" + /* 在数组的索引 index 处插入元素 num */ + void insert(List nums, int num, int index) { + // 把索引index以及之后的所有元素向后移动一位 + for (var i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // 将 num 赋给 index 处元素 + nums[index] = num; + } + ``` + 删除元素也类似,如果我们想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。 ![数组删除元素](array.assets/array_remove_element.png) @@ -677,6 +742,17 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Dart" + + ```dart title="array.dart" + /* 删除索引 index 处元素 */ + void remove(List nums, int index) { + for (var i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } + ``` + 总结来看,数组的插入与删除操作有以下缺点: - **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(n)$ ,其中 $n$ 为数组长度。 @@ -852,6 +928,27 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Dart" + + ```dart title="array.dart" + /* 遍历数组元素 */ + void traverse(List nums) { + var count = 0; + // 通过索引遍历数组 + for (var i = 0; i < nums.length; i++) { + count++; + } + // 直接遍历数组 + for (var num in nums) { + count++; + } + // 通过forEach方法遍历数组 + nums.forEach((element) { + count++; + }); + } + ``` + **数组查找**。通过遍历数组,查找数组内的指定元素,并输出对应索引。 === "Java" @@ -985,6 +1082,18 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` +=== "Dart" + + ```dart title="array.dart" + /* 在数组中查找指定元素 */ + int find(List nums, int target) { + for (var i = 0; i < nums.length; i++) { + if (nums[i] == target) return i; + } + return -1; + } + ``` + ## 4.1.4.   数组典型应用 **随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index def35cef0..3f9d51768 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -159,6 +159,12 @@ comments: true } ``` +=== "Dart" + + ```dart title="" + + ``` + !!! question "尾节点指向什么?" 我们将链表的最后一个节点称为「尾节点」,其指向的是“空”,在 Java, C++, Python 中分别记为 $\text{null}$ , $\text{nullptr}$ , $\text{None}$ 。在不引起歧义的前提下,本书都使用 $\text{null}$ 来表示空。 @@ -339,6 +345,12 @@ comments: true n3.next = &n4; ``` +=== "Dart" + + ```dart title="linked_list.dart" + + ``` + ## 4.2.1.   链表优点 **链表中插入与删除节点的操作效率高**。例如,如果我们想在链表中间的两个节点 `A` , `B` 之间插入一个新节点 `P` ,我们只需要改变两个节点指针即可,时间复杂度为 $O(1)$ ;相比之下,数组的插入操作效率要低得多。 @@ -456,6 +468,17 @@ comments: true } ``` +=== "Dart" + + ```dart title="linked_list.dart" + /* 在链表的节点 n0 之后插入节点 P */ + void insert(ListNode n0, ListNode P) { + ListNode? n1 = n0.next; + P.next = n1; + n0.next = P; + } + ``` + 在链表中删除节点也非常方便,只需改变一个节点的指针即可。如下图所示,尽管在删除操作完成后,节点 `P` 仍然指向 `n1` ,但实际上 `P` 已经不再属于此链表,因为遍历此链表时无法访问到 `P` 。 ![链表删除节点](linked_list.assets/linkedlist_remove_node.png) @@ -608,6 +631,18 @@ comments: true } ``` +=== "Dart" + + ```dart title="linked_list.dart" + /* 删除链表的节点 n0 之后的首个节点 */ + void remove(ListNode n0) { + if (n0.next == null) return; + ListNode P = n0.next!; + ListNode? n1 = P.next; + n0.next = n1; + } + ``` + ## 4.2.2.   链表缺点 **链表访问节点效率较低**。如上节所述,数组可以在 $O(1)$ 时间下访问任意元素。然而,链表无法直接访问任意节点,这是因为系统需要从头节点出发,逐个向后遍历直至找到目标节点。例如,若要访问链表索引为 `index`(即第 `index + 1` 个)的节点,则需要向后遍历 `index` 轮。 @@ -755,6 +790,19 @@ comments: true } ``` +=== "Dart" + + ```dart title="linked_list.dart" + /* 访问链表中索引为 index 的节点 */ + ListNode? access(ListNode? head, int index) { + for (var i = 0; i < index; i++) { + if (head == null) return null; + head = head.next; + } + return head; + } + ``` + **链表的内存占用较大**。链表以节点为单位,每个节点除了保存值之外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多的内存空间。 ## 4.2.3.   链表常用操作 @@ -924,6 +972,23 @@ comments: true } ``` +=== "Dart" + + ```dart title="linked_list.dart" + /* 在链表中查找值为 target 的首个节点 */ + int find(ListNode? head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) { + return index; + } + head = head.next; + index++; + } + return -1; + } + ``` + ## 4.2.4.   常见链表类型 **单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的指针(引用)两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向 $\text{null}$ 。 @@ -1091,6 +1156,12 @@ comments: true } ``` +=== "Dart" + + ```dart title="" + + ``` + ![常见链表种类](linked_list.assets/linkedlist_common_types.png)

Fig. 常见链表种类

diff --git a/chapter_array_and_linkedlist/list.md b/chapter_array_and_linkedlist/list.md index c7db97801..ff7d099c8 100755 --- a/chapter_array_and_linkedlist/list.md +++ b/chapter_array_and_linkedlist/list.md @@ -110,6 +110,12 @@ comments: true try list.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 }); ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **访问与更新元素**。由于列表的底层数据结构是数组,因此可以在 $O(1)$ 时间内访问和更新元素,效率很高。 === "Java" @@ -208,6 +214,12 @@ comments: true list.items[1] = 0; // 将索引 1 处的元素更新为 0 ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **在列表中添加、插入、删除元素**。相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但插入和删除元素的效率仍与数组相同,时间复杂度为 $O(N)$ 。 === "Java" @@ -396,6 +408,12 @@ comments: true _ = list.orderedRemove(3); // 删除索引 3 处的元素 ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **遍历列表**。与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。 === "Java" @@ -549,6 +567,12 @@ comments: true } ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **拼接两个列表**。给定一个新列表 `list1` ,我们可以将该列表拼接到原列表的尾部。 === "Java" @@ -632,6 +656,12 @@ comments: true try list.insertSlice(list.items.len, list1.items); // 将列表 list1 拼接到 list 之后 ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **排序列表**。排序也是常用的方法之一。完成列表排序后,我们便可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法。 === "Java" @@ -703,6 +733,12 @@ comments: true std.sort.sort(i32, list.items, {}, comptime std.sort.asc(i32)); ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + ## 4.3.2.   列表实现 * 为了帮助加深对列表的理解,我们在此提供一个简易版列表实现。需要关注三个核心点: @@ -1756,3 +1792,96 @@ comments: true }; } ``` + +=== "Dart" + + ```dart title="my_list.dart" + /* 列表类简易实现 */ + class MyList { + late List _nums; // 数组(存储列表元素) + int _capacity = 10; // 列表容量 + int _size = 0; // 列表长度(即当前元素数量) + int _extendRatio = 2; // 每次列表扩容的倍数 + + /* 构造方法 */ + MyList() { + _nums = List.filled(_capacity, 0); + } + + /* 获取列表长度(即当前元素数量)*/ + int size() => _size; + + /* 获取列表容量 */ + int capacity() => _capacity; + + /* 访问元素 */ + int get(int index) { + if (index >= _size) throw RangeError('索引越界'); + return _nums[index]; + } + + /* 更新元素 */ + void set(int index, int num) { + if (index >= _size) throw RangeError('索引越界'); + _nums[index] = num; + } + + /* 尾部添加元素 */ + void add(int num) { + // 元素数量超出容量时,触发扩容机制 + if (_size == _capacity) extendCapacity(); + _nums[_size] = num; + // 更新元素数量 + _size++; + } + + /* 中间插入元素 */ + void insert(int index, int num) { + if (index >= _size) throw RangeError('索引越界'); + // 元素数量超出容量时,触发扩容机制 + if (_size == _capacity) extendCapacity(); + // 将索引 index 以及之后的元素都向后移动一位 + for (var j = _size - 1; j >= index; j--) { + _nums[j + 1] = _nums[j]; + } + _nums[index] = num; + // 更新元素数量 + _size++; + } + + /* 删除元素 */ + int remove(int index) { + if (index >= _size) throw RangeError('索引越界'); + int num = _nums[index]; + // 将索引 index 之后的元素都向前移动一位 + for (var j = index; j < _size - 1; j++) { + _nums[j] = _nums[j + 1]; + } + // 更新元素数量 + _size--; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + void extendCapacity() { + // 新建一个长度为 _capacity * _extendRatio 的数组 + final _newNums = List.filled(_capacity * _extendRatio, 0); + // 将原数组拷贝到新数组 + List.copyRange(_newNums, 0, _nums); + // 更新 _nums 的引用 + _nums = _newNums; + // 更新列表容量 + _capacity = _nums.length; + } + + /* 将列表转换为数组 */ + List toArray() { + List nums = []; + for (var i = 0; i < _size; i++) { + nums.add(get(i)); + } + return nums; + } + } + ``` diff --git a/chapter_backtracking/backtracking_algorithm.md b/chapter_backtracking/backtracking_algorithm.md index 55996e985..92604a44a 100644 --- a/chapter_backtracking/backtracking_algorithm.md +++ b/chapter_backtracking/backtracking_algorithm.md @@ -159,6 +159,12 @@ comments: true [class]{}-[func]{preOrder} ``` +=== "Dart" + + ```dart title="preorder_traversal_i_compact.dart" + [class]{}-[func]{preOrder} + ``` + ![在前序遍历中搜索节点](backtracking_algorithm.assets/preorder_find_nodes.png)

Fig. 在前序遍历中搜索节点

@@ -358,6 +364,12 @@ comments: true [class]{}-[func]{preOrder} ``` +=== "Dart" + + ```dart title="preorder_traversal_ii_compact.dart" + [class]{}-[func]{preOrder} + ``` + 在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。换句话说,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为相反的。 === "<1>" @@ -592,6 +604,12 @@ comments: true [class]{}-[func]{preOrder} ``` +=== "Dart" + + ```dart title="preorder_traversal_iii_compact.dart" + [class]{}-[func]{preOrder} + ``` + 剪枝是一个非常形象的名词。在搜索过程中,**我们利用约束条件“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而提升搜索效率。 ![根据约束条件剪枝](backtracking_algorithm.assets/preorder_find_constrained_paths.png) @@ -848,6 +866,12 @@ comments: true ``` +=== "Dart" + + ```dart title="" + + ``` + 下面,我们尝试基于此框架来解决例题三。在例题三中,状态 `state` 是节点遍历路径,选择 `choices` 是当前节点的左子节点和右子节点,结果 `res` 是路径列表,实现代码如下所示。 === "Java" @@ -1289,6 +1313,22 @@ comments: true [class]{}-[func]{backtrack} ``` +=== "Dart" + + ```dart title="preorder_traversal_iii_template.dart" + [class]{}-[func]{isSolution} + + [class]{}-[func]{recordSolution} + + [class]{}-[func]{isValid} + + [class]{}-[func]{makeChoice} + + [class]{}-[func]{undoChoice} + + [class]{}-[func]{backtrack} + ``` + 相较于基于前序遍历的实现代码,基于回溯算法框架的实现代码虽然显得啰嗦,但通用性更好。实际上,**所有回溯问题都可以在该框架下解决**。我们需要根据具体问题来定义 `state` 和 `choices` ,并实现框架中的各个方法。 ## 12.1.5.   典型例题 diff --git a/chapter_backtracking/n_queens_problem.md b/chapter_backtracking/n_queens_problem.md index 3a8750857..cd79605e1 100644 --- a/chapter_backtracking/n_queens_problem.md +++ b/chapter_backtracking/n_queens_problem.md @@ -488,6 +488,14 @@ comments: true [class]{}-[func]{nQueens} ``` +=== "Dart" + + ```dart title="n_queens.dart" + [class]{}-[func]{backtrack} + + [class]{}-[func]{nQueens} + ``` + ## 12.3.1.   复杂度分析 逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n, n-1, \cdots, 2, 1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。 diff --git a/chapter_backtracking/permutations_problem.md b/chapter_backtracking/permutations_problem.md index 3898e5875..5d9f8339e 100644 --- a/chapter_backtracking/permutations_problem.md +++ b/chapter_backtracking/permutations_problem.md @@ -336,6 +336,14 @@ comments: true [class]{}-[func]{permutationsI} ``` +=== "Dart" + + ```dart title="permutations_i.dart" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutationsI} + ``` + 需要重点关注的是,我们引入了一个布尔型数组 `selected` ,它的长度与输入数组长度相等,其中 `selected[i]` 表示 `choices[i]` 是否已被选择。我们利用 `selected` 避免某个元素被重复选择,从而实现剪枝。 如下图所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1, 3 的分支。**从本质上理解,此剪枝操作可将搜索空间大小从 $O(n^n)$ 降低至 $O(n!)$** 。 @@ -683,6 +691,14 @@ comments: true [class]{}-[func]{permutationsII} ``` +=== "Dart" + + ```dart title="permutations_ii.dart" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutationsII} + ``` + 注意,虽然 `selected` 和 `duplicated` 都起到剪枝的作用,但他们剪掉的是不同的分支: - **剪枝条件一**:整个搜索过程中只有一个 `selected` 。它记录的是当前状态中包含哪些元素,作用是避免某个元素在 `state` 中重复出现。 diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md index 86336f6fe..b35bfe5ef 100755 --- a/chapter_computational_complexity/space_complexity.md +++ b/chapter_computational_complexity/space_complexity.md @@ -264,6 +264,12 @@ comments: true ``` +=== "Dart" + + ```dart title="" + + ``` + ## 2.3.2.   推算方法 空间复杂度的推算方法与时间复杂度大致相同,只是将统计对象从“计算操作数量”转为“使用空间大小”。与时间复杂度不同的是,**我们通常只关注「最差空间复杂度」**,这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。 @@ -386,6 +392,12 @@ comments: true ``` +=== "Dart" + + ```dart title="" + + ``` + **在递归函数中,需要注意统计栈帧空间**。例如,函数 `loop()` 在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行过程中会同时存在 $n$ 个未返回的 `recur()` ,从而占用 $O(n)$ 的栈帧空间。 === "Java" @@ -585,6 +597,12 @@ comments: true ``` +=== "Dart" + + ```dart title="" + + ``` + ## 2.3.3.   常见类型 设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列) @@ -826,6 +844,27 @@ $$ } ``` +=== "Dart" + + ```dart title="space_complexity.dart" + /* 常数阶 */ + void constant(int n) { + // 常量、变量、对象占用 O(1) 空间 + final int a = 0; + int b = 0; + + List nums = List.filled(10000, 0); + // 循环中的变量占用 O(1) 空间 + for (var i = 0; i < n; i++) { + int c = 0; + } + // 循环中的函数占用 O(1) 空间 + for (var i = 0; i < n; i++) { + function(); + } + } + ``` + ### 线性阶 $O(n)$ 线性阶常见于元素数量与 $n$ 成正比的数组、链表、栈、队列等。 @@ -1051,6 +1090,26 @@ $$ } ``` +=== "Dart" + + ```dart title="space_complexity.dart" + /* 线性阶 */ + void linear(int n) { + // 长度为 n 的数组占用 O(n) 空间 + List nums = List.filled(n, 0); + // 长度为 n 的列表占用 O(n) 空间 + List nodes = []; + for (var i = 0; i < n; i++) { + nodes.add(ListNode(i)); + } + // 长度为 n 的哈希表占用 O(n) 空间 + Map map = HashMap(); + for (var i = 0; i < n; i++) { + map.putIfAbsent(i, () => i.toString()); + } + } + ``` + 以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间。 === "Java" @@ -1170,6 +1229,17 @@ $$ } ``` +=== "Dart" + + ```dart title="space_complexity.dart" + /* 线性阶(递归实现) */ + void linearRecur(int n) { + print('递归 n = $n'); + if (n == 1) return; + linearRecur(n - 1); + } + ``` + ![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png)

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

@@ -1351,6 +1421,26 @@ $$ } ``` +=== "Dart" + + ```dart title="space_complexity.dart" + /* 平方阶 */ + void quadratic(int n) { + // 矩阵占用 O(n^2) 空间 + List> numMatrix = List.generate(n, (_) => List.filled(n, 0)); + // 二维列表占用 O(n^2) 空间 + List> numList = []; + + for (var i = 0; i < n; i++) { + List tmp = []; + for (int j = 0; j < n; j++) { + tmp.add(0); + } + numList.add(tmp); + } + } + ``` + 在以下递归函数中,同时存在 $n$ 个未返回的 `algorithm()` ,并且每个函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $\frac{n}{2}$ ,因此总体占用 $O(n^2)$ 空间。 === "Java" @@ -1484,6 +1574,18 @@ $$ } ``` +=== "Dart" + + ```dart title="space_complexity.dart" + /* 平方阶(递归实现) */ + int quadraticRecur(int n) { + if (n <= 0) return 0; + List nums = List.filled(n, 0); + print('递归 n = $n 中的 nums 长度 = ${nums.length}'); + return quadraticRecur(n - 1); + } + ``` + ![递归函数产生的平方阶空间复杂度](space_complexity.assets/space_complexity_recursive_quadratic.png)

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

@@ -1630,6 +1732,19 @@ $$ } ``` +=== "Dart" + + ```dart title="space_complexity.dart" + /* 指数阶(建立满二叉树) */ + TreeNode? buildTree(int n) { + if (n == 0) return null; + TreeNode root = TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + ![满二叉树产生的指数阶空间复杂度](space_complexity.assets/space_complexity_exponential.png)

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

diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md index d988d0553..033c108bd 100755 --- a/chapter_computational_complexity/time_complexity.md +++ b/chapter_computational_complexity/time_complexity.md @@ -159,6 +159,12 @@ $$ ``` +=== "Dart" + + ```dart title="" + + ``` + 然而实际上,**统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。 ## 2.2.2.   统计时间增长趋势 @@ -369,6 +375,12 @@ $$ ``` +=== "Dart" + + ```dart title="" + + ``` + ![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png)

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

@@ -521,6 +533,12 @@ $$ ``` +=== "Dart" + + ```dart title="" + + ``` + $T(n)$ 是一次函数,说明时间增长趋势是线性的,因此可以得出时间复杂度是线性阶。 我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 Big-$O$ Notation」,表示函数 $T(n)$ 的「渐近上界 Asymptotic Upper Bound」。 @@ -747,6 +765,12 @@ $$ ``` +=== "Dart" + + ```dart title="" + + ``` + ### 2) 判断渐近上界 **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。 @@ -924,6 +948,20 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 常数阶 */ + int constant(int n) { + int count = 0; + int size = 100000; + for (var i = 0; i < size; i++) { + count++; + } + return count; + } + ``` + ### 线性阶 $O(n)$ 线性阶的操作数量相对于输入数据大小以线性级别增长。线性阶通常出现在单层循环中。 @@ -1050,6 +1088,19 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 线性阶 */ + int linear(int n) { + int count = 0; + for (var i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + 遍历数组和遍历链表等操作的时间复杂度均为 $O(n)$ ,其中 $n$ 为数组或链表的长度。 !!! question "如何确定输入数据大小 $n$ ?" @@ -1194,6 +1245,20 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 线性阶(遍历数组) */ + int arrayTraversal(List nums) { + int count = 0; + // 循环次数与数组长度成正比 + for (var num in nums) { + count++; + } + return count; + } + ``` + ### 平方阶 $O(n^2)$ 平方阶的操作数量相对于输入数据大小以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ 。 @@ -1357,6 +1422,22 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 平方阶 */ + int quadratic(int n) { + int count = 0; + // 循环次数与数组长度成平方关系 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + ![常数阶、线性阶、平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png)

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

@@ -1595,6 +1676,29 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 平方阶(冒泡排序) */ + int bubbleSort(List nums) { + int count = 0; // 计数器 + // 外循环:未排序区间为 [0, i] + for (var i = nums.length - 1; i > 0; i--) { + // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 + for (var j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } + ``` + ### 指数阶 $O(2^n)$ !!! note @@ -1788,6 +1892,24 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 指数阶(循环实现) */ + int exponential(int n) { + int count = 0, base = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (var i = 0; i < n; i++) { + for (var j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + ![指数阶的时间复杂度](time_complexity.assets/time_complexity_exponential.png)

Fig. 指数阶的时间复杂度

@@ -1901,6 +2023,16 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 指数阶(递归实现) */ + int expRecur(int n) { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + ### 对数阶 $O(\log n)$ 与指数阶相反,对数阶反映了“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长缓慢,是理想的时间复杂度。 @@ -2050,6 +2182,20 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 对数阶(循环实现) */ + int logarithmic(num n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + ![对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic.png)

Fig. 对数阶的时间复杂度

@@ -2163,6 +2309,16 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 对数阶(递归实现) */ + int logRecur(num n) { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + ### 线性对数阶 $O(n \log n)$ 线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 $O(\log n)$ 和 $O(n)$ 。 @@ -2320,6 +2476,20 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 线性对数阶 */ + int linearLogRecur(num n) { + if (n <= 1) return 1; + int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (var i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + ![线性对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic_linear.png)

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

@@ -2490,6 +2660,21 @@ $$ } ``` +=== "Dart" + + ```dart title="time_complexity.dart" + /* 阶乘阶(递归实现) */ + int factorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // 从 1 个分裂出 n 个 + for (var i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + ![阶乘阶的时间复杂度](time_complexity.assets/time_complexity_factorial.png)

Fig. 阶乘阶的时间复杂度

@@ -2800,6 +2985,33 @@ $$ } ``` +=== "Dart" + + ```dart title="worst_best_time_complexity.dart" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + List randomNumbers(int n) { + final nums = List.filled(n, 0); + // 生成数组 nums = { 1, 2, 3, ..., n } + for (var i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 随机打乱数组元素 + nums.shuffle(); + + return nums; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + int findOne(List nums) { + for (var i = 0; i < nums.length; i++) { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if (nums[i] == 1) return i; + } + return -1; + } + ``` + !!! tip 实际应用中我们很少使用「最佳时间复杂度」,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。相反,「最差时间复杂度」更为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。 diff --git a/chapter_data_structure/basic_data_types.md b/chapter_data_structure/basic_data_types.md index e4ea65b73..bb40b70d4 100644 --- a/chapter_data_structure/basic_data_types.md +++ b/chapter_data_structure/basic_data_types.md @@ -133,3 +133,9 @@ comments: true ```zig title="" ``` + +=== "Dart" + + ```dart title="" + + ``` diff --git a/chapter_graph/graph_operations.md b/chapter_graph/graph_operations.md index b0bf3d8c2..c5f3dc756 100644 --- a/chapter_graph/graph_operations.md +++ b/chapter_graph/graph_operations.md @@ -762,6 +762,95 @@ comments: true ``` +=== "Dart" + + ```dart title="graph_adjacency_matrix.dart" + /* 基于邻接矩阵实现的无向图类 */ + class GraphAdjMat { + List vertices = []; // 顶点元素,元素代表“顶点值”,索引代表“顶点索引” + List> adjMat = []; //邻接矩阵,行列索引对应“顶点索引” + + /* 构造方法 */ + GraphAdjMat(List vertices, List> edges) { + this.vertices = []; + this.adjMat = []; + // 添加顶点 + for (int val in vertices) { + addVertex(val); + } + // 添加边 + // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引 + for (List e in edges) { + addEdge(e[0], e[1]); + } + } + + /* 获取顶点数量 */ + int size() { + return vertices.length; + } + + /* 添加顶点 */ + void addVertex(int val) { + int n = size(); + // 向顶点列表中添加新顶点的值 + vertices.add(val); + // 在邻接矩阵中添加一行 + List newRow = List.filled(n, 0, growable: true); + adjMat.add(newRow); + // 在邻接矩阵中添加一列 + for (List row in adjMat) { + row.add(0); + } + } + + /* 删除顶点 */ + void removeVertex(int index) { + if (index >= size()) { + throw IndexError; + } + // 在顶点列表中移除索引 index 的顶点 + vertices.removeAt(index); + // 在邻接矩阵中删除索引 index 的行 + adjMat.removeAt(index); + // 在邻接矩阵中删除索引 index 的列 + for (List row in adjMat) { + row.removeAt(index); + } + } + + /* 添加边 */ + // 参数 i, j 对应 vertices 元素索引 + void addEdge(int i, int j) { + // 索引越界与相等处理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw IndexError; + } + // 在无向图中,邻接矩阵沿主对角线对称,即满足 (i, j) == (j, i) + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* 删除边 */ + // 参数 i, j 对应 vertices 元素索引 + void removeEdge(int i, int j) { + // 索引越界与相等处理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw IndexError; + } + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* 打印邻接矩阵 */ + void printAdjMat() { + print("顶点列表 = $vertices"); + print("邻接矩阵 = "); + printMatrix(adjMat); + } + } + ``` + ## 9.2.2.   基于邻接表的实现 设无向图的顶点总数为 $n$ 、边总数为 $m$ ,则有: @@ -1451,6 +1540,86 @@ comments: true [class]{GraphAdjList}-[func]{} ``` +=== "Dart" + + ```dart title="graph_adjacency_list.dart" + /* 基于邻接表实现的无向图类 */ + class GraphAdjList { + // 邻接表,key: 顶点,value:该顶点的所有邻接顶点 + Map> adjList = {}; + + /* 构造方法 */ + GraphAdjList(List> edges) { + for (List edge in edges) { + addVertex(edge[0]); + addVertex(edge[1]); + addEdge(edge[0], edge[1]); + } + } + + /* 获取顶点数量 */ + int size() { + return adjList.length; + } + + /* 添加边 */ + void addEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || + !adjList.containsKey(vet2) || + vet1 == vet2) { + throw ArgumentError; + } + // 添加边 vet1 - vet2 + adjList[vet1]!.add(vet2); + adjList[vet2]!.add(vet1); + } + + /* 删除边 */ + void removeEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || + !adjList.containsKey(vet2) || + vet1 == vet2) { + throw ArgumentError; + } + // 删除边 vet1 - vet2 + adjList[vet1]!.remove(vet2); + adjList[vet2]!.remove(vet1); + } + + /* 添加顶点 */ + void addVertex(Vertex vet) { + if (adjList.containsKey(vet)) return; + // 在邻接表中添加一个新链表 + adjList[vet] = []; + } + + /* 删除顶点 */ + void removeVertex(Vertex vet) { + if (!adjList.containsKey(vet)) { + throw ArgumentError; + } + // 在邻接表中删除顶点 vet 对应的链表 + adjList.remove(vet); + // 遍历其他顶点的链表,删除所有包含 vet 的边 + adjList.forEach((key, value) { + value.remove(vet); + }); + } + + /* 打印邻接表 */ + void printAdjList() { + print("邻接表 ="); + adjList.forEach((key, value) { + List tmp = []; + for (Vertex vertex in value) { + tmp.add(vertex.val); + } + print("${key.val}: $tmp,"); + }); + } + } + ``` + ## 9.2.3.   效率对比 设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。 diff --git a/chapter_graph/graph_traversal.md b/chapter_graph/graph_traversal.md index 4b0885ce5..7e6c0e2c1 100644 --- a/chapter_graph/graph_traversal.md +++ b/chapter_graph/graph_traversal.md @@ -287,6 +287,38 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 [class]{}-[func]{graphBFS} ``` +=== "Dart" + + ```dart title="graph_bfs.dart" + /* 广度优先遍历 BFS */ + List graphBFS(GraphAdjList graph, Vertex startVet) { + // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点 + // 顶点遍历序列 + List res = []; + // 哈希表,用于记录已被访问过的顶点 + Set visited = {}; + visited.add(startVet); + // 队列用于实现 BFS + Queue que = Queue(); + que.add(startVet); + // 以顶点 vet 为起点,循环直至访问完所有顶点 + while (que.isNotEmpty) { + Vertex vet = que.removeFirst(); // 队首顶点出队 + res.add(vet); // 记录访问顶点 + // 遍历该顶点的所有邻接顶点 + for (Vertex adjVet in graph.adjList[vet]!) { + if (visited.contains(adjVet)) { + continue; // 跳过已被访问过的顶点 + } + que.add(adjVet); // 只入队未访问的顶点 + visited.add(adjVet); // 标记该顶点已被访问 + } + } + // 返回顶点遍历序列 + return res; + } + ``` + 代码相对抽象,建议对照以下动画图示来加深理解。 === "<1>" @@ -593,6 +625,39 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 [class]{}-[func]{graphDFS} ``` +=== "Dart" + + ```dart title="graph_dfs.dart" + /* 深度优先遍历 DFS 辅助函数 */ + void dfs( + GraphAdjList graph, + Set visited, + List res, + Vertex vet, + ) { + res.add(vet); // 记录访问顶点 + visited.add(vet); // 标记该顶点已被访问 + // 遍历该顶点的所有邻接顶点 + for (Vertex adjVet in graph.adjList[vet]!) { + if (visited.contains(adjVet)) { + continue; // 跳过已被访问过的顶点 + } + // 递归访问邻接顶点 + dfs(graph, visited, res, adjVet); + } + } + + /* 深度优先遍历 DFS */ + List graphDFS(GraphAdjList graph, Vertex startVet) { + // 顶点遍历序列 + List res = []; + // 哈希表,用于记录已被访问过的顶点 + Set visited = {}; + dfs(graph, visited, res, startVet); + return res; + } + ``` + 深度优先遍历的算法流程如下图所示,其中: - **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点; diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md index d75101baf..c48a9ff5b 100755 --- a/chapter_hashing/hash_map.md +++ b/chapter_hashing/hash_map.md @@ -229,6 +229,12 @@ comments: true ``` +=== "Dart" + + ```dart title="hash_map.dart" + + ``` + 遍历哈希表有三种方式,即 **遍历键值对、遍历键、遍历值**。 === "Java" @@ -387,6 +393,12 @@ comments: true ``` +=== "Dart" + + ```dart title="hash_map.dart" + + ``` + ## 6.1.2.   哈希函数 哈希表的底层实现为数组,同时可能包含链表、二叉树(红黑树)等数据结构,以提高查询性能(将在下节讨论)。 @@ -1263,6 +1275,96 @@ $$ } ``` +=== "Dart" + + ```dart title="array_hash_map.dart" + /* 键值对 int -> String */ + class Entry { + int key; + String val; + Entry(this.key, this.val); + } + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap { + late List _buckets; + + ArrayHashMap() { + // 初始化数组,包含 100 个桶 + _buckets = List.filled(100, null); + } + + /* 哈希函数 */ + int _hashFunc(int key) { + final int index = key % 100; + return index; + } + + /* 查询操作 */ + String? get(int key) { + final int index = _hashFunc(key); + final Entry? pair = _buckets[index]; + if (pair == null) { + return null; + } + return pair.val; + } + + /* 添加操作 */ + void put(int key, String val) { + final Entry pair = Entry(key, val); + final int index = _hashFunc(key); + _buckets[index] = pair; + } + + /* 删除操作 */ + void remove(int key) { + final int index = _hashFunc(key); + _buckets[index] = null; + } + + /* 获取所有键值对 */ + List entrySet() { + List entrySet = []; + for (final Entry? pair in _buckets) { + if (pair != null) { + entrySet.add(pair); + } + } + return entrySet; + } + + /* 获取所有键 */ + List keySet() { + List keySet = []; + for (final Entry? pair in _buckets) { + if (pair != null) { + keySet.add(pair.key); + } + } + return keySet; + } + + /* 获取所有值 */ + List values() { + List valueSet = []; + for (final Entry? pair in _buckets) { + if (pair != null) { + valueSet.add(pair.val); + } + } + return valueSet; + } + + /* 打印哈希表 */ + void printHashMap() { + for (final Entry kv in entrySet()) { + print("${kv.key} -> ${kv.val}"); + } + } + } + ``` + ## 6.1.3.   哈希冲突 细心的你可能已经注意到,**在某些情况下,哈希函数 $f(x) = x \bmod 100$ 可能无法正常工作**。具体来说,当输入的 key 后两位相同时,哈希函数的计算结果也会相同,从而指向同一个 value 。例如,查询学号为 $12836$ 和 $20336$ 的两个学生时,我们得到: diff --git a/chapter_heap/build_heap.md b/chapter_heap/build_heap.md index f0c857c88..88c486c6c 100644 --- a/chapter_heap/build_heap.md +++ b/chapter_heap/build_heap.md @@ -162,6 +162,20 @@ comments: true } ``` +=== "Dart" + + ```dart title="my_heap.dart" + /* 构造方法,根据输入列表建堆 */ + MaxHeap(List nums) { + // 将列表元素原封不动添加进堆 + _maxHeap = nums; + // 堆化除叶节点以外的其他所有节点 + for (int i = _parent(size() - 1); i >= 0; i--) { + _siftDown(i); + } + } + ``` + ## 8.2.3.   复杂度分析 为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。 diff --git a/chapter_heap/heap.md b/chapter_heap/heap.md index 7f94e9515..416eefef2 100644 --- a/chapter_heap/heap.md +++ b/chapter_heap/heap.md @@ -307,6 +307,12 @@ comments: true ``` +=== "Dart" + + ```dart title="heap.dart" + + ``` + ## 8.1.2.   堆的实现 下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。 @@ -514,6 +520,25 @@ comments: true } ``` +=== "Dart" + + ```dart title="my_heap.dart" + /* 获取左子节点索引 */ + int _left(int i) { + return 2 * i + 1; + } + + /* 获取右子节点索引 */ + int _right(int i) { + return 2 * i + 2; + } + + /* 获取父节点索引 */ + int _parent(int i) { + return (i - 1) ~/ 2; // 向下整除 + } + ``` + ### 访问堆顶元素 堆顶元素即为二叉树的根节点,也就是列表的首个元素。 @@ -607,6 +632,15 @@ comments: true } ``` +=== "Dart" + + ```dart title="my_heap.dart" + /* 访问堆顶元素 */ + int peek() { + return _maxHeap[0]; + } + ``` + ### 元素入堆 给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 Heapify」。 @@ -918,6 +952,20 @@ comments: true } ``` +=== "Dart" + + ```dart title="my_heap.dart" + /* 元素入堆 */ + void push(int val) { + // 添加节点 + _maxHeap.add(val); + // 从底至顶堆化 + _siftUp(size() - 1); + } + + [class]{MaxHeap}-[func]{siftUp} + ``` + ### 堆顶元素出堆 堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采取以下操作步骤: @@ -1354,6 +1402,26 @@ comments: true } ``` +=== "Dart" + + ```dart title="my_heap.dart" + /* 元素出堆 */ + int pop() { + // 判空处理 + if (isEmpty()) throw Exception('堆为空'); + // 交换根节点与最右叶节点(即交换首元素与尾元素) + _swap(0, size() - 1); + // 删除节点 + int val = _maxHeap.removeLast(); + // 从顶至底堆化 + _siftDown(0); + // 返回堆顶元素 + return val; + } + + [class]{MaxHeap}-[func]{siftDown} + ``` + ## 8.1.3.   堆常见应用 - **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。 diff --git a/chapter_preface/suggestions.md b/chapter_preface/suggestions.md index cf56aa52f..473c06af1 100644 --- a/chapter_preface/suggestions.md +++ b/chapter_preface/suggestions.md @@ -148,6 +148,17 @@ comments: true // 注释 ``` +=== "Dart" + + ```dart title="" + // 标题注释,用于标注函数、类、测试样例等 + + // 内容注释,用于详解代码 + + // 多行 + // 注释 + ``` + ## 0.2.2.   在动画图解中高效学习 相较于文字,视频和图片具有更高的信息密度和结构化程度,因此更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。 diff --git a/chapter_searching/binary_search.md b/chapter_searching/binary_search.md index 08952772b..7f1bda0cf 100755 --- a/chapter_searching/binary_search.md +++ b/chapter_searching/binary_search.md @@ -279,6 +279,32 @@ comments: true } ``` +=== "Dart" + + ```dart title="binary_search.dart" + /* 二分查找(双闭区间) */ + int binarySearch(List nums, int target) { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + int i = 0, j = nums.length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + int 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; + } + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + 时间复杂度为 $O(\log n)$ 。每轮缩小一半区间,因此二分循环次数为 $\log_2 n$ 。 空间复杂度为 $O(1)$ 。指针 `i` , `j` 使用常数大小空间。 @@ -520,6 +546,32 @@ comments: true } ``` +=== "Dart" + + ```dart title="binary_search.dart" + /* 二分查找(左闭右开区间) */ + int binarySearchLCRO(List nums, int target) { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + int i = 0, j = nums.length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + int 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; + } else { + // 找到目标元素,返回其索引 + return m; + } + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + 如下图所示,在两种区间表示下,二分查找算法的初始化、循环条件和缩小区间操作皆有所不同。 在“双闭区间”表示法中,由于左右边界都被定义为闭区间,因此指针 $i$ 和 $j$ 缩小区间操作也是对称的。这样更不容易出错。因此,**我们通常采用“双闭区间”的写法**。 diff --git a/chapter_searching/binary_search_edge.md b/chapter_searching/binary_search_edge.md index 46d45e322..032b9a912 100644 --- a/chapter_searching/binary_search_edge.md +++ b/chapter_searching/binary_search_edge.md @@ -218,6 +218,12 @@ comments: true [class]{}-[func]{binarySearchLeftEdge} ``` +=== "Dart" + + ```dart title="binary_search_edge.dart" + [class]{}-[func]{binarySearchLeftEdge} + ``` + ## 10.2.3.   查找右边界 类似地,我们也可以二分查找最右边的 `target` 。当 `nums[m] == target` 时,说明大于 `target` 的元素在区间 $[m + 1, j]$ 中,因此执行 `i = m + 1` ,**使得指针 $i$ 向大于 `target` 的元素靠近**。 @@ -384,6 +390,12 @@ comments: true [class]{}-[func]{binarySearchRightEdge} ``` +=== "Dart" + + ```dart title="binary_search_edge.dart" + [class]{}-[func]{binarySearchRightEdge} + ``` + 观察下图,搜索最右边元素时指针 $j$ 的作用与搜索最左边元素时指针 $i$ 的作用一致,反之亦然。也就是说,**搜索最左边元素和最右边元素的实现是镜像对称的**。 ![查找最左边和最右边元素的对称性](binary_search_edge.assets/binary_search_left_right_edge.png) diff --git a/chapter_searching/replace_linear_by_hashing.md b/chapter_searching/replace_linear_by_hashing.md index 807add3da..fc46bf8ad 100755 --- a/chapter_searching/replace_linear_by_hashing.md +++ b/chapter_searching/replace_linear_by_hashing.md @@ -193,6 +193,21 @@ comments: true } ``` +=== "Dart" + + ```dart title="two_sum.dart" + /* 方法一: 暴力枚举 */ + List twoSumBruteForce(List nums, int target) { + int size = nums.length; + for (var i = 0; i < size - 1; i++) { + for (var j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) return [i, j]; + } + } + return [0]; + } + ``` + 此方法的时间复杂度为 $O(n^2)$ ,空间复杂度为 $O(1)$ ,在大数据量下非常耗时。 ## 10.3.2.   哈希查找:以空间换时间 @@ -430,6 +445,23 @@ comments: true } ``` +=== "Dart" + + ```dart title="two_sum.dart" + /* 方法二: 辅助哈希表 */ + List twoSumHashTable(List nums, int target) { + int size = nums.length; + Map dic = HashMap(); + for (var i = 0; i < size; i++) { + if (dic.containsKey(target - nums[i])) { + return [dic[target - nums[i]]!, i]; + } + dic.putIfAbsent(nums[i], () => i); + } + return [0]; + } + ``` + 此方法通过哈希查找将时间复杂度从 $O(n^2)$ 降低至 $O(n)$ ,大幅提升运行效率。 由于需要维护一个额外的哈希表,因此空间复杂度为 $O(n)$ 。**尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法**。 diff --git a/chapter_sorting/bubble_sort.md b/chapter_sorting/bubble_sort.md index bc86ccd4b..8d5d1bca2 100755 --- a/chapter_sorting/bubble_sort.md +++ b/chapter_sorting/bubble_sort.md @@ -235,6 +235,26 @@ comments: true } ``` +=== "Dart" + + ```dart title="bubble_sort.dart" + /* 冒泡排序 */ + void bubbleSort(List nums) { + // 外循环:未排序区间为 [0, i] + for (int i = nums.length - 1; i > 0; i--) { + // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + ## 11.3.2.   效率优化 我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。 @@ -471,6 +491,29 @@ comments: true } ``` +=== "Dart" + + ```dart title="bubble_sort.dart" + /* 冒泡排序(标志优化)*/ + void bubbleSortWithFlag(List nums) { + // 外循环:未排序区间为 [0, i] + for (int i = nums.length - 1; i > 0; i--) { + bool flag = false; // 初始化标志位 + // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + int 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)$ 。 diff --git a/chapter_sorting/bucket_sort.md b/chapter_sorting/bucket_sort.md index 429f2129f..aedc038a6 100644 --- a/chapter_sorting/bucket_sort.md +++ b/chapter_sorting/bucket_sort.md @@ -285,6 +285,36 @@ comments: true [class]{}-[func]{bucketSort} ``` +=== "Dart" + + ```dart title="bucket_sort.dart" + /* 桶排序 */ + void bucketSort(List nums) { + // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素 + int k = nums.length ~/ 2; + List> buckets = List.generate(k, (index) => []); + + // 1. 将数组元素分配到各个桶中 + for (double num in nums) { + // 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1] + int i = (num * k).toInt(); + // 将 num 添加进桶 bucket_idx + buckets[i].add(num); + } + // 2. 对各个桶执行排序 + for (List bucket in buckets) { + bucket.sort(); + } + // 3. 遍历桶合并结果 + int i = 0; + for (List bucket in buckets) { + for (double num in bucket) { + nums[i++] = num; + } + } + } + ``` + !!! question "桶排序的适用场景是什么?" 桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。 diff --git a/chapter_sorting/counting_sort.md b/chapter_sorting/counting_sort.md index afd9a94e2..0b4f101f5 100644 --- a/chapter_sorting/counting_sort.md +++ b/chapter_sorting/counting_sort.md @@ -265,6 +265,33 @@ comments: true [class]{}-[func]{countingSortNaive} ``` +=== "Dart" + + ```dart title="counting_sort.dart" + /* 计数排序 */ + // 简单实现,无法用于排序对象 + void countingSortNaive(List nums) { + // 1. 统计数组最大元素 m + int m = 0; + for (int num in nums) { + m = max(m, num); + } + // 2. 统计各数字的出现次数 + // counter[num] 代表 num 的出现次数 + List counter = List.filled(m + 1, 0); + for (int num in nums) { + counter[num]++; + } + // 3. 遍历 counter ,将各元素填入原数组 nums + int i = 0; + for (int num = 0; num < m + 1; num++) { + for (int j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + } + ``` + !!! note "计数排序与桶排序的联系" 从桶排序的角度看,我们可以将计数排序中的计数数组 `counter` 的每个索引视为一个桶,将统计数量的过程看作是将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。 @@ -647,6 +674,42 @@ $$ [class]{}-[func]{countingSort} ``` +=== "Dart" + + ```dart title="counting_sort.dart" + /* 计数排序 */ + // 完整实现,可排序对象,并且是稳定排序 + void countingSort(List nums) { + // 1. 统计数组最大元素 m + int m = 0; + for (int num in nums) { + m = max(m, num); + } + // 2. 统计各数字的出现次数 + // counter[num] 代表 num 的出现次数 + List counter = List.filled(m + 1, 0); + for (int num in nums) { + counter[num]++; + } + // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引” + // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引 + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. 倒序遍历 nums ,将各元素填入结果数组 res + // 初始化数组 res 用于记录结果 + int n = nums.length; + List res = List.filled(n, 0); + for (int i = n - 1; i >= 0; i--) { + int num = nums[i]; + res[counter[num] - 1] = num; // 将 num 放置到对应索引处 + counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引 + } + // 使用结果数组 res 覆盖原数组 nums + nums.setAll(0, res); + } + ``` + ## 11.9.3.   算法特性 - **时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。 diff --git a/chapter_sorting/heap_sort.md b/chapter_sorting/heap_sort.md index 842584b32..64894bc61 100644 --- a/chapter_sorting/heap_sort.md +++ b/chapter_sorting/heap_sort.md @@ -343,6 +343,14 @@ comments: true [class]{}-[func]{heapSort} ``` +=== "Dart" + + ```dart title="heap_sort.dart" + [class]{}-[func]{siftDown} + + [class]{}-[func]{heapSort} + ``` + ## 11.7.2.   算法特性 - **时间复杂度 $O(n \log n)$ 、非自适应排序** :建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。 diff --git a/chapter_sorting/insertion_sort.md b/chapter_sorting/insertion_sort.md index 57a76acfb..c6c208691 100755 --- a/chapter_sorting/insertion_sort.md +++ b/chapter_sorting/insertion_sort.md @@ -212,6 +212,24 @@ comments: true } ``` +=== "Dart" + + ```dart title="insertion_sort.dart" + /* 插入排序 */ + void insertionSort(List nums) { + // 外循环:已排序元素数量为 1, 2, ..., n + for (int i = 1; i < nums.length; i++) { + int base = nums[i], j = i - 1; + // 内循环:将 base 插入到已排序部分的正确位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位 + j--; + } + nums[j + 1] = 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)$ 。 diff --git a/chapter_sorting/merge_sort.md b/chapter_sorting/merge_sort.md index c6008a537..b339803a1 100755 --- a/chapter_sorting/merge_sort.md +++ b/chapter_sorting/merge_sort.md @@ -528,6 +528,48 @@ comments: true } ``` +=== "Dart" + + ```dart title="merge_sort.dart" + /* 合并左子数组和右子数组 */ + // 左子数组区间 [left, mid] + // 右子数组区间 [mid + 1, right] + void merge(List nums, int left, int mid, int right) { + // 初始化辅助数组 + List tmp = nums.sublist(left, right + 1); + // 左子数组的起始索引和结束索引 + int leftStart = left - left, leftEnd = mid - left; + // 右子数组的起始索引和结束索引 + int rightStart = mid + 1 - left, rightEnd = right - left; + // i, j 分别指向左子数组、右子数组的首元素 + int i = leftStart, j = rightStart; + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for (int k = left; k <= right; k++) { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if (i > leftEnd) + nums[k] = tmp[j++]; + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + else if (j > rightEnd || tmp[i] <= tmp[j]) + nums[k] = tmp[i++]; + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else + nums[k] = tmp[j++]; + } + } + + /* 归并排序 */ + void mergeSort(List nums, int left, int right) { + // 终止条件 + if (left >= right) return; // 当子数组长度为 1 时终止递归 + // 划分阶段 + int mid = (left + right) ~/ 2; // 计算中点 + mergeSort(nums, left, mid); // 递归左子数组 + mergeSort(nums, mid + 1, right); // 递归右子数组 + // 合并阶段 + merge(nums, left, mid, right); + } + ``` + 合并方法 `merge()` 代码中的难点包括: - **在阅读代码时,需要特别注意各个变量的含义**。`nums` 的待合并区间为 `[left, right]` ,但由于 `tmp` 仅复制了 `nums` 该区间的元素,因此 `tmp` 对应区间为 `[0, right - left]` 。 diff --git a/chapter_sorting/quick_sort.md b/chapter_sorting/quick_sort.md index 93f118bcb..1eb0cf7e5 100755 --- a/chapter_sorting/quick_sort.md +++ b/chapter_sorting/quick_sort.md @@ -313,6 +313,30 @@ comments: true } ``` +=== "Dart" + + ```dart title="quick_sort.dart" + /* 元素交换 */ + void _swap(List nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + int _partition(List nums, int left, int right) { + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) i++; // 从左向右找首个大于基准数的元素 + _swap(nums, i, j); // 交换这两个元素 + } + _swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + ``` + ## 11.5.1.   算法流程 1. 首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组; @@ -507,6 +531,21 @@ comments: true } ``` +=== "Dart" + + ```dart title="quick_sort.dart" + /* 快速排序 */ + void quickSort(List nums, int left, int right) { + // 子数组长度为 1 时终止递归 + if (left >= right) return; + // 哨兵划分 + int pivot = _partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + ## 11.5.2.   算法特性 - **时间复杂度 $O(n \log n)$ 、自适应排序** :在平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。在最差情况下,每轮哨兵划分操作都将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。 @@ -904,6 +943,39 @@ comments: true } ``` +=== "Dart" + + ```dart title="quick_sort.dart" + /* 选取三个元素的中位数 */ + int _medianThree(List nums, int left, int mid, int right) { + // 此处使用异或运算来简化代码 + // 异或规则为 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; + else + return right; + } + + /* 哨兵划分(三数取中值) */ + int _partition(List nums, int left, int right) { + // 选取三个候选元素的中位数 + int med = _medianThree(nums, left, (left + right) ~/ 2, right); + // 将中位数交换至数组最左端 + _swap(nums, left, med); + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) i++; // 从左向右找首个大于基准数的元素 + _swap(nums, i, j); // 交换这两个元素 + } + _swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + ``` + ## 11.5.5.   尾递归优化 **在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。 @@ -1121,3 +1193,24 @@ comments: true } } ``` + +=== "Dart" + + ```dart title="quick_sort.dart" + /* 快速排序(尾递归优化) */ + void quickSort(List nums, int left, int right) { + // 子数组长度为 1 时终止 + while (left < right) { + // 哨兵划分操作 + int pivot = _partition(nums, left, right); + // 对两个子数组中较短的那个执行快排 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 递归排序左子数组 + left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 递归排序右子数组 + right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1] + } + } + } + ``` diff --git a/chapter_sorting/radix_sort.md b/chapter_sorting/radix_sort.md index d8a546f70..78205055e 100644 --- a/chapter_sorting/radix_sort.md +++ b/chapter_sorting/radix_sort.md @@ -583,6 +583,57 @@ $$ } ``` +=== "Dart" + + ```dart title="radix_sort.dart" + /* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */ + int digit(int num, int exp) { + // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算 + return (num ~/ exp) % 10; + } + + /* 计数排序(根据 nums 第 k 位排序) */ + void countingSortDigit(List nums, int exp) { + // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶 + List counter = List.filled(10, 0); + int n = nums.length; + // 统计 0~9 各数字的出现次数 + for (int i = 0; i < n; i++) { + int d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d + counter[d]++; // 统计数字 d 的出现次数 + } + // 求前缀和,将“出现个数”转换为“数组索引” + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // 倒序遍历,根据桶内统计结果,将各元素填入 res + List res = List.filled(n, 0); + for (int i = n - 1; i >= 0; i--) { + int d = digit(nums[i], exp); + int j = counter[d] - 1; // 获取 d 在数组中的索引 j + res[j] = nums[i]; // 将当前元素填入索引 j + counter[d]--; // 将 d 的数量减 1 + } + // 使用结果覆盖原数组 nums + for (int i = 0; i < n; i++) nums[i] = res[i]; + } + + /* 基数排序 */ + void radixSort(List nums) { + // 获取数组的最大元素,用于判断最大位数 + // dart 中 int 的长度是 64 位的 + int m = -1 << 63; + for (int num in nums) if (num > m) m = num; + // 按照从低位到高位的顺序遍历 + for (int exp = 1; exp <= m; exp *= 10) + // 对数组元素的第 k 位执行计数排序 + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // 即 exp = 10^(k-1) + countingSortDigit(nums, exp); + } + ``` + !!! question "为什么从最低位开始排序?" 在连续的排序轮次中,后一轮排序会覆盖前一轮排序的结果。举例来说,如果第一轮排序结果 $a < b$ ,而第二轮排序结果 $a > b$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。 diff --git a/chapter_sorting/selection_sort.md b/chapter_sorting/selection_sort.md index f6551447b..a67b2b03d 100644 --- a/chapter_sorting/selection_sort.md +++ b/chapter_sorting/selection_sort.md @@ -195,6 +195,12 @@ comments: true [class]{}-[func]{selectionSort} ``` +=== "Dart" + + ```dart title="selection_sort.dart" + [class]{}-[func]{selectionSort} + ``` + ## 11.2.1.   算法特性 - **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。 diff --git a/chapter_stack_and_queue/deque.md b/chapter_stack_and_queue/deque.md index e29686ea2..61ef4d8f2 100644 --- a/chapter_stack_and_queue/deque.md +++ b/chapter_stack_and_queue/deque.md @@ -289,6 +289,12 @@ comments: true ``` +=== "Dart" + + ```dart title="deque.dart" + + ``` + ## 5.3.2.   双向队列实现 * 双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。 @@ -1636,6 +1642,136 @@ comments: true } ``` +=== "Dart" + + ```dart title="linkedlist_deque.dart" + /* 双向链表节点 */ + class ListNode { + int val; // 节点值 + ListNode? next; // 后继节点引用(指针) + ListNode? prev; // 前驱节点引用(指针) + + ListNode(this.val, {this.next, this.prev}); + } + + /* 基于双向链表实现的双向对列 */ + class LinkedListDeque { + late ListNode? _front; // 头节点 _front + late ListNode? _rear; // 尾节点 _rear + int _queSize = 0; // 双向队列的长度 + + LinkedListDeque() { + this._front = null; + this._rear = null; + } + + /* 获取双向队列长度 */ + int size() { + return this._queSize; + } + + /* 判断双向队列是否为空 */ + bool isEmpty() { + return size() == 0; + } + + /* 入队操作 */ + void push(int num, bool isFront) { + final ListNode node = ListNode(num); + if (isEmpty()) { + // 若链表为空,则令 _front,_rear 都指向 node + _front = _rear = node; + } else if (isFront) { + // 队首入队操作 + // 将 node 添加至链表头部 + _front!.prev = node; + node.next = _front; + _front = node; // 更新头节点 + } else { + // 队尾入队操作 + // 将 node 添加至链表尾部 + _rear!.next = node; + node.prev = _rear; + _rear = node; // 更新尾节点 + } + _queSize++; // 更新队列长度 + } + + /* 队首入队 */ + void pushFirst(int num) { + push(num, true); + } + + /* 队尾入队 */ + void pushLast(int num) { + push(num, false); + } + + /* 出队操作 */ + int? pop(bool isFront) { + // 若队列为空,直接返回 null + if (isEmpty()) { + return null; + } + final int val; + if (isFront) { + // 队首出队操作 + val = _front!.val; // 暂存头节点值 + // 删除头节点 + ListNode? fNext = _front!.next; + if (fNext != null) { + fNext.prev = null; + _front!.next = null; + } + _front = fNext; // 更新头节点 + } else { + // 队尾出队操作 + val = _rear!.val; // 暂存尾节点值 + // 删除尾节点 + ListNode? rPrev = _rear!.prev; + if (rPrev != null) { + rPrev.next = null; + _rear!.prev = null; + } + _rear = rPrev; // 更新尾节点 + } + _queSize--; // 更新队列长度 + return val; + } + + /* 队首出队 */ + int? popFirst() { + return pop(true); + } + + /* 队尾出队 */ + int? popLast() { + return pop(false); + } + + /* 访问队首元素 */ + int? peekFirst() { + return _front?.val; + } + + /* 访问队尾元素 */ + int? peekLast() { + return _rear?.val; + } + + /* 返回数组用于打印 */ + List toArray() { + ListNode? node = _front; + final List res = []; + for (int i = 0; i < _queSize; i++) { + res.add(node!.val); + node = node.next; + } + return res; + } + } + ``` + ### 基于数组的实现 与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列。在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法。 @@ -2650,6 +2786,115 @@ comments: true [class]{ArrayDeque}-[func]{} ``` +=== "Dart" + + ```dart title="array_deque.dart" + /* 基于环形数组实现的双向队列 */ + class ArrayDeque { + late List _nums; // 用于存储双向队列元素的数组 + late int _front; // 队首指针,指向队首元素 + late int _queSize; // 双向队列长度 + + /* 构造方法 */ + ArrayDeque(int capacity) { + this._nums = List.filled(capacity, 0); + this._front = this._queSize = 0; + } + + /* 获取双向队列的容量 */ + int capacity() { + return _nums.length; + } + + /* 获取双向队列的长度 */ + int size() { + return _queSize; + } + + /* 判断双向队列是否为空 */ + bool isEmpty() { + return _queSize == 0; + } + + /* 计算环形数组索引 */ + int index(int i) { + // 通过取余操作实现数组首尾相连 + // 当 i 越过数组尾部后,回到头部 + // 当 i 越过数组头部后,回到尾部 + return (i + capacity()) % capacity(); + } + + /* 队首入队 */ + void pushFirst(int num) { + if (_queSize == capacity()) { + throw Exception("双向队列已满"); + } + // 队首指针向左移动一位 + // 通过取余操作,实现 _front 越过数组头部后回到尾部 + _front = index(_front - 1); + // 将 num 添加至队首 + _nums[_front] = num; + _queSize++; + } + + /* 队尾入队 */ + void pushLast(int num) { + if (_queSize == capacity()) { + throw Exception("双向队列已满"); + } + // 计算尾指针,指向队尾索引 + 1 + int rear = index(_front + _queSize); + // 将 num 添加至队尾 + _nums[rear] = num; + _queSize++; + } + + /* 队首出队 */ + int popFirst() { + int num = peekFirst(); + // 队首指针向右移动一位 + _front = index(_front + 1); + _queSize--; + return num; + } + + /* 队尾出队 */ + int popLast() { + int num = peekLast(); + _queSize--; + return num; + } + + /* 访问队首元素 */ + int peekFirst() { + if (isEmpty()) { + throw Exception("双向队列为空"); + } + return _nums[_front]; + } + + /* 访问队尾元素 */ + int peekLast() { + if (isEmpty()) { + throw Exception("双向队列为空"); + } + // 计算尾元素索引 + int last = index(_front + _queSize - 1); + return _nums[last]; + } + + /* 返回数组用于打印 */ + List toArray() { + // 仅转换有效长度范围内的列表元素 + List res = List.filled(_queSize, 0); + for (int i = 0, j = _front; i < _queSize; i++, j++) { + res[i] = _nums[index(j)]; + } + return res; + } + } + ``` + ## 5.3.3.   双向队列应用 双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。 diff --git a/chapter_stack_and_queue/queue.md b/chapter_stack_and_queue/queue.md index 48749d3a3..91fb5a85c 100755 --- a/chapter_stack_and_queue/queue.md +++ b/chapter_stack_and_queue/queue.md @@ -258,6 +258,12 @@ comments: true ``` +=== "Dart" + + ```dart title="queue.dart" + + ``` + ## 5.2.2.   队列实现 为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列。 @@ -993,6 +999,76 @@ comments: true } ``` +=== "Dart" + + ```dart title="linkedlist_queue.dart" + /* 基于链表实现的队列 */ + class LinkedListQueue { + ListNode? _front; // 头节点 _front + ListNode? _rear; // 尾节点 _rear + int _queSize = 0; // 队列长度 + + LinkedListQueue() { + _front = null; + _rear = null; + } + + /* 获取队列的长度 */ + int size() { + return _queSize; + } + + /* 判断队列是否为空 */ + bool isEmpty() { + return _queSize == 0; + } + + /* 入队 */ + void push(int num) { + // 尾节点后添加 num + final node = ListNode(num); + // 如果队列为空,则令头、尾节点都指向该节点 + if (_front == null) { + _front = node; + _rear = node; + } else { + // 如果队列不为空,则将该节点添加到尾节点后 + _rear!.next = node; + _rear = node; + } + _queSize++; + } + + /* 出队 */ + int pop() { + final int num = peek(); + // 删除头节点 + _front = _front!.next; + _queSize--; + return num; + } + + /* 访问队首元素 */ + int peek() { + if (_queSize == 0) { + throw Exception('队列为空'); + } + return _front!.val; + } + + /* 将链表转化为 Array 并返回 */ + List toArray() { + ListNode? node = _front; + final List queue = []; + while (node != null) { + queue.add(node.val); + node = node.next; + } + return queue; + } + } + ``` + ### 基于数组的实现 由于数组删除首元素的时间复杂度为 $O(n)$ ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。 @@ -1759,6 +1835,77 @@ comments: true } ``` +=== "Dart" + + ```dart title="array_queue.dart" + /* 基于环形数组实现的队列 */ + class ArrayQueue { + late List _nums; // 用于储存队列元素的数组 + late int _front; // 队首指针,指向队首元素 + late int _queSize; // 队列长度 + + ArrayQueue(int capacity) { + _nums = List.filled(capacity, 0); + _front = _queSize = 0; + } + + /* 获取队列的容量 */ + int capaCity() { + return _nums.length; + } + + /* 获取队列的长度 */ + int size() { + return _queSize; + } + + /* 判断队列是否为空 */ + bool isEmpty() { + return _queSize == 0; + } + + /* 入队 */ + void push(int num) { + if (_queSize == capaCity()) { + throw Exception("队列已满"); + } + // 计算尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + int rear = (_front + _queSize) % capaCity(); + // 将 num 添加至队尾 + _nums[rear] = num; + _queSize++; + } + + /* 出队 */ + int pop() { + int num = peek(); + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + _front = (_front + 1) % capaCity(); + _queSize--; + return num; + } + + /* 访问队首元素 */ + int peek() { + if (isEmpty()) { + throw Exception("队列为空"); + } + return _nums[_front]; + } + + /* 返回 Array */ + List toArray() { + // 仅转换有效长度范围内的列表元素 + final List res = List.filled(_queSize, 0); + for (int i = 0, j = _front; i < _queSize; i++, j++) { + res[i] = _nums[j % capaCity()]; + } + return res; + } + } + ``` + 以上实现的队列仍然具有局限性,即其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的同学可以尝试自行实现。 两种实现的对比结论与栈一致,在此不再赘述。 diff --git a/chapter_stack_and_queue/stack.md b/chapter_stack_and_queue/stack.md index 6b4fdf8d3..28d1ea5ee 100755 --- a/chapter_stack_and_queue/stack.md +++ b/chapter_stack_and_queue/stack.md @@ -256,6 +256,12 @@ comments: true ``` +=== "Dart" + + ```dart title="stack.dart" + + ``` + ## 5.1.2.   栈的实现 为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。 @@ -902,6 +908,66 @@ comments: true } ``` +=== "Dart" + + ```dart title="linkedlist_stack.dart" + /* 基于链表类实现的栈 */ + class LinkedListStack { + ListNode? _stackPeek; // 将头节点作为栈顶 + int _stkSize = 0; // 栈的长度 + + LinkedListStack() { + _stackPeek = null; + } + + /* 获取栈的长度 */ + int size() { + return _stkSize; + } + + /* 判断栈是否为空 */ + bool isEmpty() { + return _stkSize == 0; + } + + /* 入栈 */ + void push(int num) { + final ListNode node = ListNode(num); + node.next = _stackPeek; + _stackPeek = node; + _stkSize++; + } + + /* 出栈 */ + int pop() { + final int num = peek(); + _stackPeek = _stackPeek!.next; + _stkSize--; + return num; + } + + /* 访问栈顶元素 */ + int peek() { + if (_stackPeek == null) { + throw Exception("栈为空"); + } + return _stackPeek!.val; + } + + /* 将链表转化为 List 并返回 */ + List toList() { + ListNode? node = _stackPeek; + List list = []; + while (node != null) { + list.add(node.val); + node = node.next; + } + list = list.reversed.toList(); + return list; + } + } + ``` + ### 基于数组的实现 在基于「数组」实现栈时,我们可以将数组的尾部作为栈顶。在这样的设计下,入栈与出栈操作就分别对应在数组尾部添加元素与删除元素,时间复杂度都为 $O(1)$ 。 @@ -1409,6 +1475,52 @@ comments: true } ``` +=== "Dart" + + ```dart title="array_stack.dart" + /* 基于数组实现的栈 */ + class ArrayStack { + late List _stack; + ArrayStack() { + _stack = []; + } + + /* 获取栈的长度 */ + int size() { + return _stack.length; + } + + /* 判断栈是否为空 */ + bool isEmpty() { + return _stack.isEmpty; + } + + /* 入栈 */ + void push(int num) { + _stack.add(num); + } + + /* 出栈 */ + int pop() { + if (isEmpty()) { + throw Exception("栈为空"); + } + return _stack.removeLast(); + } + + /* 访问栈顶元素 */ + int peek() { + if (isEmpty()) { + throw Exception("栈为空"); + } + return _stack.last; + } + + /* 将栈转化为 Array 并返回 */ + List toArray() => _stack; + } + ``` + ## 5.1.3.   两种实现对比 ### 支持操作 diff --git a/chapter_tree/array_representation_of_tree.md b/chapter_tree/array_representation_of_tree.md index 53faaeade..56817fd48 100644 --- a/chapter_tree/array_representation_of_tree.md +++ b/chapter_tree/array_representation_of_tree.md @@ -108,6 +108,12 @@ comments: true ``` +=== "Dart" + + ```dart title="" + + ``` + ![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png)

Fig. 任意类型二叉树的数组表示

diff --git a/chapter_tree/avl_tree.md b/chapter_tree/avl_tree.md index 59d9e5ad6..58043fb4d 100644 --- a/chapter_tree/avl_tree.md +++ b/chapter_tree/avl_tree.md @@ -177,6 +177,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ``` +=== "Dart" + + ```dart title="" + + ``` + 「节点高度」是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。 === "Java" @@ -360,6 +366,21 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Dart" + + ```dart title="avl_tree.dart" + /* 获取节点高度 */ + int height(TreeNode? node) { + return node == null ? -1 : node.height; + } + + /* 更新节点高度 */ + void updateHeight(TreeNode? node) { + // 节点高度等于最高子树高度 + 1 + node!.height = max(height(node.left), height(node.right)) + 1; + } + ``` + ### 节点平衡因子 节点的「平衡因子 Balance Factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。 @@ -490,6 +511,18 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit } ``` +=== "Dart" + + ```dart title="avl_tree.dart" + /* 获取平衡因子 */ + int balanceFactor(TreeNode? node) { + // 空节点平衡因子为 0 + if (node == null) return 0; + // 节点平衡因子 = 左子树高度 - 右子树高度 + return height(node.left) - height(node.right); + } + ``` + !!! note 设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。 @@ -704,6 +737,24 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Dart" + + ```dart title="avl_tree.dart" + /* 右旋操作 */ + TreeNode? rightRotate(TreeNode? node) { + TreeNode? child = node!.left; + TreeNode? grandChild = child!.right; + // 以 child 为原点,将 node 向右旋转 + child.right = node; + node.left = grandChild; + // 更新节点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + ### 左旋 相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。 @@ -900,6 +951,24 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Dart" + + ```dart title="avl_tree.dart" + /* 左旋操作 */ + TreeNode? leftRotate(TreeNode? node) { + TreeNode? child = node!.right; + TreeNode? grandChild = child!.left; + // 以 child 为原点,将 node 向左旋转 + child.left = node; + node.right = grandChild; + // 更新节点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + ### 先左旋后右旋 对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 @@ -1275,6 +1344,40 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Dart" + + ```dart title="avl_tree.dart" + /* 执行旋转操作,使该子树重新恢复平衡 */ + TreeNode? rotate(TreeNode? node) { + // 获取节点 node 的平衡因子 + int factor = balanceFactor(node); + // 左偏树 + if (factor > 1) { + if (balanceFactor(node!.left) >= 0) { + // 右旋 + return rightRotate(node); + } else { + // 先左旋后右旋 + node.left = leftRotate(node.left); + return rightRotate(node); + } + } + // 右偏树 + if (factor < -1) { + if (balanceFactor(node!.right) <= 0) { + // 左旋 + return leftRotate(node); + } else { + // 先右旋后左旋 + node.right = rightRotate(node.right); + return leftRotate(node); + } + } + // 平衡树,无需旋转,直接返回 + return node; + } + ``` + ## 7.5.3.   AVL 树常用操作 ### 插入节点 @@ -1562,6 +1665,32 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Dart" + + ```dart title="avl_tree.dart" + /* 插入节点 */ + void insert(int val) { + root = insertHelper(root, val); + } + + /* 递归插入节点(辅助方法) */ + TreeNode? insertHelper(TreeNode? node, int val) { + if (node == null) return TreeNode(val); + /* 1. 查找插入位置,并插入节点 */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // 重复节点不插入,直接返回 + updateHeight(node); // 更新节点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + ``` + ### 删除节点 类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡。 @@ -2021,6 +2150,49 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 } ``` +=== "Dart" + + ```dart title="avl_tree.dart" + /* 删除节点 */ + void remove(int val) { + root = removeHelper(root, val); + } + + /* 递归删除节点(辅助方法) */ + TreeNode? removeHelper(TreeNode? node, int val) { + if (node == null) return null; + /* 1. 查找节点,并删除之 */ + if (val < node.val) + node.left = removeHelper(node.left, val); + else if (val > node.val) + node.right = removeHelper(node.right, val); + else { + if (node.left == null || node.right == null) { + TreeNode? child = node.left ?? node.right; + // 子节点数量 = 0 ,直接删除 node 并返回 + if (child == null) + return null; + // 子节点数量 = 1 ,直接删除 node + else + node = child; + } else { + // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点 + TreeNode? temp = node.right; + while (temp!.left != null) { + temp = temp.left; + } + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // 更新节点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + ``` + ### 查找节点 AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。 diff --git a/chapter_tree/binary_search_tree.md b/chapter_tree/binary_search_tree.md index c8161ecb7..abe8fcb39 100755 --- a/chapter_tree/binary_search_tree.md +++ b/chapter_tree/binary_search_tree.md @@ -265,6 +265,12 @@ comments: true } ``` +=== "Dart" + + ```dart title="binary_search_tree.dart" + [class]{BinarySearchTree}-[func]{search} + ``` + ### 插入节点 给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作分为两步: @@ -602,6 +608,12 @@ comments: true } ``` +=== "Dart" + + ```dart title="binary_search_tree.dart" + [class]{BinarySearchTree}-[func]{insert} + ``` + 为了插入节点,我们需要利用辅助节点 `pre` 保存上一轮循环的节点,这样在遍历至 $\text{null}$ 时,我们可以获取到其父节点,从而完成节点插入操作。 与查找节点相同,插入节点使用 $O(\log n)$ 时间。 @@ -1201,6 +1213,12 @@ comments: true } ``` +=== "Dart" + + ```dart title="binary_search_tree.dart" + [class]{BinarySearchTree}-[func]{remove} + ``` + ### 排序 我们知道,二叉树的中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。 diff --git a/chapter_tree/binary_tree.md b/chapter_tree/binary_tree.md index 2d81718c8..45ace000c 100644 --- a/chapter_tree/binary_tree.md +++ b/chapter_tree/binary_tree.md @@ -147,6 +147,12 @@ comments: true ``` +=== "Dart" + + ```dart title="" + + ``` + 节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树」。 **在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。例如,在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。 @@ -337,6 +343,12 @@ comments: true ``` +=== "Dart" + + ```dart title="binary_tree.dart" + + ``` + **插入与删除节点**。与链表类似,通过修改指针来实现插入与删除节点。 ![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png) @@ -455,6 +467,12 @@ comments: true ``` +=== "Dart" + + ```dart title="binary_tree.dart" + + ``` + !!! note 需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。 diff --git a/chapter_tree/binary_tree_traversal.md b/chapter_tree/binary_tree_traversal.md index a2e5ff57e..98926e71c 100755 --- a/chapter_tree/binary_tree_traversal.md +++ b/chapter_tree/binary_tree_traversal.md @@ -279,6 +279,26 @@ comments: true } ``` +=== "Dart" + + ```dart title="binary_tree_bfs.dart" + /* 层序遍历 */ + List levelOrder(TreeNode? root) { + // 初始化队列,加入根节点 + Queue queue = Queue(); + queue.add(root); + // 初始化一个列表,用于保存遍历序列 + List res = []; + while (queue.isNotEmpty) { + TreeNode? node = queue.removeFirst(); // 队列出队 + res.add(node!.val); // 保存节点值 + if (node.left != null) queue.add(node.left); // 左子节点入队 + if (node.right != null) queue.add(node.right); // 右子节点入队 + } + return res; + } + ``` + ### 复杂度分析 **时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 @@ -644,6 +664,37 @@ comments: true } ``` +=== "Dart" + + ```dart title="binary_tree_dfs.dart" + /* 前序遍历 */ + void preOrder(TreeNode? node) { + if (node == null) return; + // 访问优先级:根节点 -> 左子树 -> 右子树 + list.add(node.val); + preOrder(node.left); + preOrder(node.right); + } + + /* 中序遍历 */ + void inOrder(TreeNode? node) { + if (node == null) return; + // 访问优先级:左子树 -> 根节点 -> 右子树 + inOrder(node.left); + list.add(node.val); + inOrder(node.right); + } + + /* 后序遍历 */ + void postOrder(TreeNode? node) { + if (node == null) return; + // 访问优先级:左子树 -> 右子树 -> 根节点 + postOrder(node.left); + postOrder(node.right); + list.add(node.val); + } + ``` + !!! note 我们也可以仅基于循环实现前、中、后序遍历,有兴趣的同学可以自行实现。