数据集合有序,能够为各种操作带来便利,但有些应用并不要求所有数据都是有序的,或者在操作开始之前就变得完全有序。一些应用需要先收集一部分数据,从中挑选出最大或者最小的关键码记录开始处理,后续会收集更多数据但始终处理数据集中有最小或最大关键码的记录,比如优先级队列,优先级队列并不满足先进先出的特性,它能做到高优先级的先出队列,在优先级队列的各种实现中,堆是最高效的一种数据结构。
关键码
1.2 数组存储
最小堆存储在数组中,下图是最小堆和最大堆的示意图,其中,最小堆存储在数组min_heap_arr中
数组中的索引从0开始,元素个数为n, 在堆中给定索引为i的节点时:
-
如果i=0, 节点i是根节点,否则节点i的父节点为(i-1)/2
-
如果2*i + 1 > n-1, 则节点i无左子女,否则节点i的左子女为2*i + 1
-
如果2*i + 2 > n-1, 则节点i无右子女,否则节点i的右子女为2*i + 2
min_heap_arr = [9, 17, 65, 23, 45, 78, 87, 53, 31]
unction MinHeap(size){
var heap = new Array(size); // 数组
var curr_size = 0; // 当前堆的大小
var max_size = size; // 堆最大容量
};
// 调整算法的基本思想是找到所有的分支节点,然后根据这些分支节点的索引从大到小依次进行调整,
// 每次调整时,从该分支节点向下进行调整,使得这个分支节点和它的子孙节点构成一个最小堆,
// 假设数组的大小为n,则最后一个分支节点的索引是(n-2)/2,第一个分支节点的索引是0。// 在局部进行调整时,如果父节点的关键码小于等于两个子女中的最小关键码,说明,不需要调整了,
// 否则,将父节点和拥有最小关键码的子女进行位置互换,并继续向下比较调整。
var shif_down = function(start, m){
// 从start这个位置开始,向下下滑调整
var parent_index = start;
// start就是当前这个局部的父节点
var min_child_index = parent_index*2 + 1;
// 一定有左孩子,先让min_child_index等于左孩子的索引
while(min_child_index <= m){
// min_child_index+1 是左孩子的索引, 左孩子大于右孩子
if(min_child_index < m && heap[min_child_index] > heap[min_child_index+1]){
min_child_index = min_child_index+1; // min_child_index永远指向值小的那个孩子
}
// 父节点的值小于等于两个孩子的最小值
if(heap[parent_index] <= heap[min_child_index]){
break; // 循环结束,不需要再调整了
}else{
// 父节点和子节点的值互换
var tmp = heap[parent_index];
heap[parent_index] = heap[min_child_index];
heap[min_child_index] = tmp;
parent_index = min_child_index;
min_child_index = 2*min_child_index + 1;
}
}
};
// 传入一个数组,然后调整为最小堆
this.init = function(arr){
max_size = arr.length;
curr_size = max_size;
heap = new Array(arr.length);
// 填充heap, 目前还不是一个堆
for(var i =0; i<curr_size;i++){
heap[i] = arr[i];
}
var curr_pos = Math.floor((curr_size-2)/2); // 这是堆的最后一个分支节点
while(curr_pos >= 0){
shif_down(curr_pos, curr_size-1); // 局部自上向下下滑调整
curr_pos -= 1; // 调整下一个分支节点
}
};
insert方法,将新的元素插入到最小堆中,由于此前,最小堆已经建好,那么就可以从下向上,与父节点的关键码进行比较,对调。
下图演示了insert方法插入一个元素后的调整过程,这个过程非常像排序算法中的冒泡排序过程,最新的节点必然放到数组的最后一位,经过与父节点的比较,它的位置逐步上升。
var shif_up = function(start){
var child_index = start; // 当前节点是叶节点
var parent_index = Math.floor((child_index-1)/2); // 找到父节点
while(child_index > 0){
// 父节点更小,就不用调整了
if(heap[parent_index] <= heap[child_index]){
break;
}else{
// 父节点和子节点的值互换
var tmp = heap[child_index];
heap[child_index] = heap[parent_index];
heap[parent_index] = tmp;
child_index = parent_index;
parent_index = Math.floor((parent_index-1)/2);
}
}
};
this.insert = function(item){
// 插入一个新的元素
// 堆满了,不能再放元素
if(curr_size == max_size){
return false;
}
heap[curr_size] = item;
shif_up(curr_size);
curr_size++;
return true;
};
remove_min
删除掉最小堆的最小值,用用后一个元素取代堆顶元素,取代后,最小堆被破坏,使用shif_down方法向下做调整。
//删除最小值
this.remove_min = function(){
if(curr_size <= 0){
return null;
}
var min_value = heap[0];
heap[0] = heap[curr_size-1];
curr_size--;
shif_down(0, curr_size-1);
return min_value;
};
get_min, print , size 方法
this.size = function(){
return curr_size;
};
this.print = function(){
console.log(heap);
};
this.get_min = function(){
if(curr_size > 0){
return heap[0];
}
return null;
}
排序
可以使用最小堆进行排序,使用待排序数组初始化最小堆,然后逐个删除堆顶元素,由于堆顶元素始终最小,所以可以得到一个有序的数组
var arr = [53, 17, 78, 9, 45, 65, 87, 23];
var min_heap = new MinHeap(8);
for(var i = 0; i<arr.length; i++){
min_heap.insert(arr[i]);
}
var sort_arr = []
for(var i =0;i<arr.length;i++){
sort_arr.push(min_heap.remove_min());
}
console.log(sort_arr);
Top K 问题
一个非常大的数据集合有n个整数,求集合中最大的K个值。
用最小堆来算,非常简单,初始化一个大小为k的最小堆,先放入k个数,这时,堆顶元素最小,集合中剩余的数依次和堆顶元素比较,如果比堆顶元素大,则删除堆顶元素,并放入新的元素,全部比较以后,堆里的元素就是最大的k个值
var arr = [53, 17, 78, 9, 45, 65, 87, 23];
var min_heap = new MinHeap(3);
for(var i = 0; i<3; i++){
min_heap.insert(arr[i]);
}
for(var i =3;i<arr.length;i++){
var item = arr[i];
if(item > min_heap.get_min()){
min_heap.remove_min();
min_heap.insert(item);
}
}
min_heap.print();