Update docs for deployment on Vercel.
This commit is contained in:
BIN
docs/chapter_stack_and_queue/deque.assets/deque_operations.png
Normal file
BIN
docs/chapter_stack_and_queue/deque.assets/deque_operations.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
78
docs/chapter_stack_and_queue/deque.md
Normal file
78
docs/chapter_stack_and_queue/deque.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 双向队列
|
||||
|
||||
对于队列,我们只能在头部删除或在尾部添加元素,而「双向队列 Deque」更加灵活,在其头部和尾部都能执行元素添加或删除操作。
|
||||
|
||||

|
||||
|
||||
<p style="text-align:center"> Fig. 双向队列的操作 </p>
|
||||
|
||||
## 双向队列常用操作
|
||||
|
||||
双向队列的常用操作见下表,方法名需根据编程语言设定来具体确定。
|
||||
|
||||
<p style="text-align:center"> Table. 双向队列的常用操作 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 方法 | 描述 |
|
||||
| ------------ | ---------------- |
|
||||
| offerFirst() | 将元素添加至队首 |
|
||||
| offerLast() | 将元素添加至队尾 |
|
||||
| pollFirst() | 删除队首元素 |
|
||||
| pollLast() | 删除队尾元素 |
|
||||
| peekFirst() | 访问队首元素 |
|
||||
| peekLast() | 访问队尾元素 |
|
||||
| size() | 获取队列的长度 |
|
||||
| isEmpty() | 判断队列是否为空 |
|
||||
|
||||
</div>
|
||||
|
||||
相同地,我们可以直接使用编程语言实现好的双向队列类。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="deque.java"
|
||||
/* 初始化双向队列 */
|
||||
Deque<Integer> deque = new LinkedList<>();
|
||||
|
||||
/* 元素入队 */
|
||||
deque.offerLast(2);
|
||||
deque.offerLast(5);
|
||||
deque.offerLast(4);
|
||||
deque.offerFirst(3);
|
||||
deque.offerFirst(1);
|
||||
System.out.println("队列 deque = " + deque);
|
||||
|
||||
/* 访问队首元素 */
|
||||
int peekFirst = deque.peekFirst();
|
||||
System.out.println("队首元素 peekFirst = " + peekFirst);
|
||||
int peekLast = deque.peekLast();
|
||||
System.out.println("队尾元素 peekLast = " + peekLast);
|
||||
|
||||
/* 元素出队 */
|
||||
int pollFirst = deque.pollFirst();
|
||||
System.out.println("队首出队元素 pollFirst = " + pollFirst +
|
||||
",队首出队后 deque = " + deque);
|
||||
int pollLast = deque.pollLast();
|
||||
System.out.println("队尾出队元素 pollLast = " + pollLast +
|
||||
",队尾出队后 deque = " + deque);
|
||||
|
||||
/* 获取队列的长度 */
|
||||
int size = deque.size();
|
||||
System.out.println("队列长度 size = " + size);
|
||||
|
||||
/* 判断队列是否为空 */
|
||||
boolean isEmpty = deque.isEmpty();
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="deque.cpp"
|
||||
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="deque.py"
|
||||
|
||||
```
|
BIN
docs/chapter_stack_and_queue/queue.assets/queue_operations.png
Normal file
BIN
docs/chapter_stack_and_queue/queue.assets/queue_operations.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
218
docs/chapter_stack_and_queue/queue.md
Normal file
218
docs/chapter_stack_and_queue/queue.md
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 队列
|
||||
|
||||
「队列 Queue」是一种遵循「先入先出 first in, first out」数据操作规则的线性数据结构。顾名思义,队列模拟的是排队现象,即外面的人不断加入队列尾部,而处于队列头部的人不断地离开。
|
||||
|
||||
我们将队列头部称为「队首」,队列尾部称为「队尾」,将把元素加入队尾的操作称为「入队」,删除队首元素的操作称为「出队」。
|
||||
|
||||

|
||||
|
||||
<p style="text-align:center"> Fig. 队列的先入先出特性 </p>
|
||||
|
||||
## 队列常用操作
|
||||
|
||||
队列的常用操作见下表,方法命名需根据编程语言的设定来具体确定。
|
||||
|
||||
<p style="text-align:center"> Table. 队列的常用操作 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 方法 | 描述 |
|
||||
| --------- | ---------------------------- |
|
||||
| offer() | 元素入队,即将元素添加至队尾 |
|
||||
| poll() | 队首元素出队 |
|
||||
| front() | 访问队首元素 |
|
||||
| size() | 获取队列的长度 |
|
||||
| isEmpty() | 判断队列是否为空 |
|
||||
|
||||
</div>
|
||||
|
||||
我们可以直接使用编程语言实现好的队列类。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="queue.java"
|
||||
/* 初始化队列 */
|
||||
Queue<Integer> queue = new LinkedList<>();
|
||||
|
||||
/* 元素入队 */
|
||||
queue.offer(1);
|
||||
queue.offer(3);
|
||||
queue.offer(2);
|
||||
queue.offer(5);
|
||||
queue.offer(4);
|
||||
System.out.println("队列 queue = " + queue);
|
||||
|
||||
/* 访问队首元素 */
|
||||
int peek = queue.peek();
|
||||
System.out.println("队首元素 peek = " + peek);
|
||||
|
||||
/* 元素出队 */
|
||||
int poll = queue.poll();
|
||||
System.out.println("出队元素 poll = " + poll + ",出队后 queue = " + queue);
|
||||
|
||||
/* 获取队列的长度 */
|
||||
int size = queue.size();
|
||||
System.out.println("队列长度 size = " + size);
|
||||
|
||||
/* 判断队列是否为空 */
|
||||
boolean isEmpty = queue.isEmpty();
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="queue.cpp"
|
||||
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="queue.py"
|
||||
|
||||
```
|
||||
|
||||
## 队列实现
|
||||
|
||||
队列需要一种可以在一端添加,并在另一端删除的数据结构,也可以使用链表或数组来实现。
|
||||
|
||||
### 基于链表的实现
|
||||
|
||||
我们将链表的「头结点」和「尾结点」分别看作是队首和队尾,并规定队尾只可添加结点,队首只可删除结点。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linkedlist_queue.java"
|
||||
/* 基于链表实现的队列 */
|
||||
class LinkedListQueue {
|
||||
LinkedList<Integer> list;
|
||||
|
||||
public LinkedListQueue() {
|
||||
// 初始化链表
|
||||
list = new LinkedList<>();
|
||||
}
|
||||
/* 获取队列的长度 */
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
/* 判断队列是否为空 */
|
||||
public boolean isEmpty() {
|
||||
return list.size() == 0;
|
||||
}
|
||||
/* 入队 */
|
||||
public void offer(int num) {
|
||||
// 尾结点后添加 num
|
||||
list.addLast(num);
|
||||
}
|
||||
/* 出队 */
|
||||
public int poll() {
|
||||
// 删除头结点
|
||||
return list.removeFirst();
|
||||
}
|
||||
/* 访问队首元素 */
|
||||
public int peek() {
|
||||
return list.getFirst();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="linkedlist_queue.cpp"
|
||||
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="linkedlist_queue.py"
|
||||
|
||||
```
|
||||
|
||||
### 基于数组的实现
|
||||
|
||||
数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。
|
||||
|
||||
还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是 “环形” 的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组 “首尾相连” 了。
|
||||
|
||||
为了适应环形数组的设定,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。
|
||||
|
||||
基于数组实现的队列有一个缺点,即长度不可变。但这点我们可以通过动态数组来解决,有兴趣的同学可以自行实现。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array_queue.java"
|
||||
/* 基于环形数组实现的队列 */
|
||||
class ArrayQueue {
|
||||
int[] nums; // 用于存储队列元素的数组
|
||||
int size = 0; // 队列长度(即元素个数)
|
||||
int front = 0; // 头指针,指向队首
|
||||
int rear = 0; // 尾指针,指向队尾 + 1
|
||||
|
||||
public ArrayQueue(int capacity) {
|
||||
// 初始化数组
|
||||
nums = new int[capacity];
|
||||
}
|
||||
/* 获取队列的容量 */
|
||||
public int capacity() {
|
||||
return nums.length;
|
||||
}
|
||||
/* 获取队列的长度 */
|
||||
public int size() {
|
||||
int capacity = capacity();
|
||||
// 由于将数组看作为环形,可能 rear < front ,因此需要取余数
|
||||
return (capacity + rear - front) % capacity;
|
||||
}
|
||||
/* 判断队列是否为空 */
|
||||
public boolean isEmpty() {
|
||||
return rear - front == 0;
|
||||
}
|
||||
/* 入队 */
|
||||
public void offer(int num) {
|
||||
if (size() == capacity()) {
|
||||
System.out.println("队列已满");
|
||||
return;
|
||||
}
|
||||
// 尾结点后添加 num
|
||||
nums[rear] = num;
|
||||
// 尾指针向后移动一位,越过尾部后返回到数组头部
|
||||
rear = (rear + 1) % capacity();
|
||||
}
|
||||
/* 出队 */
|
||||
public int poll() {
|
||||
// 删除头结点
|
||||
if (isEmpty())
|
||||
throw new EmptyStackException();
|
||||
int num = nums[front];
|
||||
// 队头指针向后移动,越过尾部后返回到数组头部
|
||||
front = (front + 1) % capacity();
|
||||
return num;
|
||||
}
|
||||
/* 访问队首元素 */
|
||||
public int peek() {
|
||||
// 删除头结点
|
||||
if (isEmpty())
|
||||
throw new EmptyStackException();
|
||||
return nums[front];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array_queue.cpp"
|
||||
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array_queue.py"
|
||||
|
||||
```
|
||||
|
||||
## 队列典型应用
|
||||
|
||||
- **淘宝订单。** 购物者下单后,订单就被加入到队列之中,随后系统再根据顺序依次处理队列中的订单。在双十一时,在短时间内会产生海量的订单,如何处理「高并发」则是工程师们需要重点思考的问题。
|
||||
|
||||
- **各种待办事项。** 例如打印机的任务队列、餐厅的出餐队列等等。
|
BIN
docs/chapter_stack_and_queue/stack.assets/stack_operations.png
Normal file
BIN
docs/chapter_stack_and_queue/stack.assets/stack_operations.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
197
docs/chapter_stack_and_queue/stack.md
Normal file
197
docs/chapter_stack_and_queue/stack.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 栈
|
||||
|
||||
「栈 Stack」是一种遵循「先入后出 first in, last out」数据操作规则的线性数据结构。我们可以将栈类比为放在桌面上的一摞盘子,如果需要拿出底部的盘子,则需要先将上面的盘子依次取出。
|
||||
|
||||
我们将顶部盘子称为「栈顶」,底部盘子称为「栈底」,将把元素添加到栈顶的操作称为「入栈」,将删除栈顶元素的操作称为「出栈」。
|
||||
|
||||

|
||||
|
||||
<p style="text-align:center"> Fig. 栈的先入后出特性 </p>
|
||||
|
||||
## 栈常用操作
|
||||
|
||||
栈的常用操作见下表,方法名需根据编程语言设定来具体确定。
|
||||
|
||||
<p style="text-align:center"> Table. 栈的常用操作 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 方法 | 描述 |
|
||||
| --------- | ---------------------- |
|
||||
| push() | 元素入栈(添加至栈顶) |
|
||||
| pop() | 栈顶元素出栈 |
|
||||
| peek() | 访问栈顶元素 |
|
||||
| size() | 获取栈的长度 |
|
||||
| isEmpty() | 判断栈是否为空 |
|
||||
|
||||
</div>
|
||||
|
||||
我们可以直接使用编程语言实现好的栈类。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="stack.java"
|
||||
/* 初始化栈 */
|
||||
Stack<Integer> stack = new Stack<>();
|
||||
|
||||
/* 元素入栈 */
|
||||
stack.push(1);
|
||||
stack.push(3);
|
||||
stack.push(2);
|
||||
stack.push(5);
|
||||
stack.push(4);
|
||||
System.out.println("栈 stack = " + stack);
|
||||
|
||||
/* 访问栈顶元素 */
|
||||
int peek = stack.peek();
|
||||
System.out.println("栈顶元素 peek = " + peek);
|
||||
|
||||
/* 元素出栈 */
|
||||
int pop = stack.pop();
|
||||
System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + stack);
|
||||
|
||||
/* 获取栈的长度 */
|
||||
int size = stack.size();
|
||||
System.out.println("栈的长度 size = " + size);
|
||||
|
||||
/* 判断是否为空 */
|
||||
boolean isEmpty = stack.isEmpty();
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="stack.cpp"
|
||||
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="stack.py"
|
||||
|
||||
```
|
||||
|
||||
## 栈的实现
|
||||
|
||||
为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。
|
||||
|
||||
栈规定元素是先入后出的,因此我们只能在栈顶添加或删除元素。然而,数组或链表都可以在任意位置添加删除元素,因此 **栈可被看作是一种受约束的数组或链表**。换言之,我们可以 “屏蔽” 数组或链表的部分无关操作,使之对外的表现逻辑符合栈的规定即可。
|
||||
|
||||
### 基于链表的实现
|
||||
|
||||
使用「链表」实现栈时,将链表的尾结点看作栈顶即可。
|
||||
|
||||
受益于链表的离散存储方式,栈的扩容更加灵活,删除元素的内存也会被系统自动回收;缺点是无法像数组一样高效地随机访问,并且由于链表结点需存储指针,导致单个元素占用空间更大。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linkedlist_stack.java"
|
||||
/* 基于链表实现的栈 */
|
||||
class LinkedListStack {
|
||||
LinkedList<Integer> list;
|
||||
public LinkedListStack() {
|
||||
// 初始化链表
|
||||
list = new LinkedList<>();
|
||||
}
|
||||
/* 获取栈的长度 */
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
/* 判断栈是否为空 */
|
||||
public boolean isEmpty() {
|
||||
return size() == 0;
|
||||
}
|
||||
/* 入栈 */
|
||||
public void push(int num) {
|
||||
list.addLast(num);
|
||||
}
|
||||
/* 出栈 */
|
||||
public int pop() {
|
||||
return list.removeLast();
|
||||
}
|
||||
/* 访问栈顶元素 */
|
||||
public int peek() {
|
||||
return list.getLast();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="linkedlist_stack.cpp"
|
||||
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="linkedlist_stack.py"
|
||||
|
||||
```
|
||||
|
||||
### 基于数组的实现
|
||||
|
||||
使用「数组」实现栈时,将数组的尾部当作栈顶。准确地说,我们需要使用「列表」,因为入栈的元素可能是源源不断的,因此使用动态数组可以方便扩容。
|
||||
|
||||
基于数组实现的栈,优点是支持随机访问,缺点是会造成一定的空间浪费,因为列表的容量始终 $\geq$ 元素数量。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array_stack.java"
|
||||
/* 基于数组实现的栈 */
|
||||
class ArrayStack {
|
||||
List<Integer> list;
|
||||
public ArrayStack() {
|
||||
// 初始化列表(动态数组)
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
/* 获取栈的长度 */
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
/* 判断栈是否为空 */
|
||||
public boolean isEmpty() {
|
||||
return size() == 0;
|
||||
}
|
||||
/* 入栈 */
|
||||
public void push(int num) {
|
||||
list.add(num);
|
||||
}
|
||||
/* 出栈 */
|
||||
public int pop() {
|
||||
return list.remove(size() - 1);
|
||||
}
|
||||
/* 访问栈顶元素 */
|
||||
public int peek() {
|
||||
return list.get(size() - 1);
|
||||
}
|
||||
/* 访问索引 index 处元素 */
|
||||
public int get(int index) {
|
||||
return list.get(index);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array_stack.cpp"
|
||||
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array_stack.py"
|
||||
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
实际编程中,我们一般直接将 `ArrayList` 或 `LinkedList` 当作「栈」来使用。我们仅需通过脑补来屏蔽无关操作,而不用专门去包装它。
|
||||
|
||||
## 栈典型应用
|
||||
|
||||
- **浏览器中的后退与前进、软件中的撤销与反撤销。** 每当我们打开新的网页,浏览器就讲上一个网页执行入栈,这样我们就可以通过「后退」操作来回到上一页面,后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么则需要两个栈来配合实现。
|
||||
|
||||
- **程序内存管理。** 每当调用函数时,系统就会站栈顶添加一个栈帧,用来记录函数的上下文信息。在递归函数中,向下递推会不断执行入栈,向上回溯阶段时出栈。
|
9
docs/chapter_stack_and_queue/summary.md
Normal file
9
docs/chapter_stack_and_queue/summary.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 小结
|
||||
|
||||
- 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。
|
||||
- 队列是一种遵循先入先出的数据结构,可以使用数组或链表实现。
|
||||
- 双向队列的两端都可以添加与删除元素。
|
Reference in New Issue
Block a user