diff --git a/chapter_array_and_linkedlist/array.md b/chapter_array_and_linkedlist/array.md index d3e6875ae..7008d9644 100755 --- a/chapter_array_and_linkedlist/array.md +++ b/chapter_array_and_linkedlist/array.md @@ -10,7 +10,7 @@ comments: true

Fig. 数组定义与存储方式

-**数组初始化**。通常有无初始值和给定初始值两种方式,我们可根据需求选择合适的方法。在未给定初始值的情况下,数组的所有元素通常会被初始化为默认值 $0$ 。 +**数组初始化**。通常有无初始值和给定初始值两种方式,我们可根据需求选择合适的方法。在大多数编程语言中,若未指定初始值,数组的所有元素通常会被默认初始化为 $0$ 。 === "Java" @@ -131,7 +131,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex 观察上图,我们发现数组首个元素的索引为 $0$ ,这似乎有些反直觉,因为从 $1$ 开始计数会更自然。 - 然而,从地址计算公式的角度看,**索引本质上表示的是内存地址的偏移量**。首个元素的地址偏移量是 $0$ ,因此索引为 $0$ 也是合理的。 + 然而从地址计算公式的角度看,**索引本质上表示的是内存地址的偏移量**。首个元素的地址偏移量是 $0$ ,因此索引为 $0$ 也是合理的。 访问元素的高效性带来了诸多便利。例如,我们可以在 $O(1)$ 时间内随机获取数组中的任意一个元素。 @@ -293,7 +293,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex ## 4.1.2.   数组缺点 -**数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。 +**数组在初始化后长度不可变**。系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组。在数组很大的情况下,这是非常耗时的。 === "Java" @@ -494,7 +494,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` -**数组中插入或删除元素效率低下**。如果我们想要在数组中间插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。 +**数组中插入或删除元素效率低下**。数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。这意味着如果我们想要在数组中间插入一个元素,就不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。 ![数组插入元素](array.assets/array_insert_element.png) @@ -667,7 +667,7 @@ elementAddr = firtstElementAddr + elementLength * elementIndex } ``` -删除元素也类似,如果我们想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,我们无需特意去修改它。 +删除元素也类似,如果我们想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。值得注意的是,删除元素后,原先末尾的元素变得“无意义”了,所以我们无需特意去修改它。 ![数组删除元素](array.assets/array_remove_element.png) @@ -1196,6 +1196,6 @@ elementAddr = firtstElementAddr + elementLength * elementIndex - **随机访问**:如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 - **排序和搜索**:数组是排序和搜索算法最常用的数据结构。例如,快速排序、归并排序、二分查找等都需要在数组上进行。 -- **查找表**:当我们需要快速查找一个元素或者需要查找一个元素的对应关系时,可以使用数组作为查找表。例如,我们有一个字符到其 ASCII 码的映射,可以将字符的 ASCII 码值作为索引,对应的元素存放在数组中的对应位置。 +- **查找表**:当我们需要快速查找一个元素或者需要查找一个元素的对应关系时,可以使用数组作为查找表。假如我们想要实现字符到 ASCII 码的映射,则可以将字符的 ASCII 码值作为索引,对应的元素存放在数组中的对应位置。 - **机器学习**:神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。 -- **数据结构实现**:数组可以用于实现栈、队列、哈希表、堆、图等数据结构。例如,邻接矩阵是图的常见表示之一,它实质上是一个二维数组。 +- **数据结构实现**:数组可以用于实现栈、队列、哈希表、堆、图等数据结构。例如,图的邻接矩阵表示实际上是一个二维数组。 diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index bd64775f6..1b833c4a8 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -8,7 +8,7 @@ comments: true 「链表 Linked List」是一种线性数据结构,其每个元素都是一个节点对象,各个节点之间通过指针连接,从当前节点通过指针可以访问到下一个节点。**由于指针记录了下个节点的内存地址,因此无需保证内存地址的连续性**,从而可以将各个节点分散存储在内存各处。 -链表中的「节点 Node」包含两项数据,一是节点「值 Value」,二是指向下一节点的「指针 Pointer」,或称「引用 Reference」。 +链表中的「节点 Node」包含两项数据,一是节点「值 Value」,二是指向下一节点的「引用 Reference」,或称「指针 Pointer」。 ![链表定义与存储方式](linked_list.assets/linkedlist_definition.png) diff --git a/chapter_backtracking/backtracking_algorithm.md b/chapter_backtracking/backtracking_algorithm.md index 6e3bd9b9f..f01440318 100644 --- a/chapter_backtracking/backtracking_algorithm.md +++ b/chapter_backtracking/backtracking_algorithm.md @@ -162,7 +162,18 @@ comments: true === "Dart" ```dart title="preorder_traversal_i_compact.dart" - [class]{}-[func]{preOrder} + /* 前序遍历:例题一 */ + void preOrder(TreeNode? root, List res) { + if (root == null) { + return; + } + if (root.val == 7) { + // 记录解 + res.add(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } ``` === "Rust" @@ -407,7 +418,27 @@ comments: true === "Dart" ```dart title="preorder_traversal_ii_compact.dart" - [class]{}-[func]{preOrder} + /* 前序遍历:例题二 */ + void preOrder( + TreeNode? root, + List path, + List> res, + ) { + if (root == null) { + return; + } + + // 尝试 + path.add(root); + if (root.val == 7) { + // 记录解 + res.add(List.from(path)); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // 回退 + path.removeLast(); + } ``` === "Rust" @@ -711,7 +742,29 @@ comments: true === "Dart" ```dart title="preorder_traversal_iii_compact.dart" - [class]{}-[func]{preOrder} + /* 前序遍历:例题三 */ + void preOrder( + TreeNode? root, + List path, + List> res, + ) { + if (root == null || root.val == 3) { + return; + } + + // 尝试 + path.add(root); + if (root.val == 7) { + // 记录解 + res.add(List.from(path)); + path.removeLast(); + return; + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // 回退 + path.removeLast(); + } ``` === "Rust" @@ -1499,17 +1552,55 @@ comments: true === "Dart" ```dart title="preorder_traversal_iii_template.dart" - [class]{}-[func]{isSolution} + /* 判断当前状态是否为解 */ + bool isSolution(List state) { + return state.isNotEmpty && state.last.val == 7; + } - [class]{}-[func]{recordSolution} + /* 记录解 */ + void recordSolution(List state, List> res) { + res.add(List.from(state)); + } - [class]{}-[func]{isValid} + /* 判断在当前状态下,该选择是否合法 */ + bool isValid(List state, TreeNode? choice) { + return choice != null && choice.val != 3; + } - [class]{}-[func]{makeChoice} + /* 更新状态 */ + void makeChoice(List state, TreeNode? choice) { + state.add(choice!); + } - [class]{}-[func]{undoChoice} + /* 恢复状态 */ + void undoChoice(List state, TreeNode? choice) { + state.removeLast(); + } - [class]{}-[func]{backtrack} + /* 回溯算法:例题三 */ + void backtrack( + List state, + List choices, + List> res, + ) { + // 检查是否为解 + if (isSolution(state)) { + // 记录解 + recordSolution(state, res); + } + // 遍历所有选择 + for (TreeNode? choice in choices) { + // 剪枝:检查选择是否合法 + if (isValid(state, choice)) { + // 尝试:做出选择,更新状态 + makeChoice(state, choice); + // 进行下一轮选择 + backtrack(state, [choice!.left, choice.right], res); + // 回退:撤销选择,恢复到之前的状态 + undoChoice(state, choice); + } + } + } ``` === "Rust" diff --git a/chapter_backtracking/n_queens_problem.md b/chapter_backtracking/n_queens_problem.md index e858c16d0..f6149a0bf 100644 --- a/chapter_backtracking/n_queens_problem.md +++ b/chapter_backtracking/n_queens_problem.md @@ -501,9 +501,61 @@ comments: true === "Dart" ```dart title="n_queens.dart" - [class]{}-[func]{backtrack} + /* 回溯算法:N 皇后 */ + void backtrack( + int row, + int n, + List> state, + List>> res, + List cols, + List diags1, + List diags2, + ) { + // 当放置完所有行时,记录解 + if (row == n) { + List> copyState = []; + for (List sRow in state) { + copyState.add(List.from(sRow)); + } + res.add(copyState); + return; + } + // 遍历所有列 + for (int col = 0; col < n; col++) { + // 计算该格子对应的主对角线和副对角线 + int diag1 = row - col + n - 1; + int diag2 = row + col; + // 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后 + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // 尝试:将皇后放置在该格子 + state[row][col] = "Q"; + cols[col] = true; + diags1[diag1] = true; + diags2[diag2] = true; + // 放置下一行 + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // 回退:将该格子恢复为空位 + state[row][col] = "#"; + cols[col] = false; + diags1[diag1] = false; + diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* 求解 N 皇后 */ + List>> nQueens(int n) { + // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位 + List> state = List.generate(n, (index) => List.filled(n, "#")); + List cols = List.filled(n, false); // 记录列是否有皇后 + List diags1 = List.filled(2 * n - 1, false); // 记录主对角线是否有皇后 + List diags2 = List.filled(2 * n - 1, false); // 记录副对角线是否有皇后 + List>> res = []; + + backtrack(0, n, state, res, cols, diags1, diags2); + + return res; + } ``` === "Rust" diff --git a/chapter_backtracking/permutations_problem.md b/chapter_backtracking/permutations_problem.md index 7e735fe79..db825e917 100644 --- a/chapter_backtracking/permutations_problem.md +++ b/chapter_backtracking/permutations_problem.md @@ -399,9 +399,41 @@ comments: true === "Dart" ```dart title="permutations_i.dart" - [class]{}-[func]{backtrack} + /* 回溯算法:全排列 I */ + void backtrack( + List state, + List choices, + List selected, + List> res, + ) { + // 当状态长度等于元素数量时,记录解 + if (state.length == choices.length) { + res.add(List.from(state)); + return; + } + // 遍历所有选择 + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素 + if (!selected[i]) { + // 尝试:做出选择,更新状态 + selected[i] = true; + state.add(choice); + // 进行下一轮选择 + backtrack(state, choices, selected, res); + // 回退:撤销选择,恢复到之前的状态 + selected[i] = false; + state.removeLast(); + } + } + } - [class]{}-[func]{permutationsI} + /* 全排列 I */ + List> permutationsI(List nums) { + List> res = []; + backtrack([], nums, List.filled(nums.length, false), res); + return res; + } ``` === "Rust" @@ -791,9 +823,43 @@ comments: true === "Dart" ```dart title="permutations_ii.dart" - [class]{}-[func]{backtrack} + /* 回溯算法:全排列 II */ + void backtrack( + List state, + List choices, + List selected, + List> res, + ) { + // 当状态长度等于元素数量时,记录解 + if (state.length == choices.length) { + res.add(List.from(state)); + return; + } + // 遍历所有选择 + Set duplicated = {}; + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素 + if (!selected[i] && !duplicated.contains(choice)) { + // 尝试:做出选择,更新状态 + duplicated.add(choice); // 记录选择过的元素值 + selected[i] = true; + state.add(choice); + // 进行下一轮选择 + backtrack(state, choices, selected, res); + // 回退:撤销选择,恢复到之前的状态 + selected[i] = false; + state.removeLast(); + } + } + } - [class]{}-[func]{permutationsII} + /* 全排列 II */ + List> permutationsII(List nums) { + List> res = []; + backtrack([], nums, List.filled(nums.length, false), res); + return res; + } ``` === "Rust" diff --git a/chapter_backtracking/subset_sum_problem.md b/chapter_backtracking/subset_sum_problem.md index 31698ee47..c692ca4ba 100644 --- a/chapter_backtracking/subset_sum_problem.md +++ b/chapter_backtracking/subset_sum_problem.md @@ -359,9 +359,42 @@ comments: true === "Dart" ```dart title="subset_sum_i_naive.dart" - [class]{}-[func]{backtrack} + /* 回溯算法:子集和 I */ + void backtrack( + List state, + int target, + int total, + List choices, + List> res, + ) { + // 子集和等于 target 时,记录解 + if (total == target) { + res.add(List.from(state)); + return; + } + // 遍历所有选择 + for (int i = 0; i < choices.length; i++) { + // 剪枝:若子集和超过 target ,则跳过该选择 + if (total + choices[i] > target) { + continue; + } + // 尝试:做出选择,更新元素和 total + state.add(choices[i]); + // 进行下一轮选择 + backtrack(state, target, total + choices[i], choices, res); + // 回退:撤销选择,恢复到之前的状态 + state.removeLast(); + } + } - [class]{}-[func]{subsetSumINaive} + /* 求解子集和 I(包含重复子集) */ + List> subsetSumINaive(List nums, int target) { + List state = []; // 状态(子集) + int total = 0; // 元素和 + List> res = []; // 结果列表(子集列表) + backtrack(state, target, total, nums, res); + return res; + } ``` === "Rust" @@ -800,9 +833,45 @@ comments: true === "Dart" ```dart title="subset_sum_i.dart" - [class]{}-[func]{backtrack} + /* 回溯算法:子集和 I */ + void backtrack( + List state, + int target, + List choices, + int start, + List> res, + ) { + // 子集和等于 target 时,记录解 + if (target == 0) { + res.add(List.from(state)); + return; + } + // 遍历所有选择 + // 剪枝二:从 start 开始遍历,避免生成重复子集 + for (int i = start; i < choices.length; i++) { + // 剪枝一:若子集和超过 target ,则直接结束循环 + // 这是因为数组已排序,后边元素更大,子集和一定超过 target + if (target - choices[i] < 0) { + break; + } + // 尝试:做出选择,更新 target, start + state.add(choices[i]); + // 进行下一轮选择 + backtrack(state, target - choices[i], choices, i, res); + // 回退:撤销选择,恢复到之前的状态 + state.removeLast(); + } + } - [class]{}-[func]{subsetSumI} + /* 求解子集和 I */ + List> subsetSumI(List nums, int target) { + List state = []; // 状态(子集) + nums.sort(); // 对 nums 进行排序 + int start = 0; // 遍历起始点 + List> res = []; // 结果列表(子集列表) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Rust" @@ -1276,9 +1345,50 @@ comments: true === "Dart" ```dart title="subset_sum_ii.dart" - [class]{}-[func]{backtrack} + /* 回溯算法:子集和 II */ + void backtrack( + List state, + int target, + List choices, + int start, + List> res, + ) { + // 子集和等于 target 时,记录解 + if (target == 0) { + res.add(List.from(state)); + return; + } + // 遍历所有选择 + // 剪枝二:从 start 开始遍历,避免生成重复子集 + // 剪枝三:从 start 开始遍历,避免重复选择同一元素 + for (int i = start; i < choices.length; i++) { + // 剪枝一:若子集和超过 target ,则直接结束循环 + // 这是因为数组已排序,后边元素更大,子集和一定超过 target + if (target - choices[i] < 0) { + break; + } + // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过 + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // 尝试:做出选择,更新 target, start + state.add(choices[i]); + // 进行下一轮选择 + backtrack(state, target - choices[i], choices, i + 1, res); + // 回退:撤销选择,恢复到之前的状态 + state.removeLast(); + } + } - [class]{}-[func]{subsetSumII} + /* 求解子集和 II */ + List> subsetSumII(List nums, int target) { + List state = []; // 状态(子集) + nums.sort(); // 对 nums 进行排序 + int start = 0; // 遍历起始点 + List> res = []; // 结果列表(子集列表) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Rust" diff --git a/chapter_data_structure/basic_data_types.md b/chapter_data_structure/basic_data_types.md index aff6814f0..4a171a981 100644 --- a/chapter_data_structure/basic_data_types.md +++ b/chapter_data_structure/basic_data_types.md @@ -40,14 +40,14 @@ comments: true 对于上表,需要注意以下几点: - C, C++ 未明确规定基本数据类型大小,而因实现和平台各异。上表遵循 LP64 [数据模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用于 Unix 64 位操作系统(例如 Linux , macOS)。 -- 字符 `char` 的大小在 C, C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见「字符编码」章节。 -- 现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。因此即使表示布尔量仅需 1 位($0$ 或 $1$),它在内存中通常被存储为 1 字节。 +- 字符 `char` 的大小在 C, C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见“字符编码”章节。 +- 即使表示布尔量仅需 1 位($0$ 或 $1$),它在内存中通常被存储为 1 字节。这是因为现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。 -那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。它的主语是“结构”,而非“数据”。 +那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。它的主语是“结构”而非“数据”。 如果想要表示“一排数字”,我们自然会想到使用数组。这是因为数组的线性结构可以表示数字的相邻关系和顺序关系,但至于存储的内容是整数 `int` 、小数 `float` 、还是字符 `char` ,则与“数据结构”无关。 -换句话说,**基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”**。例如在以下代码中,我们用相同的数据结构(数组)来记录与表示不同的基本数据类型(`int` , `float` , `chat`, `bool`)。 +换句话说,**基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”**。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型(`int` , `float` , `chat`, `bool`)。 === "Java" diff --git a/chapter_data_structure/character_encoding.md b/chapter_data_structure/character_encoding.md index d9bfdcbc1..1e7617178 100644 --- a/chapter_data_structure/character_encoding.md +++ b/chapter_data_structure/character_encoding.md @@ -4,7 +4,7 @@ comments: true # 3.4.   字符编码 * -在计算机中,所有数据都是以二进制数的形式存储的,字符 `char` 也不例外。为了表示字符,我们需要建立一套「字符集」,规定每个字符和二进制数之间的一一对应关系。有了字符集之后,计算机就可以通过查表完成二进制数到字符的转换。 +在计算机中,所有数据都是以二进制数的形式存储的,字符 `char` 也不例外。为了表示字符,我们需要建立一套“字符集”,规定每个字符和二进制数之间的一一对应关系。有了字符集之后,计算机就可以通过查表完成二进制数到字符的转换。 ## 3.4.1.   ASCII 字符集 @@ -14,25 +14,27 @@ comments: true

Fig. ASCII 码

-然而,**ASCII 码仅能够表示英文**。随着计算机的全球化,诞生了一种能够表示更多语言的字符集「EASCII」。它在 ASCII 的 7 位基础上扩展到 8 位,能够表示 256 个不同的字符。在世界范围内,陆续出现了一批适用于不同地区的 EASCII 字符集。这些字符集的前 128 个字符统一为 ASCII 码,后 128 个字符定义不同,以适应不同语言的需求。 +然而,**ASCII 码仅能够表示英文**。随着计算机的全球化,诞生了一种能够表示更多语言的字符集「EASCII」。它在 ASCII 的 7 位基础上扩展到 8 位,能够表示 256 个不同的字符。 + +在世界范围内,陆续出现了一批适用于不同地区的 EASCII 字符集。这些字符集的前 128 个字符统一为 ASCII 码,后 128 个字符定义不同,以适应不同语言的需求。 ## 3.4.2.   GBK 字符集 -后来人们发现,**EASCII 码仍然无法满足许多语言的字符数量要求**。例如,汉字大约有近十万个,光日常使用的就有几千个。中国国家标准总局于 1980 年发布了「GB2312」字符集,其收录了 6763 个汉字,基本满足了汉字的计算机处理需要。 +后来人们发现,**EASCII 码仍然无法满足许多语言的字符数量要求**。比如汉字大约有近十万个,光日常使用的就有几千个。中国国家标准总局于 1980 年发布了「GB2312」字符集,其收录了 6763 个汉字,基本满足了汉字的计算机处理需要。 -然而,GB2312 无法处理部分的罕见字和繁体字。之后在 GB2312 的基础上,扩展得到了「GBK」字符集,它共收录了 21886 个汉字。在 GBK 编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。 +然而,GB2312 无法处理部分的罕见字和繁体字。「GBK」字符集是在 GB2312 的基础上扩展得到的,它共收录了 21886 个汉字。在 GBK 的编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。 ## 3.4.3.   Unicode 字符集 -随着计算机的蓬勃发展,字符集与编码标准百花齐放,而这带来了许多问题。一方面,这些字符集一般只定义了特定语言的字符,无法在多语言环境下正常工作;另一方面,同一种语言也存在多种字符集标准,如果两台电脑安装的是不同的编码标准,则在信息传递时就会出现乱码。 +随着计算机的蓬勃发展,字符集与编码标准百花齐放,而这带来了许多问题。一方面,这些字符集一般只定义了特定语言的字符,无法在多语言环境下正常工作。另一方面,同一种语言也存在多种字符集标准,如果两台电脑安装的是不同的编码标准,则在信息传递时就会出现乱码。 -那个时代的人们就在想:**如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗**?在这种想法的驱动下,一个大而全的字符集 Unicode 应运而生。 +那个时代的研究人员就在想:**如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗**?在这种想法的驱动下,一个大而全的字符集 Unicode 应运而生。 「Unicode」的全称为“统一字符编码”,理论上能容纳一百多万个字符。它致力于将全球范围内的字符纳入到统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。 自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截止 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号、甚至是表情符号等。在庞大的 Unicode 字符集中,常用的字符占用 2 字节,有些生僻的字符占 3 字节甚至 4 字节。 -Unicode 是一种字符集标准,本质上是给每个字符分配一个编号(称为“码点”),**但它并没有规定在计算机中如何存储这些字符码点**。我们不禁会问:当多种长度的 Unicode 码点同时出现在同一个文本中时,系统如何解析字符?例如,给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符? +Unicode 是一种字符集标准,本质上是给每个字符分配一个编号(称为“码点”),**但它并没有规定在计算机中如何存储这些字符码点**。我们不禁会问:当多种长度的 Unicode 码点同时出现在同一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符? 对于以上问题,**一种直接的解决方案是将所有字符存储为等长的编码**。如下图所示,“Hello”中的每个字符占用 1 字节,“算法”中的每个字符占用 2 字节。我们可以通过高位填 0 ,将“Hello 算法”中的所有字符都编码为 2 字节长度。这样系统就可以每隔 2 字节解析一个字符,恢复出这个短语的内容了。 @@ -40,7 +42,7 @@ Unicode 是一种字符集标准,本质上是给每个字符分配一个编号

Fig. Unicode 编码示例

-然而,ASCII 码已经向我们证明,编码英文只需要 1 字节。若采用上述方案,英文文本占用空间的大小将会是 ASCII 编码下大小的 2 倍,非常浪费内存空间。因此,我们需要一种更加高效的 Unicode 编码方法。 +然而 ASCII 码已经向我们证明,编码英文只需要 1 字节。若采用上述方案,英文文本占用空间的大小将会是 ASCII 编码下大小的两倍,非常浪费内存空间。因此,我们需要一种更加高效的 Unicode 编码方法。 ## 3.4.4.   UTF-8 编码 @@ -48,37 +50,43 @@ Unicode 是一种字符集标准,本质上是给每个字符分配一个编号 UTF-8 的编码规则并不复杂,分为两种情况: -- 对于长度为 1 字节的字符,将最高位设置为 $0$ 、其余 7 位设置为 Unicode 码点。值得注意的是,ASCII 字符在 Unicode 字符集中占据了前 128 个码点。也就是说,**UTF-8 编码可以向下兼容 ASCII 码**。这意味着我们可以使用 UTF-8 来解析年代久远的 ASCII 码文本。 -- 对于长度为 $n$ 字节的字符(其中 $n > 1$),将首个字节的高 $n$ 位都设置为 $1$ 、第 $n + 1$ 位设置为 $0$ ;从第二个字节开始,将每个字节的高 2 位都设置为 $10$ ;其余所有位用于填充字符的 Unicode 码点。 +1. 对于长度为 1 字节的字符,将最高位设置为 $0$ 、其余 7 位设置为 Unicode 码点。值得注意的是,ASCII 字符在 Unicode 字符集中占据了前 128 个码点。也就是说,**UTF-8 编码可以向下兼容 ASCII 码**。这意味着我们可以使用 UTF-8 来解析年代久远的 ASCII 码文本。 +2. 对于长度为 $n$ 字节的字符(其中 $n > 1$),将首个字节的高 $n$ 位都设置为 $1$ 、第 $n + 1$ 位设置为 $0$ ;从第二个字节开始,将每个字节的高 2 位都设置为 $10$ ;其余所有位用于填充字符的 Unicode 码点。 + +下图展示了“Hello算法”对应的 UTF-8 编码。观察发现,由于最高 $n$ 位都被设置为 $1$ ,因此系统可以通过读取最高位 $1$ 的个数来解析出字符的长度为 $n$ 。 -下图展示了“Hello算法”对应的 UTF-8 编码。将最高 $n$ 位设置为 $1$ 比较容易理解,可以向系统指出字符的长度为 $n$ 。那么,为什么要将其余所有字节的高 2 位都设置为 $10$ 呢?实际上,这个 $10$ 能够起到校验符的作用,因为在 UTF-8 编码规则下,不可能有字符的最高两位是 $10$ 。这是因为长度为 1 字节的字符的最高一位是 $0$ 。假设系统从一个错误的字节开始解析文本,字节头部的 $10$ 能够帮助系统快速的判断出异常。 +但为什么要将其余所有字节的高 2 位都设置为 $10$ 呢?实际上,这个 $10$ 能够起到校验符的作用。假设系统从一个错误的字节开始解析文本,字节头部的 $10$ 能够帮助系统快速的判断出异常。 + +之所以将 $10$ 当作校验符,是因为在 UTF-8 编码规则下,不可能有字符的最高两位是 $10$ 。这个结论可以用反证法来证明:假设一个字符的最高两位是 $10$ ,说明该字符的长度为 $1$ ,对应 ASCII 码。而 ASCII 码的最高位应该是 $0$ ,与假设矛盾。 ![UTF-8 编码示例](character_encoding.assets/utf-8_hello_algo.png)

Fig. UTF-8 编码示例

-除了 UTF-8 之外,常见的编码方式还包括 UTF-16 和 UTF-32 。它们为 Unicode 字符集提供了不同的编码方法。 +除了 UTF-8 之外,常见的编码方式还包括: + +- **UTF-16 编码**:使用 2 或 4 个字节来表示一个字符。所有的 ASCII 字符和常用的非英文字符,都用 2 个字节表示;少数字符需要用到 4 个字节表示。对于 2 字节的字符,UTF-16 编码与 Unicode 码点相等。 +- **UTF-32 编码**:每个字符都使用 4 个字节。这意味着 UTF-32 会比 UTF-8 和 UTF-16 更占用空间,特别是对于 ASCII 字符占比较高的文本。 -- **UTF-16 编码**:使用 2 或 4 个字节来表示一个字符。所有的 ASCII 字符和很多常用的非英文字符,都用 2 个字节表示;少数字符需要用到 4 个字节表示。对于 2 字节的字符,UTF-16 编码与 Unicode 码点相等。 -- **UTF-32 编码**:每个字符都使用 4 个字节。这意味着 UTF-32 会比 UTF-8 和 UTF-16 更占用空间,特别是对于主要使用 ASCII 字符的文本。 +从存储空间的角度看,使用 UTF-8 表示英文字符非常高效,因为它仅需 1 个字节;使用 UTF-16 编码某些非英文字符(例如中文)会更加高效,因为它只需要 2 个字节,而 UTF-8 可能需要 3 个字节。 -从存储空间的角度看,使用 UTF-8 表示英文字符非常高效,因为它仅需 1 个字节;使用 UTF-16 编码某些非英文字符(例如中文)会更加高效,因为它只需要 2 个字节,而 UTF-8 可能需要 3 个字节。从兼容性的角度看,UTF-8 的通用性最佳,许多工具和库都优先支持 UTF-8 。 +从兼容性的角度看,UTF-8 的通用性最佳,许多工具和库都优先支持 UTF-8 。 ## 3.4.5.   编程语言的字符编码 -对于以往的大多数编程语言,程序运行中的字符串都采用 UTF-16 或 UTF-32 这类等长的编码。这是因为在等长编码下,我们可以将字符串看作数组来处理,具体来说: +对于以往的大多数编程语言,程序运行中的字符串都采用 UTF-16 或 UTF-32 这类等长的编码。这是因为在等长编码下,我们可以将字符串看作数组来处理,其优点包括: - **随机访问**: UTF-16 编码的字符串可以很容易地进行随机访问。UTF-8 是一种变长编码,要找到第 $i$ 个字符,我们需要从字符串的开始处遍历到第 $i$ 个字符,这需要 $O(n)$ 的时间。 - **字符计数**: 与随机访问类似,计算 UTF-16 字符串的长度也是 $O(1)$ 的操作。但是,计算 UTF-8 编码的字符串的长度需要遍历整个字符串。 - **字符串操作**: 在 UTF-16 编码的字符串中,很多字符串操作(如分割、连接、插入、删除等)都更容易进行。在 UTF-8 编码的字符串上进行这些操作通常需要额外的计算,以确保不会产生无效的 UTF-8 编码。 -编程语言的字符编码方案设计是一个很有趣的话题,涉及到许多因素: +实际上,编程语言的字符编码方案设计是一个很有趣的话题,其涉及到许多因素: - Java 的 `String` 类型使用 UTF-16 编码,每个字符占用 2 字节。这是因为 Java 语言设计之初,人们认为 16 位足以表示所有可能的字符。然而,这是一个不正确的判断。后来 Unicode 规范扩展到了超过 16 位,所以 Java 中的字符现在可能由一对 16 位的值(称为“代理对”)表示。 - JavaScript 和 TypeScript 的字符串使用 UTF-16 编码的原因与 Java 类似。当 JavaScript 语言在 1995 年被 Netscape 公司首次引入时,Unicode 还处于相对早期的阶段,那时候使用 16 位的编码就足够表示所有的 Unicode 字符了。 - C# 使用 UTF-16 编码,主要因为 .NET 平台是由 Microsoft 设计的,而 Microsoft 的很多技术,包括 Windows 操作系统,都广泛地使用 UTF-16 编码。 -由于以上编程语言对字符数量的低估,它们不得不采取“代理对”的方式来表示超过 16 位长度的 Unicode 字符。这是一个不得已为之的无奈之举。一方面,包含代理对的字符串中,一个字符可能占用 2 字节或 4 字节,因此丧失了等长编码的优势。另一方面,处理代理对需要增加额外代码,这增加了编程的复杂性和 Debug 难度。 +由于以上编程语言对字符数量的低估,它们不得不采取“代理对”的方式来表示超过 16 位长度的 Unicode 字符。这是一个不得已为之的无奈之举。一方面,包含代理对的字符串中,一个字符可能占用 2 字节或 4 字节,从而丧失了等长编码的优势。另一方面,处理代理对需要增加额外代码,这增加了编程的复杂性和 Debug 难度。 出于以上原因,部分编程语言提出了不同的编码方案: @@ -86,4 +94,4 @@ UTF-8 的编码规则并不复杂,分为两种情况: - Go 语言的 `string` 类型在内部使用 UTF-8 编码。Go 语言还提供了 `rune` 类型,它用于表示单个 Unicode 码点。 - Rust 语言的 str 和 String 类型在内部使用 UTF-8 编码。Rust 也提供了 char 类型,用于表示单个 Unicode 码点。 -需要注意的是,以上讨论的都是字符串在编程语言中的存储方式,**这和字符串如何在文件中存储或在网络中传输是两个不同的问题**。在文件存储或网络传输中,我们一般会将字符串编码为 UTF-8 格式,以达到最优的兼容性和空间效率。 +需要注意的是,以上讨论的都是字符串在编程语言中的存储方式,**这和字符串如何在文件中存储或在网络中传输是两个不同的问题**。在文件存储或网络传输中,我们通常会将字符串编码为 UTF-8 格式,以达到最优的兼容性和空间效率。 diff --git a/chapter_data_structure/classification_of_data_structure.md b/chapter_data_structure/classification_of_data_structure.md index 055a244fb..c5d0f1fab 100644 --- a/chapter_data_structure/classification_of_data_structure.md +++ b/chapter_data_structure/classification_of_data_structure.md @@ -4,13 +4,13 @@ comments: true # 3.1.   数据结构分类 -数据结构可以从逻辑结构和物理结构两个维度进行分类。 +常见的数据结构包括数组、链表、栈、队列、哈希表、树、堆、图,它们可以从“逻辑结构”和“物理结构”两个维度进行分类。 ## 3.1.1.   逻辑结构:线性与非线性 **「逻辑结构」揭示了数据元素之间的逻辑关系**。在数组和链表中,数据按照顺序依次排列,体现了数据之间的线性关系;而在树中,数据从顶部向下按层次排列,表现出祖先与后代之间的派生关系;图则由节点和边构成,反映了复杂的网络关系。 -逻辑结构通常分为“线性”和“非线性”两类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。 +逻辑结构可被分为“线性”和“非线性”两大类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。 - **线性数据结构**:数组、链表、栈、队列、哈希表。 - **非线性数据结构**:树、堆、图、哈希表。 @@ -21,31 +21,31 @@ comments: true 非线性数据结构可以进一步被划分为树形结构和网状结构。 -- **线性结构**:数组、链表、队列、栈、哈希表,元素存在一对一的顺序关系。 -- **树形结构**:树、堆、哈希表,元素存在一对多的关系。 -- **网状结构**:图,元素存在多对多的关系。 +- **线性结构**:数组、链表、队列、栈、哈希表,元素之间是一对一的顺序关系。 +- **树形结构**:树、堆、哈希表,元素之间是一对多的关系。 +- **网状结构**:图,元素之间是多对多的关系。 ## 3.1.2.   物理结构:连续与离散 在计算机中,内存和硬盘是两种主要的存储硬件设备。硬盘主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。内存用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。 -**在算法运行过程中,相关数据都存储在内存中**。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储 1 byte 的数据,在算法运行时,所有数据都被存储在这些单元格中。 +**在算法运行过程中,相关数据都存储在内存中**。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据,在算法运行时,所有数据都被存储在这些单元格中。 -**系统通过「内存地址 Memory Location」来访问目标内存位置的数据**。计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。 +**系统通过内存地址来访问目标位置的数据**。计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。 ![内存条、内存空间、内存地址](classification_of_data_structure.assets/computer_memory_location.png)

Fig. 内存条、内存空间、内存地址

-内存是所有程序的共享资源,当内存被某个程序占用时,其他程序无法同时使用。**因此,在数据结构与算法的设计中,内存资源是一个重要的考虑因素**。例如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果运行的程序很多并且缺少大量连续的内存空间,那么所选用的数据结构必须能够存储在离散的内存空间内。 +内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。**因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素**。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在离散的内存空间内。 -**「物理结构」反映了数据在计算机内存中的存储方式**,可分为数组的连续空间存储和链表的离散空间存储。物理结构从底层决定了数据的访问、更新、增删等操作方法,同时在时间效率和空间效率方面呈现出互补的特点。 +**「物理结构」反映了数据在计算机内存中的存储方式**,可分为连续空间存储(数组)和离散空间存储(链表)。物理结构从底层决定了数据的访问、更新、增删等操作方法,同时在时间效率和空间效率方面呈现出互补的特点。 ![连续空间存储与离散空间存储](classification_of_data_structure.assets/classification_phisical_structure.png)

Fig. 连续空间存储与离散空间存储

-**所有数据结构都是基于数组、链表或二者的组合实现的**。例如,栈和队列既可以使用数组实现,也可以使用链表实现;而哈希表的实现可能同时包含数组和链表。 +值得说明的是,**所有数据结构都是基于数组、链表或二者的组合实现的**。例如,栈和队列既可以使用数组实现,也可以使用链表实现;而哈希表的实现可能同时包含数组和链表。 - **基于数组可实现**:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 $\geq 3$ 的数组)等。 - **基于链表可实现**:栈、队列、哈希表、树、堆、图等。 @@ -54,4 +54,4 @@ comments: true !!! tip - 如若感觉理解物理结构有困难,建议先阅读下一章“数组与链表”,然后再回头理解物理结构的含义。数组与链表是其他所有数据结构的基石,建议你投入更多时间深入了解这两种基本数据结构。 + 如若感觉理解物理结构有困难,建议先阅读下一章“数组与链表”,然后再回顾本节内容。 diff --git a/chapter_data_structure/number_encoding.md b/chapter_data_structure/number_encoding.md index 20f276196..27843f41c 100644 --- a/chapter_data_structure/number_encoding.md +++ b/chapter_data_structure/number_encoding.md @@ -6,11 +6,13 @@ comments: true !!! note - 在本书中,标题带有的 * 符号的是选读章节。如果你时间有限或感到理解困难,建议先跳过,等学完必读章节后再单独攻克。 + 在本书中,标题带有的 * 符号的是选读章节。如果你时间有限或感到理解困难,可以先跳过,等学完必读章节后再单独攻克。 ## 3.3.1.   原码、反码和补码 -从上一节的表格中我们发现,所有整数类型能够表示的负数都比正数多一个。例如,`byte` 的取值范围是 $[-128, 127]$ 。这个现象比较反直觉,它的内在原因涉及到原码、反码、补码的相关知识。在展开分析之前,我们首先给出三者的定义: +从上一节的表格中我们发现,所有整数类型能够表示的负数都比正数多一个。例如,`byte` 的取值范围是 $[-128, 127]$ 。这个现象比较反直觉,它的内在原因涉及到原码、反码、补码的相关知识。 + +在展开分析之前,我们首先给出三者的定义: - **原码**:我们将数字的二进制表示的最高位视为符号位,其中 $0$ 表示正数,$1$ 表示负数,其余位表示数字的值。 - **反码**:正数的反码与其原码相同,负数的反码是对其原码除符号位外的所有位取反。 @@ -20,7 +22,7 @@ comments: true

Fig. 原码、反码与补码之间的相互转换

-显然,「原码」最为直观,**然而数字却是以「补码」的形式存储在计算机中的**。这是因为原码存在一些局限性。 +显然「原码」最为直观。但实际上,**数字是以「补码」的形式存储在计算机中的**。这是因为原码存在一些局限性。 一方面,**负数的原码不能直接用于运算**。例如,我们在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。 @@ -33,20 +35,20 @@ $$ \end{aligned} $$ -为了解决此问题,计算机引入了「反码」。例如,我们先将原码转换为反码,并在反码下计算 $1 + (-2)$ ,并将结果从反码转化回原码,则可得到正确结果 $-1$ 。 +为了解决此问题,计算机引入了「反码」。如果我们先将原码转换为反码,并在反码下计算 $1 + (-2)$ ,最后将结果从反码转化回原码,则可得到正确结果 $-1$ 。 $$ \begin{aligned} & 1 + (-2) \newline -& = 0000 \space 0001 \space \text{(原码)} + 1000 \space 0010 \space \text{(原码)} \newline +& \rightarrow 0000 \space 0001 \space \text{(原码)} + 1000 \space 0010 \space \text{(原码)} \newline & = 0000 \space 0001 \space \text{(反码)} + 1111 \space 1101 \space \text{(反码)} \newline & = 1111 \space 1110 \space \text{(反码)} \newline & = 1000 \space 0001 \space \text{(原码)} \newline -& = -1 +& \rightarrow -1 \end{aligned} $$ -另一方面,**数字零的原码有 $+0$ 和 $-0$ 两种表示方式**。这意味着数字零对应着两个不同的二进制编码,而这可能会带来歧义问题。例如,在条件判断中,如果没有区分正零和负零,可能会导致错误的判断结果。如果我们想要处理正零和负零歧义,则需要引入额外的判断操作,其可能会降低计算机的运算效率。 +另一方面,**数字零的原码有 $+0$ 和 $-0$ 两种表示方式**。这意味着数字零对应着两个不同的二进制编码,其可能会带来歧义。比如在条件判断中,如果没有区分正零和负零,则可能会导致判断结果出错。而如果我们想要处理正零和负零歧义,则需要引入额外的判断操作,其可能会降低计算机的运算效率。 $$ \begin{aligned} @@ -55,7 +57,7 @@ $$ \end{aligned} $$ -与原码一样,反码也存在正负零歧义问题。为此,计算机进一步引入了「补码」。那么,补码有什么作用呢?我们先来分析一下负零的补码的计算过程: +与原码一样,反码也存在正负零歧义问题,因此计算机进一步引入了「补码」。我们先来观察一下负零的原码、反码、补码的转换过程: $$ \begin{aligned} @@ -65,30 +67,30 @@ $$ \end{aligned} $$ -在负零的反码基础上加 $1$ 会产生进位,而由于 byte 的长度只有 8 位,因此溢出到第 9 位的 $1$ 会被舍弃。**从而得到负零的补码为 $0000 \space 0000$ ,与正零的补码相同**。这意味着在补码表示中只存在一个零,从而解决了正负零歧义问题。 +在负零的反码基础上加 $1$ 会产生进位,但 `byte` 类型的长度只有 8 位,因此溢出到第 9 位的 $1$ 会被舍弃。也就是说,**负零的补码为 $0000 \space 0000$ ,与正零的补码相同**。这意味着在补码表示中只存在一个零,正负零歧义从而得到解决。 -还剩余最后一个疑惑:byte 的取值范围是 $[-128, 127]$ ,多出来的一个负数 $-128$ 是如何得到的呢?我们注意到,区间 $[-127, +127]$ 内的所有整数都有对应的原码、反码和补码,并且原码和补码之间是可以互相转换的。 +还剩余最后一个疑惑:`byte` 类型的取值范围是 $[-128, 127]$ ,多出来的一个负数 $-128$ 是如何得到的呢?我们注意到,区间 $[-127, +127]$ 内的所有整数都有对应的原码、反码和补码,并且原码和补码之间是可以互相转换的。 然而,**补码 $1000 \space 0000$ 是一个例外,它并没有对应的原码**。根据转换方法,我们得到该补码的原码为 $0000 \space 0000$ 。这显然是矛盾的,因为该原码表示数字 $0$ ,它的补码应该是自身。计算机规定这个特殊的补码 $1000 \space 0000$ 代表 $-128$ 。实际上,$(-1) + (-127)$ 在补码下的计算结果就是 $-128$ 。 $$ \begin{aligned} & (-127) + (-1) \newline -& = 1111 \space 1111 \space \text{(原码)} + 1000 \space 0001 \space \text{(原码)} \newline +& \rightarrow 1111 \space 1111 \space \text{(原码)} + 1000 \space 0001 \space \text{(原码)} \newline & = 1000 \space 0000 \space \text{(反码)} + 1111 \space 1110 \space \text{(反码)} \newline & = 1000 \space 0001 \space \text{(补码)} + 1111 \space 1111 \space \text{(补码)} \newline & = 1000 \space 0000 \space \text{(补码)} \newline -& = -128 +& \rightarrow -128 \end{aligned} $$ -你可能已经发现,上述的所有计算都是加法运算。这暗示着一个重要事实:**计算机内部的硬件电路主要是基于加法运算设计的**。这是因为加法运算相对于其他运算(比如乘法、除法和减法)来说,硬件实现起来更简单,更容易进行并行化处理,从而提高运算速度。 +你可能已经发现,上述的所有计算都是加法运算。这暗示着一个重要事实:**计算机内部的硬件电路主要是基于加法运算设计的**。这是因为加法运算相对于其他运算(比如乘法、除法和减法)来说,硬件实现起来更简单,更容易进行并行化处理,运算速度更快。 -然而,这并不意味着计算机只能做加法。**通过将加法与一些基本逻辑运算结合,计算机能够实现各种其他的数学运算**。例如,计算减法 $a - b$ 可以转换为计算加法 $a + (-b)$ ;计算乘法和除法可以转换为计算多次加法或减法。 +请注意,这并不意味着计算机只能做加法。**通过将加法与一些基本逻辑运算结合,计算机能够实现各种其他的数学运算**。例如,计算减法 $a - b$ 可以转换为计算加法 $a + (-b)$ ;计算乘法和除法可以转换为计算多次加法或减法。 -现在,我们可以总结出计算机使用补码的原因:基于补码表示,计算机可以用同样的电路和操作来处理正数和负数的加法,不需要设计特殊的硬件电路来处理减法,并且无需特别处理正负零的歧义问题。这大大简化了硬件设计,并提高了运算效率。 +现在我们可以总结出计算机使用补码的原因:基于补码表示,计算机可以用同样的电路和操作来处理正数和负数的加法,不需要设计特殊的硬件电路来处理减法,并且无需特别处理正负零的歧义问题。这大大简化了硬件设计,提高了运算效率。 -补码的设计非常精妙,由于篇幅关系我们先介绍到这里。建议有兴趣的读者进一步深度了解。 +补码的设计非常精妙,因篇幅关系我们就先介绍到这里,建议有兴趣的读者进一步深度了解。 ## 3.3.2.   浮点数编码 @@ -131,7 +133,7 @@ $$

Fig. IEEE 754 标准下的 float 表示方式

-以上图为例,$\mathrm{S} = 0$ , $\mathrm{E} = 124$ ,$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ ,易得 +给定一个示例数据 $\mathrm{S} = 0$ , $\mathrm{E} = 124$ ,$\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$ ,则有: $$ \text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875 diff --git a/chapter_data_structure/summary.md b/chapter_data_structure/summary.md index 3d8aa6926..20035f105 100644 --- a/chapter_data_structure/summary.md +++ b/chapter_data_structure/summary.md @@ -4,16 +4,11 @@ comments: true # 3.5.   小结 -**数据结构分类** - - 数据结构可以从逻辑结构和物理结构两个角度进行分类。逻辑结构描述了数据元素之间的逻辑关系,而物理结构描述了数据在计算机内存中的存储方式。 - 常见的逻辑结构包括线性、树状和网状等。通常我们根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。哈希表的实现可能同时包含线性和非线性结构。 - 当程序运行时,数据被存储在计算机内存中。每个内存空间都拥有对应的内存地址,程序通过这些内存地址访问数据。 - 物理结构主要分为连续空间存储(数组)和离散空间存储(链表)。所有数据结构都是由数组、链表或两者的组合实现的。 - -**数据类型与编码** - -- 计算机中的基本数据类型包括整数 byte, short, int, long 、浮点数 float, double 、字符 char 和布尔 boolean 。它们的取值范围取决于占用空间大小和表示方式。 +- 计算机中的基本数据类型包括整数 `byte` , `short` , `int` , `long` 、浮点数 `float` , `double` 、字符 `char` 和布尔 `boolean` 。它们的取值范围取决于占用空间大小和表示方式。 - 原码、反码和补码是在计算机中编码数字的三种方法,它们之间是可以相互转换的。整数的原码的最高位是符号位,其余位是数字的值。 - 整数在计算机中是以补码的形式存储的。在补码表示下,计算机可以对正数和负数的加法一视同仁,不需要为减法操作单独设计特殊的硬件电路,并且不存在正负零歧义的问题。 - 浮点数的编码由 1 位符号位、8 位指数位和 23 位分数位构成。由于存在指数位,浮点数的取值范围远大于整数,代价是牺牲了精度。 @@ -24,8 +19,8 @@ comments: true !!! question "为什么哈希表同时包含线性数据结构和非线性数据结构?" - 哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“拉链法”(后续散列表章节会讲)。在拉链法中,数组中每个地址(桶)指向一个链表;当这个链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。 + 哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续散列表章节会讲)。在拉链法中,数组中每个地址(桶)指向一个链表;当这个链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。 -!!! question "char 类型的长度是 1 byte 吗?" +!!! question "`char` 类型的长度是 1 byte 吗?" - char 类型的长度由编程语言采用的编码方法决定。例如,Java, JS, TS, C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。 + `char` 类型的长度由编程语言采用的编码方法决定。例如,Java, JS, TS, C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。 diff --git a/chapter_divide_and_conquer/binary_search_recur.md b/chapter_divide_and_conquer/binary_search_recur.md index f7e84eb8a..74af47b45 100644 --- a/chapter_divide_and_conquer/binary_search_recur.md +++ b/chapter_divide_and_conquer/binary_search_recur.md @@ -289,9 +289,32 @@ status: new === "Dart" ```dart title="binary_search_recur.dart" - [class]{}-[func]{dfs} + /* 二分查找:问题 f(i, j) */ + int dfs(List nums, int target, int i, int j) { + // 若区间为空,代表无目标元素,则返回 -1 + if (i > j) { + return -1; + } + // 计算中点索引 m + int m = (i + j) ~/ 2; + if (nums[m] < target) { + // 递归子问题 f(m+1, j) + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // 递归子问题 f(i, m-1) + return dfs(nums, target, i, m - 1); + } else { + // 找到目标元素,返回其索引 + return m; + } + } - [class]{}-[func]{binarySearch} + /* 二分查找 */ + int binarySearch(List nums, int target) { + int n = nums.length; + // 求解问题 f(0, n-1) + return dfs(nums, target, 0, n - 1); + } ``` === "Rust" diff --git a/chapter_divide_and_conquer/build_binary_tree_problem.md b/chapter_divide_and_conquer/build_binary_tree_problem.md index 05841a0cb..5a0890c9f 100644 --- a/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -330,9 +330,41 @@ status: new === "Dart" ```dart title="build_tree.dart" - [class]{}-[func]{dfs} + /* 构建二叉树:分治 */ + TreeNode? dfs( + List preorder, + List inorder, + Map hmap, + int i, + int l, + int r, + ) { + // 子树区间为空时终止 + if (r - l < 0) { + return null; + } + // 初始化根节点 + TreeNode? root = TreeNode(preorder[i]); + // 查询 m ,从而划分左右子树 + int m = hmap[preorder[i]]!; + // 子问题:构建左子树 + root.left = dfs(preorder, inorder, hmap, i + 1, l, m - 1); + // 子问题:构建右子树 + root.right = dfs(preorder, inorder, hmap, i + 1 + m - l, m + 1, r); + // 返回根节点 + return root; + } - [class]{}-[func]{buildTree} + /* 构建二叉树 */ + TreeNode? buildTree(List preorder, List inorder) { + // 初始化哈希表,存储 inorder 元素到索引的映射 + Map hmap = {}; + for (int i = 0; i < inorder.length; i++) { + hmap[inorder[i]] = i; + } + TreeNode? root = dfs(preorder, inorder, hmap, 0, 0, inorder.length - 1); + return root; + } ``` === "Rust" diff --git a/chapter_divide_and_conquer/hanota_problem.md b/chapter_divide_and_conquer/hanota_problem.md index 71e3dc2d4..3c40b40db 100644 --- a/chapter_divide_and_conquer/hanota_problem.md +++ b/chapter_divide_and_conquer/hanota_problem.md @@ -362,11 +362,35 @@ status: new === "Dart" ```dart title="hanota.dart" - [class]{}-[func]{move} + /* 移动一个圆盘 */ + void move(List src, List tar) { + // 从 src 顶部拿出一个圆盘 + int pan = src.removeLast(); + // 将圆盘放入 tar 顶部 + tar.add(pan); + } - [class]{}-[func]{dfs} + /* 求解汉诺塔:问题 f(i) */ + void dfs(int i, List src, List buf, List tar) { + // 若 src 只剩下一个圆盘,则直接将其移到 tar + if (i == 1) { + move(src, tar); + return; + } + // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf + dfs(i - 1, src, tar, buf); + // 子问题 f(1) :将 src 剩余一个圆盘移到 tar + move(src, tar); + // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{hanota} + /* 求解汉诺塔 */ + void hanota(List A, List B, List C) { + int n = A.length; + // 将 A 顶部 n 个圆盘借助 B 移到 C + dfs(n, A, B, C); + } ``` === "Rust" diff --git a/chapter_dynamic_programming/dp_problem_features.md b/chapter_dynamic_programming/dp_problem_features.md index 93d2472a0..e8d7f5bd5 100644 --- a/chapter_dynamic_programming/dp_problem_features.md +++ b/chapter_dynamic_programming/dp_problem_features.md @@ -209,7 +209,21 @@ $$ === "Dart" ```dart title="min_cost_climbing_stairs_dp.dart" - [class]{}-[func]{minCostClimbingStairsDP} + /* 爬楼梯最小代价:动态规划 */ + int minCostClimbingStairsDP(List cost) { + int n = cost.length - 1; + if (n == 1 || n == 2) return cost[n]; + // 初始化 dp 表,用于存储子问题的解 + List dp = List.filled(n + 1, 0); + // 初始状态:预设最小子问题的解 + dp[1] = cost[1]; + dp[2] = cost[2]; + // 状态转移:从较小子问题逐步求解较大子问题 + for (int i = 3; i <= n; i++) { + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]; + } + return dp[n]; + } ``` === "Rust" @@ -386,7 +400,18 @@ $$ === "Dart" ```dart title="min_cost_climbing_stairs_dp.dart" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* 爬楼梯最小代价:状态压缩后的动态规划 */ + int minCostClimbingStairsDPComp(List cost) { + int n = cost.length - 1; + if (n == 1 || n == 2) return cost[n]; + int a = cost[1], b = cost[2]; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = min(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "Rust" @@ -632,7 +657,25 @@ $$ === "Dart" ```dart title="climbing_stairs_constraint_dp.dart" - [class]{}-[func]{climbingStairsConstraintDP} + /* 带约束爬楼梯:动态规划 */ + int climbingStairsConstraintDP(int n) { + if (n == 1 || n == 2) { + return n; + } + // 初始化 dp 表,用于存储子问题的解 + List> dp = List.generate(n + 1, (index) => List.filled(3, 0)); + // 初始状态:预设最小子问题的解 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状态转移:从较小子问题逐步求解较大子问题 + for (int i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + return dp[n][1] + dp[n][2]; + } ``` === "Rust" diff --git a/chapter_dynamic_programming/dp_solution_pipeline.md b/chapter_dynamic_programming/dp_solution_pipeline.md index 0494e3fa2..4ca8ed4e4 100644 --- a/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/chapter_dynamic_programming/dp_solution_pipeline.md @@ -276,7 +276,23 @@ $$ === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDFS} + /* 最小路径和:暴力搜索 */ + int minPathSumDFS(List> grid, int i, int j) { + // 若为左上角单元格,则终止搜索 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,则返回 +∞ 代价 + if (i < 0 || j < 0) { + // 在 Dart 中,int 类型是固定范围的整数,不存在表示“无穷大”的值 + return BigInt.from(2).pow(31).toInt(); + } + // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价 + int left = minPathSumDFS(grid, i - 1, j); + int up = minPathSumDFS(grid, i, j - 1); + // 返回从左上角到 (i, j) 的最小路径代价 + return min(left, up) + grid[i][j]; + } ``` === "Rust" @@ -516,7 +532,28 @@ $$ === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDFSMem} + /* 最小路径和:记忆化搜索 */ + int minPathSumDFSMem(List> grid, List> mem, int i, int j) { + // 若为左上角单元格,则终止搜索 + if (i == 0 && j == 0) { + return grid[0][0]; + } + // 若行列索引越界,则返回 +∞ 代价 + if (i < 0 || j < 0) { + // 在 Dart 中,int 类型是固定范围的整数,不存在表示“无穷大”的值 + return BigInt.from(2).pow(31).toInt(); + } + // 若已有记录,则直接返回 + if (mem[i][j] != -1) { + return mem[i][j]; + } + // 左边和上边单元格的最小路径代价 + int left = minPathSumDFSMem(grid, mem, i - 1, j); + int up = minPathSumDFSMem(grid, mem, i, j - 1); + // 记录并返回左上角到 (i, j) 的最小路径代价 + mem[i][j] = min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "Rust" @@ -765,7 +802,28 @@ $$ === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDP} + /* 最小路径和:动态规划 */ + int minPathSumDP(List> grid) { + int n = grid.length, m = grid[0].length; + // 初始化 dp 表 + List> dp = List.generate(n, (i) => List.filled(m, 0)); + dp[0][0] = grid[0][0]; + // 状态转移:首行 + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // 状态转移:首列 + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // 状态转移:其余行列 + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + return dp[n - 1][m - 1]; + } ``` === "Rust" @@ -1041,7 +1099,26 @@ $$ === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDPComp} + /* 最小路径和:状态压缩后的动态规划 */ + int minPathSumDPComp(List> grid) { + int n = grid.length, m = grid[0].length; + // 初始化 dp 表 + List dp = List.filled(m, 0); + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // 状态转移:其余行 + for (int i = 1; i < n; i++) { + // 状态转移:首列 + dp[0] = dp[0] + grid[i][0]; + // 状态转移:其余列 + for (int j = 1; j < m; j++) { + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + return dp[m - 1]; + } ``` === "Rust" diff --git a/chapter_dynamic_programming/edit_distance_problem.md b/chapter_dynamic_programming/edit_distance_problem.md index 5984bde99..ce04d6799 100644 --- a/chapter_dynamic_programming/edit_distance_problem.md +++ b/chapter_dynamic_programming/edit_distance_problem.md @@ -307,7 +307,31 @@ $$ === "Dart" ```dart title="edit_distance.dart" - [class]{}-[func]{editDistanceDP} + /* 编辑距离:动态规划 */ + int editDistanceDP(String s, String t) { + int n = s.length, m = t.length; + List> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0)); + // 状态转移:首行首列 + for (int i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (int j = 1; j <= m; j++) { + dp[0][j] = j; + } + // 状态转移:其余行列 + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + if (s[i - 1] == t[j - 1]) { + // 若两字符相等,则直接跳过此两字符 + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; + } + } + } + return dp[n][m]; + } ``` === "Rust" @@ -643,7 +667,34 @@ $$ === "Dart" ```dart title="edit_distance.dart" - [class]{}-[func]{editDistanceDPComp} + /* 编辑距离:状态压缩后的动态规划 */ + int editDistanceDPComp(String s, String t) { + int n = s.length, m = t.length; + List dp = List.filled(m + 1, 0); + // 状态转移:首行 + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // 状态转移:其余行 + for (int i = 1; i <= n; i++) { + // 状态转移:首列 + int leftup = dp[0]; // 暂存 dp[i-1, j-1] + dp[0] = i; + // 状态转移:其余列 + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // 若两字符相等,则直接跳过此两字符 + dp[j] = leftup; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 更新为下一轮的 dp[i-1, j-1] + } + } + return dp[m]; + } ``` === "Rust" diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming.md b/chapter_dynamic_programming/intro_to_dynamic_programming.md index 57db94ad3..6e1d5c7dc 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -305,9 +305,31 @@ status: new === "Dart" ```dart title="climbing_stairs_backtrack.dart" - [class]{}-[func]{backtrack} + /* 回溯 */ + void backtrack(List choices, int state, int n, List res) { + // 当爬到第 n 阶时,方案数量加 1 + if (state == n) { + res[0]++; + } + // 遍历所有选择 + for (int choice in choices) { + // 剪枝:不允许越过第 n 阶 + if (state + choice > n) break; + // 尝试:做出选择,更新状态 + backtrack(choices, state + choice, n, res); + // 回退 + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* 爬楼梯:回溯 */ + int climbingStairsBacktrack(int n) { + List choices = [1, 2]; // 可选择向上爬 1 或 2 阶 + int state = 0; // 从第 0 阶开始爬 + List res = []; + res.add(0); // 使用 res[0] 记录方案数量 + backtrack(choices, state, n, res); + return res[0]; + } ``` === "Rust" @@ -550,9 +572,19 @@ $$ === "Dart" ```dart title="climbing_stairs_dfs.dart" - [class]{}-[func]{dfs} + /* 搜索 */ + int dfs(int i) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1) + dfs(i - 2); + return count; + } - [class]{}-[func]{climbingStairsDFS} + /* 爬楼梯:搜索 */ + int climbingStairsDFS(int n) { + return dfs(n); + } ``` === "Rust" @@ -840,9 +872,25 @@ $$ === "Dart" ```dart title="climbing_stairs_dfs_mem.dart" - [class]{}-[func]{dfs} + /* 记忆化搜索 */ + int dfs(int i, List mem) { + // 已知 dp[1] 和 dp[2] ,返回之 + if (i == 1 || i == 2) return i; + // 若存在记录 dp[i] ,则直接返回之 + if (mem[i] != -1) return mem[i]; + // dp[i] = dp[i-1] + dp[i-2] + int count = dfs(i - 1, mem) + dfs(i - 2, mem); + // 记录 dp[i] + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* 爬楼梯:记忆化搜索 */ + int climbingStairsDFSMem(int n) { + // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录 + List mem = List.filled(n + 1, -1); + return dfs(n, mem); + } ``` === "Rust" @@ -1071,7 +1119,20 @@ $$ === "Dart" ```dart title="climbing_stairs_dp.dart" - [class]{}-[func]{climbingStairsDP} + /* 爬楼梯:动态规划 */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) return n; + // 初始化 dp 表,用于存储子问题的解 + List dp = List.filled(n + 1, 0); + // 初始状态:预设最小子问题的解 + dp[1] = 1; + dp[2] = 2; + // 状态转移:从较小子问题逐步求解较大子问题 + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } ``` === "Rust" @@ -1270,7 +1331,17 @@ $$ === "Dart" ```dart title="climbing_stairs_dp.dart" - [class]{}-[func]{climbingStairsDPComp} + /* 爬楼梯:状态压缩后的动态规划 */ + int climbingStairsDPComp(int n) { + if (n == 1 || n == 2) return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "Rust" diff --git a/chapter_dynamic_programming/knapsack_problem.md b/chapter_dynamic_programming/knapsack_problem.md index 02fddb72e..4d894d8a5 100644 --- a/chapter_dynamic_programming/knapsack_problem.md +++ b/chapter_dynamic_programming/knapsack_problem.md @@ -228,7 +228,22 @@ $$ === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDFS} + /* 0-1 背包:暴力搜索 */ + int knapsackDFS(List wgt, List val, int i, int c) { + // 若已选完所有物品或背包无容量,则返回价值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若超过背包容量,则只能不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // 计算不放入和放入物品 i 的最大价值 + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 返回两种方案中价值更大的那一个 + return max(no, yes); + } ``` === "Rust" @@ -467,7 +482,33 @@ $$ === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 背包:记忆化搜索 */ + int knapsackDFSMem( + List wgt, + List val, + List> mem, + int i, + int c, + ) { + // 若已选完所有物品或背包无容量,则返回价值 0 + if (i == 0 || c == 0) { + return 0; + } + // 若已有记录,则直接返回 + if (mem[i][c] != -1) { + return mem[i][c]; + } + // 若超过背包容量,则只能不放入背包 + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // 计算不放入和放入物品 i 的最大价值 + int no = knapsackDFSMem(wgt, val, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // 记录并返回两种方案中价值更大的那一个 + mem[i][c] = max(no, yes); + return mem[i][c]; + } ``` === "Rust" @@ -692,7 +733,25 @@ $$ === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDP} + /* 0-1 背包:动态规划 */ + int knapsackDP(List wgt, List val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + List> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0)); + // 状态转移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } ``` === "Rust" @@ -972,7 +1031,23 @@ $$ === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDPComp} + /* 0-1 背包:状态压缩后的动态规划 */ + int knapsackDPComp(List wgt, List val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + List dp = List.filled(cap + 1, 0); + // 状态转移 + for (int i = 1; i <= n; i++) { + // 倒序遍历 + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // 不选和选物品 i 这两种方案的较大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Rust" diff --git a/chapter_dynamic_programming/unbounded_knapsack_problem.md b/chapter_dynamic_programming/unbounded_knapsack_problem.md index dfd324428..da0cfbeb9 100644 --- a/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -225,7 +225,25 @@ $$ === "Dart" ```dart title="unbounded_knapsack.dart" - [class]{}-[func]{unboundedKnapsackDP} + /* 完全背包:动态规划 */ + int unboundedKnapsackDP(List wgt, List val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + List> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0)); + // 状态转移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[i][c] = dp[i - 1][c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[n][cap]; + } ``` === "Rust" @@ -464,7 +482,25 @@ $$ === "Dart" ```dart title="unbounded_knapsack.dart" - [class]{}-[func]{unboundedKnapsackDPComp} + /* 完全背包:状态压缩后的动态规划 */ + int unboundedKnapsackDPComp(List wgt, List val, int cap) { + int n = wgt.length; + // 初始化 dp 表 + List dp = List.filled(cap + 1, 0); + // 状态转移 + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // 若超过背包容量,则不选物品 i + dp[c] = dp[c]; + } else { + // 不选和选物品 i 这两种方案的较大值 + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Rust" @@ -497,7 +533,7 @@ $$ !!! question - 给定 $n$ 种硬币,第 $i$ 个硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,**每种硬币可以重复选取**,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 $-1$ 。 + 给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,**每种硬币可以重复选取**,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 $-1$ 。 ![零钱兑换问题的示例数据](unbounded_knapsack_problem.assets/coin_change_example.png) @@ -511,7 +547,7 @@ $$ **第一步:思考每轮的决策,定义状态,从而得到 $dp$ 表** -状态 $[i, a]$ 对应的子问题为:**前 $i$ 个硬币能够凑出金额 $a$ 的最少硬币个数**,记为 $dp[i, a]$ 。 +状态 $[i, a]$ 对应的子问题为:**前 $i$ 种硬币能够凑出金额 $a$ 的最少硬币个数**,记为 $dp[i, a]$ 。 二维 $dp$ 表的尺寸为 $(n+1) \times (amt+1)$ 。 @@ -769,7 +805,30 @@ $$ === "Dart" ```dart title="coin_change.dart" - [class]{}-[func]{coinChangeDP} + /* 零钱兑换:动态规划 */ + int coinChangeDP(List coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // 初始化 dp 表 + List> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0)); + // 状态转移:首行首列 + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // 状态转移:其余行列 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + return dp[n][amt] != MAX ? dp[n][amt] : -1; + } ``` === "Rust" @@ -1065,7 +1124,27 @@ $$ === "Dart" ```dart title="coin_change.dart" - [class]{}-[func]{coinChangeDPComp} + /* 零钱兑换:状态压缩后的动态规划 */ + int coinChangeDPComp(List coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // 初始化 dp 表 + List dp = List.filled(amt + 1, MAX); + dp[0] = 0; + // 状态转移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[a] = dp[a]; + } else { + // 不选和选硬币 i 这两种方案的较小值 + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + return dp[amt] != MAX ? dp[amt] : -1; + } ``` === "Rust" @@ -1099,13 +1178,13 @@ $$ !!! question - 给定 $n$ 种硬币,第 $i$ 个硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,**问在凑出目标金额的硬币组合数量**。 + 给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,**问在凑出目标金额的硬币组合数量**。 ![零钱兑换问题 II 的示例数据](unbounded_knapsack_problem.assets/coin_change_ii_example.png)

Fig. 零钱兑换问题 II 的示例数据

-相比于上一题,本题目标是组合数量,因此子问题变为:**前 $i$ 个硬币能够凑出金额 $a$ 的组合数量**。而 $dp$ 表仍然是尺寸为 $(n+1) \times (amt + 1)$ 的二维矩阵。 +相比于上一题,本题目标是组合数量,因此子问题变为:**前 $i$ 种硬币能够凑出金额 $a$ 的组合数量**。而 $dp$ 表仍然是尺寸为 $(n+1) \times (amt + 1)$ 的二维矩阵。 当前状态的组合数量等于不选当前硬币与选当前硬币这两种决策的组合数量之和。状态转移方程为: @@ -1332,7 +1411,29 @@ $$ === "Dart" ```dart title="coin_change_ii.dart" - [class]{}-[func]{coinChangeIIDP} + /* 零钱兑换 II:动态规划 */ + int coinChangeIIDP(List coins, int amt) { + int n = coins.length; + // 初始化 dp 表 + List> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0)); + // 初始化首列 + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // 状态转移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[i][a] = dp[i - 1][a]; + } else { + // 不选和选硬币 i 这两种方案之和 + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + return dp[n][amt]; + } ``` === "Rust" @@ -1561,7 +1662,26 @@ $$ === "Dart" ```dart title="coin_change_ii.dart" - [class]{}-[func]{coinChangeIIDPComp} + /* 零钱兑换 II:状态压缩后的动态规划 */ + int coinChangeIIDPComp(List coins, int amt) { + int n = coins.length; + // 初始化 dp 表 + List dp = List.filled(amt + 1, 0); + dp[0] = 1; + // 状态转移 + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // 若超过背包容量,则不选硬币 i + dp[a] = dp[a]; + } else { + // 不选和选硬币 i 这两种方案之和 + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "Rust" diff --git a/chapter_greedy/fractional_knapsack_problem.md b/chapter_greedy/fractional_knapsack_problem.md index 153bc3c1a..d4d68fb34 100644 --- a/chapter_greedy/fractional_knapsack_problem.md +++ b/chapter_greedy/fractional_knapsack_problem.md @@ -276,9 +276,36 @@ status: new === "Dart" ```dart title="fractional_knapsack.dart" - [class]{Item}-[func]{} + /* 物品 */ + class Item { + int w; // 物品重量 + int v; // 物品价值 - [class]{}-[func]{fractionalKnapsack} + Item(this.w, this.v); + } + + /* 分数背包:贪心 */ + double fractionalKnapsack(List wgt, List val, int cap) { + // 创建物品列表,包含两个属性:重量、价值 + List items = List.generate(wgt.length, (i) => Item(wgt[i], val[i])); + // 按照单位价值 item.v / item.w 从高到低进行排序 + items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w)); + // 循环贪心选择 + double res = 0; + for (Item item in items) { + if (item.w <= cap) { + // 若剩余容量充足,则将当前物品整个装进背包 + res += item.v; + cap -= item.w; + } else { + // 若剩余容量不足,则将当前物品的一部分装进背包 + res += item.v / item.w * cap; + // 已无剩余容量,因此跳出循环 + break; + } + } + return res; + } ``` === "Rust" diff --git a/chapter_greedy/greedy_algorithm.md b/chapter_greedy/greedy_algorithm.md index 345e67a9a..f98849f48 100644 --- a/chapter_greedy/greedy_algorithm.md +++ b/chapter_greedy/greedy_algorithm.md @@ -16,7 +16,7 @@ status: new !!! question - 给定 $n$ 种硬币,第 $i$ 个硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 $-1$ 。 + 给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 $-1$ 。 这道题的贪心策略在生活中很常见:给定目标金额,**我们贪心地选择不大于且最接近它的硬币**,不断循环该步骤,直至凑出目标金额为止。 @@ -174,7 +174,24 @@ status: new === "Dart" ```dart title="coin_change_greedy.dart" - [class]{}-[func]{coinChangeGreedy} + /* 零钱兑换:贪心 */ + int coinChangeGreedy(List coins, int amt) { + // 假设 coins 列表有序 + int i = coins.length - 1; + int count = 0; + // 循环进行贪心选择,直到无剩余金额 + while (amt > 0) { + // 找到小于且最接近剩余金额的硬币 + while (i > 0 && coins[i] > amt) { + i--; + } + // 选择 coins[i] + amt -= coins[i]; + count++; + } + // 若未找到可行方案,则返回 -1 + return amt == 0 ? count : -1; + } ``` === "Rust" diff --git a/chapter_greedy/max_capacity_problem.md b/chapter_greedy/max_capacity_problem.md index 03c6e5b07..03e650ec2 100644 --- a/chapter_greedy/max_capacity_problem.md +++ b/chapter_greedy/max_capacity_problem.md @@ -245,7 +245,26 @@ $$ === "Dart" ```dart title="max_capacity.dart" - [class]{}-[func]{maxCapacity} + /* 最大容量:贪心 */ + int maxCapacity(List ht) { + // 初始化 i, j 分列数组两端 + int i = 0, j = ht.length - 1; + // 初始最大容量为 0 + int res = 0; + // 循环贪心选择,直至两板相遇 + while (i < j) { + // 更新最大容量 + int cap = min(ht[i], ht[j]) * (j - i); + res = max(res, cap); + // 向内移动短板 + if (ht[i] < ht[j]) { + i++; + } else { + j--; + } + } + return res; + } ``` === "Rust" diff --git a/chapter_greedy/max_product_cutting_problem.md b/chapter_greedy/max_product_cutting_problem.md index 183df2903..02a6e9fff 100644 --- a/chapter_greedy/max_product_cutting_problem.md +++ b/chapter_greedy/max_product_cutting_problem.md @@ -227,7 +227,26 @@ $$ === "Dart" ```dart title="max_product_cutting.dart" - [class]{}-[func]{maxProductCutting} + /* 最大切分乘积:贪心 */ + int maxProductCutting(int n) { + // 当 n <= 3 时,必须切分出一个 1 + if (n <= 3) { + return 1 * (n - 1); + } + // 贪心地切分出 3 ,a 为 3 的个数,b 为余数 + int a = n ~/ 3; + int b = n % 3; + if (b == 1) { + // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2 + return (pow(3, a - 1) * 2 * 2).toInt(); + } + if (b == 2) { + // 当余数为 2 时,不做处理 + return (pow(3, a) * 2).toInt(); + } + // 当余数为 0 时,不做处理 + return pow(3, a).toInt(); + } ``` === "Rust" diff --git a/chapter_hashing/hash_algorithm.md b/chapter_hashing/hash_algorithm.md index 284e71be2..1cc3df0be 100644 --- a/chapter_hashing/hash_algorithm.md +++ b/chapter_hashing/hash_algorithm.md @@ -231,25 +231,89 @@ index = hash(key) % capacity === "JS" ```javascript title="simple_hash.js" - [class]{}-[func]{addHash} + /* 加法哈希 */ + function addHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{mulHash} + /* 乘法哈希 */ + function mulHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (31 * hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{xorHash} + /* 异或哈希 */ + function xorHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash ^= c.charCodeAt(0); + } + return hash & MODULUS; + } - [class]{}-[func]{rotHash} + /* 旋转哈希 */ + function rotHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; + } + return hash; + } ``` === "TS" ```typescript title="simple_hash.ts" - [class]{}-[func]{addHash} + /* 加法哈希 */ + function addHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{mulHash} + /* 乘法哈希 */ + function mulHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (31 * hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{xorHash} + /* 异或哈希 */ + function xorHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash ^= c.charCodeAt(0); + } + return hash & MODULUS; + } - [class]{}-[func]{rotHash} + /* 旋转哈希 */ + function rotHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; + } + return hash; + } ``` === "C" diff --git a/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md index 87e7dd7e3..5989ea359 100644 --- a/chapter_hashing/hash_collision.md +++ b/chapter_hashing/hash_collision.md @@ -472,13 +472,217 @@ comments: true === "JS" ```javascript title="hash_map_chaining.js" - [class]{HashMapChaining}-[func]{} + /* 链式地址哈希表 */ + class HashMapChaining { + #size; // 键值对数量 + #capacity; // 哈希表容量 + #loadThres; // 触发扩容的负载因子阈值 + #extendRatio; // 扩容倍数 + #buckets; // 桶数组 + + /* 构造方法 */ + constructor() { + this.#size = 0; + this.#capacity = 4; + this.#loadThres = 2 / 3.0; + this.#extendRatio = 2; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + } + + /* 哈希函数 */ + #hashFunc(key) { + return key % this.#capacity; + } + + /* 负载因子 */ + #loadFactor() { + return this.#size / this.#capacity; + } + + /* 查询操作 */ + get(key) { + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // 遍历桶,若找到 key 则返回对应 val + for (const pair of bucket) { + if (pair.key === key) { + return pair.val; + } + } + // 若未找到 key 则返回 null + return null; + } + + /* 添加操作 */ + put(key, val) { + // 当负载因子超过阈值时,执行扩容 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // 遍历桶,若遇到指定 key ,则更新对应 val 并返回 + for (const pair of bucket) { + if (pair.key === key) { + pair.val = val; + return; + } + } + // 若无该 key ,则将键值对添加至尾部 + const pair = new Pair(key, val); + bucket.push(pair); + this.#size++; + } + + /* 删除操作 */ + remove(key) { + const index = this.#hashFunc(key); + let bucket = this.#buckets[index]; + // 遍历桶,从中删除键值对 + for (let i = 0; i < bucket.length; i++) { + if (bucket[i].key === key) { + bucket.splice(i, 1); + this.size--; + break; + } + } + } + + /* 扩容哈希表 */ + #extend() { + // 暂存原哈希表 + const bucketsTmp = this.#buckets; + // 初始化扩容后的新哈希表 + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + this.#size = 0; + // 将键值对从原哈希表搬运至新哈希表 + for (const bucket of bucketsTmp) { + for (const pair of bucket) { + this.put(pair.key, pair.val); + } + } + } + + /* 打印哈希表 */ + print() { + for (const bucket of this.#buckets) { + let res = []; + for (const pair of bucket) { + res.push(pair.key + ' -> ' + pair.val); + } + console.log(res); + } + } + } ``` === "TS" ```typescript title="hash_map_chaining.ts" - [class]{HashMapChaining}-[func]{} + /* 链式地址哈希表 */ + class HashMapChaining { + #size: number; // 键值对数量 + #capacity: number; // 哈希表容量 + #loadThres: number; // 触发扩容的负载因子阈值 + #extendRatio: number; // 扩容倍数 + #buckets: Pair[][]; // 桶数组 + + /* 构造方法 */ + constructor() { + this.#size = 0; + this.#capacity = 4; + this.#loadThres = 2 / 3.0; + this.#extendRatio = 2; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + } + + /* 哈希函数 */ + #hashFunc(key: number): number { + return key % this.#capacity; + } + + /* 负载因子 */ + #loadFactor(): number { + return this.#size / this.#capacity; + } + + /* 查询操作 */ + get(key: number): string | null { + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // 遍历桶,若找到 key 则返回对应 val + for (const pair of bucket) { + if (pair.key === key) { + return pair.val; + } + } + // 若未找到 key 则返回 null + return null; + } + + /* 添加操作 */ + put(key: number, val: string): void { + // 当负载因子超过阈值时,执行扩容 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // 遍历桶,若遇到指定 key ,则更新对应 val 并返回 + for (const pair of bucket) { + if (pair.key === key) { + pair.val = val; + return; + } + } + // 若无该 key ,则将键值对添加至尾部 + const pair = new Pair(key, val); + bucket.push(pair); + this.#size++; + } + + /* 删除操作 */ + remove(key: number): void { + const index = this.#hashFunc(key); + let bucket = this.#buckets[index]; + // 遍历桶,从中删除键值对 + for (let i = 0; i < bucket.length; i++) { + if (bucket[i].key === key) { + bucket.splice(i, 1); + this.#size--; + break; + } + } + } + + /* 扩容哈希表 */ + #extend(): void { + // 暂存原哈希表 + const bucketsTmp = this.#buckets; + // 初始化扩容后的新哈希表 + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + this.#size = 0; + // 将键值对从原哈希表搬运至新哈希表 + for (const bucket of bucketsTmp) { + for (const pair of bucket) { + this.put(pair.key, pair.val); + } + } + } + + /* 打印哈希表 */ + print(): void { + for (const bucket of this.#buckets) { + let res = []; + for (const pair of bucket) { + res.push(pair.key + ' -> ' + pair.val); + } + console.log(res); + } + } + } ``` === "C" @@ -1455,13 +1659,257 @@ comments: true === "JS" ```javascript title="hash_map_open_addressing.js" - [class]{HashMapOpenAddressing}-[func]{} + /* 开放寻址哈希表 */ + class HashMapOpenAddressing { + #size; // 键值对数量 + #capacity; // 哈希表容量 + #loadThres; // 触发扩容的负载因子阈值 + #extendRatio; // 扩容倍数 + #buckets; // 桶数组 + #removed; // 删除标记 + + /* 构造方法 */ + constructor() { + this.#size = 0; + this.#capacity = 4; + this.#loadThres = 2.0 / 3.0; + this.#extendRatio = 2; + this.#buckets = new Array(this.#capacity).fill(null); + this.#removed = new Pair(-1, '-1'); + } + + /* 哈希函数 */ + #hashFunc(key) { + return key % this.#capacity; + } + + /* 负载因子 */ + #loadFactor() { + return this.#size / this.#capacity; + } + + /* 查询操作 */ + get(key) { + const index = this.#hashFunc(key); + // 线性探测,从 index 开始向后遍历 + for (let i = 0; i < this.#capacity; i++) { + // 计算桶索引,越过尾部返回头部 + const j = (index + i) % this.#capacity; + // 若遇到空桶,说明无此 key ,则返回 null + if (this.#buckets[j] === null) return null; + // 若遇到指定 key ,则返回对应 val + if ( + this.#buckets[j].key === key && + this.#buckets[j][key] !== this.#removed.key + ) + return this.#buckets[j].val; + } + return null; + } + + /* 添加操作 */ + put(key, val) { + // 当负载因子超过阈值时,执行扩容 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + // 线性探测,从 index 开始向后遍历 + for (let i = 0; i < this.#capacity; i++) { + // 计算桶索引,越过尾部返回头部 + let j = (index + i) % this.#capacity; + // 若遇到空桶、或带有删除标记的桶,则将键值对放入该桶 + if ( + this.#buckets[j] === null || + this.#buckets[j][key] === this.#removed.key + ) { + this.#buckets[j] = new Pair(key, val); + this.#size += 1; + return; + } + // 若遇到指定 key ,则更新对应 val + if (this.#buckets[j].key === key) { + this.#buckets[j].val = val; + return; + } + } + } + + /* 删除操作 */ + remove(key) { + const index = this.#hashFunc(key); + // 线性探测,从 index 开始向后遍历 + for (let i = 0; i < this.#capacity; i++) { + // 计算桶索引,越过尾部返回头部 + const j = (index + i) % this.#capacity; + // 若遇到空桶,说明无此 key ,则直接返回 + if (this.#buckets[j] === null) { + return; + } + // 若遇到指定 key ,则标记删除并返回 + if (this.#buckets[j].key === key) { + this.#buckets[j] = this.#removed; + this.#size -= 1; + return; + } + } + } + + /* 扩容哈希表 */ + #extend() { + // 暂存原哈希表 + const bucketsTmp = this.#buckets; + // 初始化扩容后的新哈希表 + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null); + this.#size = 0; + // 将键值对从原哈希表搬运至新哈希表 + for (const pair of bucketsTmp) { + if (pair !== null && pair.key !== this.#removed.key) { + this.put(pair.key, pair.val); + } + } + } + + /* 打印哈希表 */ + print() { + for (const pair of this.#buckets) { + if (pair !== null) { + console.log(pair.key + ' -> ' + pair.val); + } else { + console.log('null'); + } + } + } + } ``` === "TS" ```typescript title="hash_map_open_addressing.ts" - [class]{HashMapOpenAddressing}-[func]{} + /* 开放寻址哈希表 */ + class HashMapOpenAddressing { + #size: number; // 键值对数量 + #capacity: number; // 哈希表容量 + #loadThres: number; // 触发扩容的负载因子阈值 + #extendRatio: number; // 扩容倍数 + #buckets: Pair[]; // 桶数组 + #removed: Pair; // 删除标记 + + /* 构造方法 */ + constructor() { + this.#size = 0; + this.#capacity = 4; + this.#loadThres = 2.0 / 3.0; + this.#extendRatio = 2; + this.#buckets = new Array(this.#capacity).fill(null); + this.#removed = new Pair(-1, '-1'); + } + + /* 哈希函数 */ + #hashFunc(key: number): number { + return key % this.#capacity; + } + + /* 负载因子 */ + #loadFactor(): number { + return this.#size / this.#capacity; + } + + /* 查询操作 */ + get(key: number): string | null { + const index = this.#hashFunc(key); + // 线性探测,从 index 开始向后遍历 + for (let i = 0; i < this.#capacity; i++) { + // 计算桶索引,越过尾部返回头部 + const j = (index + i) % this.#capacity; + // 若遇到空桶,说明无此 key ,则返回 null + if (this.#buckets[j] === null) return null; + // 若遇到指定 key ,则返回对应 val + if ( + this.#buckets[j].key === key && + this.#buckets[j][key] !== this.#removed.key + ) + return this.#buckets[j].val; + } + return null; + } + + /* 添加操作 */ + put(key: number, val: string): void { + // 当负载因子超过阈值时,执行扩容 + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + // 线性探测,从 index 开始向后遍历 + for (let i = 0; i < this.#capacity; i++) { + // 计算桶索引,越过尾部返回头部 + let j = (index + i) % this.#capacity; + // 若遇到空桶、或带有删除标记的桶,则将键值对放入该桶 + if ( + this.#buckets[j] === null || + this.#buckets[j][key] === this.#removed.key + ) { + this.#buckets[j] = new Pair(key, val); + this.#size += 1; + return; + } + // 若遇到指定 key ,则更新对应 val + if (this.#buckets[j].key === key) { + this.#buckets[j].val = val; + return; + } + } + } + + /* 删除操作 */ + remove(key: number): void { + const index = this.#hashFunc(key); + // 线性探测,从 index 开始向后遍历 + for (let i = 0; i < this.#capacity; i++) { + // 计算桶索引,越过尾部返回头部 + const j = (index + i) % this.#capacity; + // 若遇到空桶,说明无此 key ,则直接返回 + if (this.#buckets[j] === null) { + return; + } + // 若遇到指定 key ,则标记删除并返回 + if (this.#buckets[j].key === key) { + this.#buckets[j] = this.#removed; + this.#size -= 1; + return; + } + } + } + + /* 扩容哈希表 */ + #extend(): void { + // 暂存原哈希表 + const bucketsTmp = this.#buckets; + // 初始化扩容后的新哈希表 + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null); + this.#size = 0; + // 将键值对从原哈希表搬运至新哈希表 + for (const pair of bucketsTmp) { + if (pair !== null && pair.key !== this.#removed.key) { + this.put(pair.key, pair.val); + } + } + } + + /* 打印哈希表 */ + print(): void { + for (const pair of this.#buckets) { + if (pair !== null) { + console.log(pair.key + ' -> ' + pair.val); + } else { + console.log('null'); + } + } + } + } ``` === "C" diff --git a/chapter_hashing/hash_map.md b/chapter_hashing/hash_map.md index d6f247696..cb0f5521d 100755 --- a/chapter_hashing/hash_map.md +++ b/chapter_hashing/hash_map.md @@ -896,7 +896,7 @@ index = hash(key) % capacity let arr = []; for (let i = 0; i < this.#buckets.length; i++) { if (this.#buckets[i]) { - arr.push(this.#buckets[i]?.key); + arr.push(this.#buckets[i].key); } } return arr; @@ -907,7 +907,7 @@ index = hash(key) % capacity let arr = []; for (let i = 0; i < this.#buckets.length; i++) { if (this.#buckets[i]) { - arr.push(this.#buckets[i]?.val); + arr.push(this.#buckets[i].val); } } return arr; @@ -989,7 +989,7 @@ index = hash(key) % capacity let arr: (number | undefined)[] = []; for (let i = 0; i < this.buckets.length; i++) { if (this.buckets[i]) { - arr.push(this.buckets[i]?.key); + arr.push(this.buckets[i].key); } } return arr; @@ -1000,7 +1000,7 @@ index = hash(key) % capacity let arr: (string | undefined)[] = []; for (let i = 0; i < this.buckets.length; i++) { if (this.buckets[i]) { - arr.push(this.buckets[i]?.val); + arr.push(this.buckets[i].val); } } return arr; diff --git a/chapter_sorting/bubble_sort.md b/chapter_sorting/bubble_sort.md index b67a47d13..b8232cf9e 100755 --- a/chapter_sorting/bubble_sort.md +++ b/chapter_sorting/bubble_sort.md @@ -242,7 +242,7 @@ comments: true void bubbleSort(List nums) { // 外循环:未排序区间为 [0, i] for (int i = nums.length - 1; i > 0; i--) { - // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 + // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // 交换 nums[j] 与 nums[j + 1] @@ -519,7 +519,7 @@ comments: true // 外循环:未排序区间为 [0, i] for (int i = nums.length - 1; i > 0; i--) { bool flag = false; // 初始化标志位 - // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 + // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // 交换 nums[j] 与 nums[j + 1] diff --git a/chapter_sorting/counting_sort.md b/chapter_sorting/counting_sort.md index bcb24ecd9..8bc863b2d 100644 --- a/chapter_sorting/counting_sort.md +++ b/chapter_sorting/counting_sort.md @@ -276,7 +276,7 @@ comments: true for (int num in nums) { m = max(m, num); } - // 2. 统计各数字的出现次数 + // 2. 统计各数字的出现次数 // counter[num] 代表 num 的出现次数 List counter = List.filled(m + 1, 0); for (int num in nums) { @@ -728,7 +728,7 @@ $$ for (int i = n - 1; i >= 0; i--) { int num = nums[i]; res[counter[num] - 1] = num; // 将 num 放置到对应索引处 - counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引 + counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引 } // 使用结果数组 res 覆盖原数组 nums nums.setAll(0, res); diff --git a/chapter_sorting/radix_sort.md b/chapter_sorting/radix_sort.md index b28e610b7..92e41be95 100644 --- a/chapter_sorting/radix_sort.md +++ b/chapter_sorting/radix_sort.md @@ -622,7 +622,7 @@ $$ void radixSort(List nums) { // 获取数组的最大元素,用于判断最大位数 // dart 中 int 的长度是 64 位的 - int m = -1 << 63; + int m = -1 << 63; for (int num in nums) if (num > m) m = num; // 按照从低位到高位的顺序遍历 for (int exp = 1; exp <= m; exp *= 10) diff --git a/chapter_tree/array_representation_of_tree.md b/chapter_tree/array_representation_of_tree.md index 23635f8c6..c1e444341 100644 --- a/chapter_tree/array_representation_of_tree.md +++ b/chapter_tree/array_representation_of_tree.md @@ -503,13 +503,173 @@ comments: true === "JS" ```javascript title="array_binary_tree.js" - [class]{ArrayBinaryTree}-[func]{} + /* 数组表示下的二叉树类 */ + class ArrayBinaryTree { + #tree; + + /* 构造方法 */ + constructor(arr) { + this.#tree = arr; + } + + /* 节点数量 */ + size() { + return this.#tree.length; + } + + /* 获取索引为 i 节点的值 */ + val(i) { + // 若索引越界,则返回 null ,代表空位 + if (i < 0 || i >= this.size()) return null; + return this.#tree[i]; + } + + /* 获取索引为 i 节点的左子节点的索引 */ + left(i) { + return 2 * i + 1; + } + + /* 获取索引为 i 节点的右子节点的索引 */ + right(i) { + return 2 * i + 2; + } + + /* 获取索引为 i 节点的父节点的索引 */ + parent(i) { + return (i - 1) / 2; + } + + /* 层序遍历 */ + levelOrder() { + let res = []; + // 直接遍历数组 + for (let i = 0; i < this.size(); i++) { + if (this.val(i) !== null) res.push(this.val(i)); + } + return res; + } + + /* 深度优先遍历 */ + #dfs(i, order, res) { + // 若为空位,则返回 + if (this.val(i) === null) return; + // 前序遍历 + if (order === 'pre') res.push(this.val(i)); + this.#dfs(this.left(i), order, res); + // 中序遍历 + if (order === 'in') res.push(this.val(i)); + this.#dfs(this.right(i), order, res); + // 后序遍历 + if (order === 'post') res.push(this.val(i)); + } + + /* 前序遍历 */ + preOrder() { + const res = []; + this.#dfs(0, 'pre', res); + return res; + } + + /* 中序遍历 */ + inOrder() { + const res = []; + this.#dfs(0, 'in', res); + return res; + } + + /* 后序遍历 */ + postOrder() { + const res = []; + this.#dfs(0, 'post', res); + return res; + } + } ``` === "TS" ```typescript title="array_binary_tree.ts" - [class]{ArrayBinaryTree}-[func]{} + /* 数组表示下的二叉树类 */ + class ArrayBinaryTree { + #tree: (number | null)[]; + + /* 构造方法 */ + constructor(arr: (number | null)[]) { + this.#tree = arr; + } + + /* 节点数量 */ + size(): number { + return this.#tree.length; + } + + /* 获取索引为 i 节点的值 */ + val(i: number): number | null { + // 若索引越界,则返回 null ,代表空位 + if (i < 0 || i >= this.size()) return null; + return this.#tree[i]; + } + + /* 获取索引为 i 节点的左子节点的索引 */ + left(i: number): number { + return 2 * i + 1; + } + + /* 获取索引为 i 节点的右子节点的索引 */ + right(i: number): number { + return 2 * i + 2; + } + + /* 获取索引为 i 节点的父节点的索引 */ + parent(i: number): number { + return (i - 1) / 2; + } + + /* 层序遍历 */ + levelOrder(): number[] { + let res = []; + // 直接遍历数组 + for (let i = 0; i < this.size(); i++) { + if (this.val(i) !== null) res.push(this.val(i)); + } + return res; + } + + /* 深度优先遍历 */ + #dfs(i: number, order: Order, res: (number | null)[]): void { + // 若为空位,则返回 + if (this.val(i) === null) return; + // 前序遍历 + if (order === 'pre') res.push(this.val(i)); + this.#dfs(this.left(i), order, res); + // 中序遍历 + if (order === 'in') res.push(this.val(i)); + this.#dfs(this.right(i), order, res); + // 后序遍历 + if (order === 'post') res.push(this.val(i)); + } + + /* 前序遍历 */ + preOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'pre', res); + return res; + } + + /* 中序遍历 */ + inOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'in', res); + return res; + } + + /* 后序遍历 */ + postOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'post', res); + return res; + } + } ``` === "C"