|
|
|
@ -32,27 +32,27 @@ comments: true
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
我们可以直接使用编程语言实现好的栈类。
|
|
|
|
|
我们可以直接使用编程语言实现好的栈类。 某些语言并未专门提供栈类,但我们可以直接把该语言的「数组」或「链表」看作栈来使用,并通过“脑补”来屏蔽无关操作。
|
|
|
|
|
|
|
|
|
|
=== "Java"
|
|
|
|
|
|
|
|
|
|
```java title="stack.java"
|
|
|
|
|
/* 初始化栈 */
|
|
|
|
|
// 在 Java 中,推荐将 LinkedList 当作栈来使用
|
|
|
|
|
LinkedList<Integer> stack = new LinkedList<>();
|
|
|
|
|
// 在 Java 中,推荐将 ArrayList 当作栈来使用
|
|
|
|
|
List<Integer> stack = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
/* 元素入栈 */
|
|
|
|
|
stack.addLast(1);
|
|
|
|
|
stack.addLast(3);
|
|
|
|
|
stack.addLast(2);
|
|
|
|
|
stack.addLast(5);
|
|
|
|
|
stack.addLast(4);
|
|
|
|
|
stack.add(1);
|
|
|
|
|
stack.add(3);
|
|
|
|
|
stack.add(2);
|
|
|
|
|
stack.add(5);
|
|
|
|
|
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();
|
|
|
|
@ -234,23 +234,23 @@ comments: true
|
|
|
|
|
/* 初始化栈 */
|
|
|
|
|
// Swift 没有内置的栈类,可以把 Array 当作栈来使用
|
|
|
|
|
var stack: [Int] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 元素入栈 */
|
|
|
|
|
stack.append(1)
|
|
|
|
|
stack.append(3)
|
|
|
|
|
stack.append(2)
|
|
|
|
|
stack.append(5)
|
|
|
|
|
stack.append(4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 访问栈顶元素 */
|
|
|
|
|
let peek = stack.last!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 元素出栈 */
|
|
|
|
|
let pop = stack.removeLast()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 获取栈的长度 */
|
|
|
|
|
let size = stack.count
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 判断是否为空 */
|
|
|
|
|
let isEmpty = stack.isEmpty
|
|
|
|
|
```
|
|
|
|
@ -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"
|
|
|
|
|
|
|
|
|
@ -406,19 +415,19 @@ comments: true
|
|
|
|
|
// 使用内置包 list 来实现栈
|
|
|
|
|
data *list.List
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// newLinkedListStack 初始化链表
|
|
|
|
|
func newLinkedListStack() *linkedListStack {
|
|
|
|
|
return &linkedListStack{
|
|
|
|
|
data: list.New(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// push 入栈
|
|
|
|
|
func (s *linkedListStack) push(value int) {
|
|
|
|
|
s.data.PushBack(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// pop 出栈
|
|
|
|
|
func (s *linkedListStack) pop() any {
|
|
|
|
|
if s.isEmpty() {
|
|
|
|
@ -428,7 +437,7 @@ comments: true
|
|
|
|
|
s.data.Remove(e)
|
|
|
|
|
return e.Value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// peek 访问栈顶元素
|
|
|
|
|
func (s *linkedListStack) peek() any {
|
|
|
|
|
if s.isEmpty() {
|
|
|
|
@ -437,12 +446,12 @@ comments: true
|
|
|
|
|
e := s.data.Back()
|
|
|
|
|
return e.Value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// size 获取栈的长度
|
|
|
|
|
func (s *linkedListStack) size() int {
|
|
|
|
|
return s.data.Len()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// isEmpty 判断栈是否为空
|
|
|
|
|
func (s *linkedListStack) isEmpty() bool {
|
|
|
|
|
return s.data.Len() == 0
|
|
|
|
@ -634,19 +643,19 @@ comments: true
|
|
|
|
|
class LinkedListStack {
|
|
|
|
|
private var _peek: ListNode? // 将头结点作为栈顶
|
|
|
|
|
private var _size = 0 // 栈的长度
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
init() {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 获取栈的长度 */
|
|
|
|
|
func size() -> Int {
|
|
|
|
|
_size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 判断栈是否为空 */
|
|
|
|
|
func isEmpty() -> Bool {
|
|
|
|
|
size() == 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 入栈 */
|
|
|
|
|
func push(num: Int) {
|
|
|
|
|
let node = ListNode(x: num)
|
|
|
|
@ -654,7 +663,7 @@ comments: true
|
|
|
|
|
_peek = node
|
|
|
|
|
_size += 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 出栈 */
|
|
|
|
|
@discardableResult
|
|
|
|
|
func pop() -> Int {
|
|
|
|
@ -663,7 +672,7 @@ comments: true
|
|
|
|
|
_size -= 1
|
|
|
|
|
return num
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 访问栈顶元素 */
|
|
|
|
|
func peek() -> Int {
|
|
|
|
|
if isEmpty() {
|
|
|
|
@ -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"
|
|
|
|
|
|
|
|
|
@ -790,30 +808,30 @@ comments: true
|
|
|
|
|
type arrayStack struct {
|
|
|
|
|
data []int // 数据
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func newArrayStack() *arrayStack {
|
|
|
|
|
return &arrayStack{
|
|
|
|
|
// 设置栈的长度为 0,容量为 16
|
|
|
|
|
data: make([]int, 0, 16),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// size 栈的长度
|
|
|
|
|
func (s *arrayStack) size() int {
|
|
|
|
|
return len(s.data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// isEmpty 栈是否为空
|
|
|
|
|
func (s *arrayStack) isEmpty() bool {
|
|
|
|
|
return s.size() == 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// push 入栈
|
|
|
|
|
func (s *arrayStack) push(v int) {
|
|
|
|
|
// 切片会自动扩容
|
|
|
|
|
s.data = append(s.data, v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// pop 出栈
|
|
|
|
|
func (s *arrayStack) pop() any {
|
|
|
|
|
// 弹出栈前,先判断是否为空
|
|
|
|
@ -824,7 +842,7 @@ comments: true
|
|
|
|
|
s.data = s.data[:len(s.data)-1]
|
|
|
|
|
return val
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// peek 获取栈顶元素
|
|
|
|
|
func (s *arrayStack) peek() any {
|
|
|
|
|
if s.isEmpty() {
|
|
|
|
@ -965,27 +983,27 @@ comments: true
|
|
|
|
|
/* 基于数组实现的栈 */
|
|
|
|
|
class ArrayStack {
|
|
|
|
|
private var stack: [Int]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
// 初始化列表(动态数组)
|
|
|
|
|
stack = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 获取栈的长度 */
|
|
|
|
|
func size() -> Int {
|
|
|
|
|
stack.count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 判断栈是否为空 */
|
|
|
|
|
func isEmpty() -> Bool {
|
|
|
|
|
stack.isEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 入栈 */
|
|
|
|
|
func push(num: Int) {
|
|
|
|
|
stack.append(num)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 出栈 */
|
|
|
|
|
@discardableResult
|
|
|
|
|
func pop() -> Int {
|
|
|
|
@ -994,7 +1012,7 @@ comments: true
|
|
|
|
|
}
|
|
|
|
|
return stack.removeLast()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 访问栈顶元素 */
|
|
|
|
|
func peek() -> Int {
|
|
|
|
|
if isEmpty() {
|
|
|
|
@ -1005,9 +1023,30 @@ comments: true
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
!!! tip
|
|
|
|
|
## 两种实现对比
|
|
|
|
|
|
|
|
|
|
### 支持操作
|
|
|
|
|
|
|
|
|
|
两种实现都支持栈定义中的各项操作,数组实现额外支持随机访问,但这已经超出栈的定义范畴,一般不会用到。
|
|
|
|
|
|
|
|
|
|
### 时间效率
|
|
|
|
|
|
|
|
|
|
在数组(列表)实现中,入栈与出栈操作都是在预先分配好的连续内存中操作,具有很好的缓存本地性,效率很好。然而,如果入栈时超出数组容量,则会触发扩容机制,那么该次入栈操作的时间复杂度为 $O(n)$ 。
|
|
|
|
|
|
|
|
|
|
在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时变慢的问题。然而,入栈操作需要初始化结点对象并修改指针,因而效率不如数组。进一步地思考,如果入栈元素不是 `int` 而是结点对象,那么就可以省去初始化步骤,从而提升效率。
|
|
|
|
|
|
|
|
|
|
综上所述,当入栈与出栈操作的元素是基本数据类型(例如 `int` , `double` )时,则结论如下:
|
|
|
|
|
|
|
|
|
|
- 数组实现的栈在触发扩容时会变慢,但由于扩容是低频操作,因此 **总体效率更高**;
|
|
|
|
|
- 链表实现的栈可以提供 **更加稳定的效率表现**;
|
|
|
|
|
|
|
|
|
|
### 空间效率
|
|
|
|
|
|
|
|
|
|
在初始化列表时,系统会给列表分配“初始容量”,该容量可能超过我们的需求。并且扩容机制一般是按照特定倍率(比如 2 倍)进行扩容,扩容后的容量也可能超出我们的需求。因此,**数组实现栈会造成一定的空间浪费**。
|
|
|
|
|
|
|
|
|
|
当然,由于结点需要额外存储指针,因此 **链表结点比数组元素占用更大**。
|
|
|
|
|
|
|
|
|
|
某些语言并未专门提供栈类,但我们可以直接把该语言的「数组」或「链表」看作栈来使用,并通过“脑补”来屏蔽无关操作,而无需像上述代码去特意包装一层。
|
|
|
|
|
综上,我们不能简单地确定哪种实现更加省内存,需要 case-by-case 地分析。
|
|
|
|
|
|
|
|
|
|
## 栈典型应用
|
|
|
|
|
|
|
|
|
|