feat: Revised the book (#978)

* Sync recent changes to the revised Word.

* Revised the preface chapter

* Revised the introduction chapter

* Revised the computation complexity chapter

* Revised the chapter data structure

* Revised the chapter array and linked list

* Revised the chapter stack and queue

* Revised the chapter hashing

* Revised the chapter tree

* Revised the chapter heap

* Revised the chapter graph

* Revised the chapter searching

* Reivised the sorting chapter

* Revised the divide and conquer chapter

* Revised the chapter backtacking

* Revised the DP chapter

* Revised the greedy chapter

* Revised the appendix chapter

* Revised the preface chapter doubly

* Revised the figures
This commit is contained in:
Yudong Jin
2023-12-02 06:21:34 +08:00
committed by GitHub
parent b824d149cb
commit e720aa2d24
404 changed files with 1537 additions and 1558 deletions

View File

@ -1,6 +1,6 @@
# 双向队列
在队列中,我们仅能在头部删除或在尾部添加元素。如下图所示,「双向队列 double-ended queue」提供了更高的灵活性允许在头部和尾部执行元素的添加或删除操作。
在队列中,我们仅能删除头部元素或在尾部添加元素。如下图所示,「双向队列 double-ended queue」提供了更高的灵活性允许在头部和尾部执行元素的添加或删除操作。
![双向队列的操作](deque.assets/deque_operations.png)
@ -19,13 +19,15 @@
| peekFirst() | 访问队首元素 | $O(1)$ |
| peekLast() | 访问队尾元素 | $O(1)$ |
同样地,我们可以直接使用编程语言中已实现的双向队列类
同样地,我们可以直接使用编程语言中已实现的双向队列类
=== "Python"
```python title="deque.py"
from collections import deque
# 初始化双向队列
deque: deque[int] = collections.deque()
deque: deque[int] = deque()
# 元素入队
deque.append(2) # 添加至队尾
@ -369,7 +371,7 @@
=== "popFirst()"
![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_pop_first.png)
实现代码如下所示
实现代码如下所示
```src
[file]{linkedlist_deque}-[class]{linked_list_deque}-[func]{}
@ -394,7 +396,7 @@
=== "popFirst()"
![array_deque_pop_first](deque.assets/array_deque_pop_first.png)
在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法
在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法
```src
[file]{array_deque}-[class]{array_deque}-[func]{}
@ -404,4 +406,4 @@
双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。
我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 `push` 到栈中,然后通过 `pop` 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 $50$ 步)。当栈的长度超过 $50$ 时,软件需要在栈底(队首)执行删除操作。**但栈无法实现该功能,此时就需要使用双向队列来替代栈**。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。
我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 `push` 到栈中,然后通过 `pop` 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 $50$ 步)。当栈的长度超过 $50$ 时,软件需要在栈底(队首)执行删除操作。**但栈无法实现该功能,此时就需要使用双向队列来替代栈**。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。

View File

@ -10,4 +10,4 @@
栈如同叠猫猫,而队列就像猫猫排队。
两者分别代表先入后出和先入先出的逻辑关系。
两者分别代表先入后出和先入先出的逻辑关系。

View File

@ -1,8 +1,8 @@
# 队列
「队列 queue」是一种遵循先入先出规则的线性数据结构。顾名思义队列模拟了排队现象即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。
「队列 queue」是一种遵循先入先出规则的线性数据结构。顾名思义队列模拟了排队现象即新来的人不断加入队列尾部而位于队列头部的人逐个离开。
如下图所示,我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。
如下图所示,我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。
![队列的先入先出规则](queue.assets/queue_operations.png)
@ -18,15 +18,17 @@
| pop() | 队首元素出队 | $O(1)$ |
| peek() | 访问队首元素 | $O(1)$ |
我们可以直接使用编程语言中现成的队列类
我们可以直接使用编程语言中现成的队列类
=== "Python"
```python title="queue.py"
from collections import deque
# 初始化队列
# 在 Python 中,我们一般将双向队列类 deque 作队列使用
# 虽然 queue.Queue() 是纯正的队列类,但不太好用,因此不建议
que: deque[int] = collections.deque()
# 在 Python 中,我们一般将双向队列类 deque 作队列使用
# 虽然 queue.Queue() 是纯正的队列类,但不太好用,因此不推荐
que: deque[int] = deque()
# 元素入队
que.append(1)
@ -308,7 +310,7 @@
## 队列实现
为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。因此,链表和数组都可以用来实现队列
为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素。链表和数组都符合要求
### 基于链表的实现
@ -323,7 +325,7 @@
=== "pop()"
![linkedlist_queue_pop](queue.assets/linkedlist_queue_pop.png)
以下是用链表实现队列的代码
以下是用链表实现队列的代码
```src
[file]{linkedlist_queue}-[class]{linked_list_queue}-[func]{}
@ -331,7 +333,7 @@
### 基于数组的实现
由于数组删除首元素的时间复杂度为 $O(n)$ ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。
数组删除首元素的时间复杂度为 $O(n)$ ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。
我们可以使用一个变量 `front` 指向队首元素的索引,并维护一个变量 `size` 用于记录队列长度。定义 `rear = front + size` ,这个公式计算出的 `rear` 指向队尾元素之后的下一个位置。
@ -351,19 +353,19 @@
=== "pop()"
![array_queue_pop](queue.assets/array_queue_pop.png)
你可能会发现一个问题:在不断进行入队和出队的过程中,`front` 和 `rear` 都在向右移动,**当它们到达数组尾部时就无法继续移动了**。为解决此问题,我们可以将数组视为首尾相接的“环形数组”。
你可能会发现一个问题:在不断进行入队和出队的过程中,`front` 和 `rear` 都在向右移动,**当它们到达数组尾部时就无法继续移动了**。为解决此问题,我们可以将数组视为首尾相接的“环形数组”。
对于环形数组,我们需要让 `front` 或 `rear` 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示
对于环形数组,我们需要让 `front` 或 `rear` 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示
```src
[file]{array_queue}-[class]{array_queue}-[func]{}
```
以上实现的队列仍然具有局限性,即其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的同学可以尝试自行实现。
以上实现的队列仍然具有局限性其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的读者可以尝试自行实现。
两种实现的对比结论与栈一致,在此不再赘述。
## 队列典型应用
- **淘宝订单**。购物者下单后,订单将加入队列中,系统随后会根据顺序依次处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。
- **各类待办事项**。任何需要实现“先来后到”功能的场景,例如打印机的任务队列、餐厅的出餐队列等队列在这些场景中可以有效地维护处理顺序。
- **淘宝订单**。购物者下单后,订单将加入队列中,系统随后会根据顺序处理队列中的订单。在双十一期间,短时间内会产生海量订单,高并发成为工程师们需要重点攻克的问题。
- **各类待办事项**。任何需要实现“先来后到”功能的场景,例如打印机的任务队列、餐厅的出餐队列等队列在这些场景中可以有效地维护处理顺序。

View File

@ -2,9 +2,9 @@
「栈 stack」是一种遵循先入后出的逻辑的线性数据结构。
我们可以将栈类比为桌面上的一摞盘子,如果需要拿出底部的盘子,则需要先将上面的盘子依次取出。我们将盘子替换为各种类型的元素(如整数、字符、对象等),就得到了栈数据结构。
我们可以将栈类比为桌面上的一摞盘子,如果想取出底部的盘子,则需要先将上面的盘子依次移走。我们将盘子替换为各种类型的元素(如整数、字符、对象等),就得到了栈这种数据结构。
如下图所示,我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫“入栈”,删除栈顶元素的操作叫“出栈”。
如下图所示,我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫“入栈”,删除栈顶元素的操作叫“出栈”。
![栈的先入后出规则](stack.assets/stack_operations.png)
@ -20,7 +20,7 @@
| pop() | 栈顶元素出栈 | $O(1)$ |
| peek() | 访问栈顶元素 | $O(1)$ |
通常情况下,我们可以直接使用编程语言内置的栈类。然而,某些语言可能没有专门提供栈类,这时我们可以将该语言的“数组”或“链表”作栈来使用,并在程序逻辑上忽略与栈无关的操作。
通常情况下,我们可以直接使用编程语言内置的栈类。然而,某些语言可能没有专门提供栈类,这时我们可以将该语言的“数组”或“链表”作栈来使用,并在程序逻辑上忽略与栈无关的操作。
=== "Python"
@ -306,11 +306,11 @@
为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。
栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素。然而,数组和链表都可以在任意位置添加和删除元素,**因此栈可以视为一种受限制的数组或链表**。换句话说,我们可以“屏蔽”数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。
栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素。然而,数组和链表都可以在任意位置添加和删除元素,**因此栈可以视为一种受限制的数组或链表**。换句话说,我们可以“屏蔽”数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。
### 基于链表的实现
使用链表实现栈时,我们可以将链表的头节点视为栈顶,尾节点视为栈底。
使用链表实现栈时,我们可以将链表的头节点视为栈顶,尾节点视为栈底。
如下图所示,对于入栈操作,我们只需将元素插入链表头部,这种节点插入方法被称为“头插法”。而对于出栈操作,只需将头节点从链表中删除即可。
@ -323,7 +323,7 @@
=== "pop()"
![linkedlist_stack_pop](stack.assets/linkedlist_stack_pop.png)
以下是基于链表实现栈的示例代码
以下是基于链表实现栈的示例代码
```src
[file]{linkedlist_stack}-[class]{linked_list_stack}-[func]{}
@ -342,7 +342,7 @@
=== "pop()"
![array_stack_pop](stack.assets/array_stack_pop.png)
由于入栈的元素可能会源源不断地增加,因此我们可以使用动态数组,这样就无须自行处理数组扩容问题。以下为示例代码
由于入栈的元素可能会源源不断地增加,因此我们可以使用动态数组,这样就无须自行处理数组扩容问题。以下为示例代码
```src
[file]{array_stack}-[class]{array_stack}-[func]{}
@ -356,9 +356,9 @@
**时间效率**
在基于数组的实现中,入栈和出栈操作都在预先分配好的连续内存中进行,具有很好的缓存本地性,因此效率较高。然而,如果入栈时超出数组容量,会触发扩容机制,导致该次入栈操作的时间复杂度变为 $O(n)$ 。
在基于数组的实现中,入栈和出栈操作都在预先分配好的连续内存中进行,具有很好的缓存本地性,因此效率较高。然而,如果入栈时超出数组容量,会触发扩容机制,导致该次入栈操作的时间复杂度变为 $O(n)$ 。
在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。
基于链表实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。
综上所述,当入栈与出栈操作的元素是基本数据类型时,例如 `int` 或 `double` ,我们可以得出以下结论。
@ -367,7 +367,7 @@
**空间效率**
在初始化列表时,系统会为列表分配“初始容量”,该容量可能超实际需求并且,扩容机制通常是按照特定倍率(例如 2 倍)进行扩容,扩容后的容量也可能超出实际需求。因此,**基于数组实现的栈可能造成一定的空间浪费**。
在初始化列表时,系统会为列表分配“初始容量”,该容量可能超实际需求并且,扩容机制通常是按照特定倍率(例如 2 倍)进行扩容,扩容后的容量也可能超出实际需求。因此,**基于数组实现的栈可能造成一定的空间浪费**。
然而,由于链表节点需要额外存储指针,**因此链表节点占用的空间相对较大**。
@ -375,5 +375,5 @@
## 栈典型应用
- **浏览器中的后退与前进、软件中的撤销与反撤销**。每当我们打开新的网页,浏览器就会上一个网页执行入栈,这样我们就可以通过后退操作回到上一页。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。
- **程序内存管理**。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会执行出栈操作。
- **浏览器中的后退与前进、软件中的撤销与反撤销**。每当我们打开新的网页,浏览器就会上一个网页执行入栈,这样我们就可以通过后退操作回到上一个网页。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。
- **程序内存管理**。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会不断执行出栈操作。

View File

@ -3,7 +3,7 @@
### 重点回顾
- 栈是一种遵循先入后出原则的数据结构,可通过数组或链表来实现。
- 时间效率角度看,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会劣化至 $O(n)$ 。相比之下,基于链表实现的栈具有更为稳定的效率表现。
- 时间效率方面,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会劣化至 $O(n)$ 。相比之下,栈的链表实现具有更为稳定的效率表现。
- 在空间效率方面,栈的数组实现可能导致一定程度的空间浪费。但需要注意的是,链表节点所占用的内存空间比数组元素更大。
- 队列是一种遵循先入先出原则的数据结构,同样可以通过数组或链表来实现。在时间效率和空间效率的对比上,队列的结论与前述栈的结论相似。
- 双向队列是一种具有更高自由度的队列,它允许在两端进行元素的添加和删除操作。
@ -12,7 +12,7 @@
!!! question "浏览器的前进后退是否是双向链表实现?"
浏览器的前进后退功能本质上是“栈”的体现。当用户访问一个新页面时,该页面会被添加到栈顶;当用户点击后退按钮时,该页面会从栈顶弹出。使用双向队列可以方便实现一些额外操作,这个在双向队列章节有提到。
浏览器的前进后退功能本质上是“栈”的体现。当用户访问一个新页面时,该页面会被添加到栈顶;当用户点击后退按钮时,该页面会从栈顶弹出。使用双向队列可以方便实现一些额外操作,这个在双向队列章节有提到。
!!! question "在出栈后,是否需要释放出栈节点的内存?"
@ -20,7 +20,7 @@
!!! question "双向队列像是两个栈拼接在了一起,它的用途是什么?"
双向队列就像是栈和队列的组合,或者是两个栈拼在了一起。它表现的是栈 + 队列的逻辑,因此可以实现栈与队列的所有应用,并且更加灵活。
双向队列就像是栈和队列的组合,或两个栈拼在了一起。它表现的是栈 + 队列的逻辑,因此可以实现栈与队列的所有应用,并且更加灵活。
!!! question "撤销undo和反撤销redo具体是如何实现的"