队列(Queue)

一句话说明

队列最核心的特征只有一个:先进先出,所以它天然适合“按到达顺序处理”的问题。

先把它和栈彻底分开

  • 栈:后进先出,适合“回退 / 撤销 / 递归展开”
  • 队列:先进先出,适合“排队 / 分层 / 时间推进”

很多题目一旦出现下面这些信号词,你就该想到队列:

  • 一层一层扩散
  • 最短步数
  • 按时间顺序处理
  • 最近一段请求

队列的本质操作

  • Push:元素从队尾进入
  • Pop:元素从队头离开
  • Front:看队头但不删除
队头 <- [1, 2, 3, 4] <- 队尾

最早进来的 1,一定最先出去。

Go 代码:最直接的队列模板

如果只是做题,最常用的是切片模拟队列。

type Queue struct {
    data []int
}
 
func (q *Queue) Push(x int) {
    q.data = append(q.data, x)
}
 
func (q *Queue) Pop() int {
    x := q.data[0]
    q.data = q.data[1:]
    return x
}
 
func (q *Queue) Front() int {
    return q.data[0]
}
 
func (q *Queue) Empty() bool {
    return len(q.data) == 0
}
 
func (q *Queue) Size() int {
    return len(q.data)
}

这个写法的优点是简单。
在大多数算法题里已经够用了。

如果你在意底层效率:循环队列

切片头部不断弹出,长期运行可能造成底层空间不够紧凑。
如果题目就是让你“设计队列”,更标准的写法是循环数组。

type CircularQueue struct {
    data       []int
    head, tail int
    size       int
    capacity   int
}
 
func NewCircularQueue(k int) *CircularQueue {
    return &CircularQueue{
        data:     make([]int, k),
        capacity: k,
    }
}
 
func (q *CircularQueue) EnQueue(x int) bool {
    if q.size == q.capacity {
        return false
    }
    q.data[q.tail] = x
    q.tail = (q.tail + 1) % q.capacity
    q.size++
    return true
}
 
func (q *CircularQueue) DeQueue() bool {
    if q.size == 0 {
        return false
    }
    q.head = (q.head + 1) % q.capacity
    q.size--
    return true
}
 
func (q *CircularQueue) Front() int {
    if q.size == 0 {
        return -1
    }
    return q.data[q.head]
}
 
func (q *CircularQueue) Rear() int {
    if q.size == 0 {
        return -1
    }
    idx := (q.tail - 1 + q.capacity) % q.capacity
    return q.data[idx]
}

双端队列为什么也经常一起出现

双端队列(Deque)允许:

  • 头部进出
  • 尾部进出

它在算法题里特别常见,因为:

  • BFS 会用普通队列
  • 单调队列会用双端队列
  • 0-1 BFS 也会用双端队列

Go 代码:二叉树层序遍历

看到“层序遍历”,脑子里直接绑定队列。

func levelOrder(root *TreeNode) [][]int {
    if root == nil {
        return nil
    }
 
    result := [][]int{}
    queue := []*TreeNode{root}
 
    for len(queue) > 0 {
        size := len(queue)
        level := make([]int, 0, size)
 
        for i := 0; i < size; i++ {
            node := queue[0]
            queue = queue[1:]
            level = append(level, node.Val)
 
            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }
 
        result = append(result, level)
    }
 
    return result
}

Go 代码:最近请求次数

这个题本质上也是队列,因为“过期元素永远从最前面离开”。

type RecentCounter struct {
    q []int
}
 
func Constructor() RecentCounter {
    return RecentCounter{}
}
 
func (rc *RecentCounter) Ping(t int) int {
    rc.q = append(rc.q, t)
    for len(rc.q) > 0 && rc.q[0] < t-3000 {
        rc.q = rc.q[1:]
    }
    return len(rc.q)
}

单调队列和普通队列的区别

普通队列只维护“先来后到”。
单调队列还额外维护“值的单调性”。

所以滑动窗口最大值里,队列里通常存的是下标而不是值:

  • 队头永远是当前窗口最优答案
  • 队尾负责清理掉“不可能再成为答案”的元素

这个主题详见 单调栈与单调队列。

什么时候优先想到队列

  • BFS
  • 层序遍历
  • 最短步数
  • 时间窗口
  • 先到先处理

如果题目变成“只需要维护最大值 / 最小值优先出队”,那优先想到 堆,不是普通队列。

易错点

队列题最容易错的地方

  • BFS 里不要一边遍历队列一边直接用 len(queue) 动态变化,层数统计要先记当前层大小。
  • 循环队列判断空和满时,最好单独维护 size。
  • 单调队列里存下标通常比存值更稳,因为要判断元素是否过期。

经典题目

相关主题


返回:数据结构 | 算法学习导航