手写常用数据结构(JS)

本节目标

手写栈与队列(用私有字段 # 防止外部破坏内部结构):

// 运行环境:Node.js 14+(需支持私有字段 #)
// 保存为 dsa-l7.js,执行:node dsa-l7.js

class Stack {
  #items = []
  push(x) { this.#items.push(x) }
  pop() { return this.#items.pop() }
  peek() { return this.#items[this.#items.length - 1] }
  get size() { return this.#items.length }
  isEmpty() { return this.size === 0 }
}

class Queue {
  #items = []
  #head = 0
  enqueue(x) { this.#items.push(x) }
  dequeue() { return this.#head < this.#items.length ? this.#items[this.#head++] : undefined }
  peek() { return this.#items[this.#head] }
  get size() { return this.#items.length - this.#head }
  isEmpty() { return this.size === 0 }
}

手写最小二叉堆(优先队列核心):

class MinHeap {
  #h = []
  push(x) { this.#h.push(x); this.#siftUp(this.#h.length - 1) }
  pop() {
    if (!this.#h.length) return undefined
    const top = this.#h[0]
    const last = this.#h.pop()
    if (this.#h.length) { this.#h[0] = last; this.#siftDown(0) }
    return top
  }
  peek() { return this.#h[0] }
  get size() { return this.#h.length }
  #siftUp(i) {
    while (i > 0) {
      const p = (i - 1) >> 1
      if (this.#h[p] <= this.#h[i]) break
      ;[this.#h[p], this.#h[i]] = [this.#h[i], this.#h[p]]
      i = p
    }
  }
  #siftDown(i) {
    const n = this.#h.length
    while (true) {
      let m = i, l = 2 * i + 1, r = 2 * i + 2
      if (l < n && this.#h[l] < this.#h[m]) m = l
      if (r < n && this.#h[r] < this.#h[m]) m = r
      if (m === i) break
      ;[this.#h[m], this.#h[i]] = [this.#h[i], this.#h[m]]
      i = m
    }
  }
}

// 调用示例
const s = new Stack(); s.push(1); s.push(2); console.log(s.pop()) // 2
const h = new MinHeap(); [5, 3, 8, 1].forEach(x => h.push(x))
console.log(h.pop(), h.pop(), h.pop()) // 1 3 5(最小优先出队)

名词解释

课后练习

  1. 用上面 MinHeap 实现"取最大的最大堆"要改哪几处?
    • 答案:把比较符号 this.#h[p] <= this.#h[i] 和 this.#h[l] < this.#h[m] 都改成 >= / >(即父比子大才停),peek 仍是 this.#h[0] 但现在它是最大值。
  2. 为什么 Queue 用 #head 头指针而不是每次 shift?
    • 答案:shift 是 O(n)(搬移后续元素),用头指针只把下标前移,出队变 O(1);被"消费"的尾部空间在数组足够长时惰性留着,省去反复搬移。

总结

手写数据结构看起来"造轮子",实则是把抽象概念变成肌肉记忆的最佳方式。当你亲手写出 siftUp/siftDown,你才真正明白堆为什么是 O(log n);当你用头指针实现队列,你才刻骨铭心 shift 的 O(n) 代价。我建议每个前端工程师都至少手写一次 Stack、Queue、MinHeap——不是为了重复造轮子,而是为了在将来"该用哪个结构"时,脑子里有清晰的成本账。JS 的 # 私有字段值得一提:它提醒我们,好的数据结构要"封装内部、暴露最小接口",这和写组件要隐藏实现细节是同一回事。把数据结构当工具箱,每增加一件趁手的工具,你解决问题的手段就多一分。