This commit is contained in:
krahets
2025-07-10 07:14:11 +08:00
parent 718e8d4a1c
commit 85ebada8d9
29 changed files with 185 additions and 168 deletions

View File

@ -2034,7 +2034,7 @@ comments: true
panic!("索引越界")
};
let num = self.arr[index];
// 将索引 index 之后的元素都向前移动一位
// 将索引 index 之后的元素都向前移动一位
for j in index..self.size - 1 {
self.arr[j] = self.arr[j + 1];
}

View File

@ -16,13 +16,13 @@ comments: true
<div class="center-table" markdown>
| | 硬盘 | 内存 | 缓存 |
| ------ | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
| 用途 | 长期存储数据,包括操作系统、程序、文件等 | 临时存储当前运行的程序和正在处理的数据 | 存储经常访问的数据和指令,减少 CPU 访问内存的次数 |
| 易失性 | 断电后数据不会丢失 | 断电后数据会丢失 | 断电后数据会丢失 |
| 容量 | 较大TB 级别 | 较小GB 级别 | 非常小MB 级别 |
| 速度 | 较慢,几百到几千 MB/s | 较快,几十 GB/s | 非常快,几十到几百 GB/s |
| 价格 | 较便宜,几毛到几元 / GB | 较贵,几十到几百元 / GB | 非常贵,随 CPU 打包计价 |
| | 硬盘 | 内存 | 缓存 |
| -------------- | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
| 用途 | 长期存储数据,包括操作系统、程序、文件等 | 临时存储当前运行的程序和正在处理的数据 | 存储经常访问的数据和指令,减少 CPU 访问内存的次数 |
| 易失性 | 断电后数据不会丢失 | 断电后数据会丢失 | 断电后数据会丢失 |
| 容量 | 较大TB 级别 | 较小GB 级别 | 非常小MB 级别 |
| 速度 | 较慢,几百到几千 MB/s | 较快,几十 GB/s | 非常快,几十到几百 GB/s |
| 价格(人民币) | 较便宜,几毛到几元 / GB | 较贵,几十到几百元 / GB | 非常贵,随 CPU 打包计价 |
</div>

View File

@ -578,7 +578,7 @@ comments: true
/* 递归的空间复杂度为 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -598,7 +598,7 @@ comments: true
/* 递归的空间复杂度为 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -727,7 +727,7 @@ comments: true
/* 递归的空间复杂度为 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -769,7 +769,7 @@ comments: true
/* 递归的空间复杂度为 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```

View File

@ -757,7 +757,7 @@ $T(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$ 的倍数
如图 2-8 所示,计算渐近上界就是寻找一个函数 $f(n)$ ,使得当 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别,仅相差一个常数系数 $c$。
![函数的渐近上界](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" }
@ -771,9 +771,9 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因
### 1. &nbsp; 第一步:统计操作数量
针对代码,逐行从上到下计算即可。然而,由于上述 $c \cdot f(n)$ 中的常数 $c$ 可以取任意大小,**因此操作数量 $T(n)$ 中的各种系数、常数项都可以忽略**。根据此原则,可以总结出以下计数简化技巧。
针对代码,逐行从上到下计算即可。然而,由于上述 $c \cdot f(n)$ 中的常数系数 $c$ 可以取任意大小,**因此操作数量 $T(n)$ 中的各种系数、常数项都可以忽略**。根据此原则,可以总结出以下计数简化技巧。
1. **忽略 $T(n)$ 中的常数**。因为它们都与 $n$ 无关,所以对时间复杂度不产生影响。
1. **忽略 $T(n)$ 中的常数**。因为它们都与 $n$ 无关,所以对时间复杂度不产生影响。
2. **省略所有系数**。例如,循环 $2n$ 次、$5n + 1$ 次等,都可以简化记为 $n$ 次,因为 $n$ 前面的系数对时间复杂度没有影响。
3. **循环嵌套时使用乘法**。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用第 `1.` 点和第 `2.` 点的技巧。

View File

@ -410,8 +410,8 @@ $$
### 3. &nbsp; 正确性证明
使用反证法,只分析 $n \geq 3$ 的情况。
使用反证法,只分析 $n \geq 4$ 的情况。
1. **所有因子 $\leq 3$** :假设最优切分方案中存在 $\geq 4$ 的因子 $x$ ,那么一定可以将其继续划分为 $2(x-2)$ ,从而获得更大的乘积。这与假设矛盾。
1. **所有因子 $\leq 3$** :假设最优切分方案中存在 $\geq 4$ 的因子 $x$ ,那么一定可以将其继续划分为 $2(x-2)$ ,从而获得更大(或相等)的乘积。这与假设矛盾。
2. **切分方案不包含 $1$** :假设最优切分方案中存在一个因子 $1$ ,那么它一定可以合并入另外一个因子中,以获得更大的乘积。这与假设矛盾。
3. **切分方案最多包含两个 $2$** :假设最优切分方案中包含三个 $2$ ,那么一定可以替换为两个 $3$ ,乘积更大。这与假设矛盾。

View File

@ -1161,7 +1161,7 @@ comments: true
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20median_three%28nums%3A%20list%5Bint%5D,%20left%3A%20int,%20mid%3A%20int,%20right%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%80%89%E5%8F%96%E4%B8%89%E4%B8%AA%E5%80%99%E9%80%89%E5%85%83%E7%B4%A0%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0%22%22%22%0A%20%20%20%20l,%20m,%20r%20%3D%20nums%5Bleft%5D,%20nums%5Bmid%5D,%20nums%5Bright%5D%0A%20%20%20%20if%20%28l%20%3C%3D%20m%20%3C%3D%20r%29%20or%20%28r%20%3C%3D%20m%20%3C%3D%20l%29%3A%0A%20%20%20%20%20%20%20%20return%20mid%20%20%23%20m%20%E5%9C%A8%20l%20%E5%92%8C%20r%20%E4%B9%8B%E9%97%B4%0A%20%20%20%20if%20%28m%20%3C%3D%20l%20%3C%3D%20r%29%20or%20%28r%20%3C%3D%20l%20%3C%3D%20m%29%3A%0A%20%20%20%20%20%20%20%20return%20left%20%20%23%20l%20%E5%9C%A8%20m%20%E5%92%8C%20r%20%E4%B9%8B%E9%97%B4%0A%20%20%20%20return%20right%0A%0Adef%20partition%28nums%3A%20list%5Bint%5D,%20left%3A%20int,%20right%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%93%A8%E5%85%B5%E5%88%92%E5%88%86%EF%BC%88%E4%B8%89%E6%95%B0%E5%8F%96%E4%B8%AD%E5%80%BC%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E4%BB%A5%20nums%5Bleft%5D%20%E4%B8%BA%E5%9F%BA%E5%87%86%E6%95%B0%0A%20%20%20%20med%20%3D%20median_three%28nums,%20left,%20%28left%20%2B%20right%29%20//%202,%20right%29%0A%20%20%20%20%23%20%E5%B0%86%E4%B8%AD%E4%BD%8D%E6%95%B0%E4%BA%A4%E6%8D%A2%E8%87%B3%E6%95%B0%E7%BB%84%E6%9C%80%E5%B7%A6%E7%AB%AF%0A%20%20%20%20nums%5Bleft%5D,%20nums%5Bmed%5D%20%3D%20nums%5Bmed%5D,%20nums%5Bleft%5D%0A%20%20%20%20%23%20%E4%BB%A5%20nums%5Bleft%5D%20%E4%B8%BA%E5%9F%BA%E5%87%86%E6%95%B0%0A%20%20%20%20i,%20j%20%3D%20left,%20right%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20while%20i%20%3C%20j%20and%20nums%5Bj%5D%20%3E%3D%20nums%5Bleft%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%20%20%23%20%E4%BB%8E%E5%8F%B3%E5%90%91%E5%B7%A6%E6%89%BE%E9%A6%96%E4%B8%AA%E5%B0%8F%E4%BA%8E%E5%9F%BA%E5%87%86%E6%95%B0%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20while%20i%20%3C%20j%20and%20nums%5Bi%5D%20%3C%3D%20nums%5Bleft%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%20%20%23%20%E4%BB%8E%E5%B7%A6%E5%90%91%E5%8F%B3%E6%89%BE%E9%A6%96%E4%B8%AA%E5%A4%A7%E4%BA%8E%E5%9F%BA%E5%87%86%E6%95%B0%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%0A%20%20%20%20%20%20%20%20nums%5Bi%5D,%20nums%5Bj%5D%20%3D%20nums%5Bj%5D,%20nums%5Bi%5D%0A%20%20%20%20%23%20%E5%B0%86%E5%9F%BA%E5%87%86%E6%95%B0%E4%BA%A4%E6%8D%A2%E8%87%B3%E4%B8%A4%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E5%88%86%E7%95%8C%E7%BA%BF%0A%20%20%20%20nums%5Bi%5D,%20nums%5Bleft%5D%20%3D%20nums%5Bleft%5D,%20nums%5Bi%5D%0A%20%20%20%20return%20i%20%20%23%20%E8%BF%94%E5%9B%9E%E5%9F%BA%E5%87%86%E6%95%B0%E7%9A%84%E7%B4%A2%E5%BC%95%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%B8%AD%E4%BD%8D%E5%9F%BA%E5%87%86%E6%95%B0%E4%BC%98%E5%8C%96%0A%20%20%20%20nums%20%3D%20%5B2,%204,%201,%200,%203,%205%5D%0A%20%20%20%20partition%28nums,%200,%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E5%93%A8%E5%85%B5%E5%88%92%E5%88%86%EF%BC%88%E4%B8%AD%E4%BD%8D%E5%9F%BA%E5%87%86%E6%95%B0%E4%BC%98%E5%8C%96%EF%BC%89%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20median_three%28nums%3A%20list%5Bint%5D,%20left%3A%20int,%20mid%3A%20int,%20right%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%80%89%E5%8F%96%E4%B8%89%E4%B8%AA%E5%80%99%E9%80%89%E5%85%83%E7%B4%A0%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0%22%22%22%0A%20%20%20%20l,%20m,%20r%20%3D%20nums%5Bleft%5D,%20nums%5Bmid%5D,%20nums%5Bright%5D%0A%20%20%20%20if%20%28l%20%3C%3D%20m%20%3C%3D%20r%29%20or%20%28r%20%3C%3D%20m%20%3C%3D%20l%29%3A%0A%20%20%20%20%20%20%20%20return%20mid%20%20%23%20m%20%E5%9C%A8%20l%20%E5%92%8C%20r%20%E4%B9%8B%E9%97%B4%0A%20%20%20%20if%20%28m%20%3C%3D%20l%20%3C%3D%20r%29%20or%20%28r%20%3C%3D%20l%20%3C%3D%20m%29%3A%0A%20%20%20%20%20%20%20%20return%20left%20%20%23%20l%20%E5%9C%A8%20m%20%E5%92%8C%20r%20%E4%B9%8B%E9%97%B4%0A%20%20%20%20return%20right%0A%0Adef%20partition%28nums%3A%20list%5Bint%5D,%20left%3A%20int,%20right%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%93%A8%E5%85%B5%E5%88%92%E5%88%86%EF%BC%88%E4%B8%89%E6%95%B0%E5%8F%96%E4%B8%AD%E5%80%BC%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E4%BB%A5%20nums%5Bleft%5D%20%E4%B8%BA%E5%9F%BA%E5%87%86%E6%95%B0%0A%20%20%20%20med%20%3D%20median_three%28nums,%20left,%20%28left%20%2B%20right%29%20//%202,%20right%29%0A%20%20%20%20%23%20%E5%B0%86%E4%B8%AD%E4%BD%8D%E6%95%B0%E4%BA%A4%E6%8D%A2%E8%87%B3%E6%95%B0%E7%BB%84%E6%9C%80%E5%B7%A6%E7%AB%AF%0A%20%20%20%20nums%5Bleft%5D,%20nums%5Bmed%5D%20%3D%20nums%5Bmed%5D,%20nums%5Bleft%5D%0A%20%20%20%20%23%20%E4%BB%A5%20nums%5Bleft%5D%20%E4%B8%BA%E5%9F%BA%E5%87%86%E6%95%B0%0A%20%20%20%20i,%20j%20%3D%20left,%20right%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20while%20i%20%3C%20j%20and%20nums%5Bj%5D%20%3E%3D%20nums%5Bleft%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%20%20%23%20%E4%BB%8E%E5%8F%B3%E5%90%91%E5%B7%A6%E6%89%BE%E9%A6%96%E4%B8%AA%E5%B0%8F%E4%BA%8E%E5%9F%BA%E5%87%86%E6%95%B0%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20while%20i%20%3C%20j%20and%20nums%5Bi%5D%20%3C%3D%20nums%5Bleft%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%20%20%23%20%E4%BB%8E%E5%B7%A6%E5%90%91%E5%8F%B3%E6%89%BE%E9%A6%96%E4%B8%AA%E5%A4%A7%E4%BA%8E%E5%9F%BA%E5%87%86%E6%95%B0%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8D%A2%0A%20%20%20%20%20%20%20%20nums%5Bi%5D,%20nums%5Bj%5D%20%3D%20nums%5Bj%5D,%20nums%5Bi%5D%0A%20%20%20%20%23%20%E5%B0%86%E5%9F%BA%E5%87%86%E6%95%B0%E4%BA%A4%E6%8D%A2%E8%87%B3%E4%B8%A4%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E5%88%86%E7%95%8C%E7%BA%BF%0A%20%20%20%20nums%5Bi%5D,%20nums%5Bleft%5D%20%3D%20nums%5Bleft%5D,%20nums%5Bi%5D%0A%20%20%20%20return%20i%20%20%23%20%E8%BF%94%E5%9B%9E%E5%9F%BA%E5%87%86%E6%95%B0%E7%9A%84%E7%B4%A2%E5%BC%95%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%B8%AD%E4%BD%8D%E5%9F%BA%E5%87%86%E6%95%B0%E4%BC%98%E5%8C%96%0A%20%20%20%20nums%20%3D%20%5B2,%204,%201,%200,%203,%205%5D%0A%20%20%20%20partition%28nums,%200,%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E5%93%A8%E5%85%B5%E5%88%92%E5%88%86%EF%BC%88%E4%B8%AD%E4%BD%8D%E5%9F%BA%E5%87%86%E6%95%B0%E4%BC%98%E5%8C%96%EF%BC%89%E5%AE%8C%E6%88%90%E5%90%8E%20nums%20%3D%22,%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全屏观看 ></a></div>
## 11.5.5 &nbsp; 递归优化
## 11.5.5 &nbsp; 递归深度优化
**在某些输入下,快速排序可能占用空间较多**。以完全有序的输入数组为例,设递归中的子数组长度为 $m$ ,每轮哨兵划分操作都将产生长度为 $0$ 的左子数组和长度为 $m - 1$ 的右子数组,这意味着每一层递归调用减少的问题规模非常小(只减少一个元素),递归树的高度会达到 $n - 1$ ,此时需要占用 $O(n)$ 大小的栈帧空间。
@ -1171,7 +1171,7 @@ comments: true
```python title="quick_sort.py"
def quick_sort(self, nums: list[int], left: int, right: int):
"""快速排序(递归优化)"""
"""快速排序(递归深度优化)"""
# 子数组长度为 1 时终止
while left < right:
# 哨兵划分操作
@ -1188,7 +1188,7 @@ comments: true
=== "C++"
```cpp title="quick_sort.cpp"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
void quickSort(vector<int> &nums, int left, int right) {
// 子数组长度为 1 时终止
while (left < right) {
@ -1209,7 +1209,7 @@ comments: true
=== "Java"
```java title="quick_sort.java"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
void quickSort(int[] nums, int left, int right) {
// 子数组长度为 1 时终止
while (left < right) {
@ -1230,7 +1230,7 @@ comments: true
=== "C#"
```csharp title="quick_sort.cs"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
void QuickSort(int[] nums, int left, int right) {
// 子数组长度为 1 时终止
while (left < right) {
@ -1251,7 +1251,7 @@ comments: true
=== "Go"
```go title="quick_sort.go"
/* 快速排序(递归优化)*/
/* 快速排序(递归深度优化)*/
func (q *quickSortTailCall) quickSort(nums []int, left, right int) {
// 子数组长度为 1 时终止
for left < right {
@ -1272,7 +1272,7 @@ comments: true
=== "Swift"
```swift title="quick_sort.swift"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
func quickSortTailCall(nums: inout [Int], left: Int, right: Int) {
var left = left
var right = right
@ -1295,7 +1295,7 @@ comments: true
=== "JS"
```javascript title="quick_sort.js"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
quickSort(nums, left, right) {
// 子数组长度为 1 时终止
while (left < right) {
@ -1316,7 +1316,7 @@ comments: true
=== "TS"
```typescript title="quick_sort.ts"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
quickSort(nums: number[], left: number, right: number): void {
// 子数组长度为 1 时终止
while (left < right) {
@ -1337,7 +1337,7 @@ comments: true
=== "Dart"
```dart title="quick_sort.dart"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
void quickSort(List<int> nums, int left, int right) {
// 子数组长度为 1 时终止
while (left < right) {
@ -1358,7 +1358,7 @@ comments: true
=== "Rust"
```rust title="quick_sort.rs"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
pub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {
// 子数组长度为 1 时终止
while left < right {
@ -1379,7 +1379,7 @@ comments: true
=== "C"
```c title="quick_sort.c"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
void quickSortTailCall(int nums[], int left, int right) {
// 子数组长度为 1 时终止
while (left < right) {
@ -1404,7 +1404,7 @@ comments: true
=== "Kotlin"
```kotlin title="quick_sort.kt"
/* 快速排序(递归优化) */
/* 快速排序(递归深度优化) */
fun quickSortTailCall(nums: IntArray, left: Int, right: Int) {
// 子数组长度为 1 时终止
var l = left
@ -1427,7 +1427,7 @@ comments: true
=== "Ruby"
```ruby title="quick_sort.rb"
### 快速排序(递归优化)###
### 快速排序(递归深度优化)###
def quick_sort(nums, left, right)
# 子数组长度不为 1 时递归
while left < right
@ -1448,7 +1448,7 @@ comments: true
=== "Zig"
```zig title="quick_sort.zig"
// 快速排序(递归优化)
// 快速排序(递归深度优化)
fn quickSort(nums: []i32, left_: usize, right_: usize) void {
var left = left_;
var right = right_;

View File

@ -8,7 +8,7 @@ comments: true
- 冒泡排序通过交换相邻元素来实现排序。通过添加一个标志位来实现提前返回,我们可以将冒泡排序的最佳时间复杂度优化到 $O(n)$ 。
- 插入排序每轮将未排序区间内的元素插入到已排序区间的正确位置,从而完成排序。虽然插入排序的时间复杂度为 $O(n^2)$ ,但由于单元操作相对较少,因此在小数据量的排序任务中非常受欢迎。
- 快速排序基于哨兵划分操作实现排序。在哨兵划分中,有可能每次都选取到最差的基准数,导致时间复杂度劣化至 $O(n^2)$ 。引入中位数基准数或随机基准数可以降低这种劣化的概率。尾递归方法可以有效地减少递归深度,将空间复杂度优化到 $O(\log n)$ 。
- 快速排序基于哨兵划分操作实现排序。在哨兵划分中,有可能每次都选取到最差的基准数,导致时间复杂度劣化至 $O(n^2)$ 。引入中位数基准数或随机基准数可以降低这种劣化的概率。通过优先递归较短子区间,可有效减小递归深度,将空间复杂度优化到 $O(\log n)$ 。
- 归并排序包括划分和合并两个阶段,典型地体现了分治策略。在归并排序中,排序数组需要创建辅助数组,空间复杂度为 $O(n)$ ;然而排序链表的空间复杂度可以优化至 $O(1)$ 。
- 桶排序包含三个步骤:数据分桶、桶内排序和合并结果。它同样体现了分治策略,适用于数据体量很大的情况。桶排序的关键在于对数据进行平均分配。
- 计数排序是桶排序的一个特例,它通过统计数据出现的次数来实现排序。计数排序适用于数据量大但数据范围有限的情况,并且要求数据能够转换为正整数。
@ -38,11 +38,11 @@ comments: true
再深入思考一下,如果我们选择 `nums[right]` 为基准数,那么正好反过来,必须先“从左往右查找”。
**Q**:关于尾递归优化,为什么选短的数组能保证递归深度不超过 $\log n$
**Q**:关于快速排序的递归深度优化,为什么选短的数组能保证递归深度不超过 $\log n$
递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在递归优化后,向下递归的子数组长度最大为原数组长度的一半。假设最差情况,一直为一半长度,那么最终的递归深度就是 $\log n$ 。
递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在递归深度优化后,向下递归的子数组长度最大为原数组长度的一半。假设最差情况,一直为一半长度,那么最终的递归深度就是 $\log n$ 。
回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 $n$、$n - 1$、$\dots$、$2$、$1$ ,递归深度为 $n$ 。递归优化可以避免这种情况出现。
回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 $n$、$n - 1$、$\dots$、$2$、$1$ ,递归深度为 $n$ 。递归深度优化可以避免这种情况出现。
**Q**:当数组中所有元素都相等时,快速排序的时间复杂度是 $O(n^2)$ 吗?该如何处理这种退化情况?

View File

@ -1609,11 +1609,11 @@ comments: true
/* 判断双向队列是否为空 */
pub fn is_empty(&self) -> bool {
return self.size() == 0;
return self.que_size == 0;
}
/* 入队操作 */
pub fn push(&mut self, num: T, is_front: bool) {
fn push(&mut self, num: T, is_front: bool) {
let node = ListNode::new(num);
// 队首入队操作
if is_front {
@ -1661,7 +1661,7 @@ comments: true
}
/* 出队操作 */
pub fn pop(&mut self, is_front: bool) -> Option<T> {
fn pop(&mut self, is_front: bool) -> Option<T> {
// 若队列为空,直接返回 None
if self.is_empty() {
return None;
@ -3320,17 +3320,17 @@ comments: true
```rust title="array_deque.rs"
/* 基于环形数组实现的双向队列 */
struct ArrayDeque {
nums: Vec<i32>, // 用于存储双向队列元素的数组
struct ArrayDeque<T> {
nums: Vec<T>, // 用于存储双向队列元素的数组
front: usize, // 队首指针,指向队首元素
que_size: usize, // 双向队列长度
}
impl ArrayDeque {
impl<T: Copy + Default> ArrayDeque<T> {
/* 构造方法 */
pub fn new(capacity: usize) -> Self {
Self {
nums: vec![0; capacity],
nums: vec![T::default(); capacity],
front: 0,
que_size: 0,
}
@ -3356,11 +3356,11 @@ comments: true
// 通过取余操作实现数组首尾相连
// 当 i 越过数组尾部后,回到头部
// 当 i 越过数组头部后,回到尾部
return ((i + self.capacity() as i32) % self.capacity() as i32) as usize;
((i + self.capacity() as i32) % self.capacity() as i32) as usize
}
/* 队首入队 */
pub fn push_first(&mut self, num: i32) {
pub fn push_first(&mut self, num: T) {
if self.que_size == self.capacity() {
println!("双向队列已满");
return;
@ -3374,7 +3374,7 @@ comments: true
}
/* 队尾入队 */
pub fn push_last(&mut self, num: i32) {
pub fn push_last(&mut self, num: T) {
if self.que_size == self.capacity() {
println!("双向队列已满");
return;
@ -3387,7 +3387,7 @@ comments: true
}
/* 队首出队 */
fn pop_first(&mut self) -> i32 {
fn pop_first(&mut self) -> T {
let num = self.peek_first();
// 队首指针向后移动一位
self.front = self.index(self.front as i32 + 1);
@ -3396,14 +3396,14 @@ comments: true
}
/* 队尾出队 */
fn pop_last(&mut self) -> i32 {
fn pop_last(&mut self) -> T {
let num = self.peek_last();
self.que_size -= 1;
num
}
/* 访问队首元素 */
fn peek_first(&self) -> i32 {
fn peek_first(&self) -> T {
if self.is_empty() {
panic!("双向队列为空")
};
@ -3411,7 +3411,7 @@ comments: true
}
/* 访问队尾元素 */
fn peek_last(&self) -> i32 {
fn peek_last(&self) -> T {
if self.is_empty() {
panic!("双向队列为空")
};
@ -3421,9 +3421,9 @@ comments: true
}
/* 返回数组用于打印 */
fn to_array(&self) -> Vec<i32> {
fn to_array(&self) -> Vec<T> {
// 仅转换有效长度范围内的列表元素
let mut res = vec![0; self.que_size];
let mut res = vec![T::default(); self.que_size];
let mut j = self.front;
for i in 0..self.que_size {
res[i] = self.nums[self.index(j as i32)];

View File

@ -1036,7 +1036,7 @@ comments: true
/* 判断队列是否为空 */
pub fn is_empty(&self) -> bool {
return self.size() == 0;
return self.que_size == 0;
}
/* 入队 */
@ -2082,18 +2082,18 @@ comments: true
```rust title="array_queue.rs"
/* 基于环形数组实现的队列 */
struct ArrayQueue {
nums: Vec<i32>, // 用于存储队列元素的数组
struct ArrayQueue<T> {
nums: Vec<T>, // 用于存储队列元素的数组
front: i32, // 队首指针,指向队首元素
que_size: i32, // 队列长度
que_capacity: i32, // 队列容量
}
impl ArrayQueue {
impl<T: Copy + Default> ArrayQueue<T> {
/* 构造方法 */
fn new(capacity: i32) -> ArrayQueue {
fn new(capacity: i32) -> ArrayQueue<T> {
ArrayQueue {
nums: vec![0; capacity as usize],
nums: vec![T::default(); capacity as usize],
front: 0,
que_size: 0,
que_capacity: capacity,
@ -2116,7 +2116,7 @@ comments: true
}
/* 入队 */
fn push(&mut self, num: i32) {
fn push(&mut self, num: T) {
if self.que_size == self.capacity() {
println!("队列已满");
return;
@ -2130,7 +2130,7 @@ comments: true
}
/* 出队 */
fn pop(&mut self) -> i32 {
fn pop(&mut self) -> T {
let num = self.peek();
// 队首指针向后移动一位,若越过尾部,则返回到数组头部
self.front = (self.front + 1) % self.que_capacity;
@ -2139,7 +2139,7 @@ comments: true
}
/* 访问队首元素 */
fn peek(&self) -> i32 {
fn peek(&self) -> T {
if self.is_empty() {
panic!("index out of bounds");
}
@ -2147,10 +2147,10 @@ comments: true
}
/* 返回数组 */
fn to_vector(&self) -> Vec<i32> {
fn to_vector(&self) -> Vec<T> {
let cap = self.que_capacity;
let mut j = self.front;
let mut arr = vec![0; self.que_size as usize];
let mut arr = vec![T::default(); cap as usize];
for i in 0..self.que_size {
arr[i as usize] = self.nums[(j % cap) as usize];
j += 1;

View File

@ -977,13 +977,17 @@ comments: true
}
/* 将 List 转化为 Array 并返回 */
pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {
if let Some(node) = head {
let mut nums = self.to_array(node.borrow().next.as_ref());
nums.push(node.borrow().val);
return nums;
pub fn to_array(&self) -> Vec<T> {
fn _to_array<T: Sized + Copy>(head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {
if let Some(node) = head {
let mut nums = _to_array(node.borrow().next.as_ref());
nums.push(node.borrow().val);
return nums;
}
return Vec::new();
}
return Vec::new();
_to_array(self.peek())
}
}
```

View File

@ -347,7 +347,7 @@
<!-- contributors -->
<div style="margin: 2em auto;">
<h3>贡献者</h3>
<p>本书在开源社区一百多位贡献者的共同努力下不断完善,感谢他们付出的时间与精力!</p>
<p>本书在开源社区 200 多位贡献者的共同努力下不断完善,感谢他们付出的时间与精力!</p>
<a href="https://github.com/krahets/hello-algo/graphs/contributors">
<img src="https://contrib.rocks/image?repo=krahets/hello-algo&max=300&columns=12" alt="Contributors" style="width: 100%; max-width: 38.5em;">
</a>

View File

@ -16,13 +16,13 @@ There are three types of storage devices in computers: <u>hard disk</u>, <u>rand
<div class="center-table" markdown>
| | Hard Disk | Memory | Cache |
| ---------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| Usage | Long-term storage of data, including OS, programs, files, etc. | Temporary storage of currently running programs and data being processed | Stores frequently accessed data and instructions, reducing the number of CPU accesses to memory |
| Volatility | Data is not lost after power off | Data is lost after power off | Data is lost after power off |
| Capacity | Larger, TB level | Smaller, GB level | Very small, MB level |
| Speed | Slower, several hundred to thousands MB/s | Faster, several tens of GB/s | Very fast, several tens to hundreds of GB/s |
| Price | Cheaper, a few cents to a few dollars / GB | More expensive, tens to hundreds of dollars / GB | Very expensive, priced with CPU |
| | Hard Disk | Memory | Cache |
| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| Usage | Long-term storage of data, including OS, programs, files, etc. | Temporary storage of currently running programs and data being processed | Stores frequently accessed data and instructions, reducing the number of CPU accesses to memory |
| Volatility | Data is not lost after power off | Data is lost after power off | Data is lost after power off |
| Capacity | Larger, TB level | Smaller, GB level | Very small, MB level |
| Speed | Slower, several hundred to thousands MB/s | Faster, several tens of GB/s | Very fast, several tens to hundreds of GB/s |
| Price (USD) | Cheaper, a few cents / GB | More expensive, a few dollars / GB | Very expensive, priced with CPU |
</div>

View File

@ -517,7 +517,7 @@ Consider the following code, the term "worst-case" in worst-case space complexit
/* Recursion O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -537,7 +537,7 @@ Consider the following code, the term "worst-case" in worst-case space complexit
/* Recursion O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -666,7 +666,7 @@ Consider the following code, the term "worst-case" in worst-case space complexit
/* Recursion O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -708,7 +708,7 @@ Consider the following code, the term "worst-case" in worst-case space complexit
/* Recursion O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```

View File

@ -35,7 +35,7 @@ Using the data from the preceding figure, we can follow the steps shown in the n
2. Find the index of the root node 3 in the `inorder` sequence, and use this index to split `inorder` into `[ 9 | 3 1 2 7 ]`.
3. According to the split of the `inorder` sequence, it is straightforward to determine that the left and right subtrees contain 1 and 3 nodes, respectively, so we can split the `preorder` sequence into `[ 3 | 9 | 2 1 7 ]` accordingly.
![Dividing the subtrees in pre-order and in-order traversals](build_binary_tree_problem.assets/build_tree_pre-order_in-order_division.png){ class="animation-figure" }
![Dividing the subtrees in pre-order and in-order traversals](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" }
<p align="center"> Figure 12-6 &nbsp; Dividing the subtrees in pre-order and in-order traversals </p>

View File

@ -7,13 +7,13 @@ icon: material/rocket-launch-outline
A few years ago, I shared the "Sword for Offer" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most common question I encountered was "how to get started with algorithms." Gradually, I developed a keen interest in this question.
Directly solving problems seems to be the most popular method — it's simple, direct, and effective. However, problem-solving is like playing a game of Minesweeper: those with strong self-study abilities can defuse the mines one by one, but those with insufficient basics might end up metaphorically bruised from explosions, retreating step by step in frustration. Going through textbooks is also common, but for those aiming for job applications, the energy spent on thesis writing, resume submissions, and preparation for written tests and interviews leaves little for tackling thick books, turning it into a daunting challenge.
Directly solving problems seems to be the most popular method — it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-study skills can navigate the pitfalls one by one, while those lacking a solid foundation may find themselves repeatedly stumbling and retreating in frustration. Reading through textbooks is also a common practice, but for job seekers, writing graduation thesis, submitting resumes, preparing for written tests and interviews have already consumed most of their energy, and reading thick books often becomes a daunting challenge.
If you're facing similar troubles, then this book is lucky to have found you. This book is my answer to the question. While it may not be the best solution, it is at least a positive attempt. This book may not directly land you an offer, but it will guide you through the "knowledge map" in data structures and algorithms, help you understand the shapes, sizes, and locations of different "mines," and enable you to master various "demining methods." With these skills, I believe you can solve problems and read literature more comfortably, gradually building a knowledge system.
If you're facing similar troubles, then this book is lucky to have found you. This book is my answer to the question. While it may not be the best solution, it is at least a positive attempt. Although this book is not enough to get you an offer directly, it will guide you to explore the "knowledge map" of data structures and algorithms, help you understand the shapes, sizes, and locations of different "mines", and enable you to master various "mine clearance methods". With these skills, I believe you can solve problems and read literature more comfortably, gradually building a knowledge system.
I deeply agree with Professor Feynman's statement: "Knowledge isn't free. You have to pay attention." In this sense, this book is not entirely "free." To not disappoint the precious "attention" you pay for this book, I will do my best, dedicating my utmost "attention" to this book.
I deeply agree with Professor Feynman's statement: "Knowledge isn't free. You have to pay attention." In this sense, this book is not entirely "free." In order to live up to your precious "attention" for this book, I will do my best and devote my greatest "attention" to write this book.
Knowing my limitations, although the content of this book has been refined over time, there are surely many errors remaining. I sincerely request critiques and corrections from all teachers and students.
Aware of my limitations, I recognize that despite the content of this book being refined over time, errors surely remain. I sincerely welcome critiques and corrections from both teachers and students.
![Hello Algo](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" }

View File

@ -4,63 +4,63 @@ comments: true
# 1.1 &nbsp; Algorithms are everywhere
When we hear the word "algorithm," we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.
When we hear the term "algorithm," we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.
Before formally discussing algorithms, there's an interesting fact worth sharing: **you have already unconsciously learned many algorithms and have become accustomed to applying them in your daily life**. Here, I will give a few specific examples to prove this point.
Before we start discussing about algorithms officially, there's an interesting fact worth sharing: **you've learned many algorithms unconsciously and are used to applying them in your daily life**. Here, I will give a few specific examples to prove this point.
**Example 1: Looking Up a Dictionary**. In an English dictionary, words are listed alphabetically. Suppose we're searching for a word that starts with the letter $r$. This is typically done in the following way:
**Example 1: Looking Up a Dictionary**. In an English dictionary, words are listed alphabetically. Assuming we're searching for a word that starts with the letter $r$, this is typically done in the following way:
1. Open the dictionary to about halfway and check the first letter on the page, let's say the letter is $m$.
2. Since $r$ comes after $m$ in the alphabet, we can ignore the first half of the dictionary and focus on the latter half.
1. Open the dictionary to about halfway and check the first vocabulary of the page, let's say the letter starts with $m$.
2. Since $r$ comes after $m$ in the alphabet, the first half can be ignored and the search space is narrowed down to the second half.
3. Repeat steps `1.` and `2.` until you find the page where the word starts with $r$.
=== "<1>"
![Process of Looking Up a Dictionary](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" }
![Process of looking up a dictionary](algorithms_are_everywhere.assets/binary_search_dictionary_step1.png){ class="animation-figure" }
=== "<2>"
![Binary Search in Dictionary Step 2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" }
![Binary search in dictionary step 2](algorithms_are_everywhere.assets/binary_search_dictionary_step2.png){ class="animation-figure" }
=== "<3>"
![Binary Search in Dictionary Step 3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" }
![Binary search in dictionary step 3](algorithms_are_everywhere.assets/binary_search_dictionary_step3.png){ class="animation-figure" }
=== "<4>"
![Binary Search in Dictionary Step 4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" }
![Binary search in dictionary step 4](algorithms_are_everywhere.assets/binary_search_dictionary_step4.png){ class="animation-figure" }
=== "<5>"
![Binary Search in Dictionary Step 5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" }
![Binary search in dictionary step 5](algorithms_are_everywhere.assets/binary_search_dictionary_step5.png){ class="animation-figure" }
<p align="center"> Figure 1-1 &nbsp; Process of Looking Up a Dictionary </p>
<p align="center"> Figure 1-1 &nbsp; Process of looking up a dictionary </p>
This essential skill for elementary students, looking up a dictionary, is actually the famous "Binary Search" algorithm. From a data structure perspective, we can consider the dictionary as a sorted "array"; from an algorithmic perspective, the series of actions taken to look up a word in the dictionary can be viewed as "Binary Search."
Looking up a dictionary, an essential skill for elementary school students is actually the famous "Binary Search" algorithm. From a data structure perspective, we can consider the dictionary as a sorted "array"; from an algorithmic perspective, the series of actions taken to look up a word in the dictionary can be viewed as the algorithm "Binary Search."
**Example 2: Organizing Playing Cards**. When playing cards, we need to arrange the cards in our hand in ascending order, as shown in the following process.
**Example 2: Organizing Card Deck**. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process.
1. Divide the playing cards into "ordered" and "unordered" sections, assuming initially the leftmost card is already in order.
2. Take out a card from the unordered section and insert it into the correct position in the ordered section; after this, the leftmost two cards are in order.
3. Continue to repeat step `2.` until all cards are in order.
3. Repeat step `2` until all cards are in order.
![Playing cards sorting process](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" }
![Process of sorting a deck of cards](algorithms_are_everywhere.assets/playing_cards_sorting.png){ class="animation-figure" }
<p align="center"> Figure 1-2 &nbsp; Playing cards sorting process </p>
<p align="center"> Figure 1-2 &nbsp; Process of sorting a deck of cards </p>
The above method of organizing playing cards is essentially the "Insertion Sort" algorithm, which is very efficient for small datasets. Many programming languages' sorting functions include the insertion sort.
The above method of organizing playing cards is practically the "Insertion Sort" algorithm, which is very efficient for small datasets. Many programming languages' sorting functions include the insertion sort.
**Example 3: Making Change**. Suppose we buy goods worth $69$ yuan at a supermarket and give the cashier $100$ yuan, then the cashier needs to give us $31$ yuan in change. They would naturally complete the thought process as shown in Figure 1-3.
**Example 3: Making Change**. Assume making a purchase of $69$ at a supermarket. If you give the cashier $100$, they will need to provide you with $31$ in change. This process can be clearly understood as illustrated in Figure 1-3.
1. The options are currencies smaller than $31$, including $1$, $5$, $10$, and $20$.
1. The options are currencies valued below $31$, including $1$, $5$, $10$, and $20$.
2. Take out the largest $20$ from the options, leaving $31 - 20 = 11$.
3. Take out the largest $10$ from the remaining options, leaving $11 - 10 = 1$.
4. Take out the largest $1$ from the remaining options, leaving $1 - 1 = 0$.
5. Complete the change-making, with the solution being $20 + 10 + 1 = 31$.
5. Complete change-making, the solution is $20 + 10 + 1 = 31$.
![Change making process](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" }
![Process of making change](algorithms_are_everywhere.assets/greedy_change.png){ class="animation-figure" }
<p align="center"> Figure 1-3 &nbsp; Change making process </p>
<p align="center"> Figure 1-3 &nbsp; Process of making change </p>
In the above steps, we make the best choice at each step (using the largest denomination possible), ultimately resulting in a feasible change-making plan. From the perspective of data structures and algorithms, this method is essentially a "Greedy" algorithm.
In the steps described, we choose the best option at each stage by utilizing the largest denomination available, which leads to an effective change-making strategy. From a data structures and algorithms perspective, this approach is known as a "Greedy" algorithm.
From cooking a meal to interstellar travel, almost all problem-solving involves algorithms. The advent of computers allows us to store data structures in memory and write code to call the CPU and GPU to execute algorithms. In this way, we can transfer real-life problems to computers, solving various complex issues more efficiently.
From cooking a meal to interstellar travel, almost all problem-solving involves algorithms. The advent of computers allows us to store data structures in memory and write code to call the CPU and GPU to execute algorithms. In this way, we can transfer real-life problems to computers and solve various complex issues in a more efficient way.
!!! tip
If concepts such as data structures, algorithms, arrays, and binary search still seem somewhat obscure, I encourage you to continue reading. This book will gently guide you into the realm of understanding data structures and algorithms.
If you are still confused about concepts like data structures, algorithms, arrays, and binary searches, I encourage you to keep reading. This book will gently guide you into the realm of understanding data structures and algorithms.

View File

@ -8,6 +8,19 @@ comments: true
- The principle of looking up a word in a dictionary is consistent with the binary search algorithm. The binary search algorithm embodies the important algorithmic concept of divide and conquer.
- The process of organizing playing cards is very similar to the insertion sort algorithm. The insertion sort algorithm is suitable for sorting small datasets.
- The steps of making change in currency essentially follow the greedy algorithm, where each step involves making the best possible choice at the moment.
- An algorithm is a set of instructions or steps used to solve a specific problem within a finite amount of time, while a data structure is the way data is organized and stored in a computer.
- An algorithm is a set of step-by-step instructions for solving a specific problem within a finite time, while a data structure defines how data is organized and stored in a computer.
- Data structures and algorithms are closely linked. Data structures are the foundation of algorithms, and algorithms are the stage to utilize the functions of data structures.
- We can liken data structures and algorithms to building blocks. The blocks represent data, the shape and connection method of the blocks represent data structures, and the steps of assembling the blocks correspond to algorithms.
- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent data structures, and the steps of assembling the blocks correspond to algorithms.
### 1. &nbsp; Q & A
**Q**As a programmer, Ive rarely needed to implement algorithms manually in my daily work. Most commonly used algorithms are already built into programming languages and libraries, ready to use. Does this suggest that the problems we encounter in our work havent yet reached the level of complexity that demands custom algorithm design?
If specific work skills are like the "moves" in martial arts, then fundamental subjects are more like "internal strength".
I believe the significance of learning algorithms (and other fundamental subjects) isnt necessarily to implement them from scratch at work, but to enable more professional decision-making and problem-solving based on a solid understanding of the concepts. This, in turn, raises the overall quality of our work. For example, every programming language provides a built-in sorting function:
- If we have not learned data structures and algorithms, then given any data, we might just give it to this sorting function. It runs smoothly, has good performance, and seems to have no problems.
- However, if weve studied algorithms, we understand that the time complexity of a built-in sorting function is typically $O(n \log n)$. Moreover, if the data consists of integers with a fixed number of digits (such as student IDs), we can apply a more efficient approach like radix sort, reducing the time complexity to O(nk) , where k is the number of digits. When handling large volumes of data, the time saved can turn into significant value — lowering costs, improving user experience, and enhancing system performance.
In engineering, many problems are difficult to solve optimally; most are addressed with near-optimal solutions. The difficulty of a problem depends not only on its inherent complexity but also on the knowledge and experience of the person tackling it. The deeper ones expertise and experience, the more thorough the analysis, and the more elegantly the problem can be solved.

View File

@ -23,14 +23,14 @@ A <u>data structure</u> is a way of organizing and storing data in a computer, w
**Designing data structures is a balancing act, often requiring trade-offs**. If you want to improve in one aspect, you often need to compromise in another. Here are two examples:
- Compared to arrays, linked lists offer more convenience in data addition and deletion but sacrifice data access speed.
- Graphs, compared to linked lists, provide richer logical information but require more memory space.
- Compared with linked lists, graphs provide richer logical information but require more memory space.
## 1.2.3 &nbsp; Relationship between data structures and algorithms
As shown in Figure 1-4, data structures and algorithms are highly related and closely integrated, specifically in the following three aspects:
- Data structures are the foundation of algorithms. They provide structured data storage and methods for manipulating data for algorithms.
- Algorithms are the stage where data structures come into play. The data structure alone only stores data information; it is through the application of algorithms that specific problems can be solved.
- Algorithms inject vitality into data structures. The data structure alone only stores data information; it is through the application of algorithms that specific problems can be solved.
- Algorithms can often be implemented based on different data structures, but their execution efficiency can vary greatly. Choosing the right data structure is key.
![Relationship between data structures and algorithms](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" }
@ -62,4 +62,4 @@ It's worth noting that data structures and algorithms are independent of program
!!! tip "Conventional Abbreviation"
In real-life discussions, we often refer to "Data Structures and Algorithms" simply as "Algorithms". For example, the well-known LeetCode algorithm problems actually test both data structure and algorithm knowledge.
In real-life discussions, we often refer to "Data Structures and Algorithms" simply as "Algorithms". For example, the well-known LeetCode algorithm questions actually test knowledge of both data structures and algorithms.

View File

@ -414,7 +414,7 @@
<!-- contributors -->
<div style="margin: 2em auto;">
<h3>Contributors</h3>
<p>This book has been refined by the efforts of over 180 contributors. We sincerely thank them for their invaluable time and contributions!</p>
<p>This book has been refined by the efforts of over 200 contributors. We sincerely thank them for their invaluable time and contributions!</p>
<a href="https://github.com/krahets/hello-algo/graphs/contributors">
<img src="https://contrib.rocks/image?repo=krahets/hello-algo&max=300&columns=12" alt="Contributors" style="width: 100%; max-width: 38.5em;">
</a>

View File

@ -1036,7 +1036,7 @@ comments: true
為了加深對串列工作原理的理解,我們嘗試實現一個簡易版串列,包括以下三個重點設計。
- **初始容量**:選取一個合理的陣列初始容量。在本示例中,我們選擇 10 作為初始容量。
- **數量記錄**:宣告一個變數 `size` ,用於記錄串列當前元素數量,並隨著元素插入和刪除時更新。根據此變數,我們可以定位串列尾部,以及判斷是否需要擴容。
- **數量記錄**:宣告一個變數 `size` ,用於記錄串列當前元素數量,並隨著元素插入和刪除時更新。根據此變數,我們可以定位串列尾部,以及判斷是否需要擴容。
- **擴容機制**:若插入元素時串列容量已滿,則需要進行擴容。先根據擴容倍數建立一個更大的陣列,再將當前陣列的所有元素依次移動至新陣列。在本示例中,我們規定每次將陣列擴容至之前的 2 倍。
=== "Python"

View File

@ -17,13 +17,13 @@ status: new
<div class="center-table" markdown>
| | 硬碟 | 記憶體 | 快取 |
| ------ | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
| 用途 | 長期儲存資料,包括作業系統、程式、檔案等 | 臨時儲存當前執行的程式和正在處理的資料 | 儲存經常訪問的資料和指令,減少 CPU 訪問記憶體的次數 |
| 易失性 | 斷電後資料不會丟失 | 斷電後資料會丟失 | 斷電後資料會丟失 |
| 容量 | 較大TB 級別 | 較小GB 級別 | 非常小MB 級別 |
| 速度 | 較慢,幾百到幾千 MB/s | 較快,幾十 GB/s | 非常快,幾十到幾百 GB/s |
| 價格 | 較便宜,幾毛到幾元 / GB | 較貴,幾十到幾百元 / GB | 非常貴,隨 CPU 打包計價 |
| | 硬碟 | 記憶體 | 快取 |
| -------------- | ---------------------------------------- | -------------------------------------- | ------------------------------------------------- |
| 用途 | 長期儲存資料,包括作業系統、程式、檔案等 | 臨時儲存當前執行的程式和正在處理的資料 | 儲存經常訪問的資料和指令,減少 CPU 訪問記憶體的次數 |
| 易失性 | 斷電後資料不會丟失 | 斷電後資料會丟失 | 斷電後資料會丟失 |
| 容量 | 較大TB 級別 | 較小GB 級別 | 非常小MB 級別 |
| 速度 | 較慢,幾百到幾千 MB/s | 較快,幾十 GB/s | 非常快,幾十到幾百 GB/s |
| 價格(人民幣) | 較便宜,幾毛到幾元 / GB | 較貴,幾十到幾百元 / GB | 非常貴,隨 CPU 打包計價 |
</div>

View File

@ -17,7 +17,7 @@ comments: true
### 1. &nbsp; 參考全排列解法
類似於全排列問題,我們可以把子集的生成過程想象成一系列選擇的結果,並在選擇過程中時更新“元素和”,當元素和等於 `target` 時,就將子集記錄至結果串列。
類似於全排列問題,我們可以把子集的生成過程想象成一系列選擇的結果,並在選擇過程中時更新“元素和”,當元素和等於 `target` 時,就將子集記錄至結果串列。
而與全排列問題不同的是,**本題集合中的元素可以被無限次選取**,因此無須藉助 `selected` 布林串列來記錄元素是否已被選擇。我們可以對全排列程式碼進行小幅修改,初步得到解題程式碼:

View File

@ -578,7 +578,7 @@ comments: true
/* 遞迴的空間複雜度為 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -598,7 +598,7 @@ comments: true
/* 遞迴的空間複雜度為 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -727,7 +727,7 @@ comments: true
/* 遞迴的空間複雜度為 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```
@ -769,7 +769,7 @@ comments: true
/* 遞迴的空間複雜度為 O(n) */
void recur(int n) {
if (n == 1) return;
return recur(n - 1);
recur(n - 1);
}
```

View File

@ -4,37 +4,37 @@ comments: true
# 3.4 &nbsp; 字元編碼 *
在計算機中,所有資料都是以二進位制數的形式儲存的,字元 `char` 也不例外。為了表示字元,我們需要建立一套“字集”,規定每個字元和二進位制數之間的一一對應關係。有了字集之後,計算機就可以透過查表完成二進位制數到字元的轉換。
在計算機中,所有資料都是以二進位制數的形式儲存的,字元 `char` 也不例外。為了表示字元,我們需要建立一套“字集”,規定每個字元和二進位制數之間的一一對應關係。有了字集之後,計算機就可以透過查表完成二進位制數到字元的轉換。
## 3.4.1 &nbsp; ASCII 字
## 3.4.1 &nbsp; ASCII 字
<u>ASCII 碼</u>是最早出現的字集,其全稱為 American Standard Code for Information Interchange美國標準資訊交換程式碼。它使用 7 位二進位制數(一個位元組的低 7 位)表示一個字元,最多能夠表示 128 個不同的字元。如圖 3-6 所示ASCII 碼包括英文字母的大小寫、數字 0 ~ 9、一些標點符號以及一些控制字元如換行符和製表符
<u>ASCII 碼</u>是最早出現的字集,其全稱為 American Standard Code for Information Interchange美國標準資訊交換程式碼。它使用 7 位二進位制數(一個位元組的低 7 位)表示一個字元,最多能夠表示 128 個不同的字元。如圖 3-6 所示ASCII 碼包括英文字母的大小寫、數字 0 ~ 9、一些標點符號以及一些控制字元如換行符和製表符
![ASCII 碼](character_encoding.assets/ascii_table.png){ class="animation-figure" }
<p align="center"> 圖 3-6 &nbsp; ASCII 碼 </p>
然而,**ASCII 碼僅能夠表示英文**。隨著計算機的全球化,誕生了一種能夠表示更多語言的 <u>EASCII</u>集。它在 ASCII 的 7 位基礎上擴展到 8 位,能夠表示 256 個不同的字元。
然而,**ASCII 碼僅能夠表示英文**。隨著計算機的全球化,誕生了一種能夠表示更多語言的 <u>EASCII</u>集。它在 ASCII 的 7 位基礎上擴展到 8 位,能夠表示 256 個不同的字元。
在世界範圍內,陸續出現了一批適用於不同地區的 EASCII 字集。這些字集的前 128 個字元統一為 ASCII 碼,後 128 個字元定義不同,以適應不同語言的需求。
在世界範圍內,陸續出現了一批適用於不同地區的 EASCII 字集。這些字集的前 128 個字元統一為 ASCII 碼,後 128 個字元定義不同,以適應不同語言的需求。
## 3.4.2 &nbsp; GBK 字
## 3.4.2 &nbsp; GBK 字
後來人們發現,**EASCII 碼仍然無法滿足許多語言的字元數量要求**。比如漢字有近十萬個,光日常使用的就有幾千個。中國國家標準總局於 1980 年釋出了 <u>GB2312</u>集,其收錄了 6763 個漢字,基本滿足了漢字的計算機處理需要。
後來人們發現,**EASCII 碼仍然無法滿足許多語言的字元數量要求**。比如漢字有近十萬個,光日常使用的就有幾千個。中國國家標準總局於 1980 年釋出了 <u>GB2312</u>集,其收錄了 6763 個漢字,基本滿足了漢字的計算機處理需要。
然而GB2312 無法處理部分罕見字和繁體字。<u>GBK</u>集是在 GB2312 的基礎上擴展得到的,它共收錄了 21886 個漢字。在 GBK 的編碼方案中ASCII 字元使用一個位元組表示,漢字使用兩個位元組表示。
然而GB2312 無法處理部分罕見字和繁體字。<u>GBK</u>集是在 GB2312 的基礎上擴展得到的,它共收錄了 21886 個漢字。在 GBK 的編碼方案中ASCII 字元使用一個位元組表示,漢字使用兩個位元組表示。
## 3.4.3 &nbsp; Unicode 字
## 3.4.3 &nbsp; Unicode 字
隨著計算機技術的蓬勃發展,字集與編碼標準百花齊放,而這帶來了許多問題。一方面,這些字集一般只定義了特定語言的字元,無法在多語言環境下正常工作。另一方面,同一種語言存在多種字集標準,如果兩臺計算機使用的是不同的編碼標準,則在資訊傳遞時就會出現亂碼。
隨著計算機技術的蓬勃發展,字集與編碼標準百花齊放,而這帶來了許多問題。一方面,這些字集一般只定義了特定語言的字元,無法在多語言環境下正常工作。另一方面,同一種語言存在多種字集標準,如果兩臺計算機使用的是不同的編碼標準,則在資訊傳遞時就會出現亂碼。
那個時代的研究人員就在想:**如果推出一個足夠完整的字集,將世界範圍內的所有語言和符號都收錄其中,不就可以解決跨語言環境和亂碼問題了嗎**?在這種想法的驅動下,一個大而全的字集 Unicode 應運而生。
那個時代的研究人員就在想:**如果推出一個足夠完整的字集,將世界範圍內的所有語言和符號都收錄其中,不就可以解決跨語言環境和亂碼問題了嗎**?在這種想法的驅動下,一個大而全的字集 Unicode 應運而生。
<u>Unicode</u> 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字集之中,提供一種通用的字集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。
<u>Unicode</u> 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字集之中,提供一種通用的字集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。
自 1991 年釋出以來Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。在龐大的 Unicode 字集中,常用的字元佔用 2 位元組,有些生僻的字元佔用 3 位元組甚至 4 位元組。
自 1991 年釋出以來Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。在龐大的 Unicode 字集中,常用的字元佔用 2 位元組,有些生僻的字元佔用 3 位元組甚至 4 位元組。
Unicode 是一種通用字集,本質上是給每個字元分配一個編號(稱為“碼點”),**但它並沒有規定在計算機中如何儲存這些字元碼點**。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?
Unicode 是一種通用字集,本質上是給每個字元分配一個編號(稱為“碼點”),**但它並沒有規定在計算機中如何儲存這些字元碼點**。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元?
對於以上問題,**一種直接的解決方案是將所有字元儲存為等長的編碼**。如圖 3-7 所示“Hello”中的每個字元佔用 1 位元組,“演算法”中的每個字元佔用 2 位元組。我們可以透過高位填 0 將“Hello 演算法”中的所有字元都編碼為 2 位元組長度。這樣系統就可以每隔 2 位元組解析一個字元,恢復這個短語的內容了。
@ -50,7 +50,7 @@ Unicode 是一種通用字符集,本質上是給每個字元分配一個編號
UTF-8 的編碼規則並不複雜,分為以下兩種情況。
- 對於長度為 1 位元組的字元,將最高位設定為 $0$ ,其餘 7 位設定為 Unicode 碼點。值得注意的是ASCII 字元在 Unicode 字集中佔據了前 128 個碼點。也就是說,**UTF-8 編碼可以向下相容 ASCII 碼**。這意味著我們可以使用 UTF-8 來解析年代久遠的 ASCII 碼文字。
- 對於長度為 1 位元組的字元,將最高位設定為 $0$ ,其餘 7 位設定為 Unicode 碼點。值得注意的是ASCII 字元在 Unicode 字集中佔據了前 128 個碼點。也就是說,**UTF-8 編碼可以向下相容 ASCII 碼**。這意味著我們可以使用 UTF-8 來解析年代久遠的 ASCII 碼文字。
- 對於長度為 $n$ 位元組的字元(其中 $n > 1$),將首個位元組的高 $n$ 位都設定為 $1$ ,第 $n + 1$ 位設定為 $0$ ;從第二個位元組開始,將每個位元組的高 2 位都設定為 $10$ ;其餘所有位用於填充字元的 Unicode 碼點。
圖 3-8 展示了“Hello演算法”對應的 UTF-8 編碼。觀察發現,由於最高 $n$ 位都設定為 $1$ ,因此系統可以透過讀取最高位 $1$ 的個數來解析出字元的長度為 $n$ 。

View File

@ -14,7 +14,7 @@ comments: true
- 原碼、一補數和二補數是在計算機中編碼數字的三種方法,它們之間可以相互轉換。整數的原碼的最高位是符號位,其餘位是數字的值。
- 整數在計算機中是以二補數的形式儲存的。在二補數表示下,計算機可以對正數和負數的加法一視同仁,不需要為減法操作單獨設計特殊的硬體電路,並且不存在正負零歧義的問題。
- 浮點數的編碼由 1 位符號位、8 位指數位和 23 位分數位構成。由於存在指數位,因此浮點數的取值範圍遠大於整數,代價是犧牲了精度。
- ASCII 碼是最早出現的英文字集,長度為 1 位元組,共收錄 127 個字元。GBK 字集是常用的中文字共收錄兩萬多個漢字。Unicode 致力於提供一個完整的字集標準,收錄世界上各種語言的字元,從而解決由於字元編碼方法不一致而導致的亂碼問題。
- ASCII 碼是最早出現的英文字集,長度為 1 位元組,共收錄 127 個字元。GBK 字集是常用的中文字共收錄兩萬多個漢字。Unicode 致力於提供一個完整的字集標準,收錄世界上各種語言的字元,從而解決由於字元編碼方法不一致而導致的亂碼問題。
- UTF-8 是最受歡迎的 Unicode 編碼方法通用性非常好。它是一種變長的編碼方法具有很好的擴展性有效提升了儲存空間的使用效率。UTF-16 和 UTF-32 是等長的編碼方法。在編碼中文時UTF-16 佔用的空間比 UTF-8 更小。Java 和 C# 等程式語言預設使用 UTF-16 編碼。
### 2. &nbsp; Q & A

View File

@ -410,8 +410,8 @@ $$
### 3. &nbsp; 正確性證明
使用反證法,只分析 $n \geq 3$ 的情況。
使用反證法,只分析 $n \geq 4$ 的情況。
1. **所有因子 $\leq 3$** :假設最優切分方案中存在 $\geq 4$ 的因子 $x$ ,那麼一定可以將其繼續劃分為 $2(x-2)$ ,從而獲得更大的乘積。這與假設矛盾。
1. **所有因子 $\leq 3$** :假設最優切分方案中存在 $\geq 4$ 的因子 $x$ ,那麼一定可以將其繼續劃分為 $2(x-2)$ ,從而獲得更大(或相等)的乘積。這與假設矛盾。
2. **切分方案不包含 $1$** :假設最優切分方案中存在一個因子 $1$ ,那麼它一定可以合併入另外一個因子中,以獲得更大的乘積。這與假設矛盾。
3. **切分方案最多包含兩個 $2$** :假設最優切分方案中包含三個 $2$ ,那麼一定可以替換為兩個 $3$ ,乘積更大。這與假設矛盾。

View File

@ -1161,7 +1161,7 @@ comments: true
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20median_three%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20mid%3A%20int%2C%20right%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%81%B8%E5%8F%96%E4%B8%89%E5%80%8B%E5%80%99%E9%81%B8%E5%85%83%E7%B4%A0%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B8%22%22%22%0A%20%20%20%20l%2C%20m%2C%20r%20%3D%20nums%5Bleft%5D%2C%20nums%5Bmid%5D%2C%20nums%5Bright%5D%0A%20%20%20%20if%20%28l%20%3C%3D%20m%20%3C%3D%20r%29%20or%20%28r%20%3C%3D%20m%20%3C%3D%20l%29%3A%0A%20%20%20%20%20%20%20%20return%20mid%20%20%23%20m%20%E5%9C%A8%20l%20%E5%92%8C%20r%20%E4%B9%8B%E9%96%93%0A%20%20%20%20if%20%28m%20%3C%3D%20l%20%3C%3D%20r%29%20or%20%28r%20%3C%3D%20l%20%3C%3D%20m%29%3A%0A%20%20%20%20%20%20%20%20return%20left%20%20%23%20l%20%E5%9C%A8%20m%20%E5%92%8C%20r%20%E4%B9%8B%E9%96%93%0A%20%20%20%20return%20right%0A%0Adef%20partition%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20right%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%93%A8%E5%85%B5%E5%8A%83%E5%88%86%EF%BC%88%E4%B8%89%E6%95%B8%E5%8F%96%E4%B8%AD%E5%80%BC%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E4%BB%A5%20nums%5Bleft%5D%20%E7%82%BA%E5%9F%BA%E6%BA%96%E6%95%B8%0A%20%20%20%20med%20%3D%20median_three%28nums%2C%20left%2C%20%28left%20%2B%20right%29%20//%202%2C%20right%29%0A%20%20%20%20%23%20%E5%B0%87%E4%B8%AD%E4%BD%8D%E6%95%B8%E4%BA%A4%E6%8F%9B%E8%87%B3%E9%99%A3%E5%88%97%E6%9C%80%E5%B7%A6%E7%AB%AF%0A%20%20%20%20nums%5Bleft%5D%2C%20nums%5Bmed%5D%20%3D%20nums%5Bmed%5D%2C%20nums%5Bleft%5D%0A%20%20%20%20%23%20%E4%BB%A5%20nums%5Bleft%5D%20%E7%82%BA%E5%9F%BA%E6%BA%96%E6%95%B8%0A%20%20%20%20i%2C%20j%20%3D%20left%2C%20right%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20while%20i%20%3C%20j%20and%20nums%5Bj%5D%20%3E%3D%20nums%5Bleft%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%20%20%23%20%E5%BE%9E%E5%8F%B3%E5%90%91%E5%B7%A6%E6%89%BE%E9%A6%96%E5%80%8B%E5%B0%8F%E6%96%BC%E5%9F%BA%E6%BA%96%E6%95%B8%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20while%20i%20%3C%20j%20and%20nums%5Bi%5D%20%3C%3D%20nums%5Bleft%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%20%20%23%20%E5%BE%9E%E5%B7%A6%E5%90%91%E5%8F%B3%E6%89%BE%E9%A6%96%E5%80%8B%E5%A4%A7%E6%96%BC%E5%9F%BA%E6%BA%96%E6%95%B8%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%2C%20nums%5Bj%5D%20%3D%20nums%5Bj%5D%2C%20nums%5Bi%5D%0A%20%20%20%20%23%20%E5%B0%87%E5%9F%BA%E6%BA%96%E6%95%B8%E4%BA%A4%E6%8F%9B%E8%87%B3%E5%85%A9%E5%AD%90%E9%99%A3%E5%88%97%E7%9A%84%E5%88%86%E7%95%8C%E7%B7%9A%0A%20%20%20%20nums%5Bi%5D%2C%20nums%5Bleft%5D%20%3D%20nums%5Bleft%5D%2C%20nums%5Bi%5D%0A%20%20%20%20return%20i%20%20%23%20%E8%BF%94%E5%9B%9E%E5%9F%BA%E6%BA%96%E6%95%B8%E7%9A%84%E7%B4%A2%E5%BC%95%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%B8%AD%E4%BD%8D%E5%9F%BA%E6%BA%96%E6%95%B8%E6%9C%80%E4%BD%B3%E5%8C%96%0A%20%20%20%20nums%20%3D%20%5B2%2C%204%2C%201%2C%200%2C%203%2C%205%5D%0A%20%20%20%20partition%28nums%2C%200%2C%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E5%93%A8%E5%85%B5%E5%8A%83%E5%88%86%EF%BC%88%E4%B8%AD%E4%BD%8D%E5%9F%BA%E6%BA%96%E6%95%B8%E6%9C%80%E4%BD%B3%E5%8C%96%EF%BC%89%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20median_three%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20mid%3A%20int%2C%20right%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E9%81%B8%E5%8F%96%E4%B8%89%E5%80%8B%E5%80%99%E9%81%B8%E5%85%83%E7%B4%A0%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B8%22%22%22%0A%20%20%20%20l%2C%20m%2C%20r%20%3D%20nums%5Bleft%5D%2C%20nums%5Bmid%5D%2C%20nums%5Bright%5D%0A%20%20%20%20if%20%28l%20%3C%3D%20m%20%3C%3D%20r%29%20or%20%28r%20%3C%3D%20m%20%3C%3D%20l%29%3A%0A%20%20%20%20%20%20%20%20return%20mid%20%20%23%20m%20%E5%9C%A8%20l%20%E5%92%8C%20r%20%E4%B9%8B%E9%96%93%0A%20%20%20%20if%20%28m%20%3C%3D%20l%20%3C%3D%20r%29%20or%20%28r%20%3C%3D%20l%20%3C%3D%20m%29%3A%0A%20%20%20%20%20%20%20%20return%20left%20%20%23%20l%20%E5%9C%A8%20m%20%E5%92%8C%20r%20%E4%B9%8B%E9%96%93%0A%20%20%20%20return%20right%0A%0Adef%20partition%28nums%3A%20list%5Bint%5D%2C%20left%3A%20int%2C%20right%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%93%A8%E5%85%B5%E5%8A%83%E5%88%86%EF%BC%88%E4%B8%89%E6%95%B8%E5%8F%96%E4%B8%AD%E5%80%BC%EF%BC%89%22%22%22%0A%20%20%20%20%23%20%E4%BB%A5%20nums%5Bleft%5D%20%E7%82%BA%E5%9F%BA%E6%BA%96%E6%95%B8%0A%20%20%20%20med%20%3D%20median_three%28nums%2C%20left%2C%20%28left%20%2B%20right%29%20//%202%2C%20right%29%0A%20%20%20%20%23%20%E5%B0%87%E4%B8%AD%E4%BD%8D%E6%95%B8%E4%BA%A4%E6%8F%9B%E8%87%B3%E9%99%A3%E5%88%97%E6%9C%80%E5%B7%A6%E7%AB%AF%0A%20%20%20%20nums%5Bleft%5D%2C%20nums%5Bmed%5D%20%3D%20nums%5Bmed%5D%2C%20nums%5Bleft%5D%0A%20%20%20%20%23%20%E4%BB%A5%20nums%5Bleft%5D%20%E7%82%BA%E5%9F%BA%E6%BA%96%E6%95%B8%0A%20%20%20%20i%2C%20j%20%3D%20left%2C%20right%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20while%20i%20%3C%20j%20and%20nums%5Bj%5D%20%3E%3D%20nums%5Bleft%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%20%20%23%20%E5%BE%9E%E5%8F%B3%E5%90%91%E5%B7%A6%E6%89%BE%E9%A6%96%E5%80%8B%E5%B0%8F%E6%96%BC%E5%9F%BA%E6%BA%96%E6%95%B8%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20while%20i%20%3C%20j%20and%20nums%5Bi%5D%20%3C%3D%20nums%5Bleft%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%20%20%23%20%E5%BE%9E%E5%B7%A6%E5%90%91%E5%8F%B3%E6%89%BE%E9%A6%96%E5%80%8B%E5%A4%A7%E6%96%BC%E5%9F%BA%E6%BA%96%E6%95%B8%E7%9A%84%E5%85%83%E7%B4%A0%0A%20%20%20%20%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E4%BA%A4%E6%8F%9B%0A%20%20%20%20%20%20%20%20nums%5Bi%5D%2C%20nums%5Bj%5D%20%3D%20nums%5Bj%5D%2C%20nums%5Bi%5D%0A%20%20%20%20%23%20%E5%B0%87%E5%9F%BA%E6%BA%96%E6%95%B8%E4%BA%A4%E6%8F%9B%E8%87%B3%E5%85%A9%E5%AD%90%E9%99%A3%E5%88%97%E7%9A%84%E5%88%86%E7%95%8C%E7%B7%9A%0A%20%20%20%20nums%5Bi%5D%2C%20nums%5Bleft%5D%20%3D%20nums%5Bleft%5D%2C%20nums%5Bi%5D%0A%20%20%20%20return%20i%20%20%23%20%E8%BF%94%E5%9B%9E%E5%9F%BA%E6%BA%96%E6%95%B8%E7%9A%84%E7%B4%A2%E5%BC%95%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%B8%AD%E4%BD%8D%E5%9F%BA%E6%BA%96%E6%95%B8%E6%9C%80%E4%BD%B3%E5%8C%96%0A%20%20%20%20nums%20%3D%20%5B2%2C%204%2C%201%2C%200%2C%203%2C%205%5D%0A%20%20%20%20partition%28nums%2C%200%2C%20len%28nums%29%20-%201%29%0A%20%20%20%20print%28%22%E5%93%A8%E5%85%B5%E5%8A%83%E5%88%86%EF%BC%88%E4%B8%AD%E4%BD%8D%E5%9F%BA%E6%BA%96%E6%95%B8%E6%9C%80%E4%BD%B3%E5%8C%96%EF%BC%89%E5%AE%8C%E6%88%90%E5%BE%8C%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全螢幕觀看 ></a></div>
## 11.5.5 &nbsp; 遞迴最佳化
## 11.5.5 &nbsp; 遞迴深度最佳化
**在某些輸入下,快速排序可能佔用空間較多**。以完全有序的輸入陣列為例,設遞迴中的子陣列長度為 $m$ ,每輪哨兵劃分操作都將產生長度為 $0$ 的左子陣列和長度為 $m - 1$ 的右子陣列,這意味著每一層遞迴呼叫減少的問題規模非常小(只減少一個元素),遞迴樹的高度會達到 $n - 1$ ,此時需要佔用 $O(n)$ 大小的堆疊幀空間。
@ -1171,7 +1171,7 @@ comments: true
```python title="quick_sort.py"
def quick_sort(self, nums: list[int], left: int, right: int):
"""快速排序(遞迴最佳化)"""
"""快速排序(遞迴深度最佳化)"""
# 子陣列長度為 1 時終止
while left < right:
# 哨兵劃分操作
@ -1188,7 +1188,7 @@ comments: true
=== "C++"
```cpp title="quick_sort.cpp"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
void quickSort(vector<int> &nums, int left, int right) {
// 子陣列長度為 1 時終止
while (left < right) {
@ -1209,7 +1209,7 @@ comments: true
=== "Java"
```java title="quick_sort.java"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
void quickSort(int[] nums, int left, int right) {
// 子陣列長度為 1 時終止
while (left < right) {
@ -1230,7 +1230,7 @@ comments: true
=== "C#"
```csharp title="quick_sort.cs"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
void QuickSort(int[] nums, int left, int right) {
// 子陣列長度為 1 時終止
while (left < right) {
@ -1251,7 +1251,7 @@ comments: true
=== "Go"
```go title="quick_sort.go"
/* 快速排序(遞迴最佳化)*/
/* 快速排序(遞迴深度最佳化)*/
func (q *quickSortTailCall) quickSort(nums []int, left, right int) {
// 子陣列長度為 1 時終止
for left < right {
@ -1272,7 +1272,7 @@ comments: true
=== "Swift"
```swift title="quick_sort.swift"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
func quickSortTailCall(nums: inout [Int], left: Int, right: Int) {
var left = left
var right = right
@ -1295,7 +1295,7 @@ comments: true
=== "JS"
```javascript title="quick_sort.js"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
quickSort(nums, left, right) {
// 子陣列長度為 1 時終止
while (left < right) {
@ -1316,7 +1316,7 @@ comments: true
=== "TS"
```typescript title="quick_sort.ts"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
quickSort(nums: number[], left: number, right: number): void {
// 子陣列長度為 1 時終止
while (left < right) {
@ -1337,7 +1337,7 @@ comments: true
=== "Dart"
```dart title="quick_sort.dart"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
void quickSort(List<int> nums, int left, int right) {
// 子陣列長度為 1 時終止
while (left < right) {
@ -1358,7 +1358,7 @@ comments: true
=== "Rust"
```rust title="quick_sort.rs"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
pub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {
// 子陣列長度為 1 時終止
while left < right {
@ -1379,7 +1379,7 @@ comments: true
=== "C"
```c title="quick_sort.c"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
void quickSortTailCall(int nums[], int left, int right) {
// 子陣列長度為 1 時終止
while (left < right) {
@ -1404,7 +1404,7 @@ comments: true
=== "Kotlin"
```kotlin title="quick_sort.kt"
/* 快速排序(遞迴最佳化) */
/* 快速排序(遞迴深度最佳化) */
fun quickSortTailCall(nums: IntArray, left: Int, right: Int) {
// 子陣列長度為 1 時終止
var l = left
@ -1427,7 +1427,7 @@ comments: true
=== "Ruby"
```ruby title="quick_sort.rb"
### 快速排序(遞迴最佳化)###
### 快速排序(遞迴深度最佳化)###
def quick_sort(nums, left, right)
# 子陣列長度不為 1 時遞迴
while left < right
@ -1448,7 +1448,7 @@ comments: true
=== "Zig"
```zig title="quick_sort.zig"
// 快速排序(遞迴最佳化)
// 快速排序(遞迴深度最佳化)
fn quickSort(nums: []i32, left_: usize, right_: usize) void {
var left = left_;
var right = right_;

View File

@ -8,7 +8,7 @@ comments: true
- 泡沫排序透過交換相鄰元素來實現排序。透過新增一個標誌位來實現提前返回,我們可以將泡沫排序的最佳時間複雜度最佳化到 $O(n)$ 。
- 插入排序每輪將未排序區間內的元素插入到已排序區間的正確位置,從而完成排序。雖然插入排序的時間複雜度為 $O(n^2)$ ,但由於單元操作相對較少,因此在小資料量的排序任務中非常受歡迎。
- 快速排序基於哨兵劃分操作實現排序。在哨兵劃分中,有可能每次都選取到最差的基準數,導致時間複雜度劣化至 $O(n^2)$ 。引入中位數基準數或隨機基準數可以降低這種劣化的機率。尾遞迴方法可以有效地減少遞迴深度,將空間複雜度最佳化到 $O(\log n)$ 。
- 快速排序基於哨兵劃分操作實現排序。在哨兵劃分中,有可能每次都選取到最差的基準數,導致時間複雜度劣化至 $O(n^2)$ 。引入中位數基準數或隨機基準數可以降低這種劣化的機率。透過優先遞迴較短子區間,可有效減小遞迴深度,將空間複雜度最佳化到 $O(\log n)$ 。
- 合併排序包括劃分和合並兩個階段,典型地體現了分治策略。在合併排序中,排序陣列需要建立輔助陣列,空間複雜度為 $O(n)$ ;然而排序鏈結串列的空間複雜度可以最佳化至 $O(1)$ 。
- 桶排序包含三個步驟:資料分桶、桶內排序和合並結果。它同樣體現了分治策略,適用於資料體量很大的情況。桶排序的關鍵在於對資料進行平均分配。
- 計數排序是桶排序的一個特例,它透過統計資料出現的次數來實現排序。計數排序適用於資料量大但資料範圍有限的情況,並且要求資料能夠轉換為正整數。
@ -38,11 +38,11 @@ comments: true
再深入思考一下,如果我們選擇 `nums[right]` 為基準數,那麼正好反過來,必須先“從左往右查詢”。
**Q**:關於尾遞迴最佳化,為什麼選短的陣列能保證遞迴深度不超過 $\log n$
**Q**:關於快速排序的遞迴深度最佳化,為什麼選短的陣列能保證遞迴深度不超過 $\log n$
遞迴深度就是當前未返回的遞迴方法的數量。每輪哨兵劃分我們將原陣列劃分為兩個子陣列。在遞迴最佳化後,向下遞迴的子陣列長度最大為原陣列長度的一半。假設最差情況,一直為一半長度,那麼最終的遞迴深度就是 $\log n$ 。
遞迴深度就是當前未返回的遞迴方法的數量。每輪哨兵劃分我們將原陣列劃分為兩個子陣列。在遞迴深度最佳化後,向下遞迴的子陣列長度最大為原陣列長度的一半。假設最差情況,一直為一半長度,那麼最終的遞迴深度就是 $\log n$ 。
回顧原始的快速排序,我們有可能會連續地遞迴長度較大的陣列,最差情況下為 $n$、$n - 1$、$\dots$、$2$、$1$ ,遞迴深度為 $n$ 。遞迴最佳化可以避免這種情況出現。
回顧原始的快速排序,我們有可能會連續地遞迴長度較大的陣列,最差情況下為 $n$、$n - 1$、$\dots$、$2$、$1$ ,遞迴深度為 $n$ 。遞迴深度最佳化可以避免這種情況出現。
**Q**:當陣列中所有元素都相等時,快速排序的時間複雜度是 $O(n^2)$ 嗎?該如何處理這種退化情況?

View File

@ -366,7 +366,7 @@
<!-- contributors -->
<div style="margin: 2em auto;">
<h3>貢獻者</h3>
<p>本書在開源社群一百多位貢獻者的共同努力下不斷完善,感謝他們付出的時間與精力!</p>
<p>本書在開源社群 200 多位貢獻者的共同努力下不斷完善,感謝他們付出的時間與精力!</p>
<a href="https://github.com/krahets/hello-algo/graphs/contributors">
<img src="https://contrib.rocks/image?repo=krahets/hello-algo&max=300&columns=12" alt="Contributors" style="width: 100%; max-width: 38.5em;">
</a>