【数据结构与算法分析】05:优先队列(堆)

本文介绍了优先队列的概念和重要性,重点讲解了二叉堆的结构和堆序性质,包括如何通过上滤和下滤操作进行元素插入和删除最小值。此外,还提及了d-堆作为二叉堆的推广,以及其在Insert操作上的优势和DeleteMin操作的挑战。
摘要由CSDN通过智能技术生成

目录

优先队列(堆)

优先队列是一种特殊的队列,是允许至少下列两种操作的数据结构:Insert(插入)以及DeleteMin(删除最小者)。DeleteMin操作要求,优先队列要有办法能够找出最小者,在优先队列的实现中,如何做到花费最优实现插入和删除最小值,是主要需要探讨的问题。


1简单的想法

有几种显而易见的方式实现优先队列:

  1. 链表:表头以O(1)执行插入操作,并遍历该链表以删除最小元(耗费O(N)时间);
  2. 有序链表:让表保持有序状态,使得插入代价为O(N)而删除花费O(1);
  3. 二叉查找树:对两种操作的平均时间都是O(log N),但是很“浪费”,因为它支持许多并不需要的操作。

以上想法中,3明显是时间最优的方式,那么,有没有不“浪费”,同时做到最坏情形时间O(log N)的数据结构呢?下面介绍二叉堆。


1.1二叉堆

二叉堆一般我们习惯性称之为“堆”,同二叉查找树一样,堆也有两个性质,即结构性和堆序性。正如AVL树一样,对堆的一次操作可能破坏这两个性质中的一个,因此堆的操作必须要到堆的所有性质被满足时才能终止。

1.1.1结构性质

堆是一棵完全被填满的二叉树,有可能的例外是在底层,底层上的元素从左至右填入。这样的树称为完全二叉树。如下图所示:
这里写图片描述
容易证明,一棵高为h的完全二叉树,有

2h2h+11 2 h 到 2 h + 1 − 1 个 节 点
,这意味着,完全二叉树的高是[ log N](向下取整),显然它是O(log N)。

完全二叉树很有规律,可以用一个数组表示,而不需要指针,上图的二叉树用数组可以表示如下:

ABCDEFGHI
012345678910

对于数组中任一位置i上的元素,其做儿子在位置2i上,右儿子在做儿子后的单元(2i+1)中,它的父亲则在位置[i/2](向下取整)上。因此,不仅指针不需要,而且遍历该树所需要的操作也非常简单。唯一的问题在于,最大的堆大小需要事先估计。

1.1.2堆序性质

使操作被快速执行的性质是堆序性。我们想要快速的找到最小元,所以最小元应该放在根上。同理,任意节点都应该小于它的后裔。

根据堆序性质,最小元总可以在根处找到,因此我们以常数时间完成附加运算FindMin。


1.2堆操作

1.2.1基本操作

Insert:

为了将一个元素X插入到堆中,我们在下一个空闲位置创建一个空穴,否则堆将不是完全树。如果X可以放在该空穴而并不破坏堆的序,那么插入完成,否则,我们把空穴的父节点上的元素移入该空穴中,这样空穴就朝着根的方向上行一步。继续改过程直到X能被放入空穴中位置。

为了插入14,首先在堆的下一个空闲位置创建一个空穴:
这里写图片描述

由于将14插入空穴破坏了堆序性质,因此将31移入该空穴:
这里写图片描述
继续这种策略,将21移入空穴:
这里写图片描述
最后找到14的正确位置:
这里写图片描述
这种一般的策略叫做上滤(percolate up);新元素在堆中上滤直到找到正确的位置。

代码如下:

public <T extends Comparable<T>> Boolean insert(T x) {
        if (isFull()) {
            System.out.println("ERROR:Priority queue is full");
            return false;
        }

        Node node = new Node(x);
        dataArry[size] = node;
        PercolateUp(size++);  
        return true;

    }

    //上滤法
    private void PercolateUp(int index) {
        int parent = (index - 1)/2;//父节点索引
        Node bottom = dataArry[index];//新加的最后一个节点
        while(index > 0 &&
                (dataArry[parent].getElement().compareTo(bottom.getElement())<0)){
            dataArry[index] = dataArry[parent];
            index = parent;
            parent = (index-1)/2;
        }
        dataArry[index] = bottom;
    }

DeleteMin:

DeleteMin以类似于插入的方式处理,找出最小元是容易的,删除确实困难的。删除一个最小元时,在根节点处产生了一个空穴,由于现在堆少了一个元素。因此堆中最后一个元素X必须移动到该堆的某个地方。如果X可以放入空穴中,那么DeleteMin完成。若不行,那么空穴中的两个儿子中较小者移入空穴,这样就把空穴向下推一层,重复该步骤直到X可以被放入空穴中。

删除最小元13后,我们必须将31放入堆中的正确位置:
这里写图片描述

31不能放入空穴,于是我们把较小的儿子14放入空穴,同时空穴下滑一层:
这里写图片描述
重复该过程,将24置入空穴,下一层建立一个新的空穴:
这里写图片描述
最后将31置入空穴:
这里写图片描述

这种一般的策略叫做下滤(percolate down),代码实现如下:

    public <T extends Comparable<T>> Node DeleteMin() {
        if (isEmpty()) {
            System.out.println("ERROR:Priority queue is empty");
            return null;
        }
        Node<T> minNode = dataArry[0];
        PercolateDown();
        return minNode;
    }

    private void PercolateDown() {
        int index = 0;
        int largeChildIndex;
        Node lastNode = dataArry[size - 1];

        while (index < size / 2) {
            int leftChildIndex = 2 * index + 1;
            int rightChildIndex = leftChildIndex + 1;

            if (leftChildIndex < size
                    && (dataArry[leftChildIndex].getElement().compareTo(dataArry[rightChildIndex].getElement()) < 0)) {
                largeChildIndex = rightChildIndex;
            } else {
                largeChildIndex = leftChildIndex;
            }

            if (lastNode.getElement().compareTo(dataArry[largeChildIndex].getElement()) >= 0) {
                break;
            }
            dataArry[index] = dataArry[largeChildIndex];
            index = largeChildIndex;
        }
        dataArry[index] = lastNode;

    }

2.d-堆

d-堆是二叉堆的推广,它就像一个二叉堆,只是所有节点都有d个儿子(因此,二叉堆是2-堆)。

下图展示的是一个3-堆。
这里写图片描述

注意,d-堆要比二叉堆浅的多,它将Insert操作改进为O(logd N)。然而,对于大的d,DeleteMin操作费时的多,因为虽然树浅了,但是d个儿子中的最小者必须要找出,如果用标准算法,会花费d-1次比较。


先就写到这,以后再补充

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值