八年前,一个前端工程师在面试现场被问到:"给你一个乱序数组,怎么最快找出前K大的元素?"他支支吾吾说了排序,面试官摇头。后来才知道,有个叫"堆"的数据结构,能在O(n)时间内原地建堆,每次取极值只要O(log n)。今天这段JavaScript代码,就是当年那个问题的标准答案。

这段实现的核心是个叫BinaryHeap的类,它只做一件事:维护一个"父节点总比子节点优先级高"的不变式。构造函数接收两个参数——一个初始数组和一个优先级比较函数。有意思的是,它不做类型检查,注释里写着"no types here in JS, so we trust",这种信任在2024年的TypeScript时代反而显得稀缺。

打开网易新闻 查看精彩图片

堆的魔法藏在两个私有方法里。#heapifyUp负责插入时的上浮:新元素先丢到数组末尾,然后不断和父节点比较,优先级更高就交换,直到找到合适位置。时间复杂度O(log n),递归实现消耗O(log n)栈空间,改成循环能压到O(1)。#heapifyDown则处理删除时的下沉:把末尾元素提到根节点,然后和左右孩子比,谁优先级高跟谁换,一路沉到该去的地方。

最反直觉的是#buildHeapInPlace。直觉上建堆应该逐个插入,每次O(log n),总复杂度O(n log n)。但这段代码从中间往前遍历,对每个非叶子节点做一次下沉。数学上,高度为h的节点最多有⌈n/2^(h+1)⌉个,每个下沉最多h步,求和后收敛到O(n)。这种"从底向上"的策略,是堆排序能打败普通排序的关键。

公开接口很克制:pushpop都是O(log n),数组长度小于2时直接跳过堆化。代码截断在pop方法中间,但已足够看出设计意图——用私有字段封装内部状态,用函数参数解耦优先级逻辑,不硬编码大小比较。这种灵活性让同一个类既能当最小堆也能当最大堆,全看调用者怎么定义"更高优先级"。

在实际工程里,这种结构支撑着任务调度、合并K个有序流、Dijkstra算法里的优先队列。V8引擎的定时器、Node.js的事件循环、React的调度器,底层都有它的影子。当你下次写setTimeout时,可以想想:你的回调正在某个二叉堆里等着被唤醒呢。