diff --git a/chapter_array_and_linkedlist/linked_list.md b/chapter_array_and_linkedlist/linked_list.md index 63eddd32f..723ada263 100755 --- a/chapter_array_and_linkedlist/linked_list.md +++ b/chapter_array_and_linkedlist/linked_list.md @@ -1329,10 +1329,10 @@ comments: true ## 4.2.4   链表典型应用 -单向链表通常用于实现栈、队列、散列表和图等数据结构。 +单向链表通常用于实现栈、队列、哈希表和图等数据结构。 - **栈与队列**:当插入和删除操作都在链表的一端进行时,它表现出先进后出的的特性,对应栈;当插入操作在链表的一端进行,删除操作在链表的另一端进行,它表现出先进先出的特性,对应队列。 -- **散列表**:链地址法是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表中。 +- **哈希表**:链地址法是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表中。 - **图**:邻接表是表示图的一种常用方式,在其中,图的每个顶点都与一个链表相关联,链表中的每个元素都代表与该顶点相连的其他顶点。 双向链表常被用于需要快速查找前一个和下一个元素的场景。 diff --git a/chapter_backtracking/n_queens_problem.md b/chapter_backtracking/n_queens_problem.md index 5dbaa7a59..5d95dfbd3 100644 --- a/chapter_backtracking/n_queens_problem.md +++ b/chapter_backtracking/n_queens_problem.md @@ -614,6 +614,6 @@ comments: true } ``` -逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n, n-1, \cdots, 2, 1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。 +逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n, n-1, \dots, 2, 1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。 数组 `state` 使用 $O(n^2)$ 空间,数组 `cols` , `diags1` , `diags2` 皆使用 $O(n)$ 空间。最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。因此,**空间复杂度为 $O(n^2)$** 。 diff --git a/chapter_backtracking/subset_sum_problem.md b/chapter_backtracking/subset_sum_problem.md index 6f013024a..9eaf534fb 100644 --- a/chapter_backtracking/subset_sum_problem.md +++ b/chapter_backtracking/subset_sum_problem.md @@ -449,24 +449,24 @@ comments: true **我们考虑在搜索过程中通过剪枝进行去重**。观察下图,重复子集是在以不同顺序选择数组元素时产生的,具体来看: -1. 第一轮和第二轮分别选择 $3$ , $4$ ,会生成包含这两个元素的所有子集,记为 $[3, 4, \cdots]$ 。 -2. 若第一轮选择 $4$ ,**则第二轮应该跳过 $3$** ,因为该选择产生的子集 $[4, 3, \cdots]$ 和 `1.` 中生成的子集完全重复。 +1. 第一轮和第二轮分别选择 $3$ , $4$ ,会生成包含这两个元素的所有子集,记为 $[3, 4, \dots]$ 。 +2. 若第一轮选择 $4$ ,**则第二轮应该跳过 $3$** ,因为该选择产生的子集 $[4, 3, \dots]$ 和 `1.` 中生成的子集完全重复。 分支越靠右,需要排除的分支也越多,例如: -1. 前两轮选择 $3$ , $5$ ,生成子集 $[3, 5, \cdots]$ 。 -2. 前两轮选择 $4$ , $5$ ,生成子集 $[4, 5, \cdots]$ 。 -3. 若第一轮选择 $5$ ,**则第二轮应该跳过 $3$ 和 $4$** ,因为子集 $[5, 3, \cdots]$ 和子集 $[5, 4, \cdots]$ 和 `1.` , `2.` 中生成的子集完全重复。 +1. 前两轮选择 $3$ , $5$ ,生成子集 $[3, 5, \dots]$ 。 +2. 前两轮选择 $4$ , $5$ ,生成子集 $[4, 5, \dots]$ 。 +3. 若第一轮选择 $5$ ,**则第二轮应该跳过 $3$ 和 $4$** ,因为子集 $[5, 3, \dots]$ 和子集 $[5, 4, \dots]$ 和 `1.` , `2.` 中生成的子集完全重复。 ![不同选择顺序导致的重复子集](subset_sum_problem.assets/subset_sum_i_pruning.png)

图:不同选择顺序导致的重复子集

-总结来看,给定输入数组 $[x_1, x_2, \cdots, x_n]$ ,设搜索过程中的选择序列为 $[x_{i_1}, x_{i_2}, \cdots , x_{i_m}]$ ,则该选择序列需要满足 $i_1 \leq i_2 \leq \cdots \leq i_m$ ,**不满足该条件的选择序列都会造成重复,应当剪枝**。 +总结来看,给定输入数组 $[x_1, x_2, \dots, x_n]$ ,设搜索过程中的选择序列为 $[x_{i_1}, x_{i_2}, \dots , x_{i_m}]$ ,则该选择序列需要满足 $i_1 \leq i_2 \leq \dots \leq i_m$ ,**不满足该条件的选择序列都会造成重复,应当剪枝**。 ### 3.   代码实现 -为实现该剪枝,我们初始化变量 `start` ,用于指示遍历起点。**当做出选择 $x_{i}$ 后,设定下一轮从索引 $i$ 开始遍历**。这样做就可以让选择序列满足 $i_1 \leq i_2 \leq \cdots \leq i_m$ ,从而保证子集唯一。 +为实现该剪枝,我们初始化变量 `start` ,用于指示遍历起点。**当做出选择 $x_{i}$ 后,设定下一轮从索引 $i$ 开始遍历**。这样做就可以让选择序列满足 $i_1 \leq i_2 \leq \dots \leq i_m$ ,从而保证子集唯一。 除此之外,我们还对代码进行了两项优化: diff --git a/chapter_computational_complexity/index.md b/chapter_computational_complexity/index.md index 73fbe5355..f3dc24ac1 100644 --- a/chapter_computational_complexity/index.md +++ b/chapter_computational_complexity/index.md @@ -3,17 +3,17 @@ comments: true icon: material/timer-sand --- -# 第 2 章   复杂度 +# 第 2 章   时空复杂度
-![复杂度](../assets/covers/chapter_complexity_analysis.jpg){ width="600" } +![时空复杂度](../assets/covers/chapter_complexity_analysis.jpg){ width="600" }
!!! abstract - 复杂度犹如浩瀚的算法宇宙中的时空向导。 + 复杂度分析犹如浩瀚的算法宇宙中的时空向导。 它带领我们在时间与空间这两个维度上深入探索,寻找更优雅的解决方案。 diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md index e3481087b..d1659ad8a 100755 --- a/chapter_computational_complexity/space_complexity.md +++ b/chapter_computational_complexity/space_complexity.md @@ -40,7 +40,7 @@ comments: true /* 函数 */ int function() { - // do something... + // 执行某些操作... return 0; } @@ -65,7 +65,7 @@ comments: true /* 函数 */ int func() { - // do something... + // 执行某些操作... return 0; } @@ -89,7 +89,7 @@ comments: true def function() -> int: """函数""" - # do something... + # 执行某些操作... return 0 def algorithm(n) -> int: # 输入数据 @@ -116,7 +116,7 @@ comments: true /* 函数 */ func function() int { - // do something... + // 执行某些操作... return 0 } @@ -144,7 +144,7 @@ comments: true /* 函数 */ function constFunc() { - // do something + // 执行某些操作 return 0; } @@ -172,7 +172,7 @@ comments: true /* 函数 */ function constFunc(): number { - // do something + // 执行某些操作 return 0; } @@ -190,7 +190,7 @@ comments: true ```c title="" /* 函数 */ int func() { - // do something... + // 执行某些操作... return 0; } @@ -214,7 +214,7 @@ comments: true /* 函数 */ int function() { - // do something... + // 执行某些操作... return 0; } @@ -242,7 +242,7 @@ comments: true /* 函数 */ func function() -> Int { - // do something... + // 执行某些操作... return 0 } @@ -273,7 +273,7 @@ comments: true /* 函数 */ int function() { - // do something... + // 执行某些操作... return 0; } @@ -441,7 +441,7 @@ comments: true ```java title="" int function() { - // do something + // 执行某些操作 return 0; } /* 循环 O(1) */ @@ -461,7 +461,7 @@ comments: true ```cpp title="" int func() { - // do something + // 执行某些操作 return 0; } /* 循环 O(1) */ @@ -481,7 +481,7 @@ comments: true ```python title="" def function() -> int: - # do something + # 执行某些操作 return 0 def loop(n: int): @@ -499,7 +499,7 @@ comments: true ```go title="" func function() int { - // do something + // 执行某些操作 return 0 } @@ -523,7 +523,7 @@ comments: true ```javascript title="" function constFunc() { - // do something + // 执行某些操作 return 0; } /* 循环 O(1) */ @@ -543,7 +543,7 @@ comments: true ```typescript title="" function constFunc(): number { - // do something + // 执行某些操作 return 0; } /* 循环 O(1) */ @@ -563,7 +563,7 @@ comments: true ```c title="" int func() { - // do something + // 执行某些操作 return 0; } /* 循环 O(1) */ @@ -583,7 +583,7 @@ comments: true ```csharp title="" int function() { - // do something + // 执行某些操作 return 0; } /* 循环 O(1) */ @@ -604,7 +604,7 @@ comments: true ```swift title="" @discardableResult func function() -> Int { - // do something + // 执行某些操作 return 0 } @@ -634,7 +634,7 @@ comments: true ```dart title="" int function() { - // do something + // 执行某些操作 return 0; } /* 循环 O(1) */ @@ -686,7 +686,7 @@ $$ ```java title="space_complexity.java" /* 函数 */ int function() { - // do something + // 执行某些操作 return 0; } @@ -713,7 +713,7 @@ $$ ```cpp title="space_complexity.cpp" /* 函数 */ int func() { - // do something + // 执行某些操作 return 0; } @@ -740,7 +740,7 @@ $$ ```python title="space_complexity.py" def function() -> int: """函数""" - # do something + # 执行某些操作 return 0 def constant(n: int): @@ -762,7 +762,7 @@ $$ ```go title="space_complexity.go" /* 函数 */ func function() int { - // do something... + // 执行某些操作... return 0 } @@ -791,7 +791,7 @@ $$ ```javascript title="space_complexity.js" /* 函数 */ function constFunc() { - // do something + // 执行某些操作 return 0; } @@ -818,7 +818,7 @@ $$ ```typescript title="space_complexity.ts" /* 函数 */ function constFunc(): number { - // do something + // 执行某些操作 return 0; } @@ -845,7 +845,7 @@ $$ ```c title="space_complexity.c" /* 函数 */ int func() { - // do something + // 执行某些操作 return 0; } @@ -873,7 +873,7 @@ $$ ```csharp title="space_complexity.cs" /* 函数 */ int function() { - // do something + // 执行某些操作 return 0; } @@ -901,7 +901,7 @@ $$ /* 函数 */ @discardableResult func function() -> Int { - // do something + // 执行某些操作 return 0 } @@ -958,7 +958,7 @@ $$ ```dart title="space_complexity.dart" /* 函数 */ int function() { - // do something + // 执行某些操作 return 0; } @@ -985,7 +985,7 @@ $$ ```rust title="space_complexity.rs" /* 函数 */ fn function() ->i32 { - // do something + // 执行某些操作 return 0; } @@ -1274,7 +1274,7 @@ $$ } ``` -以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间: +以下函数的递归深度为 $n$ ,即同时存在 $n$ 个未返回的 `linear_recur()` 函数,使用 $O(n)$ 大小的栈帧空间: === "Java" @@ -1635,7 +1635,7 @@ $$ } ``` -在以下递归函数中,同时存在 $n$ 个未返回的 `algorithm()` ,并且每个函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $\frac{n}{2}$ ,因此总体占用 $O(n^2)$ 空间。 +以下函数的递归深度为 $n$ ,在每个递归函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $n / 2$ ,因此总体占用 $O(n^2)$ 空间。 === "Java" @@ -1972,11 +1972,9 @@ $$ ### 5.   对数阶 $O(\log n)$ -对数阶常见于分治算法和数据类型转换等。 +对数阶常见于分治算法。例如归并排序,输入长度为 $n$ 的数组,每轮递归将数组从中点划分为两半,形成高度为 $\log n$ 的递归树,使用 $O(\log n)$ 栈帧空间。 -例如归并排序算法,输入长度为 $n$ 的数组,每轮递归将数组从中点划分为两半,形成高度为 $\log n$ 的递归树,使用 $O(\log n)$ 栈帧空间。 - -再例如将数字转化为字符串,输入任意正整数 $n$ ,它的位数为 $\log_{10} n + 1$ ,即对应字符串长度为 $\log_{10} n + 1$ ,因此空间复杂度为 $O(\log_{10} n + 1) = O(\log n)$ 。 +再例如将数字转化为字符串,输入一个正整数 $n$ ,它的位数为 $\log_{10} n + 1$ ,即对应字符串长度为 $\log_{10} n + 1$ ,因此空间复杂度为 $O(\log_{10} n + 1) = O(\log n)$ 。 ## 2.3.4   权衡时间与空间 diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md index eddf50b01..f1abcef92 100755 --- a/chapter_computational_complexity/time_complexity.md +++ b/chapter_computational_complexity/time_complexity.md @@ -1606,11 +1606,7 @@ $$

图:常数阶、线性阶和平方阶的时间复杂度

-以冒泡排序为例,外层循环执行 $n - 1$ 次,内层循环执行 $n-1, n-2, \cdots, 2, 1$ 次,平均为 $\frac{n}{2}$ 次,因此时间复杂度为 $O(n^2)$ : - -$$ -O((n - 1) \frac{n}{2}) = O(n^2) -$$ +以冒泡排序为例,外层循环执行 $n - 1$ 次,内层循环执行 $n-1, n-2, \dots, 2, 1$ 次,平均为 $n / 2$ 次,因此时间复杂度为 $O((n - 1) n / 2) = O(n^2)$ 。 === "Java" @@ -2557,7 +2553,17 @@ $$ } ``` -对数阶常出现于基于分治策略的算法中,体现了“一分为多”和“化繁为简”的算法思想。它增长缓慢,是理想的时间复杂度,仅次于常数阶。 +对数阶常出现于基于分治策略的算法中,体现了“一分为多”和“化繁为简”的算法思想。它增长缓慢,是仅次于常数阶的理想的时间复杂度。 + +!!! tip + + 准确来说,“一分为 $m$”对应的时间复杂度是 $O(\log_m n)$ 。而通过对数换底公式,我们可以得到具有不同底数的、相等的时间复杂度: + + $$ + O(\log_m n) = O(\log_k n / \log_k m) = O(\log_k n) + $$ + + 因此我们通常会省略底数 $m$ ,将对数阶直接记为 $O(\log n)$ 。 ### 6.   线性对数阶 $O(n \log n)$ @@ -2756,7 +2762,7 @@ $$ 阶乘阶对应数学上的“全排列”问题。给定 $n$ 个互不重复的元素,求其所有可能的排列方案,方案数量为: $$ -n! = n \times (n - 1) \times (n - 2) \times \cdots \times 2 \times 1 +n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1 $$ 阶乘通常使用递归实现。例如在以下代码中,第一层分裂出 $n$ 个,第二层分裂出 $n - 1$ 个,以此类推,直至第 $n$ 层时停止分裂: @@ -2953,7 +2959,7 @@ $$

图:阶乘阶的时间复杂度

-请注意,因为 $n! > 2^n$ ,所以阶乘阶比指数阶增长得更快,在 $n$ 较大时也是不可接受的。 +请注意,因为当 $n \geq 4$ 时恒有 $n! > 2^n$ ,所以阶乘阶比指数阶增长得更快,在 $n$ 较大时也是不可接受的。 ## 2.2.5   最差、最佳、平均时间复杂度 @@ -3318,7 +3324,7 @@ $$ 从上述示例可以看出,最差或最佳时间复杂度只出现于“特殊的数据分布”,这些情况的出现概率可能很小,并不能真实地反映算法运行效率。相比之下,**平均时间复杂度可以体现算法在随机输入数据下的运行效率**,用 $\Theta$ 记号来表示。 -对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 $1$ 出现在任意索引的概率都是相等的,那么算法的平均循环次数就是数组长度的一半 $\frac{n}{2}$ ,平均时间复杂度为 $\Theta(\frac{n}{2}) = \Theta(n)$ 。 +对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 $1$ 出现在任意索引的概率都是相等的,那么算法的平均循环次数就是数组长度的一半 $n / 2$ ,平均时间复杂度为 $\Theta(n / 2) = \Theta(n)$ 。 但对于较为复杂的算法,计算平均时间复杂度往往是比较困难的,因为很难分析出在数据分布下的整体数学期望。在这种情况下,我们通常使用最差时间复杂度作为算法效率的评判标准。 diff --git a/chapter_data_structure/summary.md b/chapter_data_structure/summary.md index e76edfc77..30d4358f0 100644 --- a/chapter_data_structure/summary.md +++ b/chapter_data_structure/summary.md @@ -19,7 +19,7 @@ comments: true !!! question "为什么哈希表同时包含线性数据结构和非线性数据结构?" - 哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续散列表章节会讲)。在拉链法中,数组中每个地址(桶)指向一个链表;当这个链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。 + 哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续哈希表章节会讲)。在拉链法中,数组中每个地址(桶)指向一个链表;当这个链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。 !!! question "`char` 类型的长度是 1 byte 吗?" diff --git a/chapter_divide_and_conquer/divide_and_conquer.md b/chapter_divide_and_conquer/divide_and_conquer.md index 151eaa64e..a9ef14a4e 100644 --- a/chapter_divide_and_conquer/divide_and_conquer.md +++ b/chapter_divide_and_conquer/divide_and_conquer.md @@ -41,7 +41,7 @@ status: new ### 1.   操作数量优化 -以“冒泡排序”为例,其处理一个长度为 $n$ 的数组需要 $O(n^2)$ 时间。假设我们把数组从中点分为两个子数组,则划分需要 $O(n)$ 时间,排序每个子数组需要 $O((\frac{n}{2})^2)$ 时间,合并两个子数组需要 $O(n)$ 时间,总体时间复杂度为: +以“冒泡排序”为例,其处理一个长度为 $n$ 的数组需要 $O(n^2)$ 时间。假设我们把数组从中点分为两个子数组,则划分需要 $O(n)$ 时间,排序每个子数组需要 $O((n / 2)^2)$ 时间,合并两个子数组需要 $O(n)$ 时间,总体时间复杂度为: $$ O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n) diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming.md b/chapter_dynamic_programming/intro_to_dynamic_programming.md index dd7b10b6b..4849eb119 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -367,7 +367,7 @@ status: new 我们可以尝试从问题分解的角度分析这道题。设爬到第 $i$ 阶共有 $dp[i]$ 种方案,那么 $dp[i]$ 就是原问题,其子问题包括: $$ -dp[i-1] , dp[i-2] , \cdots , dp[2] , dp[1] +dp[i-1] , dp[i-2] , \dots , dp[2] , dp[1] $$ 由于每轮只能上 $1$ 阶或 $2$ 阶,因此当我们站在第 $i$ 阶楼梯上时,上一轮只可能站在第 $i - 1$ 阶或第 $i - 2$ 阶上。换句话说,我们只能从第 $i -1$ 阶或第 $i - 2$ 阶前往第 $i$ 阶。 diff --git a/chapter_greedy/max_capacity_problem.md b/chapter_greedy/max_capacity_problem.md index 05c8bb916..e74eca623 100644 --- a/chapter_greedy/max_capacity_problem.md +++ b/chapter_greedy/max_capacity_problem.md @@ -302,7 +302,7 @@ $$ 比如在状态 $cap[i, j]$ 下,$i$ 为短板、$j$ 为长板。若贪心地将短板 $i$ 向内移动一格,会导致以下状态被“跳过”。**这意味着之后无法验证这些状态的容量大小**。 $$ -cap[i, i+1], cap[i, i+2], \cdots, cap[i, j-2], cap[i, j-1] +cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1] $$ ![移动短板导致被跳过的状态](max_capacity_problem.assets/max_capacity_skipped_states.png) diff --git a/chapter_hashing/hash_algorithm.md b/chapter_hashing/hash_algorithm.md index 7bea818d1..3e1800f63 100644 --- a/chapter_hashing/hash_algorithm.md +++ b/chapter_hashing/hash_algorithm.md @@ -501,8 +501,8 @@ index = hash(key) % capacity $$ \begin{aligned} \text{modulus} & = 9 \newline -\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \cdots \} \newline -\text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\cdots \} +\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline +\text{hash} & = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \} \end{aligned} $$ @@ -511,8 +511,8 @@ $$ $$ \begin{aligned} \text{modulus} & = 13 \newline -\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \cdots \} \newline -\text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \cdots \} +\text{key} & = \{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \dots \} \newline +\text{hash} & = \{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \dots \} \end{aligned} $$ diff --git a/chapter_hashing/hash_collision.md b/chapter_hashing/hash_collision.md index 086cfe891..efe146230 100644 --- a/chapter_hashing/hash_collision.md +++ b/chapter_hashing/hash_collision.md @@ -2441,7 +2441,7 @@ comments: true ### 2.   多次哈希 -顾名思义,多次哈希方法是使用多个哈希函数 $f_1(x)$ , $f_2(x)$ , $f_3(x)$ , $\cdots$ 进行探测。 +顾名思义,多次哈希方法是使用多个哈希函数 $f_1(x)$ , $f_2(x)$ , $f_3(x)$ , $\dots$ 进行探测。 - **插入元素**:若哈希函数 $f_1(x)$ 出现冲突,则尝试 $f_2(x)$ ,以此类推,直到找到空位后插入元素。 - **查找元素**:在相同的哈希函数顺序下进行查找,直到找到目标元素时返回;或遇到空位或已尝试所有哈希函数,说明哈希表中不存在该元素,则返回 $\text{None}$ 。 diff --git a/chapter_hashing/index.md b/chapter_hashing/index.md index a477cd425..a8de2bc16 100644 --- a/chapter_hashing/index.md +++ b/chapter_hashing/index.md @@ -3,17 +3,17 @@ comments: true icon: material/table-search --- -# 第 6 章   散列表 +# 第 6 章   哈希表
-![散列表](../assets/covers/chapter_hashing.jpg){ width="600" } +![哈希表](../assets/covers/chapter_hashing.jpg){ width="600" }
!!! abstract - 在计算机世界中,散列表如同一位智能的图书管理员。 + 在计算机世界中,哈希表如同一位智能的图书管理员。 他知道如何计算索书号,从而可以快速找到目标书籍。 diff --git a/chapter_heap/build_heap.md b/chapter_heap/build_heap.md index 890557fdb..8e4e20ae7 100644 --- a/chapter_heap/build_heap.md +++ b/chapter_heap/build_heap.md @@ -211,22 +211,22 @@ comments: true 因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。 $$ -T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1 +T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1 $$ 化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,得到 $$ \begin{aligned} -T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{h-1}\times1 \newline -2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \cdots + 2^{h}\times1 \newline +T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline +2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^{h}\times1 \newline \end{aligned} $$ 使用错位相减法,用下式 $2 T(h)$ 减去上式 $T(h)$ ,可得 $$ -2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \cdots + 2^{h-1} + 2^h +2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h $$ 观察上式,发现 $T(h)$ 是一个等比数列,可直接使用求和公式,得到时间复杂度为 diff --git a/chapter_heap/top_k.md b/chapter_heap/top_k.md index f9ca95032..a4b5d0d16 100644 --- a/chapter_heap/top_k.md +++ b/chapter_heap/top_k.md @@ -12,7 +12,7 @@ comments: true ## 8.3.1   方法一:遍历选择 -我们可以进行 $k$ 轮遍历,分别在每轮中提取第 $1$ , $2$ , $\cdots$ , $k$ 大的元素,时间复杂度为 $O(nk)$ 。 +我们可以进行 $k$ 轮遍历,分别在每轮中提取第 $1$ , $2$ , $\dots$ , $k$ 大的元素,时间复杂度为 $O(nk)$ 。 该方法只适用于 $k \ll n$ 的情况,因为当 $k$ 与 $n$ 比较接近时,其时间复杂度趋向于 $O(n^2)$ ,非常耗时。 diff --git a/chapter_introduction/what_is_dsa.md b/chapter_introduction/what_is_dsa.md index a1d80e524..b490cacbd 100644 --- a/chapter_introduction/what_is_dsa.md +++ b/chapter_introduction/what_is_dsa.md @@ -30,7 +30,7 @@ comments: true 数据结构与算法高度相关、紧密结合,具体表现在以下几个方面。 - 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及用于操作数据的方法。 -- 算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,通过结合算法才能解决特定问题。 +- 算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,结合算法才能解决特定问题。 - 特定算法通常会有对应最优的数据结构。算法通常可以基于不同的数据结构进行实现,但最终执行效率可能相差很大。 ![数据结构与算法的关系](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png) diff --git a/chapter_preface/about_the_book.md b/chapter_preface/about_the_book.md index 1bcd0dcd0..346eab5d6 100644 --- a/chapter_preface/about_the_book.md +++ b/chapter_preface/about_the_book.md @@ -27,7 +27,7 @@ comments: true 本书主要内容包括: - **复杂度分析**:数据结构和算法的评价维度与方法。时间复杂度、空间复杂度的推算方法、常见类型、示例等。 -- **数据结构**:基本数据类型,数据结构的分类方法。数组、链表、栈、队列、散列表、树、堆、图等数据结构的定义、优缺点、常用操作、常见类型、典型应用、实现方法等。 +- **数据结构**:基本数据类型,数据结构的分类方法。数组、链表、栈、队列、哈希表、树、堆、图等数据结构的定义、优缺点、常用操作、常见类型、典型应用、实现方法等。 - **算法**:搜索、排序、分治、回溯、动态规划、贪心等算法的定义、优缺点、效率、应用场景、解题步骤、示例题目等。 ![Hello 算法内容结构](about_the_book.assets/hello_algo_mindmap.png) diff --git a/chapter_sorting/bubble_sort.md b/chapter_sorting/bubble_sort.md index 98648d7b5..3390efd93 100755 --- a/chapter_sorting/bubble_sort.md +++ b/chapter_sorting/bubble_sort.md @@ -561,6 +561,6 @@ comments: true ## 11.3.3   算法特性 -- **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ ,总和为 $\frac{(n - 1) n}{2}$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。 +- **时间复杂度为 $O(n^2)$ 、自适应排序** :各轮“冒泡”遍历的数组长度依次为 $n - 1$ , $n - 2$ , $\dots$ , $2$ , $1$ ,总和为 $(n - 1) n / 2$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。 - **空间复杂度为 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。 - **稳定排序**:由于在“冒泡”中遇到相等元素不交换。 diff --git a/chapter_sorting/insertion_sort.md b/chapter_sorting/insertion_sort.md index 7364cb9f5..a3a12043d 100755 --- a/chapter_sorting/insertion_sort.md +++ b/chapter_sorting/insertion_sort.md @@ -250,7 +250,7 @@ comments: true ## 11.4.2   算法特性 -- **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和得到 $\frac{(n - 1) n}{2}$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。 +- **时间复杂度 $O(n^2)$ 、自适应排序** :最差情况下,每次插入操作分别需要循环 $n - 1$ , $n-2$ , $\dots$ , $2$ , $1$ 次,求和得到 $(n - 1) n / 2$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。 - **空间复杂度 $O(1)$ 、原地排序** :指针 $i$ , $j$ 使用常数大小的额外空间。 - **稳定排序**:在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序。 diff --git a/chapter_sorting/quick_sort.md b/chapter_sorting/quick_sort.md index 77569afaa..5aec4b168 100755 --- a/chapter_sorting/quick_sort.md +++ b/chapter_sorting/quick_sort.md @@ -1057,7 +1057,7 @@ comments: true **在某些输入下,快速排序可能占用空间较多**。以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 $0$ ,递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。 -为了防止栈帧空间的累积,我们可以在每轮哨兵排序完成后,比较两个子数组的长度,**仅对较短的子数组进行递归**。由于较短子数组的长度不会超过 $\frac{n}{2}$ ,因此这种方法能确保递归深度不超过 $\log n$ ,从而将最差空间复杂度优化至 $O(\log n)$ 。 +为了防止栈帧空间的累积,我们可以在每轮哨兵排序完成后,比较两个子数组的长度,**仅对较短的子数组进行递归**。由于较短子数组的长度不会超过 $n / 2$ ,因此这种方法能确保递归深度不超过 $\log n$ ,从而将最差空间复杂度优化至 $O(\log n)$ 。 === "Java" diff --git a/chapter_sorting/selection_sort.md b/chapter_sorting/selection_sort.md index f7d8c6b41..36f4b4940 100644 --- a/chapter_sorting/selection_sort.md +++ b/chapter_sorting/selection_sort.md @@ -286,7 +286,7 @@ comments: true ## 11.2.1   算法特性 -- **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\cdots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。 +- **时间复杂度为 $O(n^2)$ 、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$ , $n - 1$ , $\dots$ , $2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。 - **空间复杂度 $O(1)$ 、原地排序**:指针 $i$ , $j$ 使用常数大小的额外空间。 - **非稳定排序**:在交换元素时,有可能将 `nums[i]` 交换至其相等元素的右边,导致两者的相对顺序发生改变。 diff --git a/chapter_tree/binary_tree_traversal.md b/chapter_tree/binary_tree_traversal.md index 12c0cbd82..83ca4e3f6 100755 --- a/chapter_tree/binary_tree_traversal.md +++ b/chapter_tree/binary_tree_traversal.md @@ -324,7 +324,7 @@ comments: true **时间复杂度**:所有节点被访问一次,使用 $O(n)$ 时间,其中 $n$ 为节点数量。 -**空间复杂度**:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $\frac{n + 1}{2}$ 个节点,占用 $O(n)$ 空间。 +**空间复杂度**:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 $(n + 1) / 2$ 个节点,占用 $O(n)$ 空间。 ## 7.2.2   前序、中序、后序遍历