diff --git a/codes/dart/chapter_array_and_linkedlist/array.dart b/codes/dart/chapter_array_and_linkedlist/array.dart index 64209c115..e04748499 100644 --- a/codes/dart/chapter_array_and_linkedlist/array.dart +++ b/codes/dart/chapter_array_and_linkedlist/array.dart @@ -6,70 +6,68 @@ import 'dart:math'; -class Array { - /* 随机返回一个 数组元素 */ - int randomAccess(List nums) { - // 在区间[0,size) 中随机抽取一个数字 - int randomIndex = Random().nextInt(nums.length); - // 获取并返回随机元素 - int randomNum = nums[randomIndex]; - return randomNum; - } +/* 随机返回一个 数组元素 */ +int randomAccess(List nums) { + // 在区间[0,size) 中随机抽取一个数字 + int randomIndex = Random().nextInt(nums.length); + // 获取并返回随机元素 + int randomNum = nums[randomIndex]; + return randomNum; +} - /* 扩展数组长度 */ - List extend(List nums, int enlarge) { - // 初始化一个扩展长度后的数组,元素初始值为0 - List res = List.filled(nums.length + enlarge, 0); +/* 扩展数组长度 */ +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; + // 将原数组中的所有元素复制到新数组 + for (var i = 0; i < nums.length; i++) { + res[i] = nums[i]; } + // 返回扩展后的新数组 + return res; +} - /* 在数组的索引 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; +/* 在数组的索引 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; +} - /* 删除索引 index 处元素 */ - void remove(List nums, int index) { - for (var i = index; i < nums.length - 1; i++) { - nums[i] = nums[i + 1]; - } +/* 删除索引 index 处元素 */ +void remove(List nums, int index) { + for (var i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; } +} - /* 遍历数组元素 */ - 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++; - }); +/* 遍历数组元素 */ +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++; + }); +} - /* 在数组中查找指定元素 */ - int find(List nums, int target) { - for (var i = 0; i < nums.length; i++) { - if (nums[i] == target) return i; - } - return -1; +/* 在数组中查找指定元素 */ +int find(List nums, int target) { + for (var i = 0; i < nums.length; i++) { + if (nums[i] == target) return i; } + return -1; } /* Driver Code */ @@ -81,26 +79,26 @@ int main() { print('数组 nums = $nums'); /* 随机访问 */ - int randomNum = Array().randomAccess(nums); + int randomNum = randomAccess(nums); print('在 nums 中获取随机元素 $randomNum'); /* 长度扩展 */ - nums = Array().extend(nums, 3); + nums = extend(nums, 3); print('将数组长度扩展至 8 ,得到 nums = $nums'); /* 插入元素 */ - Array().insert(nums, 6, 3); + insert(nums, 6, 3); print("在索引 3 处插入数字 6 ,得到 nums = $nums"); /* 删除元素 */ - Array().remove(nums, 2); + remove(nums, 2); print("删除索引 2 处的元素,得到 nums = $nums"); /* 遍历元素 */ - Array().traverse(nums); + traverse(nums); /* 查找元素 */ - int index = Array().find(nums, 3); + int index = find(nums, 3); print("在 nums 中查找元素 3 ,得到索引 = $index"); return 0; diff --git a/codes/dart/chapter_array_and_linkedlist/linked_list.dart b/codes/dart/chapter_array_and_linkedlist/linked_list.dart index 6c6f1859b..7fd9afac5 100644 --- a/codes/dart/chapter_array_and_linkedlist/linked_list.dart +++ b/codes/dart/chapter_array_and_linkedlist/linked_list.dart @@ -7,43 +7,41 @@ import '../utils/list_node.dart'; import '../utils/print_util.dart'; -class LinkedList { - /* 在链表的节点 n0 之后插入节点 P */ - void insert(ListNode n0, ListNode P) { - ListNode? n1 = n0.next; - P.next = n1; - n0.next = P; - } +/* 在链表的节点 n0 之后插入节点 P */ +void insert(ListNode n0, ListNode P) { + ListNode? n1 = n0.next; + P.next = n1; + n0.next = P; +} - /* 删除链表的节点 n0 之后的首个节点 */ - void remove(ListNode n0) { - if (n0.next == null) return; - ListNode P = n0.next!; - ListNode? n1 = P.next; - n0.next = n1; - } +/* 删除链表的节点 n0 之后的首个节点 */ +void remove(ListNode n0) { + if (n0.next == null) return; + ListNode P = n0.next!; + ListNode? n1 = P.next; + n0.next = n1; +} - /* 访问链表中索引为 index 的节点 */ - ListNode? access(ListNode? head, int index) { - for (var i = 0; i < index; i++) { - if (head == null) return null; - head = head.next; +/* 访问链表中索引为 index 的节点 */ +ListNode? access(ListNode? head, int index) { + for (var i = 0; i < index; i++) { + if (head == null) return null; + head = head.next; + } + return head; +} + +/* 在链表中查找值为 target 的首个节点 */ +int find(ListNode? head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) { + return index; } - return head; - } - - /* 在链表中查找值为 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; + head = head.next; + index++; } + return -1; } /* Driver Code */ @@ -65,21 +63,21 @@ int main() { printLinkedList(n0); /* 插入节点 */ - LinkedList().insert(n0, ListNode(0)); + insert(n0, ListNode(0)); print('插入节点后的链表为'); printLinkedList(n0); /* 删除节点 */ - LinkedList().remove(n0); + remove(n0); print('删除节点后的链表为'); printLinkedList(n0); /* 访问节点 */ - ListNode? node = LinkedList().access(n0, 3); + ListNode? node = access(n0, 3); print('链表中索引 3 处的节点的值 = ${node!.val}'); /* 查找节点 */ - int index = LinkedList().find(n0, 2); + int index = find(n0, 2); print('链表中值为 2 的节点的索引 = $index'); return 0; diff --git a/codes/dart/chapter_computational_complexity/worst_best_time_complexity.dart b/codes/dart/chapter_computational_complexity/worst_best_time_complexity.dart index 2a3d6b283..35282fdca 100644 --- a/codes/dart/chapter_computational_complexity/worst_best_time_complexity.dart +++ b/codes/dart/chapter_computational_complexity/worst_best_time_complexity.dart @@ -24,6 +24,7 @@ int findOne(List nums) { // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) if (nums[i] == 1) return i; } + return -1; } @@ -36,5 +37,6 @@ int main() { print('\n数组 [ 1, 2, ..., n ] 被打乱后 = $nums'); print('数字 1 的索引为 + $index'); } + return 0; } diff --git a/docs/chapter_array_and_linkedlist/array.md b/docs/chapter_array_and_linkedlist/array.md index 3cf14830c..37537340b 100755 --- a/docs/chapter_array_and_linkedlist/array.md +++ b/docs/chapter_array_and_linkedlist/array.md @@ -92,6 +92,12 @@ var nums = [_]i32{ 1, 3, 2, 5, 4 }; ``` +=== "Dart" + + ```dart title="array.dart" + + ``` + ## 数组优点 **在数组中访问元素非常高效**。由于数组元素被存储在连续的内存空间中,因此计算数组元素的内存地址非常容易。给定数组首个元素的地址和某个元素的索引,我们可以使用以下公式计算得到该元素的内存地址,从而直接访问此元素。 @@ -171,6 +177,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{randomAccess} ``` +=== "Dart" + + ```dart title="array.dart" + [class]{}-[func]{randomAccess} + ``` + ## 数组缺点 **数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。 @@ -235,6 +247,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{extend} ``` +=== "Dart" + + ```dart title="array.dart" + [class]{}-[func]{extend} + ``` + **数组中插入或删除元素效率低下**。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。 ![数组插入元素](array.assets/array_insert_element.png) @@ -293,6 +311,18 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{insert} ``` +=== "Zig" + + ```zig title="array.zig" + [class]{}-[func]{insert} + ``` + +=== "Dart" + + ```dart title="array.dart" + [class]{}-[func]{insert} + ``` + 删除元素也类似,如果我们想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。 ![数组删除元素](array.assets/array_remove_element.png) @@ -357,6 +387,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{remove} ``` +=== "Dart" + + ```dart title="array.dart" + [class]{}-[func]{remove} + ``` + 总结来看,数组的插入与删除操作有以下缺点: - **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(n)$ ,其中 $n$ 为数组长度。 @@ -427,6 +463,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{traverse} ``` +=== "Dart" + + ```dart title="array.dart" + [class]{}-[func]{traverse} + ``` + **数组查找**。通过遍历数组,查找数组内的指定元素,并输出对应索引。 === "Java" @@ -489,6 +531,12 @@ elementAddr = firtstElementAddr + elementLength * elementIndex [class]{}-[func]{find} ``` +=== "Dart" + + ```dart title="array.dart" + [class]{}-[func]{find} + ``` + ## 数组典型应用 **随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 diff --git a/docs/chapter_array_and_linkedlist/linked_list.md b/docs/chapter_array_and_linkedlist/linked_list.md index 4edc16018..8753d77f3 100755 --- a/docs/chapter_array_and_linkedlist/linked_list.md +++ b/docs/chapter_array_and_linkedlist/linked_list.md @@ -153,6 +153,12 @@ } ``` +=== "Dart" + + ```dart title="" + + ``` + !!! question "尾节点指向什么?" 我们将链表的最后一个节点称为「尾节点」,其指向的是“空”,在 Java, C++, Python 中分别记为 $\text{null}$ , $\text{nullptr}$ , $\text{None}$ 。在不引起歧义的前提下,本书都使用 $\text{null}$ 来表示空。 @@ -333,6 +339,12 @@ n3.next = &n4; ``` +=== "Dart" + + ```dart title="linked_list.dart" + + ``` + ## 链表优点 **链表中插入与删除节点的操作效率高**。例如,如果我们想在链表中间的两个节点 `A` , `B` 之间插入一个新节点 `P` ,我们只需要改变两个节点指针即可,时间复杂度为 $O(1)$ ;相比之下,数组的插入操作效率要低得多。 @@ -399,6 +411,12 @@ [class]{}-[func]{insert} ``` +=== "Dart" + + ```dart title="linked_list.dart" + [class]{}-[func]{insert} + ``` + 在链表中删除节点也非常方便,只需改变一个节点的指针即可。如下图所示,尽管在删除操作完成后,节点 `P` 仍然指向 `n1` ,但实际上 `P` 已经不再属于此链表,因为遍历此链表时无法访问到 `P` 。 ![链表删除节点](linked_list.assets/linkedlist_remove_node.png) @@ -463,6 +481,12 @@ [class]{}-[func]{remove} ``` +=== "Dart" + + ```dart title="linked_list.dart" + [class]{}-[func]{remove} + ``` + ## 链表缺点 **链表访问节点效率较低**。如上节所述,数组可以在 $O(1)$ 时间下访问任意元素。然而,链表无法直接访问任意节点,这是因为系统需要从头节点出发,逐个向后遍历直至找到目标节点。例如,若要访问链表索引为 `index`(即第 `index + 1` 个)的节点,则需要向后遍历 `index` 轮。 @@ -527,6 +551,12 @@ [class]{}-[func]{access} ``` +=== "Dart" + + ```dart title="linked_list.dart" + [class]{}-[func]{access} + ``` + **链表的内存占用较大**。链表以节点为单位,每个节点除了保存值之外,还需额外保存指针(引用)。这意味着在相同数据量的情况下,链表比数组需要占用更多的内存空间。 ## 链表常用操作 @@ -593,6 +623,12 @@ [class]{}-[func]{find} ``` +=== "Dart" + + ```dart title="linked_list.dart" + [class]{}-[func]{find} + ``` + ## 常见链表类型 **单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的指针(引用)两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向 $\text{null}$ 。 @@ -760,4 +796,10 @@ } ``` +=== "Dart" + + ```dart title="" + + ``` + ![常见链表种类](linked_list.assets/linkedlist_common_types.png) diff --git a/docs/chapter_array_and_linkedlist/list.md b/docs/chapter_array_and_linkedlist/list.md index cbff25827..2d3b06d9e 100755 --- a/docs/chapter_array_and_linkedlist/list.md +++ b/docs/chapter_array_and_linkedlist/list.md @@ -106,6 +106,12 @@ try list.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 }); ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **访问与更新元素**。由于列表的底层数据结构是数组,因此可以在 $O(1)$ 时间内访问和更新元素,效率很高。 === "Java" @@ -204,6 +210,12 @@ list.items[1] = 0; // 将索引 1 处的元素更新为 0 ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **在列表中添加、插入、删除元素**。相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但插入和删除元素的效率仍与数组相同,时间复杂度为 $O(N)$ 。 === "Java" @@ -392,6 +404,12 @@ _ = list.orderedRemove(3); // 删除索引 3 处的元素 ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **遍历列表**。与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。 === "Java" @@ -545,6 +563,12 @@ } ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **拼接两个列表**。给定一个新列表 `list1` ,我们可以将该列表拼接到原列表的尾部。 === "Java" @@ -628,6 +652,12 @@ try list.insertSlice(list.items.len, list1.items); // 将列表 list1 拼接到 list 之后 ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + **排序列表**。排序也是常用的方法之一。完成列表排序后,我们便可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法。 === "Java" @@ -699,6 +729,12 @@ std.sort.sort(i32, list.items, {}, comptime std.sort.asc(i32)); ``` +=== "Dart" + + ```dart title="list.dart" + + ``` + ## 列表实现 * 为了帮助加深对列表的理解,我们在此提供一个简易版列表实现。需要关注三个核心点: @@ -768,3 +804,9 @@ ```zig title="my_list.zig" [class]{MyList}-[func]{} ``` + +=== "Dart" + + ```dart title="my_list.dart" + [class]{MyList}-[func]{} + ``` diff --git a/docs/chapter_backtracking/backtracking_algorithm.md b/docs/chapter_backtracking/backtracking_algorithm.md index 75bb89d05..b48e43770 100644 --- a/docs/chapter_backtracking/backtracking_algorithm.md +++ b/docs/chapter_backtracking/backtracking_algorithm.md @@ -70,6 +70,12 @@ [class]{}-[func]{preOrder} ``` +=== "Dart" + + ```dart title="preorder_traversal_i_compact.dart" + [class]{}-[func]{preOrder} + ``` + ![在前序遍历中搜索节点](backtracking_algorithm.assets/preorder_find_nodes.png) ## 尝试与回退 @@ -146,6 +152,12 @@ [class]{}-[func]{preOrder} ``` +=== "Dart" + + ```dart title="preorder_traversal_ii_compact.dart" + [class]{}-[func]{preOrder} + ``` + 在每次“尝试”中,我们通过将当前节点添加进 `path` 来记录路径;而在“回退”前,我们需要将该节点从 `path` 中弹出,**以恢复本次尝试之前的状态**。换句话说,**我们可以将尝试和回退理解为“前进”与“撤销”**,两个操作是互为相反的。 === "<1>" @@ -251,6 +263,12 @@ [class]{}-[func]{preOrder} ``` +=== "Dart" + + ```dart title="preorder_traversal_iii_compact.dart" + [class]{}-[func]{preOrder} + ``` + 剪枝是一个非常形象的名词。在搜索过程中,**我们利用约束条件“剪掉”了不满足约束条件的搜索分支**,避免许多无意义的尝试,从而提升搜索效率。 ![根据约束条件剪枝](backtracking_algorithm.assets/preorder_find_constrained_paths.png) @@ -505,6 +523,12 @@ ``` +=== "Dart" + + ```dart title="" + + ``` + 下面,我们尝试基于此框架来解决例题三。在例题三中,状态 `state` 是节点遍历路径,选择 `choices` 是当前节点的左子节点和右子节点,结果 `res` 是路径列表,实现代码如下所示。 === "Java" @@ -667,6 +691,22 @@ [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` ,并实现框架中的各个方法。 ## 典型例题 diff --git a/docs/chapter_backtracking/n_queens_problem.md b/docs/chapter_backtracking/n_queens_problem.md index f544b8007..d1fb01db8 100644 --- a/docs/chapter_backtracking/n_queens_problem.md +++ b/docs/chapter_backtracking/n_queens_problem.md @@ -110,6 +110,14 @@ [class]{}-[func]{nQueens} ``` +=== "Dart" + + ```dart title="n_queens.dart" + [class]{}-[func]{backtrack} + + [class]{}-[func]{nQueens} + ``` + ## 复杂度分析 逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n, n-1, \cdots, 2, 1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。 diff --git a/docs/chapter_backtracking/permutations_problem.md b/docs/chapter_backtracking/permutations_problem.md index 94e46b7f5..38d137435 100644 --- a/docs/chapter_backtracking/permutations_problem.md +++ b/docs/chapter_backtracking/permutations_problem.md @@ -110,6 +110,14 @@ [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!)$** 。 @@ -214,6 +222,14 @@ [class]{}-[func]{permutationsII} ``` +=== "Dart" + + ```dart title="permutations_ii.dart" + [class]{}-[func]{backtrack} + + [class]{}-[func]{permutationsII} + ``` + 注意,虽然 `selected` 和 `duplicated` 都起到剪枝的作用,但他们剪掉的是不同的分支: - **剪枝条件一**:整个搜索过程中只有一个 `selected` 。它记录的是当前状态中包含哪些元素,作用是避免某个元素在 `state` 中重复出现。 diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index d47df2421..0cd426d64 100755 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -258,6 +258,12 @@ ``` +=== "Dart" + + ```dart title="" + + ``` + ## 推算方法 空间复杂度的推算方法与时间复杂度大致相同,只是将统计对象从“计算操作数量”转为“使用空间大小”。与时间复杂度不同的是,**我们通常只关注「最差空间复杂度」**,这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。 @@ -380,6 +386,12 @@ ``` +=== "Dart" + + ```dart title="" + + ``` + **在递归函数中,需要注意统计栈帧空间**。例如,函数 `loop()` 在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行过程中会同时存在 $n$ 个未返回的 `recur()` ,从而占用 $O(n)$ 的栈帧空间。 === "Java" @@ -579,6 +591,12 @@ ``` +=== "Dart" + + ```dart title="" + + ``` + ## 常见类型 设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列) @@ -662,6 +680,12 @@ $$ [class]{}-[func]{constant} ``` +=== "Dart" + + ```dart title="space_complexity.dart" + [class]{}-[func]{constant} + ``` + ### 线性阶 $O(n)$ 线性阶常见于元素数量与 $n$ 成正比的数组、链表、栈、队列等。 @@ -728,6 +752,12 @@ $$ [class]{}-[func]{linear} ``` +=== "Dart" + + ```dart title="space_complexity.dart" + [class]{}-[func]{linear} + ``` + 以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间。 === "Java" @@ -790,6 +820,12 @@ $$ [class]{}-[func]{linearRecur} ``` +=== "Dart" + + ```dart title="space_complexity.dart" + [class]{}-[func]{linearRecur} + ``` + ![递归函数产生的线性阶空间复杂度](space_complexity.assets/space_complexity_recursive_linear.png) ### 平方阶 $O(n^2)$ @@ -856,6 +892,12 @@ $$ [class]{}-[func]{quadratic} ``` +=== "Dart" + + ```dart title="space_complexity.dart" + [class]{}-[func]{quadratic} + ``` + 在以下递归函数中,同时存在 $n$ 个未返回的 `algorithm()` ,并且每个函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $\frac{n}{2}$ ,因此总体占用 $O(n^2)$ 空间。 === "Java" @@ -918,6 +960,12 @@ $$ [class]{}-[func]{quadraticRecur} ``` +=== "Dart" + + ```dart title="space_complexity.dart" + [class]{}-[func]{quadraticRecur} + ``` + ![递归函数产生的平方阶空间复杂度](space_complexity.assets/space_complexity_recursive_quadratic.png) ### 指数阶 $O(2^n)$ @@ -984,6 +1032,12 @@ $$ [class]{}-[func]{buildTree} ``` +=== "Dart" + + ```dart title="space_complexity.dart" + [class]{}-[func]{buildTree} + ``` + ![满二叉树产生的指数阶空间复杂度](space_complexity.assets/space_complexity_exponential.png) ### 对数阶 $O(\log n)$ diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index 9718a10cc..58fd8be2a 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -155,6 +155,12 @@ $$ ``` +=== "Dart" + + ```dart title="" + + ``` + 然而实际上,**统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。 ## 统计时间增长趋势 @@ -365,6 +371,12 @@ $$ ``` +=== "Dart" + + ```dart title="" + + ``` + ![算法 A, B, C 的时间增长趋势](time_complexity.assets/time_complexity_simple_example.png) 相较于直接统计算法运行时间,时间复杂度分析有哪些优势和局限性呢? @@ -515,6 +527,12 @@ $$ ``` +=== "Dart" + + ```dart title="" + + ``` + $T(n)$ 是一次函数,说明时间增长趋势是线性的,因此可以得出时间复杂度是线性阶。 我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 Big-$O$ Notation」,表示函数 $T(n)$ 的「渐近上界 Asymptotic Upper Bound」。 @@ -739,6 +757,12 @@ $$ ``` +=== "Dart" + + ```dart title="" + + ``` + ### 2) 判断渐近上界 **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。 @@ -840,6 +864,12 @@ $$ [class]{}-[func]{constant} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{constant} + ``` + ### 线性阶 $O(n)$ 线性阶的操作数量相对于输入数据大小以线性级别增长。线性阶通常出现在单层循环中。 @@ -904,6 +934,12 @@ $$ [class]{}-[func]{linear} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{linear} + ``` + 遍历数组和遍历链表等操作的时间复杂度均为 $O(n)$ ,其中 $n$ 为数组或链表的长度。 !!! question "如何确定输入数据大小 $n$ ?" @@ -970,6 +1006,12 @@ $$ [class]{}-[func]{arrayTraversal} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{arrayTraversal} + ``` + ### 平方阶 $O(n^2)$ 平方阶的操作数量相对于输入数据大小以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ 。 @@ -1034,6 +1076,12 @@ $$ [class]{}-[func]{quadratic} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{quadratic} + ``` + ![常数阶、线性阶、平方阶的时间复杂度](time_complexity.assets/time_complexity_constant_linear_quadratic.png) 以「冒泡排序」为例,外层循环执行 $n - 1$ 次,内层循环执行 $n-1, n-2, \cdots, 2, 1$ 次,平均为 $\frac{n}{2}$ 次,因此时间复杂度为 $O(n^2)$ 。 @@ -1102,6 +1150,12 @@ $$ [class]{}-[func]{bubbleSort} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{bubbleSort} + ``` + ### 指数阶 $O(2^n)$ !!! note @@ -1170,6 +1224,12 @@ $$ [class]{}-[func]{exponential} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{exponential} + ``` + ![指数阶的时间复杂度](time_complexity.assets/time_complexity_exponential.png) 在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,经过 $n$ 次分裂后停止。 @@ -1234,6 +1294,12 @@ $$ [class]{}-[func]{expRecur} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{expRecur} + ``` + ### 对数阶 $O(\log n)$ 与指数阶相反,对数阶反映了“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长缓慢,是理想的时间复杂度。 @@ -1302,6 +1368,12 @@ $$ [class]{}-[func]{logarithmic} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{logarithmic} + ``` + ![对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic.png) 与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 $\log_2 n$ 的递归树。 @@ -1366,6 +1438,12 @@ $$ [class]{}-[func]{logRecur} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{logRecur} + ``` + ### 线性对数阶 $O(n \log n)$ 线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 $O(\log n)$ 和 $O(n)$ 。 @@ -1432,6 +1510,12 @@ $$ [class]{}-[func]{linearLogRecur} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{linearLogRecur} + ``` + ![线性对数阶的时间复杂度](time_complexity.assets/time_complexity_logarithmic_linear.png) ### 阶乘阶 $O(n!)$ @@ -1504,6 +1588,12 @@ $$ [class]{}-[func]{factorialRecur} ``` +=== "Dart" + + ```dart title="time_complexity.dart" + [class]{}-[func]{factorialRecur} + ``` + ![阶乘阶的时间复杂度](time_complexity.assets/time_complexity_factorial.png) ## 最差、最佳、平均时间复杂度 @@ -1614,6 +1704,14 @@ $$ } ``` +=== "Dart" + + ```dart title="worst_best_time_complexity.dart" + [class]{}-[func]{randomNumbers} + + [class]{}-[func]{findOne} + ``` + !!! tip 实际应用中我们很少使用「最佳时间复杂度」,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。相反,「最差时间复杂度」更为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。 diff --git a/docs/chapter_data_structure/basic_data_types.md b/docs/chapter_data_structure/basic_data_types.md index 6773f7268..d6f9e1043 100644 --- a/docs/chapter_data_structure/basic_data_types.md +++ b/docs/chapter_data_structure/basic_data_types.md @@ -129,3 +129,9 @@ ```zig title="" ``` + +=== "Dart" + + ```dart title="" + + ``` diff --git a/docs/chapter_graph/graph_operations.md b/docs/chapter_graph/graph_operations.md index 675b22575..c5d4c92e4 100644 --- a/docs/chapter_graph/graph_operations.md +++ b/docs/chapter_graph/graph_operations.md @@ -88,6 +88,12 @@ ``` +=== "Dart" + + ```dart title="graph_adjacency_matrix.dart" + [class]{GraphAdjMat}-[func]{} + ``` + ## 基于邻接表的实现 设无向图的顶点总数为 $n$ 、边总数为 $m$ ,则有: @@ -179,6 +185,12 @@ [class]{GraphAdjList}-[func]{} ``` +=== "Dart" + + ```dart title="graph_adjacency_list.dart" + [class]{GraphAdjList}-[func]{} + ``` + ## 效率对比 设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。 diff --git a/docs/chapter_graph/graph_traversal.md b/docs/chapter_graph/graph_traversal.md index b568e39dc..a02dfabf1 100644 --- a/docs/chapter_graph/graph_traversal.md +++ b/docs/chapter_graph/graph_traversal.md @@ -84,6 +84,12 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 [class]{}-[func]{graphBFS} ``` +=== "Dart" + + ```dart title="graph_bfs.dart" + [class]{}-[func]{graphBFS} + ``` + 代码相对抽象,建议对照以下动画图示来加深理解。 === "<1>" @@ -219,6 +225,14 @@ BFS 通常借助「队列」来实现。队列具有“先入先出”的性质 [class]{}-[func]{graphDFS} ``` +=== "Dart" + + ```dart title="graph_dfs.dart" + [class]{}-[func]{dfs} + + [class]{}-[func]{graphDFS} + ``` + 深度优先遍历的算法流程如下图所示,其中: - **直虚线代表向下递推**,表示开启了一个新的递归方法来访问新顶点; diff --git a/docs/chapter_hashing/hash_map.md b/docs/chapter_hashing/hash_map.md index 5a0716980..801b6c77e 100755 --- a/docs/chapter_hashing/hash_map.md +++ b/docs/chapter_hashing/hash_map.md @@ -223,6 +223,12 @@ ``` +=== "Dart" + + ```dart title="hash_map.dart" + + ``` + 遍历哈希表有三种方式,即 **遍历键值对、遍历键、遍历值**。 === "Java" @@ -381,6 +387,12 @@ ``` +=== "Dart" + + ```dart title="hash_map.dart" + + ``` + ## 哈希函数 哈希表的底层实现为数组,同时可能包含链表、二叉树(红黑树)等数据结构,以提高查询性能(将在下节讨论)。 @@ -484,6 +496,14 @@ $$ [class]{ArrayHashMap}-[func]{} ``` +=== "Dart" + + ```dart title="array_hash_map.dart" + [class]{Entry}-[func]{} + + [class]{ArrayHashMap}-[func]{} + ``` + ## 哈希冲突 细心的你可能已经注意到,**在某些情况下,哈希函数 $f(x) = x \bmod 100$ 可能无法正常工作**。具体来说,当输入的 key 后两位相同时,哈希函数的计算结果也会相同,从而指向同一个 value 。例如,查询学号为 $12836$ 和 $20336$ 的两个学生时,我们得到: diff --git a/docs/chapter_heap/build_heap.md b/docs/chapter_heap/build_heap.md index 11bc0d643..d50365dcd 100644 --- a/docs/chapter_heap/build_heap.md +++ b/docs/chapter_heap/build_heap.md @@ -72,6 +72,12 @@ [class]{MaxHeap}-[func]{init} ``` +=== "Dart" + + ```dart title="my_heap.dart" + [class]{MaxHeap}-[func]{MaxHeap} + ``` + ## 复杂度分析 为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。 diff --git a/docs/chapter_heap/heap.md b/docs/chapter_heap/heap.md index 23f18d885..ab20d8b75 100644 --- a/docs/chapter_heap/heap.md +++ b/docs/chapter_heap/heap.md @@ -301,6 +301,12 @@ ``` +=== "Dart" + + ```dart title="heap.dart" + + ``` + ## 堆的实现 下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。 @@ -417,6 +423,16 @@ [class]{MaxHeap}-[func]{parent} ``` +=== "Dart" + + ```dart title="my_heap.dart" + [class]{MaxHeap}-[func]{_left} + + [class]{MaxHeap}-[func]{_right} + + [class]{MaxHeap}-[func]{_parent} + ``` + ### 访问堆顶元素 堆顶元素即为二叉树的根节点,也就是列表的首个元素。 @@ -481,6 +497,12 @@ [class]{MaxHeap}-[func]{peek} ``` +=== "Dart" + + ```dart title="my_heap.dart" + [class]{MaxHeap}-[func]{peek} + ``` + ### 元素入堆 给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 Heapify」。 @@ -596,6 +618,14 @@ [class]{MaxHeap}-[func]{siftUp} ``` +=== "Dart" + + ```dart title="my_heap.dart" + [class]{MaxHeap}-[func]{push} + + [class]{MaxHeap}-[func]{siftUp} + ``` + ### 堆顶元素出堆 堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采取以下操作步骤: @@ -718,6 +748,14 @@ [class]{MaxHeap}-[func]{siftDown} ``` +=== "Dart" + + ```dart title="my_heap.dart" + [class]{MaxHeap}-[func]{pop} + + [class]{MaxHeap}-[func]{siftDown} + ``` + ## 堆常见应用 - **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。 diff --git a/docs/chapter_preface/suggestions.md b/docs/chapter_preface/suggestions.md index 6d4dd9bc8..1551bb9bb 100644 --- a/docs/chapter_preface/suggestions.md +++ b/docs/chapter_preface/suggestions.md @@ -144,6 +144,17 @@ // 注释 ``` +=== "Dart" + + ```dart title="" + // 标题注释,用于标注函数、类、测试样例等 + + // 内容注释,用于详解代码 + + // 多行 + // 注释 + ``` + ## 在动画图解中高效学习 相较于文字,视频和图片具有更高的信息密度和结构化程度,因此更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。 diff --git a/docs/chapter_searching/binary_search.md b/docs/chapter_searching/binary_search.md index 9600e140b..6ba6729af 100755 --- a/docs/chapter_searching/binary_search.md +++ b/docs/chapter_searching/binary_search.md @@ -104,6 +104,12 @@ [class]{}-[func]{binarySearch} ``` +=== "Dart" + + ```dart title="binary_search.dart" + [class]{}-[func]{binarySearch} + ``` + 时间复杂度为 $O(\log n)$ 。每轮缩小一半区间,因此二分循环次数为 $\log_2 n$ 。 空间复杂度为 $O(1)$ 。指针 `i` , `j` 使用常数大小空间。 @@ -174,6 +180,12 @@ [class]{}-[func]{binarySearchLCRO} ``` +=== "Dart" + + ```dart title="binary_search.dart" + [class]{}-[func]{binarySearchLCRO} + ``` + 如下图所示,在两种区间表示下,二分查找算法的初始化、循环条件和缩小区间操作皆有所不同。 在“双闭区间”表示法中,由于左右边界都被定义为闭区间,因此指针 $i$ 和 $j$ 缩小区间操作也是对称的。这样更不容易出错。因此,**我们通常采用“双闭区间”的写法**。 diff --git a/docs/chapter_searching/binary_search_edge.md b/docs/chapter_searching/binary_search_edge.md index 0527e20df..bc70f0873 100644 --- a/docs/chapter_searching/binary_search_edge.md +++ b/docs/chapter_searching/binary_search_edge.md @@ -112,6 +112,12 @@ [class]{}-[func]{binarySearchLeftEdge} ``` +=== "Dart" + + ```dart title="binary_search_edge.dart" + [class]{}-[func]{binarySearchLeftEdge} + ``` + ## 查找右边界 类似地,我们也可以二分查找最右边的 `target` 。当 `nums[m] == target` 时,说明大于 `target` 的元素在区间 $[m + 1, j]$ 中,因此执行 `i = m + 1` ,**使得指针 $i$ 向大于 `target` 的元素靠近**。 @@ -178,6 +184,12 @@ [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/docs/chapter_searching/replace_linear_by_hashing.md b/docs/chapter_searching/replace_linear_by_hashing.md index e06f9b9b4..59fa35c42 100755 --- a/docs/chapter_searching/replace_linear_by_hashing.md +++ b/docs/chapter_searching/replace_linear_by_hashing.md @@ -72,6 +72,12 @@ [class]{}-[func]{twoSumBruteForce} ``` +=== "Dart" + + ```dart title="two_sum.dart" + [class]{}-[func]{twoSumBruteForce} + ``` + 此方法的时间复杂度为 $O(n^2)$ ,空间复杂度为 $O(1)$ ,在大数据量下非常耗时。 ## 哈希查找:以空间换时间 @@ -154,6 +160,12 @@ [class]{}-[func]{twoSumHashTable} ``` +=== "Dart" + + ```dart title="two_sum.dart" + [class]{}-[func]{twoSumHashTable} + ``` + 此方法通过哈希查找将时间复杂度从 $O(n^2)$ 降低至 $O(n)$ ,大幅提升运行效率。 由于需要维护一个额外的哈希表,因此空间复杂度为 $O(n)$ 。**尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法**。 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index 85ffd9d81..cfbe82f86 100755 --- a/docs/chapter_sorting/bubble_sort.md +++ b/docs/chapter_sorting/bubble_sort.md @@ -96,6 +96,12 @@ [class]{}-[func]{bubbleSort} ``` +=== "Dart" + + ```dart title="bubble_sort.dart" + [class]{}-[func]{bubbleSort} + ``` + ## 效率优化 我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。 @@ -162,6 +168,12 @@ [class]{}-[func]{bubbleSortWithFlag} ``` +=== "Dart" + + ```dart title="bubble_sort.dart" + [class]{}-[func]{bubbleSortWithFlag} + ``` + ## 算法特性 - **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。 diff --git a/docs/chapter_sorting/bucket_sort.md b/docs/chapter_sorting/bucket_sort.md index b872889bd..bcb40b7c8 100644 --- a/docs/chapter_sorting/bucket_sort.md +++ b/docs/chapter_sorting/bucket_sort.md @@ -74,6 +74,12 @@ [class]{}-[func]{bucketSort} ``` +=== "Dart" + + ```dart title="bucket_sort.dart" + [class]{}-[func]{bucketSort} + ``` + !!! question "桶排序的适用场景是什么?" 桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。 diff --git a/docs/chapter_sorting/counting_sort.md b/docs/chapter_sorting/counting_sort.md index 22a239d9e..c40d0a2bd 100644 --- a/docs/chapter_sorting/counting_sort.md +++ b/docs/chapter_sorting/counting_sort.md @@ -72,6 +72,12 @@ [class]{}-[func]{countingSortNaive} ``` +=== "Dart" + + ```dart title="counting_sort.dart" + [class]{}-[func]{countingSortNaive} + ``` + !!! note "计数排序与桶排序的联系" 从桶排序的角度看,我们可以将计数排序中的计数数组 `counter` 的每个索引视为一个桶,将统计数量的过程看作是将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。 @@ -179,6 +185,12 @@ $$ [class]{}-[func]{countingSort} ``` +=== "Dart" + + ```dart title="counting_sort.dart" + [class]{}-[func]{countingSort} + ``` + ## 算法特性 - **时间复杂度 $O(n + m)$** :涉及遍历 `nums` 和遍历 `counter` ,都使用线性时间。一般情况下 $n \gg m$ ,时间复杂度趋于 $O(n)$ 。 diff --git a/docs/chapter_sorting/heap_sort.md b/docs/chapter_sorting/heap_sort.md index 88fe9e794..ed60e3fa2 100644 --- a/docs/chapter_sorting/heap_sort.md +++ b/docs/chapter_sorting/heap_sort.md @@ -140,6 +140,14 @@ [class]{}-[func]{heapSort} ``` +=== "Dart" + + ```dart title="heap_sort.dart" + [class]{}-[func]{siftDown} + + [class]{}-[func]{heapSort} + ``` + ## 算法特性 - **时间复杂度 $O(n \log n)$ 、非自适应排序** :建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。 diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index 7de52cb70..21f746f7d 100755 --- a/docs/chapter_sorting/insertion_sort.md +++ b/docs/chapter_sorting/insertion_sort.md @@ -79,6 +79,12 @@ [class]{}-[func]{insertionSort} ``` +=== "Dart" + + ```dart title="insertion_sort.dart" + [class]{}-[func]{insertionSort} + ``` + ## 算法特性 - **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。 diff --git a/docs/chapter_sorting/merge_sort.md b/docs/chapter_sorting/merge_sort.md index 8d8ff2c29..d1d2f169d 100755 --- a/docs/chapter_sorting/merge_sort.md +++ b/docs/chapter_sorting/merge_sort.md @@ -131,6 +131,14 @@ [class]{}-[func]{mergeSort} ``` +=== "Dart" + + ```dart title="merge_sort.dart" + [class]{}-[func]{merge} + + [class]{}-[func]{mergeSort} + ``` + 合并方法 `merge()` 代码中的难点包括: - **在阅读代码时,需要特别注意各个变量的含义**。`nums` 的待合并区间为 `[left, right]` ,但由于 `tmp` 仅复制了 `nums` 该区间的元素,因此 `tmp` 对应区间为 `[0, right - left]` 。 diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index cec3deba5..b81ffb23d 100755 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -117,6 +117,14 @@ [class]{QuickSort}-[func]{partition} ``` +=== "Dart" + + ```dart title="quick_sort.dart" + [class]{QuickSort}-[func]{_swap} + + [class]{QuickSort}-[func]{_partition} + ``` + ## 算法流程 1. 首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组; @@ -185,6 +193,12 @@ [class]{QuickSort}-[func]{quickSort} ``` +=== "Dart" + + ```dart title="quick_sort.dart" + [class]{QuickSort}-[func]{quickSort} + ``` + ## 算法特性 - **时间复杂度 $O(n \log n)$ 、自适应排序** :在平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。在最差情况下,每轮哨兵划分操作都将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。 @@ -289,6 +303,14 @@ [class]{QuickSortMedian}-[func]{partition} ``` +=== "Dart" + + ```dart title="quick_sort.dart" + [class]{QuickSortMedian}-[func]{_medianThree} + + [class]{QuickSortMedian}-[func]{_partition} + ``` + ## 尾递归优化 **在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。 @@ -354,3 +376,9 @@ ```zig title="quick_sort.zig" [class]{QuickSortTailCall}-[func]{quickSort} ``` + +=== "Dart" + + ```dart title="quick_sort.dart" + [class]{QuickSortTailCall}-[func]{quickSort} + ``` diff --git a/docs/chapter_sorting/radix_sort.md b/docs/chapter_sorting/radix_sort.md index 413c0bcca..acadcb506 100644 --- a/docs/chapter_sorting/radix_sort.md +++ b/docs/chapter_sorting/radix_sort.md @@ -124,6 +124,16 @@ $$ [class]{}-[func]{radixSort} ``` +=== "Dart" + + ```dart title="radix_sort.dart" + [class]{}-[func]{digit} + + [class]{}-[func]{countingSortDigit} + + [class]{}-[func]{radixSort} + ``` + !!! question "为什么从最低位开始排序?" 在连续的排序轮次中,后一轮排序会覆盖前一轮排序的结果。举例来说,如果第一轮排序结果 $a < b$ ,而第二轮排序结果 $a > b$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。 diff --git a/docs/chapter_sorting/selection_sort.md b/docs/chapter_sorting/selection_sort.md index 7a17fa6f2..daaffd86d 100644 --- a/docs/chapter_sorting/selection_sort.md +++ b/docs/chapter_sorting/selection_sort.md @@ -105,6 +105,12 @@ [class]{}-[func]{selectionSort} ``` +=== "Dart" + + ```dart title="selection_sort.dart" + [class]{}-[func]{selectionSort} + ``` + ## 算法特性 - **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。 diff --git a/docs/chapter_stack_and_queue/deque.md b/docs/chapter_stack_and_queue/deque.md index 8d2576c87..d761b6caa 100644 --- a/docs/chapter_stack_and_queue/deque.md +++ b/docs/chapter_stack_and_queue/deque.md @@ -283,6 +283,12 @@ ``` +=== "Dart" + + ```dart title="deque.dart" + + ``` + ## 双向队列实现 * 双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。 @@ -390,6 +396,14 @@ [class]{LinkedListDeque}-[func]{} ``` +=== "Dart" + + ```dart title="linkedlist_deque.dart" + [class]{ListNode}-[func]{} + + [class]{LinkedListDeque}-[func]{} + ``` + ### 基于数组的实现 与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列。在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法。 @@ -471,6 +485,12 @@ [class]{ArrayDeque}-[func]{} ``` +=== "Dart" + + ```dart title="array_deque.dart" + [class]{ArrayDeque}-[func]{} + ``` + ## 双向队列应用 双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。 diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index 28677e5e3..6091944b9 100755 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -252,6 +252,12 @@ ``` +=== "Dart" + + ```dart title="queue.dart" + + ``` + ## 队列实现 为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列。 @@ -331,6 +337,12 @@ [class]{LinkedListQueue}-[func]{} ``` +=== "Dart" + + ```dart title="linkedlist_queue.dart" + [class]{LinkedListQueue}-[func]{} + ``` + ### 基于数组的实现 由于数组删除首元素的时间复杂度为 $O(n)$ ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。 @@ -417,6 +429,12 @@ [class]{ArrayQueue}-[func]{} ``` +=== "Dart" + + ```dart title="array_queue.dart" + [class]{ArrayQueue}-[func]{} + ``` + 以上实现的队列仍然具有局限性,即其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的同学可以尝试自行实现。 两种实现的对比结论与栈一致,在此不再赘述。 diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index da852436e..8f1574d9c 100755 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -250,6 +250,12 @@ ``` +=== "Dart" + + ```dart title="stack.dart" + + ``` + ## 栈的实现 为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。 @@ -333,6 +339,12 @@ [class]{LinkedListStack}-[func]{} ``` +=== "Dart" + + ```dart title="linkedlist_stack.dart" + [class]{LinkedListStack}-[func]{} + ``` + ### 基于数组的实现 在基于「数组」实现栈时,我们可以将数组的尾部作为栈顶。在这样的设计下,入栈与出栈操作就分别对应在数组尾部添加元素与删除元素,时间复杂度都为 $O(1)$ 。 @@ -408,6 +420,12 @@ [class]{ArrayStack}-[func]{} ``` +=== "Dart" + + ```dart title="array_stack.dart" + [class]{ArrayStack}-[func]{} + ``` + ## 两种实现对比 ### 支持操作 diff --git a/docs/chapter_tree/array_representation_of_tree.md b/docs/chapter_tree/array_representation_of_tree.md index 704e61442..952b08e60 100644 --- a/docs/chapter_tree/array_representation_of_tree.md +++ b/docs/chapter_tree/array_representation_of_tree.md @@ -100,6 +100,12 @@ ``` +=== "Dart" + + ```dart title="" + + ``` + ![任意类型二叉树的数组表示](binary_tree.assets/array_representation_with_empty.png) ## 优势与局限性 diff --git a/docs/chapter_tree/avl_tree.md b/docs/chapter_tree/avl_tree.md index a8e5b091e..90c2e094b 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -169,6 +169,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit ``` +=== "Dart" + + ```dart title="" + + ``` + 「节点高度」是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。 === "Java" @@ -251,6 +257,14 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit [class]{AVLTree}-[func]{updateHeight} ``` +=== "Dart" + + ```dart title="avl_tree.dart" + [class]{AVLTree}-[func]{height} + + [class]{AVLTree}-[func]{updateHeight} + ``` + ### 节点平衡因子 节点的「平衡因子 Balance Factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。 @@ -315,6 +329,12 @@ G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorit [class]{AVLTree}-[func]{balanceFactor} ``` +=== "Dart" + + ```dart title="avl_tree.dart" + [class]{AVLTree}-[func]{balanceFactor} + ``` + !!! note 设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。 @@ -407,6 +427,12 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{rightRotate} ``` +=== "Dart" + + ```dart title="avl_tree.dart" + [class]{AVLTree}-[func]{rightRotate} + ``` + ### 左旋 相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。 @@ -479,6 +505,12 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{leftRotate} ``` +=== "Dart" + + ```dart title="avl_tree.dart" + [class]{AVLTree}-[func]{leftRotate} + ``` + ### 先左旋后右旋 对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 @@ -572,6 +604,12 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{rotate} ``` +=== "Dart" + + ```dart title="avl_tree.dart" + [class]{AVLTree}-[func]{rotate} + ``` + ## AVL 树常用操作 ### 插入节点 @@ -658,6 +696,14 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{insertHelper} ``` +=== "Dart" + + ```dart title="avl_tree.dart" + [class]{AVLTree}-[func]{insert} + + [class]{AVLTree}-[func]{insertHelper} + ``` + ### 删除节点 类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶地执行旋转操作,使所有失衡节点恢复平衡。 @@ -742,6 +788,14 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉 [class]{AVLTree}-[func]{removeHelper} ``` +=== "Dart" + + ```dart title="avl_tree.dart" + [class]{AVLTree}-[func]{remove} + + [class]{AVLTree}-[func]{removeHelper} + ``` + ### 查找节点 AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。 diff --git a/docs/chapter_tree/binary_search_tree.md b/docs/chapter_tree/binary_search_tree.md index c477049ce..92ebbc943 100755 --- a/docs/chapter_tree/binary_search_tree.md +++ b/docs/chapter_tree/binary_search_tree.md @@ -91,6 +91,12 @@ [class]{BinarySearchTree}-[func]{search} ``` +=== "Dart" + + ```dart title="binary_search_tree.dart" + [class]{BinarySearchTree}-[func]{search} + ``` + ### 插入节点 给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作分为两步: @@ -162,6 +168,12 @@ [class]{BinarySearchTree}-[func]{insert} ``` +=== "Dart" + + ```dart title="binary_search_tree.dart" + [class]{BinarySearchTree}-[func]{insert} + ``` + 为了插入节点,我们需要利用辅助节点 `pre` 保存上一轮循环的节点,这样在遍历至 $\text{null}$ 时,我们可以获取到其父节点,从而完成节点插入操作。 与查找节点相同,插入节点使用 $O(\log n)$ 时间。 @@ -258,6 +270,12 @@ [class]{BinarySearchTree}-[func]{remove} ``` +=== "Dart" + + ```dart title="binary_search_tree.dart" + [class]{BinarySearchTree}-[func]{remove} + ``` + ### 排序 我们知道,二叉树的中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。 diff --git a/docs/chapter_tree/binary_tree.md b/docs/chapter_tree/binary_tree.md index eb861598a..4ed829eec 100644 --- a/docs/chapter_tree/binary_tree.md +++ b/docs/chapter_tree/binary_tree.md @@ -143,6 +143,12 @@ ``` +=== "Dart" + + ```dart title="" + + ``` + 节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树」。 **在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。例如,在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。 @@ -329,6 +335,12 @@ ``` +=== "Dart" + + ```dart title="binary_tree.dart" + + ``` + **插入与删除节点**。与链表类似,通过修改指针来实现插入与删除节点。 ![在二叉树中插入与删除节点](binary_tree.assets/binary_tree_add_remove.png) @@ -445,6 +457,12 @@ ``` +=== "Dart" + + ```dart title="binary_tree.dart" + + ``` + !!! note 需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除操作通常是由一套操作配合完成的,以实现有实际意义的操作。 diff --git a/docs/chapter_tree/binary_tree_traversal.md b/docs/chapter_tree/binary_tree_traversal.md index 0e5902552..4a7925896 100755 --- a/docs/chapter_tree/binary_tree_traversal.md +++ b/docs/chapter_tree/binary_tree_traversal.md @@ -76,6 +76,12 @@ [class]{}-[func]{levelOrder} ``` +=== "Dart" + + ```dart title="binary_tree_bfs.dart" + [class]{}-[func]{levelOrder} + ``` + ### 复杂度分析 **时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 @@ -202,6 +208,16 @@ [class]{}-[func]{postOrder} ``` +=== "Dart" + + ```dart title="binary_tree_dfs.dart" + [class]{}-[func]{preOrder} + + [class]{}-[func]{inOrder} + + [class]{}-[func]{postOrder} + ``` + !!! note 我们也可以仅基于循环实现前、中、后序遍历,有兴趣的同学可以自行实现。