优先队列是允许至少下列两种操作的数据结构:insert和deleteMin。
使用场景例如打印机排队打印,和操作系统调用执行时,一有空闲就执行的任务、短且快的任务、长任务,优先级高的任务、优先级低的任务之间的调度。同时也是贪心算法的实现基础。
插入的操作等价于入队enqueue,而删除最小值是dequeue(出队)在优先队列中的等价操作。
优先队里的几种实现:
1.链表方式O(1)插入,O(N)删除 | 链表保持排序,插入O(N),删除O(1) | 查找树 这些都不常用。
二叉堆(简称堆)
结构性质
完全二叉树:一棵高为h的完全二叉树有(2的h次方)到(2的h+1次方-1)个节点,其特定结构可以用一个数组表示。
对于数组任一位置i上的元素,其左儿子在2i,右儿子在2i+1,其父亲在位置i/2上。堆数组从1位开始,其0位有特殊处理,待补。

堆序性质
让操作快速执行的性质是堆序性质。对于最小堆来说,每个节点N,其所有子节点的值均大于N,也就是说,根节点为最小节点,每个独立堆块的子节点都大于N。同理,构成最大堆则根节点最大,依次向下类推。
根据堆序性质,最小(大)元总可以在根处找到,因此可以以O(1)时间findMin(Max).
基本堆操作
插入insert
为将一个元素x插入堆,第一步在下一个空位置创建一个空穴(防止破坏完全树结构)。
1.如果x可以置入,并不破坏堆序,则插入完成。
2.否则,把空穴父节点上的元素移入空穴中(由于堆只有父子有序,移入的过程中,空穴的兄弟节点和原父节点的大小并不会影响堆结构),这样,空穴就向父节点位置上移一次。持续该过程直到空穴被放到合适的位置为止,再将x放入。
这种一般的策略叫做上滤,新元素在堆中上滤直到找到正确的位置。
代码如下:
//对于最小元素堆的insert
public void insert(AnyType x){
if(currentSiz==array.length-1){
//扩容
}
int hole=++currentSiz;
//最小元素堆,hole为坑,x的值与其父节点(hole/2)对比,如果小于父节点,则将父节点下移,空穴上移
while (hole>1&&x.compareTo(array[hole/2])<0){
//将父节点的值赋给空穴
array[hole]=array[hole/2];
//将空穴定义为其父节点原始位置。
hole/=2;
}
array[hole]=x; //最后将x赋给空穴
}
//使用hole赋值比值交换在每次循环时省掉2次赋值操作,这种实现方式效率更高一些
deleteMin(删除最小元操作)
删除操作类似于插入,找出最小元是容易的,因为一定在根节点处。而删除它则几乎一定需要下滤操作。
由于堆少了一个元素,所以堆中的最后一个元素x必须移动到某个地方,堆结构才能维持。
下滤的逻辑是:将空穴的两个儿子中较小的一个移入空穴,重复此操作直至x可以放入适当的空穴为止。
此处容易出现问题在于堆节点不总是有两个儿子(当堆元素为偶数时),需要附加处理。
最坏情形运行时间为O(logN),一般来说,被放到根处的x几乎都会下滤到底层(之前x所在的层),因此平均运行时间为O(logN)。
public AnyType deleteMin(){
if(array==null)throw new Exception();
//array[1];
AnyType minItem = findMin();
//删除先将尾节点赋给根,再下滤到合适的位置
array[1]=array[currentSiz-1];
percolateDown(1);
return minItem;
}
private void percolateDown(int hole){
int child;
AnyType tmp=array[hole];
//循环条件是在不大于边界时,hole每次等于自己的儿子位置,再对比
for (;hole*2<=currentSiz;hole=child){
//找到坑的子节点用于对比,与插入时找父节点做法相似
child=hole*2;
//没遍历到头,同时找两个儿子中小的儿子
if(child!=currentSiz&&array[child+1].compareTo(array[child])<0);
child+=1;
//用小的儿子与坑变量对比,小的往上移,坑往下移
if(array[child].compareTo(tmp)<0){
array[hole]=array[child];
}else break;
}
//最后把变量填到合适的坑
array[hole]=tmp;
}
buildHeap(构建堆)
有时二叉堆是由初始集合构造而成。这种方法以n项作为输入,并直接放到一个堆中。可以用insert操作完成。平均时间为O(N),最坏情形为O(NlogN).
另外一种方法是先将数据放入数组,构建堆结构,然后进行调堆。此方法的时间复杂度为O(N).
//堆的初始构建
public BinaryHeap(AnyType[] items){
currentSiz=array.length;
//创建一个新大小的集合 赋值给array
array=(AnyType[])new Comparable[(currentSiz+2)*11/10];
int i=1;
for (AnyType item:items){
//赋值,将item装进新数组
array[i++]=item;
}
buildHeap();
}
private void buildHeap(){
//调堆,从最后一个父节点开始
for(int i=currentSiz/2;i>0;i--){
//下滤操作
percolateDown(i);
}
}
优先队列的应用
选择问题(TopK)
- 找出一组大量数据中第K(前K)个最小元素。
建堆-调堆-循环deleteMin(k次)-值放入数组即可。
- 找出第K个最大的元素。
维护一个k个最大元素的集合(堆)S,在前k个元素读入以后,再读入新元素时,与第k个最大元素(即S中最小的元素)对比。如果新元素更大,那么用新元素替换Sk。
代码待补
此时,新替换进来的Sk可能是最小的元素,也可能不是。(需要调堆?)所以再循环比较时,再比较第k个最大元素(即S中最小的元素),直到输入结束。找到S中最小元素输出即可。
代码待补
本文介绍了优先队列,其允许insert和deleteMin操作,是贪心算法的实现基础,还提及了几种实现方式。重点阐述了二叉堆,包括结构性质、堆序性质及基本操作。最后说明了优先队列在选择问题(TopK)中的应用,如找出大量数据中第K个最小或最大元素。
1246

被折叠的 条评论
为什么被折叠?



