mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-24 10:16:28 +08:00
Remove center-table from docs.
Add header to the tables.
This commit is contained in:
parent
4eb621dda7
commit
70227c82cb
18 changed files with 26 additions and 78 deletions
|
@ -711,7 +711,7 @@
|
|||
|
||||
下表总结对比了数组和链表的各项特点与操作效率。由于它们采用两种相反的存储策略,因此各种性质和操作效率也呈现对立的特点。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:数组与链表的效率对比 </p>
|
||||
|
||||
| | 数组 | 链表 |
|
||||
| ---------- | ------------------------ | ------------ |
|
||||
|
@ -723,8 +723,6 @@
|
|||
| 添加元素 | $O(n)$ | $O(1)$ |
|
||||
| 删除元素 | $O(n)$ | $O(1)$ |
|
||||
|
||||
</div>
|
||||
|
||||
## 常见链表类型
|
||||
|
||||
**单向链表**。即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点成为尾节点,尾节点指向空 $\text{None}$ 。
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
下表列举了几个示例数据,包括输入数组和对应的所有排列。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:数组与链表的效率对比 </p>
|
||||
|
||||
| 输入数组 | 所有排列 |
|
||||
| :---------- | :----------------------------------------------------------------- |
|
||||
|
@ -12,8 +12,6 @@
|
|||
| $[1, 2]$ | $[1, 2], [2, 1]$ |
|
||||
| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ |
|
||||
|
||||
</div>
|
||||
|
||||
## 无相等元素的情况
|
||||
|
||||
!!! question
|
||||
|
|
|
@ -872,8 +872,7 @@ $$
|
|||
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。
|
||||
|
||||
以下表格展示了一些例子,其中一些夸张的值是为了强调“系数无法撼动阶数”这一结论。当 $n$ 趋于无穷大时,这些常数变得无足轻重。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:多项式时间复杂度示例 </p>
|
||||
|
||||
| 操作数量 $T(n)$ | 时间复杂度 $O(f(n))$ |
|
||||
| ---------------------- | -------------------- |
|
||||
|
@ -883,8 +882,6 @@ $$
|
|||
| $n^3 + 10000n^2$ | $O(n^3)$ |
|
||||
| $2^n + 10000n^{10000}$ | $O(2^n)$ |
|
||||
|
||||
</div>
|
||||
|
||||
## 常见类型
|
||||
|
||||
设输入数据大小为 $n$ ,常见的时间复杂度类型包括(按照从低到高的顺序排列):
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
- 整数类型 `int` 占用 $4$ bytes = $32$ bits ,可以表示 $2^{32}$ 个数字。
|
||||
|
||||
下表列举了各种基本数据类型的占用空间、取值范围和默认值。此表格无需硬背,大致理解即可,需要时可以通过查表来回忆。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:基本数据类型的占用空间和取值范围 </p>
|
||||
|
||||
| 类型 | 符号 | 占用空间 | 最小值 | 最大值 | 默认值 |
|
||||
| ------ | -------- | ---------------- | ------------------------ | ----------------------- | -------------- |
|
||||
|
@ -31,8 +30,6 @@
|
|||
| 字符 | `char` | 2 bytes / 1 byte | $0$ | $2^{16} - 1$ | $0$ |
|
||||
| 布尔 | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ |
|
||||
|
||||
</div>
|
||||
|
||||
对于上表,需要注意以下几点:
|
||||
|
||||
- C, C++ 未明确规定基本数据类型大小,而因实现和平台各异。上表遵循 LP64 [数据模型](https://en.cppreference.com/w/cpp/language/types#Properties),其用于 Unix 64 位操作系统(例如 Linux , macOS)。
|
||||
|
|
|
@ -136,8 +136,7 @@ $$
|
|||
**尽管浮点数 `float` 扩展了取值范围,但其副作用是牺牲了精度**。整数类型 `int` 将全部 32 位用于表示数字,数字是均匀分布的;而由于指数位的存在,浮点数 `float` 的数值越大,相邻两个数字之间的差值就会趋向越大。
|
||||
|
||||
进一步地,指数位 $E = 0$ 和 $E = 255$ 具有特殊含义,**用于表示零、无穷大、$\mathrm{NaN}$ 等**。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:指数位含义 </p>
|
||||
|
||||
| 指数位 E | 分数位 $\mathrm{N} = 0$ | 分数位 $\mathrm{N} \ne 0$ | 计算公式 |
|
||||
| ------------------ | ----------------------- | ------------------------- | ---------------------------------------------------------------------- |
|
||||
|
@ -145,8 +144,6 @@ $$
|
|||
| $1, 2, \dots, 254$ | 正规数 | 正规数 | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ |
|
||||
| $255$ | $\pm \infty$ | $\mathrm{NaN}$ | |
|
||||
|
||||
</div>
|
||||
|
||||
特别地,次正规数显著提升了浮点数的精度,这是因为:
|
||||
|
||||
- 最小正正规数为 $2^{-126} \approx 1.18 \times 10^{-38}$ 。
|
||||
|
|
|
@ -40,8 +40,7 @@
|
|||
- 将当前树在 `inorder` 中的索引区间记为 $[l, r]$ 。
|
||||
|
||||
如下表所示,通过以上变量即可表示根节点在 `preorder` 中的索引,以及子树在 `inorder` 中的索引区间。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:根节点和子树在前序和中序遍历中的索引 </p>
|
||||
|
||||
| | 根节点在 `preorder` 中的索引 | 子树在 `inorder` 中的索引区间 |
|
||||
| ------ | -------------------------------- | ----------------------------- |
|
||||
|
@ -49,8 +48,6 @@
|
|||
| 左子树 | $i + 1$ | $[l, m-1]$ |
|
||||
| 右子树 | $i + 1 + (m - l)$ | $[m+1, r]$ |
|
||||
|
||||
</div>
|
||||
|
||||
请注意,右子树根节点索引中的 $(m-l)$ 的含义是“左子树的节点数量”,建议配合下图理解。
|
||||
|
||||
![根节点和左右子树的索引区间表示](build_binary_tree_problem.assets/build_tree_division_pointers.png)
|
||||
|
|
|
@ -73,13 +73,10 @@ $$
|
|||
## 图常见应用
|
||||
|
||||
实际应用中,许多系统都可以用图来建模,相应的待求解问题也可以约化为图计算问题。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:现实生活中常见的图 </p>
|
||||
|
||||
| | 顶点 | 边 | 图计算问题 |
|
||||
| ------ | ---- | --------------- | ------------ |
|
||||
| 社交网络 | 用户 | 好友关系 | 潜在好友推荐 |
|
||||
| 地铁线路 | 站点 | 站点间的连通性 | 最短路线推荐 |
|
||||
| 太阳系 | 星体 | 星体间的万有引力作用 | 行星轨道计算 |
|
||||
|
||||
</div>
|
||||
|
|
|
@ -206,8 +206,7 @@
|
|||
## 效率对比
|
||||
|
||||
设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:邻接矩阵与邻接表对比 </p>
|
||||
|
||||
| | 邻接矩阵 | 邻接表(链表) | 邻接表(哈希表) |
|
||||
| ------------ | -------- | -------------- | ---------------- |
|
||||
|
@ -218,6 +217,4 @@
|
|||
| 删除顶点 | $O(n^2)$ | $O(n + m)$ | $O(n)$ |
|
||||
| 内存空间占用 | $O(n^2)$ | $O(n + m)$ | $O(n + m)$ |
|
||||
|
||||
</div>
|
||||
|
||||
观察上表,似乎邻接表(哈希表)的时间与空间效率最优。但实际上,在邻接矩阵中操作边的效率更高,只需要一次数组访问或赋值操作即可。综合来看,邻接矩阵体现了“以空间换时间”的原则,而邻接表体现了“以时间换空间”的原则。
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
- **添加元素**:仅需将元素添加至数组(链表)的尾部即可,使用 $O(1)$ 时间。
|
||||
- **查询元素**:由于数组(链表)是乱序的,因此需要遍历其中的所有元素,使用 $O(n)$ 时间。
|
||||
- **删除元素**:需要先查询到元素,再从数组中删除,使用 $O(n)$ 时间。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:元素查询效率对比 </p>
|
||||
|
||||
| | 数组 | 链表 | 哈希表 |
|
||||
| -------- | ------ | ------ | ------ |
|
||||
|
@ -20,8 +19,6 @@
|
|||
| 添加元素 | $O(1)$ | $O(1)$ | $O(1)$ |
|
||||
| 删除元素 | $O(n)$ | $O(n)$ | $O(1)$ |
|
||||
|
||||
</div>
|
||||
|
||||
观察发现,**在哈希表中进行增删查改的时间复杂度都是 $O(1)$** ,非常高效。
|
||||
|
||||
## 哈希表常用操作
|
||||
|
|
|
@ -20,8 +20,7 @@
|
|||
实际上,**堆通常用作实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将「优先队列」和「堆」看作等价的数据结构。因此,本书对两者不做特别区分,统一使用「堆」来命名。
|
||||
|
||||
堆的常用操作见下表,方法名需要根据编程语言来确定。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:堆的操作效率 </p>
|
||||
|
||||
| 方法名 | 描述 | 时间复杂度 |
|
||||
| --------- | ------------------------------------------ | ----------- |
|
||||
|
@ -31,8 +30,6 @@
|
|||
| size() | 获取堆的元素数量 | $O(1)$ |
|
||||
| isEmpty() | 判断堆是否为空 | $O(1)$ |
|
||||
|
||||
</div>
|
||||
|
||||
在实际应用中,我们可以直接使用编程语言提供的堆类(或优先队列类)。
|
||||
|
||||
!!! tip
|
||||
|
|
|
@ -36,18 +36,15 @@
|
|||
![拼装积木](what_is_dsa.assets/assembling_blocks.jpg)
|
||||
|
||||
两者的详细对应关系如下表所示。
|
||||
<p align="center"> 表:将数据结构与算法类比为积木 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 数据结构与算法 | LEGO 乐高 |
|
||||
| 数据结构与算法 | 积木 |
|
||||
| -------------- | ---------------------------------------- |
|
||||
| 输入数据 | 未拼装的积木 |
|
||||
| 数据结构 | 积木组织形式,包括形状、大小、连接方式等 |
|
||||
| 算法 | 把积木拼成目标形态的一系列操作步骤 |
|
||||
| 输出数据 | 积木模型 |
|
||||
|
||||
</div>
|
||||
|
||||
值得说明的是,数据结构与算法是独立于编程语言的。正因如此,本书得以提供多种编程语言的实现。
|
||||
|
||||
!!! tip "约定俗成的简称"
|
||||
|
|
|
@ -43,8 +43,7 @@
|
|||
![多种搜索策略](searching_algorithm_revisited.assets/searching_algorithms.png)
|
||||
|
||||
上述几种方法的操作效率与特性如下表所示。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:查找算法效率对比 </p>
|
||||
|
||||
| | 线性搜索 | 二分查找 | 树查找 | 哈希查找 |
|
||||
| ------------ | -------- | ------------------ | ------------------ | --------------- |
|
||||
|
@ -55,8 +54,6 @@
|
|||
| 数据预处理 | / | 排序 $O(n \log n)$ | 建树 $O(n \log n)$ | 建哈希表 $O(n)$ |
|
||||
| 数据是否有序 | 无序 | 有序 | 有序 | 无序 |
|
||||
|
||||
</div>
|
||||
|
||||
除了以上表格内容,搜索算法的选择还取决于数据体量、搜索性能要求、数据查询与更新频率等。
|
||||
|
||||
**线性搜索**
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
## 双向队列常用操作
|
||||
|
||||
双向队列的常用操作如下表所示,具体的方法名称需要根据所使用的编程语言来确定。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:双向队列操作效率 </p>
|
||||
|
||||
| 方法名 | 描述 | 时间复杂度 |
|
||||
| ----------- | -------------- | ---------- |
|
||||
|
@ -19,8 +18,6 @@
|
|||
| peekFirst() | 访问队首元素 | $O(1)$ |
|
||||
| peekLast() | 访问队尾元素 | $O(1)$ |
|
||||
|
||||
</div>
|
||||
|
||||
同样地,我们可以直接使用编程语言中已实现的双向队列类。
|
||||
|
||||
=== "Java"
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
## 队列常用操作
|
||||
|
||||
队列的常见操作如下表所示。需要注意的是,不同编程语言的方法名称可能会有所不同。我们在此采用与栈相同的方法命名。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:队列操作效率 </p>
|
||||
|
||||
| 方法名 | 描述 | 时间复杂度 |
|
||||
| --------- | -------------------------- | -------- |
|
||||
|
@ -18,8 +17,6 @@
|
|||
| pop() | 队首元素出队 | $O(1)$ |
|
||||
| peek() | 访问队首元素 | $O(1)$ |
|
||||
|
||||
</div>
|
||||
|
||||
我们可以直接使用编程语言中现成的队列类。
|
||||
|
||||
=== "Java"
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
## 栈常用操作
|
||||
|
||||
栈的常用操作如下表所示,具体的方法名需要根据所使用的编程语言来确定。在此,我们以常见的 `push()` , `pop()` , `peek()` 命名为例。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:栈的操作效率 </p>
|
||||
|
||||
| 方法 | 描述 | 时间复杂度 |
|
||||
| --------- | ---------------------- | ---------- |
|
||||
|
@ -20,8 +19,6 @@
|
|||
| pop() | 栈顶元素出栈 | $O(1)$ |
|
||||
| peek() | 访问栈顶元素 | $O(1)$ |
|
||||
|
||||
</div>
|
||||
|
||||
通常情况下,我们可以直接使用编程语言内置的栈类。然而,某些语言可能没有专门提供栈类,这时我们可以将该语言的「数组」或「链表」视作栈来使用,并通过“脑补”来忽略与栈无关的操作。
|
||||
|
||||
=== "Java"
|
||||
|
|
|
@ -569,17 +569,14 @@ AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉
|
|||
![AVL 树的四种旋转情况](avl_tree.assets/avltree_rotation_cases.png)
|
||||
|
||||
在代码中,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于上图中的哪种情况。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:四种旋转情况的选择条件 </p>
|
||||
|
||||
| 失衡节点的平衡因子 | 子节点的平衡因子 | 应采用的旋转方法 |
|
||||
| ---------------- | ---------------- | ---------------- |
|
||||
| $>1$ (即左偏树) | $\geq 0$ | 右旋 |
|
||||
| $>1$ (即左偏树) | $<0$ | 先左旋后右旋 |
|
||||
| $<-1$ (即右偏树) | $\leq 0$ | 左旋 |
|
||||
| $<-1$ (即右偏树) | $>0$ | 先右旋后左旋 |
|
||||
|
||||
</div>
|
||||
| $> 1$ (即左偏树) | $\geq 0$ | 右旋 |
|
||||
| $> 1$ (即左偏树) | $<0$ | 先左旋后右旋 |
|
||||
| $< -1$ (即右偏树) | $\leq 0$ | 左旋 |
|
||||
| $< -1$ (即右偏树) | $>0$ | 先右旋后左旋 |
|
||||
|
||||
为了便于使用,我们将旋转操作封装成一个函数。**有了这个函数,我们就能对各种失衡情况进行旋转,使失衡节点重新恢复平衡**。
|
||||
|
||||
|
|
|
@ -310,8 +310,7 @@
|
|||
给定一组数据,我们考虑使用数组或二叉搜索树存储。
|
||||
|
||||
观察可知,二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能表现。只有在高频添加、低频查找删除的数据适用场景下,数组比二叉搜索树的效率更高。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:数组与搜索树的效率对比 </p>
|
||||
|
||||
| | 无序数组 | 二叉搜索树 |
|
||||
| -------- | -------- | ----------- |
|
||||
|
@ -319,8 +318,6 @@
|
|||
| 插入元素 | $O(1)$ | $O(\log n)$ |
|
||||
| 删除元素 | $O(n)$ | $O(\log n)$ |
|
||||
|
||||
</div>
|
||||
|
||||
在理想情况下,二叉搜索树是“平衡”的,这样就可以在 $\log n$ 轮循环内查找任意节点。
|
||||
|
||||
然而,如果我们在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为链表,这时各种操作的时间复杂度也会退化为 $O(n)$ 。
|
||||
|
|
|
@ -548,14 +548,11 @@
|
|||
![二叉树的最佳与最差结构](binary_tree.assets/binary_tree_best_worst_cases.png)
|
||||
|
||||
如下表所示,在最佳和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大或极小值。
|
||||
|
||||
<div class="center-table" markdown>
|
||||
<p align="center"> 表:二叉树的最佳与最差情况 </p>
|
||||
|
||||
| | 完美二叉树 | 链表 |
|
||||
| ----------------------------- | ---------- | ---------- |
|
||||
| 第 $i$ 层的节点数量 | $2^{i-1}$ | $1$ |
|
||||
| 树的高度为 $h$ 时的叶节点数量 | $2^h$ | $1$ |
|
||||
| 树的高度为 $h$ 时的节点总数 | $2^{h+1} - 1$ | $h + 1$ |
|
||||
| 树的节点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
|
||||
|
||||
</div>
|
||||
| 高度 $h$ 树的叶节点数量 | $2^h$ | $1$ |
|
||||
| 高度 $h$ 树的节点总数 | $2^{h+1} - 1$ | $h + 1$ |
|
||||
| 节点总数 $n$ 树的高度 | $\log_2 (n+1) - 1$ | $n - 1$ |
|
||||
|
|
Loading…
Reference in a new issue