Modify 。** to **。 for better visualization.

This commit is contained in:
Yudong Jin
2023-01-07 20:34:32 +08:00
parent fc4d7e5e3b
commit 694ea4f665
22 changed files with 252 additions and 220 deletions

View File

@ -8,8 +8,8 @@ comments: true
在开始学习算法之前,我们首先要想清楚算法的设计目标是什么,或者说,如何来评判算法的好与坏。整体上看,我们设计算法时追求两个层面的目标。
1. **找到问题解法** 算法需要能够在规定的输入范围下,可靠地求得问题的正确解。
2. **寻求最优解法** 同一个问题可能存在多种解法,而我们希望算法效率尽可能的高。
1. **找到问题解法**算法需要能够在规定的输入范围下,可靠地求得问题的正确解。
2. **寻求最优解法**同一个问题可能存在多种解法,而我们希望算法效率尽可能的高。
换言之,在可以解决问题的前提下,算法效率则是主要评价维度,包括:
@ -24,9 +24,9 @@ comments: true
假设我们现在有算法 A 和 算法 B ,都能够解决同一问题,现在需要对比两个算法之间的效率。我们能够想到的最直接的方式,就是找一台计算机,把两个算法都完整跑一遍,并监控记录运行时间和内存占用情况。这种评估方式能够反映真实情况,但是也存在很大的硬伤。
**难以排除测试环境的干扰因素** 硬件配置会影响到算法的性能表现。例如,在某台计算机中,算法 A 比算法 B 运行时间更短;但换到另一台配置不同的计算机中,可能会得到相反的测试结果。这意味着我们需要在各种机器上展开测试,而这是不现实的。
**难以排除测试环境的干扰因素**硬件配置会影响到算法的性能表现。例如,在某台计算机中,算法 A 比算法 B 运行时间更短;但换到另一台配置不同的计算机中,可能会得到相反的测试结果。这意味着我们需要在各种机器上展开测试,而这是不现实的。
**展开完整测试非常耗费资源** 随着输入数据量的大小变化,算法会呈现出不同的效率表现。比如,有可能输入数据量较小时,算法 A 运行时间短于算法 B ,而在输入数据量较大时,测试结果截然相反。因此,若想要达到具有说服力的对比结果,那么需要输入各种体量数据,这样的测试需要占用大量计算资源。
**展开完整测试非常耗费资源**随着输入数据量的大小变化,算法会呈现出不同的效率表现。比如,有可能输入数据量较小时,算法 A 运行时间短于算法 B ,而在输入数据量较大时,测试结果截然相反。因此,若想要达到具有说服力的对比结果,那么需要输入各种体量数据,这样的测试需要占用大量计算资源。
### 理论估算
@ -34,7 +34,7 @@ comments: true
**复杂度分析评估随着输入数据量的增长,算法的运行时间和占用空间的增长趋势** 。根据时间和空间两方面,复杂度可分为「时间复杂度 Time Complexity」和「空间复杂度 Space Complexity」。
**复杂度分析克服了实际测试方法的弊端** 一是独立于测试环境,分析结果适用于所有运行平台。二是可以体现不同数据量下的算法效率,尤其是可以反映大数据量下的算法性能。
**复杂度分析克服了实际测试方法的弊端**一是独立于测试环境,分析结果适用于所有运行平台。二是可以体现不同数据量下的算法效率,尤其是可以反映大数据量下的算法性能。
## 复杂度分析的重要性

View File

@ -208,8 +208,8 @@ comments: true
**最差空间复杂度中的“最差”有两层含义**,分别为输入数据的最差分布、算法运行中的最差时间点。
- **以最差输入数据为准** 当 $n < 10$ 时,空间复杂度为 $O(1)$ ;但是当 $n > 10$ 时,初始化的数组 `nums` 使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$
- **以算法运行过程中的峰值内存为准** 程序在执行最后一行之前,使用 $O(1)$ 空间;当初始化数组 `nums` 时,程序使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$
- **以最差输入数据为准**当 $n < 10$ 时,空间复杂度为 $O(1)$ ;但是当 $n > 10$ 时,初始化的数组 `nums` 使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$
- **以算法运行过程中的峰值内存为准**程序在执行最后一行之前,使用 $O(1)$ 空间;当初始化数组 `nums` 时,程序使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$
=== "Java"
@ -301,7 +301,7 @@ comments: true
}
```
**在递归函数中,需要注意统计栈帧空间** 例如函数 `loop()`,在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行中会同时存在 $n$ 个未返回的 `recur()` ,从而使用 $O(n)$ 的栈帧空间。
**在递归函数中,需要注意统计栈帧空间**例如函数 `loop()`,在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行中会同时存在 $n$ 个未返回的 `recur()` ,从而使用 $O(n)$ 的栈帧空间。
=== "Java"

View File

@ -6,7 +6,7 @@ comments: true
理想情况下,我们希望算法的时间复杂度和空间复杂度都能够达到最优,而实际上,同时优化时间复杂度和空间复杂度是非常困难的。
**降低时间复杂度,往往是以提升空间复杂度为代价的,反之亦然** 我们把牺牲内存空间来提升算法运行速度的思路称为「以空间换时间」;反之,称之为「以时间换空间」。选择哪种思路取决于我们更看重哪个方面。
**降低时间复杂度,往往是以提升空间复杂度为代价的,反之亦然**我们把牺牲内存空间来提升算法运行速度的思路称为「以空间换时间」;反之,称之为「以时间换空间」。选择哪种思路取决于我们更看重哪个方面。
大多数情况下,时间都是比空间更宝贵的,只要空间复杂度不要太离谱、能接受就行,**因此以空间换时间最为常用**。

View File

@ -153,7 +153,7 @@ $$
}
```
但实际上, **统计算法的运行时间既不合理也不现实** 首先,我们不希望预估时间和运行平台绑定,毕竟算法需要跑在各式各样的平台之上。其次,我们很难获知每一种操作的运行时间,这为预估过程带来了极大的难度。
但实际上, **统计算法的运行时间既不合理也不现实**首先,我们不希望预估时间和运行平台绑定,毕竟算法需要跑在各式各样的平台之上。其次,我们很难获知每一种操作的运行时间,这为预估过程带来了极大的难度。
## 统计时间增长趋势
@ -363,11 +363,11 @@ $$
相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足?
**时间复杂度可以有效评估算法效率** 算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。
**时间复杂度可以有效评估算法效率**算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。
**时间复杂度的推算方法更加简便** 在时间复杂度分析中,我们可以将统计「计算操作的运行时间」简化为统计「计算操作的数量」,这是因为,无论是运行平台还是计算操作类型,都与算法运行时间的增长趋势无关。因而,我们可以简单地将所有计算操作的执行时间统一看作是相同的“单位时间”,这样的简化做法大大降低了估算难度。
**时间复杂度的推算方法更加简便**在时间复杂度分析中,我们可以将统计「计算操作的运行时间」简化为统计「计算操作的数量」,这是因为,无论是运行平台还是计算操作类型,都与算法运行时间的增长趋势无关。因而,我们可以简单地将所有计算操作的执行时间统一看作是相同的“单位时间”,这样的简化做法大大降低了估算难度。
**时间复杂度也存在一定的局限性** 比如,虽然算法 `A` 和 `C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B` 比 `C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。对于以上情况,我们很难仅凭时间复杂度来判定算法效率高低。然而,即使存在这些问题,计算复杂度仍然是评判算法效率的最有效且常用的方法。
**时间复杂度也存在一定的局限性**比如,虽然算法 `A` 和 `C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B` 比 `C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。对于以上情况,我们很难仅凭时间复杂度来判定算法效率高低。然而,即使存在这些问题,计算复杂度仍然是评判算法效率的最有效且常用的方法。
## 函数渐近上界
@ -538,9 +538,9 @@ $T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得
对着代码,从上到下一行一行地计数即可。然而,**由于上述 $c \cdot f(n)$ 中的常数项 $c$ 可以取任意大小,因此操作数量 $T(n)$ 中的各种系数、常数项都可以被忽略**。根据此原则,可以总结出以下计数偷懒技巧:
1. **跳过数量与 $n$ 无关的操作** 因为他们都是 $T(n)$ 中的常数项,对时间复杂度不产生影响。
2. **省略所有系数** 例如,循环 $2n$ 次、$5n + 1$ 次、……,都可以化简记为 $n$ 次,因为 $n$ 前面的系数对时间复杂度也不产生影响。
3. **循环嵌套时使用乘法** 总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用上述 `1.` 和 `2.` 技巧。
1. **跳过数量与 $n$ 无关的操作**因为他们都是 $T(n)$ 中的常数项,对时间复杂度不产生影响。
2. **省略所有系数**例如,循环 $2n$ 次、$5n + 1$ 次、……,都可以化简记为 $n$ 次,因为 $n$ 前面的系数对时间复杂度也不产生影响。
3. **循环嵌套时使用乘法**总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用上述 `1.` 和 `2.` 技巧。
根据以下示例,使用上述技巧前、后的统计结果分别为
@ -1004,7 +1004,7 @@ $$
!!! tip
**数据大小 $n$ 是根据输入数据的类型来确定的** 比如,在上述示例中,我们直接将 $n$ 看作输入数据大小;以下遍历数组示例中,数据大小 $n$ 为数组的长度。
**数据大小 $n$ 是根据输入数据的类型来确定的**比如,在上述示例中,我们直接将 $n$ 看作输入数据大小;以下遍历数组示例中,数据大小 $n$ 为数组的长度。
=== "Java"
@ -2308,7 +2308,7 @@ $$
## 最差、最佳、平均时间复杂度
**某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关** 举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论:
**某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关**举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论:
- 当 `nums = [?, ?, ..., 1]`,即当末尾元素是 $1$ 时,则需完整遍历数组,此时达到 **最差时间复杂度 $O(n)$**
- 当 `nums = [1, ?, ?, ...]` ,即当首个数字为 $1$ 时,无论数组多长都不需要继续遍历,此时达到 **最佳时间复杂度 $\Omega(1)$**