栈与队列:实现与经典应用

本节目标

栈(LIFO)与队列(FIFO):

// 运行环境:Node.js 14+
// 保存为 dsa-l6.js,执行:node dsa-l6.js

// 栈:数组 push/pop 即可(O(1) 均摊)
const stack = []
stack.push(1); stack.pop()

// 队列别用 shift(O(n))!用"头指针"实现 O(1) 队列:
function createQueue() {
  const q = []
  let head = 0
  return {
    enqueue(x) { q.push(x) },
    dequeue() { return head < q.length ? q[head++] : undefined },
    size() { return q.length - head },
    isEmpty() { return head >= q.length }
  }
}

栈的经典应用:有效括号、逆波兰表达式、函数调用栈:

// 有效括号:遇到左括号入栈,右括号弹栈匹配
function isValid(s) {
  const map = { ')': '(', ']': '[', '}': '{' }
  const st = []
  for (const ch of s) {
    if (ch === '(' || ch === '[' || ch === '{') st.push(ch)
    else if (st.pop() !== map[ch]) return false
  }
  return st.length === 0
}
console.log(isValid('()[]{}')) // true

单调栈(Monotonic Stack)——"下一个更大元素"的标准解法:维护"栈内元素单调"的栈,入栈时弹出破坏单调性的元素。

function nextGreater(nums) {
  const res = new Array(nums.length).fill(-1)
  const st = [] // 存下标,栈底→栈顶 递减
  for (let i = 0; i < nums.length; i++) {
    while (st.length && nums[st[st.length - 1]] < nums[i]) {
      res[st.pop()] = nums[i] // 弹出的元素遇到"下一个更大"
    }
    st.push(i)
  }
  return res
}
console.log(nextGreater([2, 1, 2, 4, 3])) // [4, 2, 4, -1, -1]

循环队列:固定大小数组 + 头尾指针取模,避免扩容,常用于流式处理。优先队列:按优先级出队,底层是堆(第四章详讲);JS 无内置,需手写或用库。

名词解释

课后练习

  1. 有效括号(leetcode 20)的关键点?
    • 答案:用栈:遇左括号压栈;遇右括号弹栈比对是否匹配;最后栈必须空(防止 "(" 这种情况)。上面 isValid 即标准解。
  2. 每日温度(单调栈,739)怎么套模板?
    • 答案:维护"递减栈"存下标,当遇到更高温度 t[i] 时,弹栈并把 res[弹出的下标] = i - 弹出的下标(等待天数)。与 nextGreater 同构。

总结

栈和队列是所有高级数据结构的基石,理解它们关键在于抓住"进出顺序"这个灵魂:栈是后进先出,天然适合"配对/撤销/嵌套"类问题;队列是先进先出,天然适合"排队/广度遍历"类问题。很多人不知道的是,JS 里用数组当队列直接 shift 是性能陷阱——它是 O(n),正确姿势是用头指针或 Deque。单调栈则是把"找下一个更大/更小元素"这类看似困难的问题,化成一个优雅的模板:维护单调性,入栈即结算。我特别想强调"栈"在真实工程里的存在感:你写的每一个函数调用、每一次浏览器后退、每一回 Ctrl+Z 撤销,底层都是栈。学会它,不只是为了刷题,更是为了看懂程序是怎么"记住来路"的。