JS数据结构之堆
源码
来自半年后的说明:我把代码转放到 gists 里了,当时代码风格受 Python 影响较大,这里就不再更改了,毕竟大家本地都有自己的格式化工具。
前往Github获取本文源码。
介绍
通常情况下,堆指的是二叉堆,它是一颗完全二叉树。完全二叉树指的是要么是满二叉树(都填满了),要么最底层从左向右排列。这里给出一个例子:
二叉堆除了需要满足是一个完全二叉树之外,还必须满足下方的数据永远比上方的大(或小),也被称为堆序性质。因此,进行插入/删除操作的时候可能要破坏这个性质,所以就需要我们对这个堆进行重新调整。
由于堆序性质,我们可以很方便地在一个堆中求最小(或最大)值,所以它在需要动态插入数据并且求出最值的时候就显得非常有用了。
实现
由于堆是一个完全二叉树,那么我们就不用非得用链表的形式来实现了,用数组足以应对,我们还用上面的图为例:
为了计算方便,我们这里不用0
号元素,让数组下表从1开始。那么,对于任意节点array[i]
,它的左节点就是array[i * 2]
,右节点就是array[i * 2 + 1]
,父节点就是array[Math.floor(i / 2)]
。
整体结构
我们这里来实现一个最小堆,就是根节点是最小值的堆,定义如下:
1 | class Heap { |
其中,top
这个getter
使用了双问号运算符来判断this.vals[1]
是否为null
或者undefined
,如果是则返回一个NaN
。
插入
由于插入可能会破坏堆序性质,所以我们需要进行上滤(percolate up
)操作,使得它能不断在一个堆中上升到合适的位置。用一个图来表示我们的意思:
有两个时机来停止这个操作:当元素已经到达顶端时,或者元素已经符合堆序性质时。代码如下:
1 | push(val) { |
删除
删除指的是删除顶部的元素,在当前这个最小堆中,就是删除最小值。所以与插入相反,我们在删除时要进行下滤(percolate down)操作。用一个图表示我们的意思:
同样有两个时机停止这个操作:当元素已经到达底端(没有子节点)时,或元素满足堆序性质时。
不同于上滤,因为一个节点可能有多个子节点,当选择时我们就要尽可能挑最小的换上来,保证堆序性质。代码如下:
1 | pop() { |
这样,我们的最小堆就基本实现完毕了。接下来进行一个实际应用,求最大的k
个元素。
实际应用
对于求最大的k
个元素,我们可以维护一个最小堆:如果堆中元素的数量还不到k
个,那就直接把它加入堆中;否则,如果当前值比堆中的最小值大,那么就弹出堆的最小值,并且把当前值放入堆中。代码如下:
1 | function getTopK(nums, k) { |
我们来看一下整个复杂度:由于遍历了n
个数字,此时的复杂度是O(n)
;每一次遍历中我们都操作了堆,由于堆最多有k
个元素,所以循环内部的复杂度是O(log k)
。总体的复杂度是O(n*log k)
。
我们把结果放到数组中时,由于有k
个数,此时复杂度时O(k)
;每一次循环都操作了堆,由于堆最多k
个元素,所以循环内部的复杂度是O(log k)
。总体的复杂度是O(k*log k)
。
那我们知道,k <= n
,所以加起来的复杂度还是O(n*log k)
。
如果我们用排序的话,最少也要O(n*log n)
,不如堆的操作快。