diff --git a/chapter_computational_complexity/index.md b/chapter_computational_complexity/index.md index a3d3bcc1c..231efa2ca 100644 --- a/chapter_computational_complexity/index.md +++ b/chapter_computational_complexity/index.md @@ -3,11 +3,11 @@ comments: true icon: material/timer-sand --- -# 第 2 章   时空复杂度 +# 第 2 章   复杂度分析
-![时空复杂度](../assets/covers/chapter_complexity_analysis.jpg){ width="600" } +![复杂度分析](../assets/covers/chapter_complexity_analysis.jpg){ width="600" }
diff --git a/chapter_computational_complexity/iteration_and_recursion.md b/chapter_computational_complexity/iteration_and_recursion.md index 7d6596fb7..b6642fa73 100644 --- a/chapter_computational_complexity/iteration_and_recursion.md +++ b/chapter_computational_complexity/iteration_and_recursion.md @@ -102,7 +102,15 @@ status: new === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{forLoop} + /* for 循环 */ + int forLoop(int n) { + int res = 0; + // 循环求和 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Rust" @@ -216,7 +224,17 @@ status: new === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{whileLoop} + /* while 循环 */ + int whileLoop(int n) { + int res = 0; + int i = 1; // 初始化条件变量 + // 循环求和 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // 更新条件变量 + } + return res; + } ``` === "Rust" @@ -326,7 +344,19 @@ status: new === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{whileLoopII} + /* while 循环(两次更新) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // 初始化条件变量 + // 循环求和 1, 4, ... + while (i <= n) { + res += i; + // 更新条件变量 + i++; + i *= 2; + } + return res; + } ``` === "Rust" @@ -434,7 +464,18 @@ status: new === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{nestedForLoop} + /* 双层 for 循环 */ + String nestedForLoop(int n) { + String res = ""; + // 循环 i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + // 循环 j = 1, 2, ..., n-1, n + for (int j = 1; j <= n; j++) { + res += "($i, $j), "; + } + } + return res; + } ``` === "Rust" @@ -557,7 +598,15 @@ status: new === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{recur} + /* 递归 */ + int recur(int n) { + // 终止条件 + if (n == 1) return 1; + // 递:递归调用 + int res = recur(n - 1); + // 归:返回结果 + return n + res; + } ``` === "Rust" @@ -689,7 +738,13 @@ status: new === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{tailRecur} + /* 尾递归 */ + int tailRecur(int n, int res) { + // 终止条件 + if (n == 0) return res; + // 尾递归调用 + return tailRecur(n - 1, res + n); + } ``` === "Rust" @@ -813,7 +868,15 @@ status: new === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{fib} + /* 斐波那契数列:递归 */ + int fib(int n) { + // 终止条件 f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) return n - 1; + // 递归调用 f(n) = f(n-1) + f(n-2) + int res = fib(n - 1) + fib(n - 2); + // 返回结果 f(n) + return res; + } ``` === "Rust" diff --git a/chapter_computational_complexity/performance_evaluation.md b/chapter_computational_complexity/performance_evaluation.md index 2f5ec2f23..9d2b964a8 100644 --- a/chapter_computational_complexity/performance_evaluation.md +++ b/chapter_computational_complexity/performance_evaluation.md @@ -9,7 +9,7 @@ comments: true 1. **找到问题解法**:算法需要在规定的输入范围内,可靠地求得问题的正确解。 2. **寻求最优解法**:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。 -也就是说,在能够解决问题的前提下,算法效率是衡量算法优劣的主要评价指标,它包括以下两个维度。 +也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度。 - **时间效率**:算法运行速度的快慢。 - **空间效率**:算法占用内存空间的大小。 @@ -24,7 +24,7 @@ comments: true 一方面,**难以排除测试环境的干扰因素**。硬件配置会影响算法的性能表现。比如在某台计算机中,算法 `A` 的运行时间比算法 `B` 短;但在另一台配置不同的计算机中,我们可能得到相反的测试结果。这意味着我们需要在各种机器上进行测试,统计平均效率,而这是不现实的。 -另一方面,**展开完整测试非常耗费资源**。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 `A` 的运行时间比算法 `B` 更少;而输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这样需要耗费大量的计算资源。 +另一方面,**展开完整测试非常耗费资源**。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 `A` 的运行时间比算法 `B` 更少;而输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这需要耗费大量的计算资源。 ## 2.1.2   理论估算 diff --git a/chapter_computational_complexity/space_complexity.md b/chapter_computational_complexity/space_complexity.md index 65ec48072..0b7c8b80e 100755 --- a/chapter_computational_complexity/space_complexity.md +++ b/chapter_computational_complexity/space_complexity.md @@ -671,10 +671,6 @@ $$

图 2-16   常见的空间复杂度类型

-!!! tip - - 部分示例代码需要一些前置知识,包括数组、链表、二叉树、递归算法等。如果你遇到看不懂的地方,可以在学完后面章节后再来复习。 - ### 1.   常数阶 $O(1)$ 常数阶常见于数量与输入数据大小 $n$ 无关的常量、变量、对象。 diff --git a/chapter_computational_complexity/summary.md b/chapter_computational_complexity/summary.md index 82c88cc4c..f4b9df6b2 100644 --- a/chapter_computational_complexity/summary.md +++ b/chapter_computational_complexity/summary.md @@ -15,7 +15,7 @@ comments: true - 时间复杂度用于衡量算法运行时间随数据量增长的趋势,可以有效评估算法效率,但在某些情况下可能失效,如在输入的数据量较小或时间复杂度相同时,无法精确对比算法效率的优劣。 - 最差时间复杂度使用大 $O$ 符号表示,对应函数渐近上界,反映当 $n$ 趋向正无穷时,操作数量 $T(n)$ 的增长级别。 - 推算时间复杂度分为两步,首先统计操作数量,然后判断渐近上界。 -- 常见时间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$、$O(n!)$ 等。 +- 常见时间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n \log n)$、$O(n^2)$、$O(2^n)$ 和 $O(n!)$ 等。 - 某些算法的时间复杂度非固定,而是与输入数据的分布有关。时间复杂度分为最差、最佳、平均时间复杂度,最佳时间复杂度几乎不用,因为输入数据一般需要满足严格条件才能达到最佳情况。 - 平均时间复杂度反映算法在随机数据输入下的运行效率,最接近实际应用中的算法性能。计算平均时间复杂度需要统计输入数据分布以及综合后的数学期望。 @@ -24,7 +24,7 @@ comments: true - 空间复杂度的作用类似于时间复杂度,用于衡量算法占用空间随数据量增长的趋势。 - 算法运行过程中的相关内存空间可分为输入空间、暂存空间、输出空间。通常情况下,输入空间不计入空间复杂度计算。暂存空间可分为指令空间、数据空间、栈帧空间,其中栈帧空间通常仅在递归函数中影响空间复杂度。 - 我们通常只关注最差空间复杂度,即统计算法在最差输入数据和最差运行时间点下的空间复杂度。 -- 常见空间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$、$O(2^n)$ 等。 +- 常见空间复杂度从小到大排列有 $O(1)$、$O(\log n)$、$O(n)$、$O(n^2)$ 和 $O(2^n)$ 等。 ## 2.5.1   Q & A @@ -42,7 +42,7 @@ comments: true - Java 和 C# 是面向对象的编程语言,代码块(方法)通常都是作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。 - C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。 -!!! question "图“空间复杂度的常见类型”反映的是否是占用空间的绝对大小?" +!!! question "图“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?" 不是,该图片展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。 diff --git a/chapter_computational_complexity/time_complexity.md b/chapter_computational_complexity/time_complexity.md index 7d8f253a8..f063701bd 100755 --- a/chapter_computational_complexity/time_complexity.md +++ b/chapter_computational_complexity/time_complexity.md @@ -619,18 +619,11 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因 我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 big-$O$ notation」,表示函数 $T(n)$ 的「渐近上界 asymptotic upper bound」。 -时间复杂度分析本质上是计算“操作数量函数 $T(n)$”的渐近上界。接下来,我们来看函数渐近上界的数学定义。 +时间复杂度分析本质上是计算“操作数量函数 $T(n)$”的渐近上界,其具有明确的数学定义。 !!! abstract "函数渐近上界" - 若存在正实数 $c$ 和实数 $n_0$ ,使得对于所有的 $n > n_0$ ,均有 - $$ - T(n) \leq c \cdot f(n) - $$ - 则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为 - $$ - T(n) = O(f(n)) - $$ + 若存在正实数 $c$ 和实数 $n_0$ ,使得对于所有的 $n > n_0$ ,均有 $T(n) \leq c \cdot f(n)$ ,则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为 $T(n) = O(f(n))$ 。 如图 2-8 所示,计算渐近上界就是寻找一个函数 $f(n)$ ,使得当 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别,仅相差一个常数项 $c$ 的倍数。 @@ -650,17 +643,9 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因 1. **忽略 $T(n)$ 中的常数项**。因为它们都与 $n$ 无关,所以对时间复杂度不产生影响。 2. **省略所有系数**。例如,循环 $2n$ 次、$5n + 1$ 次等,都可以简化记为 $n$ 次,因为 $n$ 前面的系数对时间复杂度没有影响。 -3. **循环嵌套时使用乘法**。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用上述 `1.` 和 `2.` 技巧。 - -以下代码与公式分别展示了使用上述技巧前后的统计结果。两者推出的时间复杂度相同,都为 $O(n^2)$ 。 +3. **循环嵌套时使用乘法**。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用第 `1.` 点和第 `2.` 点的技巧。、 -$$ -\begin{aligned} -T(n) & = 2n(n + 1) + (5n + 1) + 2 & \text{完整统计 (-.-|||)} \newline -& = 2n^2 + 7n + 3 \newline -T(n) & = n^2 + n & \text{偷懒统计 (o.O)} -\end{aligned} -$$ +给定一个函数,我们可以用上述技巧来统计操作数量。 === "Java" @@ -875,6 +860,16 @@ $$ } ``` +以下公式展示了使用上述技巧前后的统计结果,两者推出的时间复杂度都为 $O(n^2)$ 。 + +$$ +\begin{aligned} +T(n) & = 2n(n + 1) + (5n + 1) + 2 & \text{完整统计 (-.-|||)} \newline +& = 2n^2 + 7n + 3 \newline +T(n) & = n^2 + n & \text{偷懒统计 (o.O)} +\end{aligned} +$$ + ### 2.   第二步:判断渐近上界 **时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。 @@ -910,15 +905,11 @@ $$

图 2-9   常见的时间复杂度类型

-!!! tip - - 部分示例代码需要一些预备知识,包括数组、递归等。如果你遇到不理解的部分,可以在学完后面章节后再回顾。现阶段,请先专注于理解时间复杂度的含义和推算方法。 - ### 1.   常数阶 $O(1)$ 常数阶的操作数量与输入数据大小 $n$ 无关,即不随着 $n$ 的变化而变化。 -对于以下算法,尽管操作数量 `size` 可能很大,但由于其与输入数据大小 $n$ 无关,因此时间复杂度仍为 $O(1)$ : +在以下函数中,尽管操作数量 `size` 可能很大,但由于其与输入数据大小 $n$ 无关,因此时间复杂度仍为 $O(1)$ : === "Java" @@ -1407,7 +1398,7 @@ $$ ### 3.   平方阶 $O(n^2)$ -平方阶的操作数量相对于输入数据大小以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ : +平方阶的操作数量相对于输入数据大小 $n$ 以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环都为 $O(n)$ ,因此总体为 $O(n^2)$ : === "Java" @@ -2967,7 +2958,7 @@ $$ ## 2.3.5   最差、最佳、平均时间复杂度 -**算法的时间效率往往不是固定的,而是与输入数据的分布有关**。假设输入一个长度为 $n$ 的数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,每个数字只出现一次,但元素顺序是随机打乱的,任务目标是返回元素 $1$ 的索引。我们可以得出以下结论。 +**算法的时间效率往往不是固定的,而是与输入数据的分布有关**。假设输入一个长度为 $n$ 的数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,每个数字只出现一次;但元素顺序是随机打乱的,任务目标是返回元素 $1$ 的索引。我们可以得出以下结论。 - 当 `nums = [?, ?, ..., 1]` ,即当末尾元素是 $1$ 时,需要完整遍历数组,**达到最差时间复杂度 $O(n)$** 。 - 当 `nums = [1, ?, ?, ...]` ,即当首个元素为 $1$ 时,无论数组多长都不需要继续遍历,**达到最佳时间复杂度 $\Omega(1)$** 。 diff --git a/chapter_introduction/algorithms_are_everywhere.md b/chapter_introduction/algorithms_are_everywhere.md index f164e7229..235b2d976 100644 --- a/chapter_introduction/algorithms_are_everywhere.md +++ b/chapter_introduction/algorithms_are_everywhere.md @@ -45,7 +45,7 @@ comments: true 上述整理扑克牌的方法本质上是“插入排序”算法,它在处理小型数据集时非常高效。许多编程语言的排序库函数中都存在插入排序的身影。 -**例三:货币找零**。假设我们在超市购买了 $69$ 元的商品,给收银员付了 $100$ 元,则收银员需要找我们 $31$ 元。他会很自然地完成图 1-3 所示的思考。 +**例三:货币找零**。假设我们在超市购买了 $69$ 元的商品,给了收银员 $100$ 元,则收银员需要找我们 $31$ 元。他会很自然地完成如图 1-3 所示的思考。 1. 可选项是比 $31$ 元面值更小的货币,包括 $1$ 元、$5$ 元、$10$ 元、$20$ 元。 2. 从可选项中拿出最大的 $20$ 元,剩余 $31 - 20 = 11$ 元。 @@ -63,4 +63,4 @@ comments: true !!! tip - 如果你对数据结构、算法、数组和二分查找等概念仍感到一知半解,请不要担心,继续往下阅读,这本书将引导你迈入数据结构与算法的知识殿堂。 + 如果你对数据结构、算法、数组和二分查找等概念仍感到一知半解,请继续往下阅读,这本书将引导你迈入数据结构与算法的知识殿堂。 diff --git a/chapter_introduction/what_is_dsa.md b/chapter_introduction/what_is_dsa.md index 8fd0862f4..903ca9ffb 100644 --- a/chapter_introduction/what_is_dsa.md +++ b/chapter_introduction/what_is_dsa.md @@ -27,7 +27,7 @@ comments: true ## 1.2.3   数据结构与算法的关系 -数据结构与算法高度相关、紧密结合,具体表现在图 1-4 所示的几个方面。 +如图 1-4 所示,数据结构与算法高度相关、紧密结合,具体表现以下三个方面。 - 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及用于操作数据的方法。 - 算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,结合算法才能解决特定问题。