队列、双端队列、双栈队列与滑动窗口最大值

队列、双端队列、双栈队列与滑动窗口最大值

一、队列到底解决什么问题?

  • o **队列(Queue)是一种先进先出(FIFO)**的线性结构:
    • o 入队(enqueue):从尾部插入
    • o 出队(dequeue):从头部删除
  • o 典型应用:
    • o 任务调度(按到达时间顺序处理)、宽度优先搜索(BFS)缓存/异步消息流式窗口统计(配合单调队列)、生产者-消费者模型等。

二、三种基础实现(数组 / 链表 / 循环数组)

2.1 数组(顺序表)实现:直观但会“搬家”

  • o 思路:用数组存数据,用两个指针 headtail 指示队首/队尾。
  • o 问题:频繁 dequeue 会导致前半段空间“空了但不能复用”,常见做法是懒移动定期挪动
  • o 适用:规模可预估、或愿意做循环优化
# 语言:Python — 简易数组队列(未循环化)
class ArrayQueue:
    def __init__(self, capacity=8):
        self.data = [None]*capacity
        self.head = 0           # 指向当前队首元素
        self.tail = 0           # 指向将要插入元素的位置
        self.n = 0

    def is_empty(self):
        return self.n == 0

    def is_full(self):
        return self.n == len(self.data)

    def enqueue(self, x):
        if self.is_full():
            self._resize(len(self.data)*2)
        self.data[self.tail] = x
        self.tail += 1
        self.n += 1

    def dequeue(self):
        if self.is_empty():
            raise IndexError("dequeue from empty")
        x = self.data[self.head]
        self.data[self.head] = None
        self.head += 1
        self.n -= 1
        # 可选:当浪费空间过多,触发一次“挪动”或“缩容”
        if self.n and self.head > len(self.data)//4 and self.head > self.tail//2:
            self._compact()
        return x

    def _compact(self):
        # 把有效区间整体搬到数组前部
        new = [None]*len(self.data)
        for i in range(self.n):
            new[i] = self.data[self.head + i]
        self.data = new
        self.head, self.tail = 0, self.n

    def _resize(self, cap):
        new = [None]*cap
        for i in range(self.n):
            new[i] = self.data[self.head + i]
        self.data = new
        self.head, self.tail = 0, self.n
// 语言:Swift — 简易数组队列(未循环化)
struct ArrayQueue<T> {
    private var data: [T?]
    private var head = 0
    private var tail = 0
    private(set) var count = 0

    init(_ cap: Int = 8) { data = Array(repeating: nil, count: cap) }

    var isEmpty: Bool { count == 0 }
    var isFull: Bool { count == data.count }

    mutating func enqueue(_ x: T) {
        if isFull { resize(data.count * 2) }
        data[tail] = x
        tail += 1
        count += 1
    }

    mutating func dequeue() -> T {
        precondition(!isEmpty, "dequeue from empty")
        let v = data[head]!
        data[head] = nil
        head += 1
        count -= 1
        if count > 0 && head > data.count/4 && head > tail/2 { compact() }
        return v
    }

    private mutating func compact() {
        var newData = Array(repeating: Optional<T>.none, count: data.count)
        for i in 0..<count { newData[i] = data[head + i] }
        data = newData; head = 0; tail = count
    }

    private mutating func resize(_ cap: Int) {
        var newData = Array(repeating: Optional<T>.none, count: cap)
        for i in 0..<count { newData[i] = data[head + i] }
        data = newData; head = 0; tail = count
    }
}

2.2 链表(链式)实现:永远O(1)入队/出队

  • o 思路:维护 headtail 两个指针
  • o 优点:不需要搬移元素;enqueue/dequeue 都是 O(1)
  • o 缺点:额外指针开销,不具备数组的局部性。
# 语言:Python — 链式队列(单链表,tail 指向尾节点)
class Node:
    def __init__(self, val, nxt=None):
        self.val, self.next = val, nxt

class LinkedQueue:
    def __init__(self):
        self.head = self.tail = None
        self.n = 0

    def is_empty(self):
        return self.n == 0

    def enqueue(self, x):
        node = Node(x)
        if self.tail is None:        # 空队列
            self.head = self.tail = node
        else:
            self.tail.next = node
            self.tail = node
        self.n += 1

    def dequeue(self):
        if self.is_empty():
            raise IndexError("dequeue from empty")
        x = self.head.val
        self.head = self.head.next
        if self.head is None:        # 出队后队空,tail 也要清空
            self.tail = None
        self.n -= 1
        return x
// 语言:Swift — 链式队列
final class QNode<T> { var val: T; var next: QNode<T>?; init(_ v: T, _ n: QNode<T>? = nil){ val=v; next=n } }
final class LinkedQueue<T> {
    private var head: QNode<T>? = nil
    private var tail: QNode<T>? = nil
    private(set) var count = 0
    var isEmpty: Bool { count == 0 }

    func enqueue(_ x: T) {
        let node = QNode(x)
        if tail == nil { head = node; tail = node }
        else { tail!.next = node; tail = node }
        count += 1
    }
    func dequeue() -> T {
        precondition(!isEmpty, "dequeue from empty")
        let v = head!.val
        head = head!.next
        if head == nil { tail = nil }
        count -= 1
        return v
    }
}

2.3 循环队列(Circular Queue):不挪动也不浪费

  • o 核心:把数组看成一个,下标通过 % capacity 回绕。
  • o 两种常见定义:
  • 1. 空一格:以 (tail + 1) % cap == head 作为“满”的判定,空间利用率 cap-1
  • 2. 记录 size:用 size 区分空/满,空间利用率 cap,实现更直观。
  • o 这里用记录 size版本。
# 语言:Python — 循环队列(记录 size)
class CircularQueue:
    def __init__(self, capacity=8):
        self.data = [None]*capacity
        self.cap = capacity
        self.head = 0     # 指向当前队首
        self.tail = 0     # 指向下一个写入位置
        self.size = 0

    def is_empty(self): return self.size == 0
    def is_full(self):  return self.size == self.cap

    def enqueue(self, x):
        if self.is_full(): self._resize(self.cap * 2)
        self.data[self.tail] = x
        self.tail = (self.tail + 1) % self.cap
        self.size += 1

    def dequeue(self):
        if self.is_empty(): raise IndexError("dequeue from empty")
        x = self.data[self.head]
        self.data[self.head] = None
        self.head = (self.head + 1) % self.cap
        self.size -= 1
        if 0 < self.size <= self.cap // 4: self._resize(max(8, self.cap // 2))
        return x

    def _resize(self, new_cap):
        new = [None]*new_cap
        for i in range(self.size):
            new[i] = self.data[(self.head + i) % self.cap]
        self.data = new
        self.cap = new_cap
        self.head = 0
        self.tail = self.size
// 语言:Swift — 循环队列(记录 size)
struct CircularQueue<T> {
    private var data: [T?]
    private var head = 0, tail = 0, size = 0
    private var cap: Int { data.count }
    init(_ capacity: Int = 8) { data = Array(repeating: nil, count: max(8, capacity)) }

    var isEmpty: Bool { size == 0 }
    var isFull:  Bool { size == cap }

    mutating func enqueue(_ x: T) {
        if isFull { resize(cap * 2) }
        data[tail] = x
        tail = (tail + 1) % cap
        size += 1
    }
    mutating func dequeue() -> T {
        precondition(!isEmpty, "dequeue from empty")
        let v = data[head]!
        data[head] = nil
        head = (head + 1) % cap
        size -= 1
        if size > 0 && size <= cap/4 { resize(max(8, cap/2)) }
        return v
    }
    private mutating func resize(_ newCap: Int) {
        var newData = Array(repeating: Optional<T>.none, count: newCap)
        for i in 0..<size { newData[i] = data[(head + i) % cap] }
        data = newData; head = 0; tail = size
    }
}

三、双端队列(Deque):两头都能进出的“队列加强版”

  • o 操作:push_front / push_back / pop_front / pop_back
  • o 实现:数组双指针或双向链表。
  • o 应用:滑动窗口、LRU 的双向链表骨架、回文检验两端收缩等。
# 语言:Python — 双端队列(简化数组环实现)
class Deque:
    def __init__(self, capacity=8):
        self.data = [None]*capacity
        self.cap = capacity
        self.front = 0  # 指向当前队首
        self.back = 0   # 指向下一个写入的“队尾位置”
        self.size = 0

    def _idx(self, i): return i % self.cap
    def is_empty(self): return self.size == 0
    def _grow(self):
        new = [None]*(self.cap*2)
        for i in range(self.size):
            new[i] = self.data[(self.front + i) % self.cap]
        self.data = new; self.cap *= 2; self.front = 0; self.back = self.size

    def push_back(self, x):
        if self.size == self.cap: self._grow()
        self.data[self.back] = x
        self.back = self._idx(self.back + 1)
        self.size += 1

    def push_front(self, x):
        if self.size == self.cap: self._grow()
        self.front = self._idx(self.front - 1)
        self.data[self.front] = x
        self.size += 1

    def pop_front(self):
        if self.is_empty(): raise IndexError("pop_front from empty")
        x = self.data[self.front]; self.data[self.front] = None
        self.front = self._idx(self.front + 1); self.size -= 1; return x

    def pop_back(self):
        if self.is_empty(): raise IndexError("pop_back from empty")
        self.back = self._idx(self.back - 1)
        x = self.data[self.back]; self.data[self.back] = None
        self.size -= 1; return x
// 语言:Swift — 双端队列(双向链表骨架)
final class DNode<T> { var val: T; var prev: DNode<T>?; var next: DNode<T>?; init(_ v: T){ val=v } }
final class Deque<T> {
    private var head: DNode<T>? = nil  // 头部(队首)
    private var tail: DNode<T>? = nil  // 尾部(队尾)
    private(set) var count = 0
    var isEmpty: Bool { count == 0 }

    func pushFront(_ x: T) {
        let n = DNode(x)
        if head == nil { head = n; tail = n }
        else { n.next = head; head!.prev = n; head = n }
        count += 1
    }
    func pushBack(_ x: T) {
        let n = DNode(x)
        if tail == nil { head = n; tail = n }
        else { tail!.next = n; n.prev = tail; tail = n }
        count += 1
    }
    func popFront() -> T {
        precondition(!isEmpty, "popFront from empty")
        let v = head!.val
        head = head!.next
        head?.prev = nil
        if head == nil { tail = nil }
        count -= 1; return v
    }
    func popBack() -> T {
        precondition(!isEmpty, "popBack from empty")
        let v = tail!.val
        tail = tail!.prev
        tail?.next = nil
        if tail == nil { head = nil }
        count -= 1; return v
    }
}

四、双栈实现队列:用“栈”把“队列”装出来

  • o 维护两个栈:sIn(仅入)和 sOut(仅出)
  • o 出队时:若 sOut 为空,把 sIn 逐个弹出并压入 sOut(翻转顺序),随后从 sOut 弹出即是队首。
  • o 摊还复杂度:每个元素最多被搬运两次,均摊 O(1)
# 语言:Python — 双栈队列
class MyQueue:
    def __init__(self):
        self._in, self._out = [], []

    def push(self, x: int):
        self._in.append(x)

    def _move(self):
        if not self._out:
            while self._in:
                self._out.append(self._in.pop())

    def pop(self) -> int:
        self._move()
        return self._out.pop()

    def peek(self) -> int:
        self._move()
        return self._out[-1]

    def empty(self) -> bool:
        return not self._in and not self._out
// 语言:Swift — 双栈队列
struct MyQueue {
    private var sIn: [Int] = []
    private var sOut: [Int] = []
    mutating func push(_ x: Int) { sIn.append(x) }
    private mutating func move() {
        if sOut.isEmpty { while let v = sIn.popLast() { sOut.append(v) } }
    }
    mutating func pop() -> Int { move(); return sOut.removeLast() }
    mutating func peek() -> Int { move(); return sOut.last! }
    var empty: Bool { sIn.isEmpty && sOut.isEmpty }
}

五、单调队列(Monotonic Queue):滑动窗口最大值的王牌

单调队列常用一个双端队列来维护候选下标,队列中的元素值单调递减,这样队首永远是当前窗口最大值的下标。

  • o 处理新元素 nums[i] 时:
  • 1. 弹出队尾:把所有小于等于 nums[i] 的下标从队尾移除(它们不可能再成为最大值);
  • 2. 加入队尾:把下标 i 放入;
  • 3. 移出窗口:若队首的下标 < i-k+1,说明已离开窗口,弹出;
  • 4. 当 i >= k-1 时,窗口形成,输出 nums[deque.front]
# 语言:Python — 滑动窗口最大值(单调队列)
from collections import deque
def max_sliding_window(nums: list[int], k: int) -> list[int]:
    dq = deque()    # 存“下标”,保持 nums[dq[0]] >= nums[dq[1]] >= ...
    ans = []
    for i, x in enumerate(nums):
        # 1) 弹出所有比 x 小的队尾元素
        while dq and nums[dq[-1]] <= x:
            dq.pop()
        # 2) 新下标入队尾
        dq.append(i)
        # 3) 队首过期则弹出
        if dq[0] <= i - k:
            dq.popleft()
        # 4) 窗口就绪则输出
        if i >= k - 1:
            ans.append(nums[dq[0]])
    return ans
// 语言:Swift — 滑动窗口最大值(单调队列)
func maxSlidingWindow(_ nums: [Int], _ k: Int) -> [Int] {
    var dq: [Int] = []  // 存索引,保持 nums[dq[0]] >= nums[dq[1]] >= ...
    var res: [Int] = []
    for i in nums.indices {
        // 1) 弹出小于等于 nums[i] 的队尾
        while let last = dq.last, nums[last] <= nums[i] { _ = dq.popLast() }
        // 2) 入队尾
        dq.append(i)
        // 3) 队首过期移除
        if dq.first! <= i - k { dq.removeFirst() }
        // 4) 窗口形成
        if i >= k - 1 { res.append(nums[dq[0]]) }
    }
    return res
}

六、复杂度与边界清单

场景时间复杂度额外空间要点与坑位数组队列(未循环)enqueue/dequeue 摊还 O(1)O(n)注意“空洞”与挪动/缩容时机链式队列O(1)O(n)出队空时同步置空 tail循环队列O(1)O(n)% capacity 回绕;判满/判空策略统一双端队列O(1)O(n)两端 push/pop 的下标回绕或链指针维护双栈队列均摊 O(1)O(n)只在 sOut 空时搬运,避免每次搬运单调队列O(n)O(k)每个元素最多入队出队一次,维护递减性


七、练习题

练习 1:设计循环队列

题意:实现
enQueue/deQueue/Front/Rear/isEmpty/isFull

思路:记录 size 的循环数组实现,见 §2.3。
正确性head 指向队首,tail 指向下一个写入位置;回绕使用 % cap
复杂度:所有操作 O(1)
代码:见 §2.3(Swift & Python 均提供)。


练习 2:用两个栈实现队列

题意:实现 push/pop/peek/empty,要求摊还 O(1)
思路sIn 入、sOut 出;sOut 空时集中倒腾。
正确性:倒腾一次即顺序翻转,保证 sOut 栈顶是队头。
复杂度:均摊 O(1)
代码:见 §4(Swift & Python)。


练习 3:滑动窗口最大值

题意:给定数组 nums 与窗口大小 k,返回每个窗口的最大值。
思路:单调队列(递减);见 §5。
正确性:队首总是当前窗口最大值下标,过期/更小者即时移除。
复杂度O(n)
代码:见 §5(Swift & Python)。


练习 4:双端队列基本操作

题意:实现
push_front/push_back/pop_front/pop_back
并通过 10^5 次随机操作压力测试。
思路:数组环或双向链表;参见 §3。
要点:索引回绕、缩扩容策略、空队边界。
代码:见 §3(Swift & Python)。


练习 5:课程表(BFS 拓扑排序)

题意numCourses 与先修关系 prerequisites,判断是否能完成所有课程。
思路:构建入度表与邻接表,队列做 BFS(入度为 0 入队)。
复杂度O(V+E)

# 语言:Python — BFS 拓扑排序
from collections import deque, defaultdict
def can_finish(numCourses: int, prerequisites: list[list[int]]) -> bool:
    g = defaultdict(list)
    indeg = [0]*numCourses
    for a,b in prerequisites:
        g[b].append(a)
        indeg[a] += 1
    q = deque([i for i in range(numCourses) if indeg[i]==0])
    taken = 0
    while q:
        u = q.popleft(); taken += 1
        for v in g[u]:
            indeg[v] -= 1
            if indeg[v]==0: q.append(v)
    return taken == numCourses
// 语言:Swift — BFS 拓扑排序
func canFinish(_ numCourses: Int, _ prerequisites: [[Int]]) -> Bool {
    var g = Array(repeating: [Int](), count: numCourses)
    var indeg = Array(repeating: 0, count: numCourses)
    for p in prerequisites { let a = p[0], b = p[1]; g[b].append(a); indeg[a] += 1 }
    var q: [Int] = []
    for i in 0..<numCourses { if indeg[i] == 0 { q.append(i) } }
    var head = 0, taken = 0
    while head < q.count {
        let u = q[head]; head += 1; taken += 1
        for v in g[u] {
            indeg[v] -= 1
            if indeg[v] == 0 { q.append(v) }
        }
    }
    return taken == numCourses
}

八、常见坑位与面试问答

  • o 循环队列判满/判空:要么空一格,要么记录 size;混用会错。
  • o 链式队列出队为最后一个元素:记得同时置空 tail
  • o 双栈队列:不要每次 pop/peek 都搬运,仅在 sOut 为空时执行搬运。
  • o 单调队列边界
    • o 新值入队前一定要弹出更小(或小于等于)的队尾
    • o 过期下标(i-k+1 左边)要及时移除;
    • o 维护的是索引而不是值,方便判过期。
  • o 扩容/缩容策略:避免频繁搬家造成抖动,设置上下触发阈值更稳。

九、知识小结

  • o 队列 = FIFO:尾入头出;实现首选循环数组链表
  • o 双端队列 = 两头都能进出,是滑动窗口LRU的好伙伴。
  • o 双栈可实现队列:均摊 O(1),面试常考。
  • o 单调队列:O(n) 解决滑动窗口最大值,每个元素最多入队/出队一次。
  • o BFS 拓扑排序:入度为 0 入队、逐层释放依赖。

原文链接:,转发请注明来源!