欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-02-15 20:52:22
...

目录

优先队列(堆)

优先队列是一种特殊的队列,是允许至少下列两种操作的数据结构: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结构性质

堆是一棵完全被填满的二叉树,有可能的例外是在底层,底层上的元素从左至右填入。这样的树称为完全二叉树。如下图所示:
【数据结构与算法分析】05:优先队列(堆)
容易证明,一棵高为h的完全二叉树,有

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

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

A B C D E F G H I
0 1 2 3 4 5 6 7 8 9 10

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

1.1.2堆序性质

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

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


1.2堆操作

1.2.1基本操作

Insert:

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

为了插入14,首先在堆的下一个空闲位置创建一个空穴:
【数据结构与算法分析】05:优先队列(堆)

由于将14插入空穴破坏了堆序性质,因此将31移入该空穴:
【数据结构与算法分析】05:优先队列(堆)
继续这种策略,将21移入空穴:
【数据结构与算法分析】05:优先队列(堆)
最后找到14的正确位置:
【数据结构与算法分析】05:优先队列(堆)
这种一般的策略叫做上滤(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放入堆中的正确位置:
【数据结构与算法分析】05:优先队列(堆)

31不能放入空穴,于是我们把较小的儿子14放入空穴,同时空穴下滑一层:
【数据结构与算法分析】05:优先队列(堆)
重复该过程,将24置入空穴,下一层建立一个新的空穴:
【数据结构与算法分析】05:优先队列(堆)
最后将31置入空穴:
【数据结构与算法分析】05:优先队列(堆)

这种一般的策略叫做下滤(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-堆。
【数据结构与算法分析】05:优先队列(堆)

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


先就写到这,以后再补充