JavaScript小根堆手写实现 2025.3.23
小根堆是一种数据结构,内部本质为二叉树,能保证子节点(的值)永远比父节点要大,根节点(的值)是最小的,JS用库函数建立小根堆的方式是利用优先队列库函数
var minHeap=new PriorityQueue((a,b)=>a.value-b.value)
来实现(有的版本是传入一个对象,其中的compare属性值为比较函数),内置方法有size()(优先队列大小),enqueue()(入队并根据compare函数排序),dequeue()(出队并根据compare函数排序)
那么手写小根堆的核心点有:
1.底层使用怎样的数据结构来承载小根堆的数据值? 答:对于JavaScript来说,底层数据一般为数组array形式,有方便的push,pop,shift等方法可供使用。数组的下标是广度遍历二叉树的顺序。
2.如何维护插入(入队)逻辑? 答:堆排序要求先把入队的元素放到最后一位,然后冒泡上升:如果比父节点小,就跟父节点交换位置。
3.如何维护输出最小值(出队)逻辑? 答:二叉树根节点代表最小值,将根节点输出,原根节点位置被二叉树最后一位节点代替,根节点位置再不断下沉。
4.如何确定compare函数(决定这是大根堆还是小根堆)? 答案:要从两种堆的基本功能和compare函数的关系出发,小根堆要求小的元素在二叉树上层,二叉树调整顺序的依据正式把两个节点值放入compare(父元素值a,子元素值b)中比较,如果结果为1,就代表交换;如果为-1,就代表不交换。因此可执果索因推导出:小根堆父元素值只有比子元素大的时候才交换,即a比b大的时候返回1。所以compare函数为:
//小根堆
(a,b)=>{return a.value-b.value}
同理推得大根堆compare函数为
//大根堆当子节点元素b比父节点元素a大的时候才做交换
(a,b)=>{return b.value-a.value}
——————————————————————————————————————————————————
理论完成,开始实践JS手写小根堆
//手写小根堆
class minHeap{
constructor(fn){
this.quene=new Array();
this.compareFn=fn;
}
size(){
return this.quene.length;
}
push(obj){
//先把加入元素放到二叉树最后,不断冒泡上升,最终形成顶小底大的二叉树
this.quene.push(obj);
let index=this.size()-1;
let parentIndex=Math.floor((index-1)>>1);
while(parentIndex>=0 && this.compare(index,parentIndex)<0){
[this.quene[index],this.quene[parentIndex]]=[this.quene[parentIndex],this.quene[index]];
index=parentIndex;
parentIndex=Math.floor((index-1)>>1);
}
}
pop(){
//把顶端的数据值pop传出,再把最后一个下标的值放到顶部,下沉维护小根堆
if(this.size()<=1){
return this.quene.pop();
}
const res=this.quene[0];
this.quene[0]=this.quene[this.size()-1];
this.quene.pop();
let index=0;
let leftChildIndex=1;
let realChild= this.compare(leftChildIndex,leftChildIndex+1)<0?leftChildIndex:leftChildIndex+1;
while(this.compare(index,realChild)>0){
[this.quene[realChild],this.quene[index]]=[this.quene[index],this.quene[realChild]];
index=realChild;
leftChildIndex= index*2+1;
realChild= this.compare(leftChildIndex,leftChildIndex+1)<0?leftChildIndex:leftChildIndex+1;
}
return res;
}
compare(indexA,indexB){
//compare的作用是把底层比较值大小的函数compareFn转换成用下标代表比较值的形式,并处理越界情况
//如果B不存在,根据compareFn的逻辑,如果A大于B,就返回1,代表父节点如果大于子节点,就交换,此时B不存在,不能交换,所以选择返回-1。也可以用“选择存在的孩子节点”来解释
if(this.quene[indexA]===undefined) return 1;
if(this.quene[indexB]===undefined) return -1;
return this.compareFn(this.quene[indexA],this.quene[indexB]);
}
}
写在后面: 1.compare的意义是把传入的compareFn方法进行越界处理,在这里的小根堆中,如果子元素b不存在,那父元素肯定不能和子元素做交换,因此返回-1;在pop方法的下沉逻辑中,也用到compare选定较小的子节点,作为父节点交换的对象,如果这个节点为undefined,就不能被交换,相当于值为无限大;这一点也能佐证compare函数的边界处理逻辑。
2.push的冒泡上浮逻辑中,要注意父节点的index必须有效,以此来对应冒泡到根节点的情况。