Skip to content

第三章 栈、队列、双端队列与递归


1. 受限线性结构的思想

数组和链表都允许比较自由地访问和修改元素,而栈与队列进一步对操作位置加了限制。表面上看,这是“功能受限”;实际上,这种受限恰恰对应了一类非常稳定的问题模式。

  1. 栈描述“最后进入,最先处理”。
  2. 队列描述“先进入,先处理”。
  3. 双端队列描述“两端都可进入与移出”。

很多系统行为,例如函数调用、任务调度、请求排队、广度优先搜索,都天然符合这种访问规则。因此,受限线性结构是对问题规律的刻意匹配,而不是能力缩水。

2. 栈:后进先出

2.1 结构语义

栈(Stack)只允许在一端插入和删除,该端称为栈顶,遵循 LIFO, Last In First Out。

2.2 典型应用

  1. 函数调用栈。
  2. 括号匹配。
  3. 表达式求值。
  4. 深度优先搜索。
  5. 撤销操作和回溯。

2.3 Go 实现一个整数栈

go
package stack

import "fmt"

type Stack struct {
    data []int
}

func (s *Stack) Push(v int) {
    s.data = append(s.data, v)
}

func (s *Stack) Pop() (int, error) {
    if len(s.data) == 0 {
        return 0, fmt.Errorf("stack is empty")
    }
    idx := len(s.data) - 1
    v := s.data[idx]
    s.data = s.data[:idx]
    return v, nil
}

func (s *Stack) Peek() (int, error) {
    if len(s.data) == 0 {
        return 0, fmt.Errorf("stack is empty")
    }
    return s.data[len(s.data)-1], nil
}

func (s *Stack) Len() int {
    return len(s.data)
}

这个实现的关键在于:只在切片尾部操作,因此 PushPop 通常都是均摊 O(1)。如果你反过来把栈顶设计在切片头部,每次弹出都要搬移元素,结构语义虽然没错,但实现就退化了。

3. 用栈解决括号匹配

4. 队列:先进先出

4.1 结构语义

4.2 队列的典型应用

4.3 为什么不能直接用切片头删做高性能队列

5. Go 实现循环队列

6. 双端队列:允许两端操作

7. 递归和栈的关系

7.1 一个简单的递归例子

8. BFS 为什么天然依赖队列

9. 栈与队列的比较

10. 本章小结