优先队列(堆)与TopK问题

本文介绍了优先队列,其允许insert和deleteMin操作,是贪心算法的实现基础,还提及了几种实现方式。重点阐述了二叉堆,包括结构性质、堆序性质及基本操作。最后说明了优先队列在选择问题(TopK)中的应用,如找出大量数据中第K个最小或最大元素。

优先队列是允许至少下列两种操作的数据结构: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中最小元素输出即可。

代码待补

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值