AIGC标识 队列(Queue)

队列(Queue)

一、队列的核心概念

队列是一种先进先出(FIFO, First In First Out)的线性数据结构。就像排队买票——先排队的人先买到票,后到的人排在队尾。

队列有两端:

  • 队尾(Rear/Tail):入队操作(Enqueue)发生的一端
  • 队头(Front/Head):出队操作(Dequeue)发生的一端

栈 vs 队列

维度 队列
出入规则 LIFO 后进先出 FIFO 先进先出
开放端 一端(栈顶) 两端(队头出,队尾入)
典型应用 函数调用、DFS、Undo 任务调度、BFS、消息队列

队列的典型应用场景

  1. BFS 广度优先搜索:逐层访问图/树的节点
  2. 任务调度:操作系统进程调度、线程池任务队列
  3. 消息队列:生产者-消费者模式中的缓冲区
  4. 打印队列:先提交的文档先打印
  5. 限流:滑动窗口限流算法

二、普通队列的问题:假溢出

如果用数组实现队列,维护 frontrear 两个下标指针:

初始: [_ _ _ _ _]  front=0, rear=0
入队 ABCD: [A B C D _]  front=0, rear=4
出队 AB: [_ _ C D _]  front=2, rear=4
入队 E: [_ _ C D E]  front=2, rear=5
入队 F: ???  rear=5 已经越界,但前面还有空闲空间!

这就是假溢出问题:数组前面有空间,但 rear 指针已到达末尾,无法继续入队。

解决方案

方案 思路 缺点
元素搬迁 每次出队后把所有元素前移 O(n) 出队,效率差
循环队列 让 rear 绕回数组开头 需处理空满判断

循环队列是最优解,下面重点讲解。

三、循环队列

3.1 核心思想

将数组视为一个首尾相连的环形结构。当 rearfront 到达数组末尾时,通过取模运算绕回开头:

rear = (rear + 1) % capacity
front = (front + 1) % capacity
容量为 5 的循环队列:

    [0] -> [1] -> [2]
     ^                 |
     |                 v
    [4] <- [3] <-

front 和 rear 在环上循环移动

3.2 空满判断难题

循环队列面临一个棘手问题:front == rear 既可以表示队空,也可以表示队满。两种解决方案:

方案一:牺牲一个槽位(推荐)

  • 队空:front == rear
  • 队满:(rear + 1) % capacity == front
  • 实际容量 = capacity - 1

方案二:维护 size 计数器

  • 队空:size == 0
  • 队满:size == capacity
  • 实际容量 = capacity,但每次操作需维护 size

3.3 完整实现

package main

import "fmt"

// CircularQueue 循环队列(牺牲一个槽位方案)
type CircularQueue struct {
    data     []int
    front    int // 队头指针
    rear     int // 队尾指针(指向下一个可写入位置)
    capacity int // 数组总容量
}

// NewCircularQueue 创建容量为 capacity 的循环队列
// 实际可存储 capacity-1 个元素
func NewCircularQueue(capacity int) *CircularQueue {
    if capacity < 2 {
        panic("capacity must be at least 2")
    }
    return &CircularQueue{
        data:     make([]int, capacity),
        front:    0,
        rear:     0,
        capacity: capacity,
    }
}

// Enqueue 入队 O(1)
func (q *CircularQueue) Enqueue(val int) bool {
    // 判满:(rear+1) % capacity == front
    if (q.rear+1)%q.capacity == q.front {
        return false // 队列已满
    }
    q.data[q.rear] = val
    q.rear = (q.rear + 1) % q.capacity
    return true
}

// Dequeue 出队 O(1)
func (q *CircularQueue) Dequeue() (int, bool) {
    // 判空:front == rear
    if q.front == q.rear {
        return 0, false // 队列为空
    }
    val := q.data[q.front]
    q.front = (q.front + 1) % q.capacity
    return val, true
}

// Front 查看队头元素 O(1)
func (q *CircularQueue) Front() (int, bool) {
    if q.front == q.rear {
        return 0, false
    }
    return q.data[q.front], true
}

// IsEmpty 判空
func (q *CircularQueue) IsEmpty() bool {
    return q.front == q.rear
}

// IsFull 判满
func (q *CircularQueue) IsFull() bool {
    return (q.rear+1)%q.capacity == q.front
}

// Size 返回当前元素个数 O(1)
func (q *CircularQueue) Size() int {
    return (q.rear - q.front + q.capacity) % q.capacity
}

// AllElements 返回队列中所有元素(从队头到队尾)
func (q *CircularQueue) AllElements() []int {
    result := make([]int, 0, q.Size())
    i := q.front
    for i != q.rear {
        result = append(result, q.data[i])
        i = (i + 1) % q.capacity
    }
    return result
}

func main() {
    // 容量 5,实际可存 4 个元素
    q := NewCircularQueue(5)

    // 入队 1,2,3,4
    q.Enqueue(1)
    q.Enqueue(2)
    q.Enqueue(3)
    q.Enqueue(4)
    fmt.Println("入队4个:", q.AllElements()) // [1 2 3 4]
    fmt.Println("队满?", q.IsFull())           // true

    // 第5个入队失败
    ok := q.Enqueue(5)
    fmt.Println("第5个入队成功?", ok) // false

    // 出队 2 个
    v, _ := q.Dequeue()
    fmt.Println("出队:", v) // 1
    v, _ = q.Dequeue()
    fmt.Println("出队:", v) // 2
    fmt.Println("剩余:", q.AllElements()) // [3 4]

    // 再入队 5,6(复用前面释放的空间,解决假溢出)
    q.Enqueue(5)
    q.Enqueue(6)
    fmt.Println("入队5,6后:", q.AllElements()) // [3 4 5 6]
    fmt.Println("队满?", q.IsFull())              // true

    // 全部出队
    fmt.Print("全部出队: ")
    for !q.IsEmpty() {
        v, _ := q.Dequeue()
        fmt.Print(v, " ") // 3 4 5 6
    }
    fmt.Println()
    fmt.Println("队空?", q.IsEmpty()) // true
}

运行结果

入队4个: [1 2 3 4]
队满? true
第5个入队成功? false
出队: 1
出队: 2
剩余: [3 4]
入队5,6后: [3 4 5 6]
队满? true
全部出队: 3 4 5 6 
队空? true

四、链式队列

链式队列用链表实现,不存在假溢出问题,也不需要预分配固定容量。

package main

import "fmt"

// queueNode 链式队列节点
type queueNode struct {
    data int
    next *queueNode
}

// LinkedQueue 链式队列
type LinkedQueue struct {
    front *queueNode // 队头指针(出队端)
    rear  *queueNode // 队尾指针(入队端)
    len   int
}

func NewLinkedQueue() *LinkedQueue {
    return &LinkedQueue{}
}

// Enqueue 入队 O(1) — 尾插法
func (q *LinkedQueue) Enqueue(val int) {
    newNode := &queueNode{data: val}
    if q.rear == nil {
        // 空队列
        q.front = newNode
        q.rear = newNode
    } else {
        q.rear.next = newNode
        q.rear = newNode
    }
    q.len++
}

// Dequeue 出队 O(1) — 头删法
func (q *LinkedQueue) Dequeue() (int, bool) {
    if q.front == nil {
        return 0, false
    }
    val := q.front.data
    q.front = q.front.next
    if q.front == nil {
        // 队列已空,重置 rear
        q.rear = nil
    }
    q.len--
    return val, true
}

func (q *LinkedQueue) IsEmpty() bool {
    return q.front == nil
}

func (q *LinkedQueue) Size() int {
    return q.len
}

func (q *LinkedQueue) AllElements() []int {
    result := make([]int, 0, q.len)
    cur := q.front
    for cur != nil {
        result = append(result, cur.data)
        cur = cur.next
    }
    return result
}

func main() {
    q := NewLinkedQueue()

    q.Enqueue(10)
    q.Enqueue(20)
    q.Enqueue(30)
    fmt.Println("入队3个:", q.AllElements()) // [10 20 30]

    v, _ := q.Dequeue()
    fmt.Println("出队:", v) // 10
    fmt.Println("剩余:", q.AllElements()) // [20 30]

    // 不受容量限制,继续入队
    for i := 0; i < 10; i++ {
        q.Enqueue(i * 100)
    }
    fmt.Println("继续入队10个:", q.AllElements())
    fmt.Println("队列大小:", q.Size())
}

运行结果

入队3个: [10 20 30]
出队: 10
剩余: [20 30]
继续入队10个: [20 30 0 100 200 300 400 500 600 700 800 900]
队列大小: 12

五、两种实现对比

维度 循环队列(数组) 链式队列(链表)
Enqueue O(1) O(1)
Dequeue O(1) O(1)
容量限制 固定(预分配) 无限制(按需分配)
内存连续性 好(缓存友好) 差(指针跳转)
假溢出 已解决(取模绕回) 不存在此问题
适用场景 容量已知的场景 容量不确定的场景

六、双端队列(Deque)简介

双端队列(Double-Ended Queue)是队列的扩展版本,两端都可以进行入队和出队操作。它同时具备栈和队列的能力:

  • 头部入队/出队 → 栈的 Push/Pop
  • 尾部入队/出队 → 队列的 Enqueue/Dequeue

Go 标准库 container/list 可以直接用作双端队列的实现。

七、循环队列的关键公式速查

判空:front == rear
判满:(rear + 1) % capacity == front
元素个数:(rear - front + capacity) % capacity
入队后 rear 移动:rear = (rear + 1) % capacity
出队后 front 移动:front = (front + 1) % capacity

理解要点:取模运算是循环队列的灵魂。它让线性数组在逻辑上变成环形,指针到达末尾后自动绕回开头。所有 O(1) 操作都建立在取模绕回这一机制上。

八、Go 标准库的选择

在实际工程中,如果不需要固定容量,Go 的切片就能很好地充当队列:

// 简易切片队列
queue := []int{}
queue = append(queue, 1)    // Enqueue
val := queue[0]             // Front
queue = queue[1:]           // Dequeue(注意:频繁操作会导致内存泄漏)

queue = queue[1:] 会导致底层数组前端的空间无法被 GC 回收。更安全的做法是用环形队列或链式队列,或者使用 copy 手动搬移。

生产环境推荐container/list 或成熟的第三方队列库。

九、总结

操作 循环队列 链式队列
Enqueue O(1) O(1)
Dequeue O(1) O(1)
Front O(1) O(1)
容量 固定 动态
假溢出 取模绕回解决 不存在

队列的核心是 FIFO——先进先出。理解循环队列的关键在于掌握取模运算如何让线性数组在逻辑上变成环形,以及"牺牲一个槽位"区分空满状态的技巧。链式队列则更直观,通过头尾指针实现 O(1) 的入队出队,且不受容量限制。

posted @ 2026-07-27 09:04  FfHUCisI  阅读(7)  评论(0)    收藏  举报