优先队列的实现—二叉堆

我相信大家都用过STL中的priority_queue,并且你可能也知道其底层原理是二叉堆(binary heap),但是你真正了解它具体是怎么实现的吗?你能自己写个优先队列模板而不用STL中的吗?刨根问底才是学习的最高境界(当然,这里探讨的只是简单的实现)。

优先队列模型

图0. 优先队列的基本模型

二叉堆

对于优先队列的实现,二叉堆的使用很常见。与二叉查找树(binary search tree)一样,堆也有两个性质,即结构性质和堆序性质。类似于AVL树,对堆的操作可能破坏其中一个性质,因此,堆的操作必须到堆的所有性质都被满足是才能终止。

结构性质

堆是一棵完全二叉树(complete binary tree)。我们知道,一棵高为 h 的完全二叉树有 2h 到 2h+1-1 个结点。这意味着,完全二叉树的高为O(logN)。

一个重要的观察发现,因为完全二叉树很有规律,所以可以用一个数组表示而不需要使用链。下图2中的数组对应着图1中用二叉树表示的堆。


图1. 一棵完全二叉树
图2. 完全二叉树的数组实现

对于数组中任一位置 i 上的元素,其左儿崽在位置 2i 上,右儿子在位置 2i+1 中,它的父亲则在位置 \lfloori /2\rfloor上。因此,这里不仅不需要链,而且遍历该树所需要的操作也极为简单。这种实现方法的唯一问题在于,最大的堆大小需要事先估计,但一般情况下这并不成问题(而且如果需要我们可以重新调整)。

因此,一个堆数据结构由一个数组(Comparable对象的)和一个代表当前堆大小的整数组成。下面的代码为一个优先队列接口:

template<typename Comparable>
class BinaryHeap
{
public:
    // constructor
    explicit BinaryHeap(int capacity = 100);
    explicit BinaryHeap(const vector<Comparable>& items);

    bool isEmpty() const;
    const Comparable& findMin() const;

    void insert(const Comparable& x);
    void deleteMin();
    void deleteMin(const Comparable& minItem);
    void makeEmpty();
    void printArray();

private:
    int currentSize;    //Number of elements in heap
    vector<Comparable> array;   //The heap array

    void buildHeap();
    void percolateDown(int hole);
};

我们始终把堆画成树,是便于理解,但实际上具体的实现是采用简单的数组。

堆序性质

使操作可以快速执行的性质是堆序性质(heap-order property)。由于想要快速地找出最小元,因此最小元应该在根上。如果考虑任意子树也应该是堆,那么任意结点就应该小于它的所有后裔。

应用这个逻辑,可以得到堆序性质。在堆中,对于每一个结点 XX 的父亲中的键小于或等于 X 中的键,根节点除外(它没有父亲)。下图左边的树是一个堆,但是,右边的不是。

图3. 两棵完全树(只有左边的是heap)

根据堆序性质,最小元总可以在根处找到。因此,我们可以以 O(1) 时间得到附加操作 findMin。

基本的堆操作

1. insert

为将一个元素 X 插入到堆中,我们在下一个空闲位置创建一个空穴。如果 X 可以放在空穴中而不破坏堆序性质,那么插入完成。否则,按上滤(percolate up)策略进行,该操作如下图所示:

图4. 尝试插入14:创建一个空穴,再将空穴上冒

图5. 将14插入到堆中的最后两步

使用下面的代码很容易实现插入:

/**
*   Insert item x, allowing duplicates.
*/
template<class Comparable>
void BinaryHeap<Comparable>::insert(const Comparable& x)
{
    if (currentSize == array.size() - 1)
    {
        array.resize(array.size() * 2);
    }

    //  percolate up(上滤)
    int hole = ++currentSize;
    for (; hole > 1 && x < array[hole/2]; hole /= 2)  //array[0] don't put heap element
    {
        array[hole] = array[hole / 2];
    }
    array[hole] = x;
}

我们本可以通过反复实施交换操作直至建立正确的顺序来实现上滤过程,可是一次交换需要 3 条赋值语句。如果一个元素上滤 d 层,那么由于交换而实施的赋值次数达到 3d,而这里的代码只需要 d+1 次赋值。

显然,insert 操作的最坏时间复杂度为 O(logN)。平均看来,这种上滤终止得要早;业已证明,其平均时间复杂度为 O(1)。

2. deleteMin

findMin操作是简单的,困难的部分在于:删除它。我们的策略是用堆中最后一个元素替换根,然后实行下滤(percolate down)操作。如下图所示:


图6. 用堆中最后一个元素替换根

图7. 在deleteMin中的接下来的两步

图7. 在deleteMin中的接下来的两步

在堆的实现中经常发生的错误为:当堆中存在偶数个元素时,最后一个非叶子结点将只有一个崽崽,我们每次都是取崽崽中小的那个作为下滤的对象,所以这个时候就需要一个附加的测试。下面代码的 if 语句中的 child != currentSize 避免了这个容易发生的错误。

/**
*   Remove the minimum item.
*   Throws UnderflowException if empty.
*/
template<typename Comparable>
void BinaryHeap<Comparable>::deleteMin()
{
    if (isEmpty())
    {
        //throw UnderflowException();
        cout << "the array is empty!";
    }

    array[1] = array[currentSize--];
    percolateDown(1);
}

/**
*   Internal method to percolate down in the heap.
*   hole is the index which the percolate down begins.
*/
template<typename Comparable>
void BinaryHeap<Comparable>::percolateDown(int hole)
{
    Comparable temp = array[hole];
    int child;

    for (; hole * 2 <= currentSize; hole = child)
    {
        child = hole * 2;
        if (child != currentSize && array[child + 1] < array[child])
        {
            child++;
        }
        if (array[child] < temp)
        {
            array[hole] = array[child];
        }
        else
        {
            break;
        }
    }
    array[hole] = temp;
}

这种操作的平均运行时间为 O(logN)。

3. buildHeap

初始堆通过项的原始集合来构造。构造函数将 N 项作为输入并把它们放入一个堆中。显然,这可以通过 N 次连续的 insert 来完成。由于每个 insert 操作都花费 O(1) 的平均时间,以及 O(logN) 的最坏情形时间,这个算法的总的运行时间就是 O(logN) 平均时间,其最坏情形时间为 O(NlogN)。

通常的算法是将 N 项以任意顺序放入树中,并保持结构特性。然后从最后一个非叶子结点开始,一直到根,对这些结点执行下滤(percolate down)操作,可以保证能够生成一棵具有堆序性质的树(heap-ordered tree)。

template<class Comparable>
BinaryHeap<Comparable>::BinaryHeap(const vector<Comparable>& items):currentSize(items.size()),array(items.size()+10)
{
    /*currentSize = items.size();
    array(items.size() + 10);*/
    for (int i = 0; i < (int)items.size(); ++i)
    {
        array[i + 1] = items[i];
    }
    this->buildHeap();
}

/**
*   Establish heap order property from an arbitrary arrangement of items.
*   Runs in linear time.
*/
template<typename Comparable>
void BinaryHeap<Comparable>::buildHeap()
{
    for (int i = currentSize / 2; i > 0; --i)
    {
        percolateDown(i);
    }
}

可以证明(略):buildHeap 的运行时间的界为:O(N)。

以上,便是优先队列的具体实现。我在文章开头说了,这只是基本的实现,高级实现还远远不够,包括左式堆、斜堆、二项队列等等(这些我也还没看呢嘻嘻)。

PS:
看别人的代码永远只是看看,自己将其实现,以后还可以拿来用的勒,到时候需要另外的功能自己扩充就行了 sha !

有什么error评论指出来啊!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容