Update stack, queue, space_time_tradeoff
|
@ -11,23 +11,23 @@ import java.util.*;
|
||||||
public class stack {
|
public class stack {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
/* 初始化栈 */
|
/* 初始化栈 */
|
||||||
// 在 Java 中,推荐将 LinkedList 当作栈来使用
|
// 在 Java 中,推荐将 ArrayList 当作栈来使用
|
||||||
LinkedList<Integer> stack = new LinkedList<>();
|
List<Integer> stack = new ArrayList<>();
|
||||||
|
|
||||||
/* 元素入栈 */
|
/* 元素入栈 */
|
||||||
stack.addLast(1);
|
stack.add(1);
|
||||||
stack.addLast(3);
|
stack.add(3);
|
||||||
stack.addLast(2);
|
stack.add(2);
|
||||||
stack.addLast(5);
|
stack.add(5);
|
||||||
stack.addLast(4);
|
stack.add(4);
|
||||||
System.out.println("栈 stack = " + stack);
|
System.out.println("栈 stack = " + stack);
|
||||||
|
|
||||||
/* 访问栈顶元素 */
|
/* 访问栈顶元素 */
|
||||||
int peek = stack.peekLast();
|
int peek = stack.get(stack.size() - 1);
|
||||||
System.out.println("栈顶元素 peek = " + peek);
|
System.out.println("栈顶元素 peek = " + peek);
|
||||||
|
|
||||||
/* 元素出栈 */
|
/* 元素出栈 */
|
||||||
int pop = stack.removeLast();
|
int pop = stack.remove(stack.size() - 1);
|
||||||
System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + stack);
|
System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + stack);
|
||||||
|
|
||||||
/* 获取栈的长度 */
|
/* 获取栈的长度 */
|
||||||
|
|
|
@ -16,7 +16,7 @@ comments: true
|
||||||
|
|
||||||
!!! question "两数之和"
|
!!! question "两数之和"
|
||||||
|
|
||||||
给定一个整数数组 `nums` 和一个整数目标值 `target` ,请你在该数组中找出 和为目标值 `target` 的那两个整数,并返回它们的数组下标。
|
给定一个整数数组 `nums` 和一个整数目标值 `target` ,请你在该数组中找出“和”为目标值 `target` 的那两个整数,并返回它们的数组下标。
|
||||||
|
|
||||||
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
|
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
|
||||||
|
|
||||||
|
|
BIN
docs/chapter_stack_and_queue/queue.assets/array_queue.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/chapter_stack_and_queue/queue.assets/array_queue_poll.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
docs/chapter_stack_and_queue/queue.assets/array_queue_push.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
docs/chapter_stack_and_queue/queue.assets/linkedlist_queue.png
Normal file
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 62 KiB |
|
@ -264,6 +264,17 @@ comments: true
|
||||||
|
|
||||||
我们将链表的「头结点」和「尾结点」分别看作是队首和队尾,并规定队尾只可添加结点,队首只可删除结点。
|
我们将链表的「头结点」和「尾结点」分别看作是队首和队尾,并规定队尾只可添加结点,队首只可删除结点。
|
||||||
|
|
||||||
|
=== "LinkedListQueue"
|
||||||
|
![linkedlist_queue](queue.assets/linkedlist_queue.png)
|
||||||
|
|
||||||
|
=== "push()"
|
||||||
|
![linkedlist_queue_push](queue.assets/linkedlist_queue_push.png)
|
||||||
|
|
||||||
|
=== "poll()"
|
||||||
|
![linkedlist_queue_poll](queue.assets/linkedlist_queue_poll.png)
|
||||||
|
|
||||||
|
以下是使用链表实现队列的示例代码。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
```java title="linkedlist_queue.java"
|
```java title="linkedlist_queue.java"
|
||||||
|
@ -712,11 +723,18 @@ comments: true
|
||||||
|
|
||||||
数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。
|
数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。
|
||||||
|
|
||||||
还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是“环形”的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组“首尾相连”了。
|
=== "ArrayQueue"
|
||||||
|
![array_queue](queue.assets/array_queue.png)
|
||||||
|
|
||||||
为了适应环形数组的设定,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。
|
=== "push()"
|
||||||
|
![array_queue_push](queue.assets/array_queue_push.png)
|
||||||
|
|
||||||
基于数组实现的队列有一个缺点,即长度不可变。但这点我们可以通过动态数组来解决,有兴趣的同学可以自行实现。
|
=== "poll()"
|
||||||
|
![array_queue_poll](queue.assets/array_queue_poll.png)
|
||||||
|
|
||||||
|
细心的同学可能会发现一个问题,即在入队与出队的过程中,两个指针都在向后移动,**在到达尾部后则无法继续移动了**。
|
||||||
|
|
||||||
|
为了解决此问题,我们可以采取一个取巧方案,**即将数组看作是“环形”的**。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组“首尾相连”了。在环形数组的设定下,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
@ -1182,6 +1200,12 @@ comments: true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
以上代码仍存在局限性,即长度不可变。然而,我们可以通过将数组替换为列表(即动态数组)来引入扩容机制,有兴趣的同学可以尝试实现。
|
||||||
|
|
||||||
|
## 两种实现对比
|
||||||
|
|
||||||
|
与栈的结论一致,在此不再赘述。
|
||||||
|
|
||||||
## 队列典型应用
|
## 队列典型应用
|
||||||
|
|
||||||
- **淘宝订单**。购物者下单后,订单就被加入到队列之中,随后系统再根据顺序依次处理队列中的订单。在双十一时,在短时间内会产生海量的订单,如何处理「高并发」则是工程师们需要重点思考的问题。
|
- **淘宝订单**。购物者下单后,订单就被加入到队列之中,随后系统再根据顺序依次处理队列中的订单。在双十一时,在短时间内会产生海量的订单,如何处理「高并发」则是工程师们需要重点思考的问题。
|
||||||
|
|
BIN
docs/chapter_stack_and_queue/stack.assets/array_stack.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
docs/chapter_stack_and_queue/stack.assets/array_stack_pop.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/chapter_stack_and_queue/stack.assets/array_stack_push.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
docs/chapter_stack_and_queue/stack.assets/linkedlist_stack.png
Normal file
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 62 KiB |
|
@ -32,27 +32,27 @@ comments: true
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
我们可以直接使用编程语言实现好的栈类。
|
我们可以直接使用编程语言实现好的栈类。 某些语言并未专门提供栈类,但我们可以直接把该语言的「数组」或「链表」看作栈来使用,并通过“脑补”来屏蔽无关操作。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
```java title="stack.java"
|
```java title="stack.java"
|
||||||
/* 初始化栈 */
|
/* 初始化栈 */
|
||||||
// 在 Java 中,推荐将 LinkedList 当作栈来使用
|
// 在 Java 中,推荐将 ArrayList 当作栈来使用
|
||||||
LinkedList<Integer> stack = new LinkedList<>();
|
List<Integer> stack = new ArrayList<>();
|
||||||
|
|
||||||
/* 元素入栈 */
|
/* 元素入栈 */
|
||||||
stack.addLast(1);
|
stack.add(1);
|
||||||
stack.addLast(3);
|
stack.add(3);
|
||||||
stack.addLast(2);
|
stack.add(2);
|
||||||
stack.addLast(5);
|
stack.add(5);
|
||||||
stack.addLast(4);
|
stack.add(4);
|
||||||
|
|
||||||
/* 访问栈顶元素 */
|
/* 访问栈顶元素 */
|
||||||
int peek = stack.peekLast();
|
int peek = stack.get(stack.size() - 1);
|
||||||
|
|
||||||
/* 元素出栈 */
|
/* 元素出栈 */
|
||||||
int pop = stack.removeLast();
|
int pop = stack.remove(stack.size() - 1);
|
||||||
|
|
||||||
/* 获取栈的长度 */
|
/* 获取栈的长度 */
|
||||||
int size = stack.size();
|
int size = stack.size();
|
||||||
|
@ -263,11 +263,20 @@ comments: true
|
||||||
|
|
||||||
### 基于链表的实现
|
### 基于链表的实现
|
||||||
|
|
||||||
使用「链表」实现栈时,将链表的头结点看作栈顶,尾结点看作栈底。
|
使用「链表」实现栈时,将链表的头结点看作栈顶,将尾结点看作栈底。
|
||||||
|
|
||||||
对于入栈操作,将元素插入到链表头部即可,这种结点添加方式被称为“头插法”。而对于出栈操作,则将头结点从链表中删除即可。
|
对于入栈操作,将元素插入到链表头部即可,这种结点添加方式被称为“头插法”。而对于出栈操作,则将头结点从链表中删除即可。
|
||||||
|
|
||||||
受益于链表的离散存储方式,栈的扩容更加灵活,删除元素的内存也会被系统自动回收;缺点是无法像数组一样高效地随机访问,并且由于链表结点需存储指针,导致单个元素占用空间更大。
|
=== "LinkedListStack"
|
||||||
|
![linkedlist_stack](stack.assets/linkedlist_stack.png)
|
||||||
|
|
||||||
|
=== "push()"
|
||||||
|
![linkedlist_stack_push](stack.assets/linkedlist_stack_push.png)
|
||||||
|
|
||||||
|
=== "pop()"
|
||||||
|
![linkedlist_stack_pop](stack.assets/linkedlist_stack_pop.png)
|
||||||
|
|
||||||
|
以下是基于链表实现栈的示例代码。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
@ -676,9 +685,18 @@ comments: true
|
||||||
|
|
||||||
### 基于数组的实现
|
### 基于数组的实现
|
||||||
|
|
||||||
使用「数组」实现栈时,将数组的尾部当作栈顶,这样可以保证入栈与出栈操作的时间复杂度都为 $O(1)$ 。准确地说,由于入栈的元素可能是源源不断的,我们需要使用可以动态扩容的「列表」。
|
使用「数组」实现栈时,考虑将数组的尾部当作栈顶。这样设计下,「入栈」与「出栈」操作就对应在数组尾部「添加元素」与「删除元素」,时间复杂度都为 $O(1)$ 。
|
||||||
|
|
||||||
基于数组实现的栈,优点是支持随机访问,缺点是会造成一定的空间浪费,因为列表的容量始终 $\geq$ 元素数量。
|
=== "ArrayStack"
|
||||||
|
![array_stack](stack.assets/array_stack.png)
|
||||||
|
|
||||||
|
=== "push()"
|
||||||
|
![array_stack_push](stack.assets/array_stack_push.png)
|
||||||
|
|
||||||
|
=== "pop()"
|
||||||
|
![array_stack_pop](stack.assets/array_stack_pop.png)
|
||||||
|
|
||||||
|
由于入栈的元素可能是源源不断的,因此可以使用支持动态扩容的「列表」,这样就无需自行实现数组扩容了。以下是示例代码。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
@ -1005,9 +1023,30 @@ comments: true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! tip
|
## 两种实现对比
|
||||||
|
|
||||||
某些语言并未专门提供栈类,但我们可以直接把该语言的「数组」或「链表」看作栈来使用,并通过“脑补”来屏蔽无关操作,而无需像上述代码去特意包装一层。
|
### 支持操作
|
||||||
|
|
||||||
|
两种实现都支持栈定义中的各项操作,数组实现额外支持随机访问,但这已经超出栈的定义范畴,一般不会用到。
|
||||||
|
|
||||||
|
### 时间效率
|
||||||
|
|
||||||
|
在数组(列表)实现中,入栈与出栈操作都是在预先分配好的连续内存中操作,具有很好的缓存本地性,效率很好。然而,如果入栈时超出数组容量,则会触发扩容机制,那么该次入栈操作的时间复杂度为 $O(n)$ 。
|
||||||
|
|
||||||
|
在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时变慢的问题。然而,入栈操作需要初始化结点对象并修改指针,因而效率不如数组。进一步地思考,如果入栈元素不是 `int` 而是结点对象,那么就可以省去初始化步骤,从而提升效率。
|
||||||
|
|
||||||
|
综上所述,当入栈与出栈操作的元素是基本数据类型(例如 `int` , `double` )时,则结论如下:
|
||||||
|
|
||||||
|
- 数组实现的栈在触发扩容时会变慢,但由于扩容是低频操作,因此 **总体效率更高**;
|
||||||
|
- 链表实现的栈可以提供 **更加稳定的效率表现**;
|
||||||
|
|
||||||
|
### 空间效率
|
||||||
|
|
||||||
|
在初始化列表时,系统会给列表分配“初始容量”,该容量可能超过我们的需求。并且扩容机制一般是按照特定倍率(比如 2 倍)进行扩容,扩容后的容量也可能超出我们的需求。因此,**数组实现栈会造成一定的空间浪费**。
|
||||||
|
|
||||||
|
当然,由于结点需要额外存储指针,因此 **链表结点比数组元素占用更大**。
|
||||||
|
|
||||||
|
综上,我们不能简单地确定哪种实现更加省内存,需要 case-by-case 地分析。
|
||||||
|
|
||||||
## 栈典型应用
|
## 栈典型应用
|
||||||
|
|
||||||
|
|
|
@ -5,5 +5,7 @@ comments: true
|
||||||
# 小结
|
# 小结
|
||||||
|
|
||||||
- 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。
|
- 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。
|
||||||
- 队列是一种遵循先入先出的数据结构,可以使用数组或链表实现。
|
- 在时间效率方面,栈的数组实现具有更好的平均效率,但扩容时会导致单次入栈操作的时间复杂度劣化至 $O(n)$ 。相对地,栈的链表实现具有更加稳定的效率表现。
|
||||||
|
- 在空间效率方面,栈的数组实现会造成一定空间浪费,然而链表结点比数组元素占用内存更大。
|
||||||
|
- 队列是一种遵循先入先出的数据结构,可以使用数组或链表实现。对于两种实现的时间效率与空间效率对比,与上述栈的结论相同。
|
||||||
- 双向队列的两端都可以添加与删除元素。
|
- 双向队列的两端都可以添加与删除元素。
|
||||||
|
|