堆 (heap)

堆是一棵特殊的完全二叉树,由于这个特性,它的底层采用数组储存数据,这样,我们通过一个数组以及一组 heap 算法就可以轻易地实现一个堆。

概述

根据数组的特点,我们使用一个技巧。如果将 #i 处的元素看作根,则它的左右子树分别为 #(2*i+1) 和 #(2*i+2),这样堆就可定义如下:

  • 堆总是一棵完全二叉树;
  • 对于大堆,树中每个节点的值总是大于其左右孩子节点的值,对于小堆,树中每个节点的值总是小于其左右孩子节点的值;
  • 根节点最大的堆称为大堆,根节点最小的堆称为小堆;

下图形象的展示了小堆:
这里写图片描述

有了以上定义,我们就能通过一个数组和一组堆算法(用来建堆、插入、删除、取堆顶、调整堆)轻易地实现一个堆了。

普通数组无法动态改变大小,而堆则需要这项功能,故采用 vector 代替数组 是堆更好的选择。

堆算法

根据元素排列方式,堆有大小堆之分,我们在接下来的讨论中皆以小堆为例,大堆相关和小堆如出一辙。

push_heap()

入堆操作必须保证两点:

  • 新加元素后不破坏完全二叉树的性质;
  • 新加元素后不破坏孩子节点和父亲节点的大小关系;

对于第一点,我们很容易做到,只需调用 vector 的 尾插算法即可完成;
对于第二点,通过一个向上调整算法来实现。

下图演示的是 调整算法的实际操演情况:

这里写图片描述

插入之前,堆满足其性质,在插入一个节点 (25) 之后, 显然堆的性质被破坏,故需要调整;

如果将插入的节点作为 child 节点, 则父亲节点为 parent, 下标为:(#child-1)/2;

当插入 child时,由于它比根 parent 小, 故破坏了以 parent 为根节点的那棵树的(堆)性质,为满足小堆性质,将 childparent 交换,此时以 parent 为根的这棵树已经满足堆的性质,但这种操作又破坏了以 parent 的父节点为根的那棵树的堆性质,故一直上溯,直到不需要交换或者到达根节点为止。

下面是该算法的实现细节:

    void PushHeap(T value)
    {
        _a.push_back(value);//_a为vector的对象
        _AdjustUp(_a.size() - 1);
    }

    //该函数进行向上调整细节
    void _AdjustUp(size_t child)
    {
        size_t parent = (child - 1) / 2;

        while (0 != child){
            if (Com()(_a[child], _a[parent])){//仿函数实现比较
                swap(_a[child], _a[parent]);
                child = parent;//进行上溯
                parent = (child - 1) / 2;
            }
            else//说明已经不需要交换,则可以终止循环了
                break;
        }
    }
pop_heap()

出堆也必须保证和入堆同样的两点;

出堆操作总是要取走根节点,因为最大值(最小值)总是在根节点处。为了要保证第一点:完全二叉树的性质,我们不能直接拿走根节点,因为拿走根节点所导致的调整工作的开销实在是太大了。我们采用一个投机取巧的办法:将根节点元素和最后一个叶子节点元素交换,然后将最后一个叶子节点出堆,这样对于底层容器 vector 来说负担不至于太大, 也不会导致我们后面的调整算法太复杂。

至于第二步的调整,通过向下调整算法实现:

下图演示了出堆操作的整个步骤:

这里写图片描述

结合这图来看,我们将向下调整算法描述为以下:

从根节点开始,如果某个孩子节点的值比根节点的小,则将该孩子与跟交换,然后让该被交换的节点作为新的根节点,继续执行上述步骤,直至不需要交换或者孩子孩子节点不存在(孩子节点下标超出 vector 的 size 范围)。

下面是 pop_heap() 算法的实现细节:

    void Pop()
    {
        assert(!_a.empty());

        swap(_a[0], _a[_a.size() - 1]);//交换
        _a.pop_back();//删掉最后一个元素
        _AdjustDown(0);
    }
    //向下调整细节
    void _AdjustDown(int parent)
    {
        int child = parent * 2 + 1;

        while (child < _a.size()){
            if (child + 1 < _a.size() && Com()(_a[child + 1], _a[child]))
                ++child;

            if (Com()(_a[child], _a[parent]))
                swap(_a[child], _a[parent]);

            parent = child;
            child = parent * 2 + 1;
        }
    }
make_heap()

‘make_heap()’算法用来调整一段现有的数据,使之满足堆的性质。其主要依据二叉树的数组表示法以及上文提到的向下调整算法。

如果把堆看作由“左子树”、“右子树”、“根”组成的话,仔细观察上文堆的结构,不难发现,每棵子树都满足根的性质。也就是说,如果我们能调整一棵树的结构,使它的左子树,右子树满足堆的性质,并且它本身也满足堆的性质的话,那么这可树整体就满足堆的性质。

上文中的向下调整算法可以帮助我们做到这一点。具体做法是,先找到这棵树的最后一个孩子节点不为空(至少有一个孩子节点)的节点,以它作为根,使用向下调整算法调整,再找前一棵子树,继续调整,直至调整到根节点。

下图演示了这一过程(数据过小,演示的可能不全面):

这里写图片描述

heap_sort()

既然每次的 pop_heap()'都可以得到数组中的最小的元素,如果持续对堆进行 pop_heap() 操作,并且每将讲范围缩小一个(因为要将出堆的那个元素置于尾端),那么当整个程序执行完毕时,我们就得到了一个有序序列。

用这种方法,我们可以对一组数据进行排序:

  • 升序:大堆
  • 降序:小堆

这里写图片描述

如上图所示,我们通过堆的调整算法和出堆算法就对一组数列排好了序。

下面是它的逻辑顺序:

  • 建堆
  • 交换堆顶和堆尾的元素
  • 缩小堆的范围
  • 重新调整
  • 跳到第二步

下面给出完整实现代码:

//向下调整
//a为数据的数组, n为调整的范围, p为调整的起始位置
void AdjustDown(int *a, size_t n, size_t p)
{
    assert(a);

    int parent = p;
    int child = p * 2 + 1;
    while (child <= n)
    {
        if (child + 1 < n && a[child] < a[child + 1])
            ++child;

        if (a[parent] < a[child]){
            swap(a[parent], a[child]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
            break;
    }
}

void HeapSort(int *a, int size)
{

    for (int i = size / 2 - 1; i >= 0; --i)
        AdjustDown(a, size, i);

    int end = size - 1;
    while(end > 0)  {
        swap(a[0], a[end]);
        --end;
        AdjustDown(a, end, 0);
    }
}

排序测试:

这里写图片描述

参考资料

【作者:果冻 http://blog.csdn.net/jelly_9
——谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值