mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-24 02:46:28 +08:00
Use underline format for the technical terms (#1213)
* Use underline format for the technical terms * Bug fixes
This commit is contained in:
parent
06068927cd
commit
2b1a98fb61
42 changed files with 105 additions and 105 deletions
|
@ -1,6 +1,6 @@
|
|||
# 数组
|
||||
|
||||
「数组 array」是一种线性数据结构,其将相同类型的元素存储在连续的内存空间中。我们将元素在数组中的位置称为该元素的「索引 index」。下图展示了数组的主要概念和存储方式。
|
||||
<u>数组(array)</u>是一种线性数据结构,其将相同类型的元素存储在连续的内存空间中。我们将元素在数组中的位置称为该元素的<u>索引(index)</u>。下图展示了数组的主要概念和存储方式。
|
||||
|
||||
![数组定义与存储方式](array.assets/array_definition.png)
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处。我们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间。此时链表的灵活性优势就体现出来了。
|
||||
|
||||
「链表 linked list」是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。
|
||||
<u>链表(linked list)</u>是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。
|
||||
|
||||
链表的设计使得各个节点可以分散存储在内存各处,它们的内存地址无须连续。
|
||||
|
||||
![链表定义与存储方式](linked_list.assets/linkedlist_definition.png)
|
||||
|
||||
观察上图,链表的组成单位是「节点 node」对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。
|
||||
观察上图,链表的组成单位是<u>节点(node)</u>对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。
|
||||
|
||||
- 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。
|
||||
- 尾节点指向的是“空”,它在 Java、C++ 和 Python 中分别被记为 `null`、`nullptr` 和 `None` 。
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# 列表
|
||||
|
||||
「列表 list」是一个抽象的数据结构概念,它表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无须使用者考虑容量限制的问题。列表可以基于链表或数组实现。
|
||||
<u>列表(list)</u>是一个抽象的数据结构概念,它表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无须使用者考虑容量限制的问题。列表可以基于链表或数组实现。
|
||||
|
||||
- 链表天然可以看作一个列表,其支持元素增删查改操作,并且可以灵活动态扩容。
|
||||
- 数组也支持元素增删查改,但由于其长度不可变,因此只能看作一个具有长度限制的列表。
|
||||
|
||||
当使用数组实现列表时,**长度不可变的性质会导致列表的实用性降低**。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间浪费。
|
||||
|
||||
为解决此问题,我们可以使用「动态数组 dynamic array」来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。
|
||||
为解决此问题,我们可以使用<u>动态数组(dynamic array)</u>来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。
|
||||
|
||||
实际上,**许多编程语言中的标准库提供的列表是基于动态数组实现的**,例如 Python 中的 `list` 、Java 中的 `ArrayList` 、C++ 中的 `vector` 和 C# 中的 `List` 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
## 计算机存储设备
|
||||
|
||||
计算机中包括三种类型的存储设备:「硬盘 hard disk」、「内存 random-access memory, RAM」、「缓存 cache memory」。下表展示了它们在计算机系统中的不同角色和性能特点。
|
||||
计算机中包括三种类型的存储设备:<u>硬盘(hard disk)</u>、<u>内存(random-access memory, RAM)</u>、<u>缓存(cache memory)</u>。下表展示了它们在计算机系统中的不同角色和性能特点。
|
||||
|
||||
<p align="center"> 表 <id> 计算机的存储设备 </p>
|
||||
|
||||
|
@ -45,9 +45,9 @@
|
|||
|
||||
## 数据结构的缓存效率
|
||||
|
||||
缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,只能存储一小部分频繁访问的数据,因此当 CPU 尝试访问的数据不在缓存中时,就会发生「缓存未命中 cache miss」,此时 CPU 不得不从速度较慢的内存中加载所需数据。
|
||||
缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,只能存储一小部分频繁访问的数据,因此当 CPU 尝试访问的数据不在缓存中时,就会发生<u>缓存未命中(cache miss)</u>,此时 CPU 不得不从速度较慢的内存中加载所需数据。
|
||||
|
||||
显然,**“缓存未命中”越少,CPU 读写数据的效率就越高**,程序性能也就越好。我们将 CPU 从缓存中成功获取数据的比例称为「缓存命中率 cache hit rate」,这个指标通常用来衡量缓存效率。
|
||||
显然,**“缓存未命中”越少,CPU 读写数据的效率就越高**,程序性能也就越好。我们将 CPU 从缓存中成功获取数据的比例称为<u>缓存命中率(cache hit rate)</u>,这个指标通常用来衡量缓存效率。
|
||||
|
||||
为了尽可能达到更高的效率,缓存会采取以下数据加载机制。
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 回溯算法
|
||||
|
||||
「回溯算法 backtracking algorithm」是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。
|
||||
<u>回溯算法(backtracking algorithm)</u>是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。
|
||||
|
||||
回溯算法通常采用“深度优先搜索”来遍历解空间。在“二叉树”章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## 迭代
|
||||
|
||||
「迭代 iteration」是一种重复执行某个任务的控制结构。在迭代中,程序会在满足一定的条件下重复执行某段代码,直到这个条件不再满足。
|
||||
<u>迭代(iteration)</u>是一种重复执行某个任务的控制结构。在迭代中,程序会在满足一定的条件下重复执行某段代码,直到这个条件不再满足。
|
||||
|
||||
### for 循环
|
||||
|
||||
|
@ -60,7 +60,7 @@
|
|||
|
||||
## 递归
|
||||
|
||||
「递归 recursion」是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段。
|
||||
<u>递归(recursion)</u>是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段。
|
||||
|
||||
1. **递**:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
|
||||
2. **归**:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。
|
||||
|
@ -106,7 +106,7 @@
|
|||
|
||||
### 尾递归
|
||||
|
||||
有趣的是,**如果函数在返回前的最后一步才进行递归调用**,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。这种情况被称为「尾递归 tail recursion」。
|
||||
有趣的是,**如果函数在返回前的最后一步才进行递归调用**,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。这种情况被称为<u>尾递归(tail recursion)</u>。
|
||||
|
||||
- **普通递归**:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下文。
|
||||
- **尾递归**:递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无须继续执行其他操作,因此系统无须保存上一层函数的上下文。
|
||||
|
@ -147,7 +147,7 @@
|
|||
[file]{recursion}-[class]{}-[func]{fib}
|
||||
```
|
||||
|
||||
观察以上代码,我们在函数内递归调用了两个函数,**这意味着从一个调用产生了两个调用分支**。如下图所示,这样不断递归调用下去,最终将产生一棵层数为 $n$ 的「递归树 recursion tree」。
|
||||
观察以上代码,我们在函数内递归调用了两个函数,**这意味着从一个调用产生了两个调用分支**。如下图所示,这样不断递归调用下去,最终将产生一棵层数为 $n$ 的<u>递归树(recursion tree)</u>。
|
||||
|
||||
![斐波那契数列的递归树](iteration_and_recursion.assets/recursion_tree.png)
|
||||
|
||||
|
|
|
@ -24,11 +24,11 @@
|
|||
|
||||
## 理论估算
|
||||
|
||||
由于实际测试具有较大的局限性,因此我们可以考虑仅通过一些计算来评估算法的效率。这种估算方法被称为「渐近复杂度分析 asymptotic complexity analysis」,简称「复杂度分析」。
|
||||
由于实际测试具有较大的局限性,因此我们可以考虑仅通过一些计算来评估算法的效率。这种估算方法被称为<u>渐近复杂度分析(asymptotic complexity analysis)</u>,简称<u>复杂度分析</u>。
|
||||
|
||||
复杂度分析能够体现算法运行所需的时间和空间资源与输入数据大小之间的关系。**它描述了随着输入数据大小的增加,算法执行所需时间和空间的增长趋势**。这个定义有些拗口,我们可以将其分为三个重点来理解。
|
||||
|
||||
- “时间和空间资源”分别对应「时间复杂度 time complexity」和「空间复杂度 space complexity」。
|
||||
- “时间和空间资源”分别对应<u>时间复杂度(time complexity)</u>和<u>空间复杂度(space complexity)</u>。
|
||||
- “随着输入数据大小的增加”意味着复杂度反映了算法运行效率与输入数据体量之间的关系。
|
||||
- “时间和空间的增长趋势”表示复杂度分析关注的不是运行时间或占用空间的具体值,而是时间或空间增长的“快慢”。
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 空间复杂度
|
||||
|
||||
「空间复杂度 space complexity」用于衡量算法占用内存空间随着数据量变大时的增长趋势。这个概念与时间复杂度非常类似,只需将“运行时间”替换为“占用内存空间”。
|
||||
<u>空间复杂度(space complexity)</u>用于衡量算法占用内存空间随着数据量变大时的增长趋势。这个概念与时间复杂度非常类似,只需将“运行时间”替换为“占用内存空间”。
|
||||
|
||||
## 算法相关空间
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
**Q**:函数和方法这两个术语的区别是什么?
|
||||
|
||||
「函数 function」可以被独立执行,所有参数都以显式传递。「方法 method」与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。
|
||||
<u>函数(function)</u>可以被独立执行,所有参数都以显式传递。<u>方法(method)</u>与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。
|
||||
|
||||
下面以几种常见的编程语言为例来说明。
|
||||
|
||||
|
|
|
@ -713,7 +713,7 @@ $$
|
|||
|
||||
$T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因此它的时间复杂度是线性阶。
|
||||
|
||||
我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为「大 $O$ 记号 big-$O$ notation」,表示函数 $T(n)$ 的「渐近上界 asymptotic upper bound」。
|
||||
我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号称为<u>大($O$ 记号 big-$O$ notation)</u>,表示函数 $T(n)$ 的<u>渐近上界(asymptotic upper bound)</u>。
|
||||
|
||||
时间复杂度分析本质上是计算“操作数量 $T(n)$”的渐近上界,它具有明确的数学定义。
|
||||
|
||||
|
|
|
@ -4,19 +4,19 @@
|
|||
|
||||
## ASCII 字符集
|
||||
|
||||
「ASCII 码」是最早出现的字符集,其全称为 American Standard Code for Information Interchange(美国标准信息交换代码)。它使用 7 位二进制数(一个字节的低 7 位)表示一个字符,最多能够表示 128 个不同的字符。如下图所示,ASCII 码包括英文字母的大小写、数字 0 ~ 9、一些标点符号,以及一些控制字符(如换行符和制表符)。
|
||||
<u>ASCII(码)</u>是最早出现的字符集,其全称为 American Standard Code for Information Interchange(美国标准信息交换代码)。它使用 7 位二进制数(一个字节的低 7 位)表示一个字符,最多能够表示 128 个不同的字符。如下图所示,ASCII 码包括英文字母的大小写、数字 0 ~ 9、一些标点符号,以及一些控制字符(如换行符和制表符)。
|
||||
|
||||
![ASCII 码](character_encoding.assets/ascii_table.png)
|
||||
|
||||
然而,**ASCII 码仅能够表示英文**。随着计算机的全球化,诞生了一种能够表示更多语言的「EASCII」字符集。它在 ASCII 的 7 位基础上扩展到 8 位,能够表示 256 个不同的字符。
|
||||
然而,**ASCII 码仅能够表示英文**。随着计算机的全球化,诞生了一种能够表示更多语言的<u>EASCII</u>字符集。它在 ASCII 的 7 位基础上扩展到 8 位,能够表示 256 个不同的字符。
|
||||
|
||||
在世界范围内,陆续出现了一批适用于不同地区的 EASCII 字符集。这些字符集的前 128 个字符统一为 ASCII 码,后 128 个字符定义不同,以适应不同语言的需求。
|
||||
|
||||
## GBK 字符集
|
||||
|
||||
后来人们发现,**EASCII 码仍然无法满足许多语言的字符数量要求**。比如汉字有近十万个,光日常使用的就有几千个。中国国家标准总局于 1980 年发布了「GB2312」字符集,其收录了 6763 个汉字,基本满足了汉字的计算机处理需要。
|
||||
后来人们发现,**EASCII 码仍然无法满足许多语言的字符数量要求**。比如汉字有近十万个,光日常使用的就有几千个。中国国家标准总局于 1980 年发布了<u>GB2312</u>字符集,其收录了 6763 个汉字,基本满足了汉字的计算机处理需要。
|
||||
|
||||
然而,GB2312 无法处理部分罕见字和繁体字。「GBK」字符集是在 GB2312 的基础上扩展得到的,它共收录了 21886 个汉字。在 GBK 的编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。
|
||||
然而,GB2312 无法处理部分罕见字和繁体字。<u>GBK</u>字符集是在 GB2312 的基础上扩展得到的,它共收录了 21886 个汉字。在 GBK 的编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。
|
||||
|
||||
## Unicode 字符集
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
|||
|
||||
那个时代的研究人员就在想:**如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗**?在这种想法的驱动下,一个大而全的字符集 Unicode 应运而生。
|
||||
|
||||
「Unicode」的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。
|
||||
<u>Unicode</u>的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。
|
||||
|
||||
自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截至 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号甚至表情符号等。在庞大的 Unicode 字符集中,常用的字符占用 2 字节,有些生僻的字符占用 3 字节甚至 4 字节。
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
![原码、反码与补码之间的相互转换](number_encoding.assets/1s_2s_complement.png)
|
||||
|
||||
「原码 sign-magnitude」虽然最直观,但存在一些局限性。一方面,**负数的原码不能直接用于运算**。例如在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。
|
||||
<u>原码(sign-magnitude)</u>虽然最直观,但存在一些局限性。一方面,**负数的原码不能直接用于运算**。例如在原码下计算 $1 + (-2)$ ,得到的结果是 $-3$ ,这显然是不对的。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
|
@ -29,7 +29,7 @@ $$
|
|||
\end{aligned}
|
||||
$$
|
||||
|
||||
为了解决此问题,计算机引入了「反码 1's complement」。如果我们先将原码转换为反码,并在反码下计算 $1 + (-2)$ ,最后将结果从反码转换回原码,则可得到正确结果 $-1$ 。
|
||||
为了解决此问题,计算机引入了<u>反码(1's complement)</u>。如果我们先将原码转换为反码,并在反码下计算 $1 + (-2)$ ,最后将结果从反码转换回原码,则可得到正确结果 $-1$ 。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
|
@ -51,7 +51,7 @@ $$
|
|||
\end{aligned}
|
||||
$$
|
||||
|
||||
与原码一样,反码也存在正负零歧义问题,因此计算机进一步引入了「补码 2's complement」。我们先来观察一下负零的原码、反码、补码的转换过程:
|
||||
与原码一样,反码也存在正负零歧义问题,因此计算机进一步引入了<u>补码(2's complement)</u>。我们先来观察一下负零的原码、反码、补码的转换过程:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 分治算法
|
||||
|
||||
「分治 divide and conquer」,全称分而治之,是一种非常重要且常见的算法策略。分治通常基于递归实现,包括“分”和“治”两个步骤。
|
||||
<u>分治(divide and conquer)</u>,全称分而治之,是一种非常重要且常见的算法策略。分治通常基于递归实现,包括“分”和“治”两个步骤。
|
||||
|
||||
1. **分(划分阶段)**:递归地将原问题分解为两个或多个子问题,直至到达最小子问题时终止。
|
||||
2. **治(合并阶段)**:从已知解的最小子问题开始,从底至顶地将子问题的解进行合并,从而构建出原问题的解。
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 初探动态规划
|
||||
|
||||
「动态规划 dynamic programming」是一个重要的算法范式,它将一个问题分解为一系列更小的子问题,并通过存储子问题的解来避免重复计算,从而大幅提升时间效率。
|
||||
<u>动态规划(dynamic programming)</u>是一个重要的算法范式,它将一个问题分解为一系列更小的子问题,并通过存储子问题的解来避免重复计算,从而大幅提升时间效率。
|
||||
|
||||
在本节中,我们从一个经典例题入手,先给出它的暴力回溯解法,观察其中包含的重叠子问题,再逐步导出更高效的动态规划解法。
|
||||
|
||||
|
@ -93,9 +93,9 @@ $$
|
|||
|
||||
根据以上内容,我们可以总结出动态规划的常用术语。
|
||||
|
||||
- 将数组 `dp` 称为「$dp$ 表」,$dp[i]$ 表示状态 $i$ 对应子问题的解。
|
||||
- 将最小子问题对应的状态(第 $1$ 阶和第 $2$ 阶楼梯)称为「初始状态」。
|
||||
- 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为「状态转移方程」。
|
||||
- 将数组 `dp` 称为<u>$dp$(表)</u>,$dp[i]$ 表示状态 $i$ 对应子问题的解。
|
||||
- 将最小子问题对应的状态(第 $1$ 阶和第 $2$ 阶楼梯)称为<u>初始状态</u>。
|
||||
- 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为<u>状态转移方程</u>。
|
||||
|
||||
## 空间优化
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 图
|
||||
|
||||
「图 graph」是一种非线性数据结构,由「顶点 vertex」和「边 edge」组成。我们可以将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。以下示例展示了一个包含 5 个顶点和 7 条边的图。
|
||||
<u>图(graph)</u>是一种非线性数据结构,由<u>顶点(vertex)</u>和<u>边(edge)</u>组成。我们可以将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。以下示例展示了一个包含 5 个顶点和 7 条边的图。
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
|
@ -16,29 +16,29 @@ $$
|
|||
|
||||
## 图的常见类型与术语
|
||||
|
||||
根据边是否具有方向,可分为「无向图 undirected graph」和「有向图 directed graph」,如下图所示。
|
||||
根据边是否具有方向,可分为<u>无向图(undirected graph)</u>和<u>有向图(directed graph)</u>,如下图所示。
|
||||
|
||||
- 在无向图中,边表示两顶点之间的“双向”连接关系,例如微信或 QQ 中的“好友关系”。
|
||||
- 在有向图中,边具有方向性,即 $A \rightarrow B$ 和 $A \leftarrow B$ 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系。
|
||||
|
||||
![有向图与无向图](graph.assets/directed_graph.png)
|
||||
|
||||
根据所有顶点是否连通,可分为「连通图 connected graph」和「非连通图 disconnected graph」,如下图所示。
|
||||
根据所有顶点是否连通,可分为<u>连通图(connected graph)</u>和<u>非连通图(disconnected graph)</u>,如下图所示。
|
||||
|
||||
- 对于连通图,从某个顶点出发,可以到达其余任意顶点。
|
||||
- 对于非连通图,从某个顶点出发,至少有一个顶点无法到达。
|
||||
|
||||
![连通图与非连通图](graph.assets/connected_graph.png)
|
||||
|
||||
我们还可以为边添加“权重”变量,从而得到如下图所示的「有权图 weighted graph」。例如在《王者荣耀》等手游中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以用有权图来表示。
|
||||
我们还可以为边添加“权重”变量,从而得到如下图所示的<u>有权图(weighted graph)</u>。例如在《王者荣耀》等手游中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以用有权图来表示。
|
||||
|
||||
![有权图与无权图](graph.assets/weighted_graph.png)
|
||||
|
||||
图数据结构包含以下常用术语。
|
||||
|
||||
- 「邻接 adjacency」:当两顶点之间存在边相连时,称这两顶点“邻接”。在上图中,顶点 1 的邻接顶点为顶点 2、3、5。
|
||||
- 「路径 path」:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。在上图中,边序列 1-5-2-4 是顶点 1 到顶点 4 的一条路径。
|
||||
- 「度 degree」:一个顶点拥有的边数。对于有向图,「入度 in-degree」表示有多少条边指向该顶点,「出度 out-degree」表示有多少条边从该顶点指出。
|
||||
- <u>邻接(adjacency)</u>:当两顶点之间存在边相连时,称这两顶点“邻接”。在上图中,顶点 1 的邻接顶点为顶点 2、3、5。
|
||||
- <u>路径(path)</u>:从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的“路径”。在上图中,边序列 1-5-2-4 是顶点 1 到顶点 4 的一条路径。
|
||||
- <u>度(degree)</u>:一个顶点拥有的边数。对于有向图,<u>入度(in-degree)</u>表示有多少条边指向该顶点,<u>出度(out-degree)</u>表示有多少条边从该顶点指出。
|
||||
|
||||
## 图的表示
|
||||
|
||||
|
@ -46,7 +46,7 @@ $$
|
|||
|
||||
### 邻接矩阵
|
||||
|
||||
设图的顶点数量为 $n$ ,「邻接矩阵 adjacency matrix」使用一个 $n \times n$ 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 $1$ 或 $0$ 表示两个顶点之间是否存在边。
|
||||
设图的顶点数量为 $n$ ,<u>邻接矩阵(adjacency matrix)</u>使用一个 $n \times n$ 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 $1$ 或 $0$ 表示两个顶点之间是否存在边。
|
||||
|
||||
如下图所示,设邻接矩阵为 $M$、顶点列表为 $V$ ,那么矩阵元素 $M[i, j] = 1$ 表示顶点 $V[i]$ 到顶点 $V[j]$ 之间存在边,反之 $M[i, j] = 0$ 表示两顶点之间无边。
|
||||
|
||||
|
@ -62,7 +62,7 @@ $$
|
|||
|
||||
### 邻接表
|
||||
|
||||
「邻接表 adjacency list」使用 $n$ 个链表来表示图,链表节点表示顶点。第 $i$ 个链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点(与该顶点相连的顶点)。下图展示了一个使用邻接表存储的图的示例。
|
||||
<u>邻接表(adjacency list)</u>使用 $n$ 个链表来表示图,链表节点表示顶点。第 $i$ 个链表对应顶点 $i$ ,其中存储了该顶点的所有邻接顶点(与该顶点相连的顶点)。下图展示了一个使用邻接表存储的图的示例。
|
||||
|
||||
![图的邻接表表示](graph.assets/adjacency_list.png)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
树代表的是“一对多”的关系,而图则具有更高的自由度,可以表示任意的“多对多”关系。因此,我们可以把树看作图的一种特例。显然,**树的遍历操作也是图的遍历操作的一种特例**。
|
||||
|
||||
图和树都需要应用搜索算法来实现遍历操作。图的遍历方式也可分为两种:「广度优先遍历」和「深度优先遍历」。
|
||||
图和树都需要应用搜索算法来实现遍历操作。图的遍历方式也可分为两种:<u>广度优先遍历</u>和<u>深度优先遍历</u>。
|
||||
|
||||
## 广度优先遍历
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 贪心算法
|
||||
|
||||
「贪心算法 greedy algorithm」是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期获得全局最优解。贪心算法简洁且高效,在许多实际问题中有着广泛的应用。
|
||||
<u>贪心算法(greedy algorithm)</u>是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期获得全局最优解。贪心算法简洁且高效,在许多实际问题中有着广泛的应用。
|
||||
|
||||
贪心算法和动态规划都常用于解决优化问题。它们之间存在一些相似之处,比如都依赖最优子结构性质,但工作原理不同。
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
## 链式地址
|
||||
|
||||
在原始哈希表中,每个桶仅能存储一个键值对。「链式地址 separate chaining」将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。下图展示了一个链式地址哈希表的例子。
|
||||
在原始哈希表中,每个桶仅能存储一个键值对。<u>链式地址(separate chaining)</u>将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。下图展示了一个链式地址哈希表的例子。
|
||||
|
||||
![链式地址哈希表](hash_collision.assets/hash_table_chaining.png)
|
||||
|
||||
|
@ -39,7 +39,7 @@
|
|||
|
||||
## 开放寻址
|
||||
|
||||
「开放寻址 open addressing」不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测和多次哈希等。
|
||||
<u>开放寻址(open addressing)</u>不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测和多次哈希等。
|
||||
|
||||
下面以线性探测为例,介绍开放寻址哈希表的工作机制。
|
||||
|
||||
|
@ -60,7 +60,7 @@
|
|||
|
||||
![在开放寻址中删除元素导致的查询问题](hash_collision.assets/hash_table_open_addressing_deletion.png)
|
||||
|
||||
为了解决该问题,我们可以采用「懒删除 lazy deletion」机制:它不直接从哈希表中移除元素,**而是利用一个常量 `TOMBSTONE` 来标记这个桶**。在该机制下,`None` 和 `TOMBSTONE` 都代表空桶,都可以放置键值对。但不同的是,线性探测到 `TOMBSTONE` 时应该继续遍历,因为其之下可能还存在键值对。
|
||||
为了解决该问题,我们可以采用<u>懒删除(lazy deletion)</u>机制:它不直接从哈希表中移除元素,**而是利用一个常量 `TOMBSTONE` 来标记这个桶**。在该机制下,`None` 和 `TOMBSTONE` 都代表空桶,都可以放置键值对。但不同的是,线性探测到 `TOMBSTONE` 时应该继续遍历,因为其之下可能还存在键值对。
|
||||
|
||||
然而,**懒删除可能会加速哈希表的性能退化**。这是因为每次删除操作都会产生一个删除标记,随着 `TOMBSTONE` 的增加,搜索时间也会增加,因为线性探测可能需要跳过多个 `TOMBSTONE` 才能找到目标元素。
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 哈希表
|
||||
|
||||
「哈希表 hash table」,又称「散列表」,它通过建立键 `key` 与值 `value` 之间的映射,实现高效的元素查询。具体而言,我们向哈希表中输入一个键 `key` ,则可以在 $O(1)$ 时间内获取对应的值 `value` 。
|
||||
<u>哈希表(hash table)</u>,又称<u>散列表</u>,它通过建立键 `key` 与值 `value` 之间的映射,实现高效的元素查询。具体而言,我们向哈希表中输入一个键 `key` ,则可以在 $O(1)$ 时间内获取对应的值 `value` 。
|
||||
|
||||
如下图所示,给定 $n$ 个学生,每个学生都有“姓名”和“学号”两项数据。假如我们希望实现“输入一个学号,返回对应的姓名”的查询功能,则可以采用下图所示的哈希表来实现。
|
||||
|
||||
|
@ -527,9 +527,9 @@
|
|||
|
||||
## 哈希表简单实现
|
||||
|
||||
我们先考虑最简单的情况,**仅用一个数组来实现哈希表**。在哈希表中,我们将数组中的每个空位称为「桶 bucket」,每个桶可存储一个键值对。因此,查询操作就是找到 `key` 对应的桶,并在桶中获取 `value` 。
|
||||
我们先考虑最简单的情况,**仅用一个数组来实现哈希表**。在哈希表中,我们将数组中的每个空位称为<u>桶(bucket)</u>,每个桶可存储一个键值对。因此,查询操作就是找到 `key` 对应的桶,并在桶中获取 `value` 。
|
||||
|
||||
那么,如何基于 `key` 定位对应的桶呢?这是通过「哈希函数 hash function」实现的。哈希函数的作用是将一个较大的输入空间映射到一个较小的输出空间。在哈希表中,输入空间是所有 `key` ,输出空间是所有桶(数组索引)。换句话说,输入一个 `key` ,**我们可以通过哈希函数得到该 `key` 对应的键值对在数组中的存储位置**。
|
||||
那么,如何基于 `key` 定位对应的桶呢?这是通过<u>哈希函数(hash function)</u>实现的。哈希函数的作用是将一个较大的输入空间映射到一个较小的输出空间。在哈希表中,输入空间是所有 `key` ,输出空间是所有桶(数组索引)。换句话说,输入一个 `key` ,**我们可以通过哈希函数得到该 `key` 对应的键值对在数组中的存储位置**。
|
||||
|
||||
输入一个 `key` ,哈希函数的计算过程分为以下两步。
|
||||
|
||||
|
@ -563,7 +563,7 @@ index = hash(key) % capacity
|
|||
20336 % 100 = 36
|
||||
```
|
||||
|
||||
如下图所示,两个学号指向了同一个姓名,这显然是不对的。我们将这种多个输入对应同一输出的情况称为「哈希冲突 hash collision」。
|
||||
如下图所示,两个学号指向了同一个姓名,这显然是不对的。我们将这种多个输入对应同一输出的情况称为<u>哈希冲突(hash collision)</u>。
|
||||
|
||||
![哈希冲突示例](hash_map.assets/hash_collision.png)
|
||||
|
||||
|
@ -575,4 +575,4 @@ index = hash(key) % capacity
|
|||
|
||||
类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时;并且由于哈希表容量 `capacity` 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步增加了扩容过程的计算开销。为此,编程语言通常会预留足够大的哈希表容量,防止频繁扩容。
|
||||
|
||||
「负载因子 load factor」是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,**也常作为哈希表扩容的触发条件**。例如在 Java 中,当负载因子超过 $0.75$ 时,系统会将哈希表扩容至原先的 $2$ 倍。
|
||||
<u>负载因子(load factor)</u>是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,**也常作为哈希表扩容的触发条件**。例如在 Java 中,当负载因子超过 $0.75$ 时,系统会将哈希表扩容至原先的 $2$ 倍。
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# 堆
|
||||
|
||||
「堆 heap」是一种满足特定条件的完全二叉树,主要可分为两种类型,如下图所示。
|
||||
<u>堆(heap)</u>是一种满足特定条件的完全二叉树,主要可分为两种类型,如下图所示。
|
||||
|
||||
- 「小顶堆 min heap」:任意节点的值 $\leq$ 其子节点的值。
|
||||
- 「大顶堆 max heap」:任意节点的值 $\geq$ 其子节点的值。
|
||||
- <u>小顶堆(min heap)</u>:任意节点的值 $\leq$ 其子节点的值。
|
||||
- <u>大顶堆(max heap)</u>:任意节点的值 $\geq$ 其子节点的值。
|
||||
|
||||
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
|||
|
||||
## 堆的常用操作
|
||||
|
||||
需要指出的是,许多编程语言提供的是「优先队列 priority queue」,这是一种抽象的数据结构,定义为具有优先级排序的队列。
|
||||
需要指出的是,许多编程语言提供的是<u>优先队列(priority queue)</u>,这是一种抽象的数据结构,定义为具有优先级排序的队列。
|
||||
|
||||
实际上,**堆通常用于实现优先队列,大顶堆相当于元素按从大到小的顺序出队的优先队列**。从使用角度来看,我们可以将“优先队列”和“堆”看作等价的数据结构。因此,本书对两者不做特别区分,统一称作“堆”。
|
||||
|
||||
|
@ -448,7 +448,7 @@
|
|||
|
||||
### 元素入堆
|
||||
|
||||
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 `val` 可能大于堆中其他元素,堆的成立条件可能已被破坏,**因此需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 heapify」。
|
||||
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 `val` 可能大于堆中其他元素,堆的成立条件可能已被破坏,**因此需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为<u>堆化(heapify)</u>。
|
||||
|
||||
考虑从入堆节点开始,**从底至顶执行堆化**。如下图所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## 算法定义
|
||||
|
||||
「算法 algorithm」是在有限时间内解决特定问题的一组指令或操作步骤,它具有以下特性。
|
||||
<u>算法(algorithm)</u>是在有限时间内解决特定问题的一组指令或操作步骤,它具有以下特性。
|
||||
|
||||
- 问题是明确的,包含清晰的输入和输出定义。
|
||||
- 具有可行性,能够在有限步骤、时间和内存空间下完成。
|
||||
|
@ -10,7 +10,7 @@
|
|||
|
||||
## 数据结构定义
|
||||
|
||||
「数据结构 data structure」是计算机中组织和存储数据的方式,具有以下设计目标。
|
||||
<u>数据结构(data structure)</u>是计算机中组织和存储数据的方式,具有以下设计目标。
|
||||
|
||||
- 空间占用尽量少,以节省计算机内存。
|
||||
- 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
## 行文风格约定
|
||||
|
||||
- 标题后标注 `*` 的是选读章节,内容相对困难。如果你的时间有限,可以先跳过。
|
||||
- 重要专有名词及其英文翻译会用 `「 」` 括号标注,例如 `「数组 array」` 。建议记住它们,以便阅读文献。
|
||||
- 专有名词和有特指含义的词句会使用 `“引号”` 标注,以避免歧义。
|
||||
- 重要名词、重点内容和总结性语句会 **加粗** ,这类文字值得特别关注。
|
||||
- 专业术语会使用黑体(纸质版和 PDF 版)或添加下划线(网页版),例如<u>数组(array)</u>。建议记住它们,以便阅读文献。
|
||||
- 重点内容和总结性语句会 **加粗**,这类文字值得特别关注。
|
||||
- 有特指含义的词句会使用“引号”标注,以避免歧义。
|
||||
- 当涉及编程语言之间不一致的名词时,本书均以 Python 为准,例如使用 `None` 来表示“空”。
|
||||
- 本书部分放弃了编程语言的注释规范,以换取更加紧凑的内容排版。注释主要分为三种类型:标题注释、内容注释、多行注释。
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 二分查找
|
||||
|
||||
「二分查找 binary search」是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。
|
||||
<u>二分查找(binary search)</u>是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。
|
||||
|
||||
!!! question
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 重识搜索算法
|
||||
|
||||
「搜索算法 searching algorithm」用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。
|
||||
<u>搜索算法(searching algorithm)</u>用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。
|
||||
|
||||
搜索算法可根据实现思路分为以下两类。
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 冒泡排序
|
||||
|
||||
「冒泡排序 bubble sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。
|
||||
<u>冒泡排序(bubble sort)</u>通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。
|
||||
|
||||
如下图所示,冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换二者。遍历完成后,最大的元素会被移动到数组的最右端。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
前述几种排序算法都属于“基于比较的排序算法”,它们通过比较元素间的大小来实现排序。此类排序算法的时间复杂度无法超越 $O(n \log n)$ 。接下来,我们将探讨几种“非比较排序算法”,它们的时间复杂度可以达到线性阶。
|
||||
|
||||
「桶排序 bucket sort」是分治策略的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。
|
||||
<u>桶排序(bucket sort)</u>是分治策略的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。
|
||||
|
||||
## 算法流程
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 计数排序
|
||||
|
||||
「计数排序 counting sort」通过统计元素数量来实现排序,通常应用于整数数组。
|
||||
<u>计数排序(counting sort)</u>通过统计元素数量来实现排序,通常应用于整数数组。
|
||||
|
||||
## 简单实现
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
阅读本节前,请确保已学完“堆“章节。
|
||||
|
||||
「堆排序 heap sort」是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序。
|
||||
<u>堆排序(heap sort)</u>是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序。
|
||||
|
||||
1. 输入数组并建立小顶堆,此时最小元素位于堆顶。
|
||||
2. 不断执行出堆操作,依次记录出堆元素,即可得到从小到大排序的序列。
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 插入排序
|
||||
|
||||
「插入排序 insertion sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。
|
||||
<u>插入排序(insertion sort)</u>是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。
|
||||
|
||||
具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 归并排序
|
||||
|
||||
「归并排序 merge sort」是一种基于分治策略的排序算法,包含下图所示的“划分”和“合并”阶段。
|
||||
<u>归并排序(merge sort)</u>是一种基于分治策略的排序算法,包含下图所示的“划分”和“合并”阶段。
|
||||
|
||||
1. **划分阶段**:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。
|
||||
2. **合并阶段**:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 快速排序
|
||||
|
||||
「快速排序 quick sort」是一种基于分治策略的排序算法,运行高效,应用广泛。
|
||||
<u>快速排序(quick sort)</u>是一种基于分治策略的排序算法,运行高效,应用广泛。
|
||||
|
||||
快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。具体来说,哨兵划分的流程如下图所示。
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
上一节介绍了计数排序,它适用于数据量 $n$ 较大但数据范围 $m$ 较小的情况。假设我们需要对 $n = 10^6$ 个学号进行排序,而学号是一个 $8$ 位数字,这意味着数据范围 $m = 10^8$ 非常大,使用计数排序需要分配大量内存空间,而基数排序可以避免这种情况。
|
||||
|
||||
「基数排序 radix sort」的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。
|
||||
<u>基数排序(radix sort)</u>的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。
|
||||
|
||||
## 算法流程
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 选择排序
|
||||
|
||||
「选择排序 selection sort」的工作原理非常简单:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。
|
||||
<u>选择排序(selection sort)</u>的工作原理非常简单:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。
|
||||
|
||||
设数组的长度为 $n$ ,选择排序的算法流程如下图所示。
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 排序算法
|
||||
|
||||
「排序算法 sorting algorithm」用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用,因为有序数据通常能够被更高效地查找、分析和处理。
|
||||
<u>排序算法(sorting algorithm)</u>用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用,因为有序数据通常能够被更高效地查找、分析和处理。
|
||||
|
||||
如下图所示,排序算法中的数据类型可以是整数、浮点数、字符或字符串等。排序的判断规则可根据需求设定,如数字大小、字符 ASCII 码顺序或自定义规则。
|
||||
|
||||
|
@ -10,11 +10,11 @@
|
|||
|
||||
**运行效率**:我们期望排序算法的时间复杂度尽量低,且总体操作数量较少(时间复杂度中的常数项变小)。对于大数据量的情况,运行效率显得尤为重要。
|
||||
|
||||
**就地性**:顾名思义,「原地排序」通过在原数组上直接操作实现排序,无须借助额外的辅助数组,从而节省内存。通常情况下,原地排序的数据搬运操作较少,运行速度也更快。
|
||||
**就地性**:顾名思义,<u>原地排序</u>通过在原数组上直接操作实现排序,无须借助额外的辅助数组,从而节省内存。通常情况下,原地排序的数据搬运操作较少,运行速度也更快。
|
||||
|
||||
**稳定性**:「稳定排序」在完成排序后,相等元素在数组中的相对顺序不发生改变。
|
||||
**稳定性**:<u>稳定排序</u>在完成排序后,相等元素在数组中的相对顺序不发生改变。
|
||||
|
||||
稳定排序是多级排序场景的必要条件。假设我们有一个存储学生信息的表格,第 1 列和第 2 列分别是姓名和年龄。在这种情况下,「非稳定排序」可能导致输入数据的有序性丧失:
|
||||
稳定排序是多级排序场景的必要条件。假设我们有一个存储学生信息的表格,第 1 列和第 2 列分别是姓名和年龄。在这种情况下,<u>非稳定排序</u>可能导致输入数据的有序性丧失:
|
||||
|
||||
```shell
|
||||
# 输入数据是按照姓名排序好的
|
||||
|
@ -35,11 +35,11 @@
|
|||
('E', 23)
|
||||
```
|
||||
|
||||
**自适应性**:「自适应排序」的时间复杂度会受输入数据的影响,即最佳时间复杂度、最差时间复杂度、平均时间复杂度并不完全相等。
|
||||
**自适应性**:<u>自适应排序</u>的时间复杂度会受输入数据的影响,即最佳时间复杂度、最差时间复杂度、平均时间复杂度并不完全相等。
|
||||
|
||||
自适应性需要根据具体情况来评估。如果最差时间复杂度差于平均时间复杂度,说明排序算法在某些数据下性能可能劣化,因此被视为负面属性;而如果最佳时间复杂度优于平均时间复杂度,则被视为正面属性。
|
||||
|
||||
**是否基于比较**:「基于比较的排序」依赖比较运算符($<$、$=$、$>$)来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 $O(n \log n)$ 。而「非比较排序」不使用比较运算符,时间复杂度可达 $O(n)$ ,但其通用性相对较差。
|
||||
**是否基于比较**:<u>基于比较的排序</u>依赖比较运算符($<$、$=$、$>$)来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 $O(n \log n)$ 。而<u>非比较排序</u>不使用比较运算符,时间复杂度可达 $O(n)$ ,但其通用性相对较差。
|
||||
|
||||
## 理想排序算法
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 双向队列
|
||||
|
||||
在队列中,我们仅能删除头部元素或在尾部添加元素。如下图所示,「双向队列 double-ended queue」提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
|
||||
在队列中,我们仅能删除头部元素或在尾部添加元素。如下图所示,<u>双向队列(double-ended queue)</u>提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
|
||||
|
||||
![双向队列的操作](deque.assets/deque_operations.png)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 队列
|
||||
|
||||
「队列 queue」是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。
|
||||
<u>队列(queue)</u>是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。
|
||||
|
||||
如下图所示,我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 栈
|
||||
|
||||
「栈 stack」是一种遵循先入后出逻辑的线性数据结构。
|
||||
<u>栈(stack)</u>是一种遵循先入后出逻辑的线性数据结构。
|
||||
|
||||
我们可以将栈类比为桌面上的一摞盘子,如果想取出底部的盘子,则需要先将上面的盘子依次移走。我们将盘子替换为各种类型的元素(如整数、字符、对象等),就得到了栈这种数据结构。
|
||||
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
|
||||
![AVL 树在插入节点后发生退化](avl_tree.assets/avltree_degradation_from_inserting_node.png)
|
||||
|
||||
1962 年 G. M. Adelson-Velsky 和 E. M. Landis 在论文“An algorithm for the organization of information”中提出了「AVL 树」。论文中详细描述了一系列操作,确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 $O(\log n)$ 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。
|
||||
1962 年 G. M. Adelson-Velsky 和 E. M. Landis 在论文“An algorithm for the organization of information”中提出了<u>AVL(树)</u>。论文中详细描述了一系列操作,确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 $O(\log n)$ 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。
|
||||
|
||||
## AVL 树常见术语
|
||||
|
||||
AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种「平衡二叉搜索树 balanced binary search tree」。
|
||||
AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种<u>平衡二叉搜索树(balanced binary search tree)</u>。
|
||||
|
||||
### 节点高度
|
||||
|
||||
|
@ -231,7 +231,7 @@ AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二
|
|||
|
||||
### 节点平衡因子
|
||||
|
||||
节点的「平衡因子 balance factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 $0$ 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用:
|
||||
节点的<u>平衡因子(balance factor)</u>定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 $0$ 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用:
|
||||
|
||||
```src
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{balance_factor}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 二叉搜索树
|
||||
|
||||
如下图所示,「二叉搜索树 binary search tree」满足以下条件。
|
||||
如下图所示,<u>二叉搜索树(binary search tree)</u>满足以下条件。
|
||||
|
||||
1. 对于根节点,左子树中所有节点的值 $<$ 根节点的值 $<$ 右子树中所有节点的值。
|
||||
2. 任意节点的左、右子树也是二叉搜索树,即同样满足条件 `1.` 。
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 二叉树
|
||||
|
||||
「二叉树 binary tree」是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。
|
||||
<u>二叉树(binary tree)</u>是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。
|
||||
|
||||
=== "Python"
|
||||
|
||||
|
@ -198,7 +198,7 @@
|
|||
|
||||
```
|
||||
|
||||
每个节点都有两个引用(指针),分别指向「左子节点 left-child node」和「右子节点 right-child node」,该节点被称为这两个子节点的「父节点 parent node」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树 left subtree」,同理可得「右子树 right subtree」。
|
||||
每个节点都有两个引用(指针),分别指向<u>左子节点(left-child node)</u>和<u>右子节点(right-child node)</u>,该节点被称为这两个子节点的<u>父节点(parent node)</u>。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的<u>左子树(left subtree)</u>,同理可得<u>右子树(right subtree)</u>。
|
||||
|
||||
**在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。如下图所示,如果将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。
|
||||
|
||||
|
@ -208,14 +208,14 @@
|
|||
|
||||
二叉树的常用术语如下图所示。
|
||||
|
||||
- 「根节点 root node」:位于二叉树顶层的节点,没有父节点。
|
||||
- 「叶节点 leaf node」:没有子节点的节点,其两个指针均指向 `None` 。
|
||||
- 「边 edge」:连接两个节点的线段,即节点引用(指针)。
|
||||
- 节点所在的「层 level」:从顶至底递增,根节点所在层为 1 。
|
||||
- 节点的「度 degree」:节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
|
||||
- 二叉树的「高度 height」:从根节点到最远叶节点所经过的边的数量。
|
||||
- 节点的「深度 depth」:从根节点到该节点所经过的边的数量。
|
||||
- 节点的「高度 height」:从距离该节点最远的叶节点到该节点所经过的边的数量。
|
||||
- <u>根节点(root node)</u>:位于二叉树顶层的节点,没有父节点。
|
||||
- <u>叶节点(leaf node)</u>:没有子节点的节点,其两个指针均指向 `None` 。
|
||||
- <u>边(edge)</u>:连接两个节点的线段,即节点引用(指针)。
|
||||
- 节点所在的<u>层(level)</u>:从顶至底递增,根节点所在层为 1 。
|
||||
- 节点的<u>度(degree)</u>:节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2 。
|
||||
- 二叉树的<u>高度(height)</u>:从根节点到最远叶节点所经过的边的数量。
|
||||
- 节点的<u>深度(depth)</u>:从根节点到该节点所经过的边的数量。
|
||||
- 节点的<u>高度(height)</u>:从距离该节点最远的叶节点到该节点所经过的边的数量。
|
||||
|
||||
![二叉树的常用术语](binary_tree.assets/binary_tree_terminology.png)
|
||||
|
||||
|
@ -615,29 +615,29 @@
|
|||
|
||||
### 完美二叉树
|
||||
|
||||
如下图所示,「完美二叉树 perfect binary tree」所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 $0$ ,其余所有节点的度都为 $2$ ;若树的高度为 $h$ ,则节点总数为 $2^{h+1} - 1$ ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。
|
||||
如下图所示,<u>完美二叉树(perfect binary tree)</u>所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 $0$ ,其余所有节点的度都为 $2$ ;若树的高度为 $h$ ,则节点总数为 $2^{h+1} - 1$ ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。
|
||||
|
||||
!!! tip
|
||||
|
||||
请注意,在中文社区中,完美二叉树常被称为「满二叉树」。
|
||||
请注意,在中文社区中,完美二叉树常被称为<u>满二叉树</u>。
|
||||
|
||||
![完美二叉树](binary_tree.assets/perfect_binary_tree.png)
|
||||
|
||||
### 完全二叉树
|
||||
|
||||
如下图所示,「完全二叉树 complete binary tree」只有最底层的节点未被填满,且最底层节点尽量靠左填充。
|
||||
如下图所示,<u>完全二叉树(complete binary tree)</u>只有最底层的节点未被填满,且最底层节点尽量靠左填充。
|
||||
|
||||
![完全二叉树](binary_tree.assets/complete_binary_tree.png)
|
||||
|
||||
### 完满二叉树
|
||||
|
||||
如下图所示,「完满二叉树 full binary tree」除了叶节点之外,其余所有节点都有两个子节点。
|
||||
如下图所示,<u>完满二叉树(full binary tree)</u>除了叶节点之外,其余所有节点都有两个子节点。
|
||||
|
||||
![完满二叉树](binary_tree.assets/full_binary_tree.png)
|
||||
|
||||
### 平衡二叉树
|
||||
|
||||
如下图所示,「平衡二叉树 balanced binary tree」中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
|
||||
如下图所示,<u>平衡二叉树(balanced binary tree)</u>中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。
|
||||
|
||||
![平衡二叉树](binary_tree.assets/balanced_binary_tree.png)
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
## 层序遍历
|
||||
|
||||
如下图所示,「层序遍历 level-order traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
|
||||
如下图所示,<u>层序遍历(level-order traversal)</u>从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。
|
||||
|
||||
层序遍历本质上属于「广度优先遍历 breadth-first traversal」,也称「广度优先搜索 breadth-first search, BFS」,它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
|
||||
层序遍历本质上属于<u>广度优先遍历(breadth-first traversal)</u>,也称<u>广度优先搜索(breadth-first search, BFS)</u>,它体现了一种“一圈一圈向外扩展”的逐层遍历方式。
|
||||
|
||||
![二叉树的层序遍历](binary_tree_traversal.assets/binary_tree_bfs.png)
|
||||
|
||||
|
@ -27,7 +27,7 @@
|
|||
|
||||
## 前序、中序、后序遍历
|
||||
|
||||
相应地,前序、中序和后序遍历都属于「深度优先遍历 depth-first traversal」,也称「深度优先搜索 depth-first search, DFS」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。
|
||||
相应地,前序、中序和后序遍历都属于<u>深度优先遍历(depth-first traversal)</u>,也称<u>深度优先搜索(depth-first search, DFS)</u>,它体现了一种“先走到尽头,再回溯继续”的遍历方式。
|
||||
|
||||
下图展示了对二叉树进行深度优先遍历的工作原理。**深度优先遍历就像是绕着整棵二叉树的外围“走”一圈**,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
## Writing Conventions
|
||||
|
||||
- Chapters marked with '*' after the title are optional and contain relatively challenging content. If you are short on time, it is advisable to skip them.
|
||||
- Key technical terms and their English equivalents are enclosed in **Bold** + *italics* brackets, for example, ***array***. It's advisable to familiarize yourself with these for better comprehension of technical texts.
|
||||
- Proprietary terms and words with specific meanings are indicated with “quotation marks” to avoid ambiguity.
|
||||
- Technical terms will be in boldface (in the print and PDF versions) or underlined (in the web version), for instance, <u>array</u>. It's advisable to familiarize yourself with these for better comprehension of technical texts.
|
||||
- **Bolded text** indicates key content or summary statements, which deserve special attention.
|
||||
- When it comes to terms that are inconsistent between programming languages, this book follows Python, for example using $\text{None}$ to mean "null".
|
||||
- Words and phrases with specific meanings are indicated with “quotation marks” to avoid ambiguity.
|
||||
- When it comes to terms that are inconsistent between programming languages, this book follows Python, for example using `None` to mean `null`.
|
||||
- This book partially ignores the comment conventions for programming languages in exchange for a more compact layout of the content. The comments primarily consist of three types: title comments, content comments, and multi-line comments.
|
||||
|
||||
=== "Python"
|
||||
|
|
Loading…
Reference in a new issue