【数据结构与算法】队列详解

190 阅读5分钟

上一次讲了 这一数据结构和它适合解决的问题,今天我们讲讲它的兄弟:队列

🚀 队列(Queue):先进先出的魔法窗口

在数据结构的世界中,队列(Queue) 是另一种基础但强大的工具。它遵循 「先进先出」(FIFO, First In First Out) 的原则,就像现实中的排队场景:先到的人先被服务,后到的人依次排在队尾。队列在操作系统任务调度、缓冲区管理、广度优先搜索(BFS)等场景中广泛应用。


🧱 队列(Queue)的基本概念

队列是一种运算受限的线性表,仅允许在表的一端(队尾)插入元素,在另一端(队头)删除元素。这种限制使得队列天然适合处理 顺序敏感 的问题。

🛠 操作

  • push:将元素添加到队尾。
  • shift:从队头移除并返回元素。
  • peek:查看队头元素,但不移除。
  • isEmpty:检查队列是否为空。

队列的核心特性先进先出(FIFO),就像银行排队叫号系统。

🖼 图解

队列图解

好了,你已经知道栈这一数据结构的用法和规则,现在和我一起做点题目试试吧


🧩 LeetCode 239:滑动窗口最大值

📝 问题描述

给定一个整数数组 nums 和一个滑动窗口的大小 k,请找出每个窗口中最大的值。例如:

输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]

❗ 暴力法思路

对于每个窗口都遍历窗口内的所有元素,找出最大值。

时间复杂度为 O(nk),在 k 较大时性能较差。

function maxSlidingWindow(nums, k) {
    const n = nums.length;
    const result = [];
    for (let i = 0; i <= n - k; i++) {
        let max = -Infinity;
        for (let j = i; j < i + k; j++) {
            max = Math.max(max, nums[j]);
        }
        result.push(max);
    }
    return result;
}

🚀 优化思路:单调队列

单调队列 是一种特殊的数据结构,通过维护队列中元素的单调性(递减或递增),快速找到滑动窗口的最大值或最小值。

🧠 核心思想
  1. 使用双端队列(Deque)存储 可能成为最大值的元素索引
  2. 队列中的元素按 递减顺序 排列(从队头到队尾)。
  3. 遍历数组时,维护队列的单调性:
    • 如果当前元素大于队尾元素,则移除队尾元素,直到队列为空或找到更大的元素。
    • 将当前元素索引入队。
  4. 确保队头元素始终是当前窗口的最大值。

双端队列是指可以双端都能进出,不局限于尾进头出

💡 JavaScript 实现
var maxSlidingWindow = function (nums, k) {
  const len = nums.length
  const result = []

  //维护一个单调递减队列做到实时监控最大值
  const deque = [] // 双端队列,宽度等于窗口大小
  if (len === 0 || k === 0) return []
  if (k === 1) return nums

  for (let i = 0; i < len; i++) {
    // 队头的元素被排除在窗口之外时,移除原本队头元素
    // 即当队头的索引小于当前索引减去窗口大小 k 时,移除队头元素
    if (deque.length && deque[0] < i - k + 1) {
      deque.shift()
    }

    // 不断地移除小于当前元素的所有元素
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop()
    }

    // 添加当前元素的索引
    deque.push(i)

    // 当窗口大小达到 k 时,添加最大值到结果中
    if (i >= k - 1) {
      result.push(nums[deque[0]])
    }
  }

  return result
}

// 示例测试
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3)); 
// 输出: [3,3,5,5,6,7]
🧾 时间复杂度分析
  • 时间复杂度:O(n)
    每个元素最多入队和出队一次。
  • 空间复杂度:O(n)
    双端队列最多存储 n 个元素。

🧠 为什么选择单调队列?

🔍 单调队列的适用场景

  1. 滑动窗口问题:如寻找窗口中的最大值、最小值。
  2. 动态维护极值:当需要频繁查询当前数据流中的极值时。
  3. 避免重复比较:通过维护队列的单调性,减少冗余操作。

📌 单调队列的特性

  • 单调性:队列中的元素按递增或递减顺序排列。
  • 高效性:每次操作的时间复杂度接近 O(1)。
  • 灵活性:可根据问题需求调整维护的单调性方向。

🎯 队列的适用范围

队列在以下场景中表现尤为出色:

  1. 滑动窗口问题:如本例中的滑动窗口最大值。
  2. 广度优先搜索(BFS):用于图或树的层序遍历。
  3. 任务调度:如操作系统的进程调度、打印机任务队列。
  4. 缓冲区管理:如网络数据包的缓存和传输。
  5. 模拟现实中的排队场景:如银行叫号系统、餐厅点餐队列。

📚 课后练习

  1. LeetCode 649. 神奇字符串
    • 通过队列模拟字符生成规则。
  2. LeetCode 933. 最近的请求次数
    • 使用队列记录请求时间,计算最近 3000 毫秒内的请求数。
  3. LeetCode 1670. 设计前缀和搜索
    • 结合队列和前缀和技巧解决搜索问题。

🌟 总结

队列作为一种基础数据结构,其 先进先出 的特性使其在处理顺序敏感问题时表现出色。

掌握队列的应用场景和优化技巧,不仅能提升算法题的解题效率,还能为实际开发中的任务调度、缓冲区管理等问题提供解决方案。