继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

堆(heap)的原理与实现详解【Van0512】

慕姐3585284
关注TA
已关注
手记 10
粉丝 22
获赞 199

本文主要包括的内容:

  • 堆的原理
  • 建堆、调堆
  • C++中 priority_queue (Java中也有一个 PriorityQueue,有必要再补上)
  • 手动建堆的实例(C++类实现)

先吐个槽O(∩_∩)O:
网站上的编辑器我总是不太会用,排版总是让我很揪心,大概是写得太少了吧。慢慢习惯吧,希望每一篇文章的排版都能进步一点点。言归正传。


堆(heap)
是一种优先队列(priority queue)。
取出元素的顺序是按照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。
特性:

  1. 结构性:用数组表示的完全二叉树
  2. 有序性:任一节点的关键字是其子树所有节点的最大(小)值。
    MaxHeap、MinHeap

主要操作:建堆、判空、判满、插入、删除堆顶元素。

直接生成堆的时间复杂性是O(nlog(n))。
在线性时间复杂度O(n)下建堆:

  1. 将N个元素按输入顺序存入,先满足完全二叉树(complete binary tree)的结构特性。
  2. 调整节点位置:从最后一个节点的父节点开始。

这边提供一份C语言的代码,主要关注它的原理:


/*----------- 建造最大堆 -----------*/
void PercDown( MaxHeap H, int p )
{ /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
    int Parent, Child;
    ElementType X;

    X = H->Data[p]; /* 取出根结点存放的值 */
    for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
        Child = Parent * 2;
        if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
            Child++;  /* Child指向左右子结点的较大者 */
        if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
        else  /* 下滤X */
            H->Data[Parent] = H->Data[Child];
    }
    H->Data[Parent] = X;
}

void BuildHeap( MaxHeap H )
{ /* 调整H->Data[]中的元素,使满足最大堆的有序性  */
  /* 这里假设所有H->Size个元素已经存在H->Data[]中 */

    int i;

    /* 从最后一个结点的父节点开始,到根结点1 */
    for( i = H->Size/2; i>0; i-- )
        PercDown( H, i );
}

重要算法:上虑、下虑。


在C++中,提供了一种容器适配器(container adaptor)来实现这种数据结构,放在头文件 queue 中。
具体使用方式:


#include <queue>
using namespace std;
...
priority_queue<ElemType> priQue;

主要操作有:


    bool empty()        //Test whether container is empty.
    int size()          //Return size.
    void push(elem)     //Insert element.
    void emplace(elem)  //Construct and insert element.
    void pop()          //Remove top element.
    elemType top()      //Access top element.

priority_queue 默认按照严格弱序(strict weak ordering)排序规则生成最大堆,若要生成最小堆,看下面这个例子:


    #include <iostream>
    #include <queue>
    #include <vector>
    #include <functional>
    using namespace std;

    int main(void) {
        priority_queue<int> priQue;
        for (int i = 0; i < 10; i++)
            priQue.emplace(i);
        while (!priQue.empty()) {
            cout << priQue.top() << " ";
            priQue.pop();
        }
        cout << endl << endl;

        /*
        第一个参数:元素类型;
        第二个参数:底层容器类型,可以是vector或者deque;
        第三个参数:比较器,可以自定义。
            比如在 main 函数前写下:
            struct greater {
                bool operator() (int a, int b) {
                    return a > b;
                }
            };
            在下面的一行代码中,把 greater<int> 改成 greater 即可,头文件 functional 也可删除。
        */
        priority_queue<int, vector<int>, greater<int>> minHeap;
        for (int i = 0; i < 10; i++)
            minHeap.push(i);
        while (!minHeap.empty()) {
            cout << minHeap.top() << " ";
            minHeap.pop();
        }

        system("pause");
        return 0;
    }

但许多时候,常常有其他的需求。为此,我们就需要手动建一个堆,然后就可以在堆上增加一些函数,提供我们想要的功能。
写一个最小堆类:


typedef int ElemType;
class MinHeap {
public:
    MinHeap(int capacity);  //建堆,传入堆的容量。
    ~MinHeap();
    bool isEmpty() const;
    bool isFull() const;
    bool insertElem(ElemType &elem);
    ElemType deleteTop();
private:
    ElemType *arr;
    int capacity;
    int len;    //堆中元素个数,是堆的最后一个元素的索引。
};

/*
数组长度为堆容量+1。
0号元素作为“哨兵”,在最小堆中,设置为比所有元素都小的值。
1号元素作为根节点,那么父子节点下标有如下关系:
    leftChild = parent * 2;
    rightChild = parent * 2 + 1;
    parent = Child/2;
*/
MinHeap::MinHeap(int capacity) {
    arr = new ElemType[capacity + 1];
    this->capacity = capacity;
    len = 0;
    arr[0] = INT_MIN;    //定义在头文件 climits 里面
}

MinHeap::~MinHeap() {
    delete[] arr;
    arr = NULL;
}

bool MinHeap::isEmpty() const {
    return 0 == len;
}

bool MinHeap::isFull() const {
    return len == capacity;
}

bool MinHeap::insertElem(ElemType &elem) {
    if (isFull()) return false;
    /*
    上滤。
    如果父节点的数据值比elem大,那么把父节点的数据值拷贝到要插入的子节点中。
    把子节点的下标更新为父节点,继续。
    若父节点的数据值比elem小,那么elem就可以插入。
    */
    int child = ++len;
    for (; arr[child / 2] > elem; child /= 2)
        arr[child] = arr[child / 2];        //上滤 child = parent
    arr[child] = elem;
    return true;
}

ElemType MinHeap::deleteTop() {
    if (isEmpty()) return INT_MIN;  //error
    ElemType minItem = arr[1];
    /*
    下滤。
    模拟将最后一个元素从根节点的位置向下层节点过滤的过程。
    */
    ElemType tmp = arr[len--];
    int parent = 1;
    for (int child = 2 * parent; child <= len; child *= 2) {    //如果有左孩子
        if (child < len && arr[child] > arr[child + 1]) child++;    //child指向数据值较小的子节点。
        if (tmp <= arr[child]) break;
        else arr[parent] = arr[child];  //下滤。
        parent = child;
    }
    arr[parent] = tmp;
    return minItem;
}

比如,如果要打印从一个给定索引到根节点的路径,就可以在 MinHeap 中增加一个函数,代码如下:

class MinHeap {
public:
    void printPath(int index);
}

void MinHeap::printPath(int index) {
    if (index > len) {
        cout << "没有这个元素" << endl;
        return;
    }
    cout << arr[index];
    while (index > 1) {
        index /= 2;
        cout << " " << arr[index];
    }
    cout << endl;
}

好了,就写到这里吧。
慕友若有什么高见,可在下面回复。
祝开心。

打开App,阅读手记
2人推荐
发表评论
随时随地看视频慕课网APP

热门评论

插入代码的时候,在第一行敲个回车,空出来,在第二行开始写,就不会自动缩进了。——做了个小笔记。

查看全部评论