手写常用数据结构(JS)
本节目标
- 手写 Stack / Queue / Deque 的完整实现
- 实现一个基于数组的最小二叉堆(优先队列)
- 建立"数据结构是工具"的工程视角
手写栈与队列(用私有字段 # 防止外部破坏内部结构):
// 运行环境: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(最小优先出队)名词解释
- 私有字段(Private Field,JS
#):以#开头的类字段,外部无法直接读写,强制封装。类比:发动机舱盖锁,用户只能踩油门,不能伸手改内部零件。 - 二叉堆(Binary Heap):一种用数组表示的完全二叉树,父节点恒小于等于(最小堆)或大于等于(最大堆)子节点。它让"取最小/最大"变成 O(1),插入/删除 O(log n)。优先队列的底层。
- 数据结构(Data Structure):组织数据的"容器 + 操作规则"。选对结构,算法复杂度能降一两个数量级;选错则寸步难行。
课后练习
- 用上面 MinHeap 实现"取最大的最大堆"要改哪几处?
- 答案:把比较符号
this.#h[p] <= this.#h[i]和this.#h[l] < this.#h[m]都改成>=/>(即父比子大才停),peek 仍是this.#h[0]但现在它是最大值。
- 答案:把比较符号
- 为什么 Queue 用
#head头指针而不是每次shift?- 答案:
shift是 O(n)(搬移后续元素),用头指针只把下标前移,出队变 O(1);被"消费"的尾部空间在数组足够长时惰性留着,省去反复搬移。
- 答案:
总结
手写数据结构看起来"造轮子",实则是把抽象概念变成肌肉记忆的最佳方式。当你亲手写出 siftUp/siftDown,你才真正明白堆为什么是 O(log n);当你用头指针实现队列,你才刻骨铭心 shift 的 O(n) 代价。我建议每个前端工程师都至少手写一次 Stack、Queue、MinHeap——不是为了重复造轮子,而是为了在将来"该用哪个结构"时,脑子里有清晰的成本账。JS 的 # 私有字段值得一提:它提醒我们,好的数据结构要"封装内部、暴露最小接口",这和写组件要隐藏实现细节是同一回事。把数据结构当工具箱,每增加一件趁手的工具,你解决问题的手段就多一分。