数据结构之堆(Heap)

本文采用图文码结合的方式介绍堆来实现优先队列

什么是优先队列?

队列是一种先进先出(FIFO)的数据结构。虽然,优先队列中含有队列两个字,但是,他一点也不像队列了。个人感觉,应该叫他优先群。怎么说那,一群守秩序(FIFO)的人去排队买东西当然是队列结构。但是,一群不守秩序的人去买东西,当然谁的拳头大谁就先结账。这个拳头的大小就是我们所谓的优先级。哎~我拳头不大~

优先队列的实现方式有:

1.线性表

2.堆(Heap)

3.左高树(Leftist Tree)

本文先从堆(Heap)开始讨论实现优先队列。

介绍堆之前,先介绍一种叫做最大树和最小树的数据结构:

最大(小)树:

1.根的值大(小)于等于所有子树中所有的值

2.子树也是最大(小)树


最大(小)堆:

1.完全二叉树 2.最大(小)树


首先,我们要清楚,最大堆中最大的元素一定出现在根上,最小的元素一定在树的叶子上;第二大的元素一定在第二层上等等。

其次,因为堆是完全二叉树,所以,使用数组描述这种结构最好不过了。

上图是一棵最大堆。

我们想对这中数据结构进行插入、删除时,要如何完成?

插入(就是拉父亲,拉不拉得动就另一说了):

1)当插入5时,因为完全树,所以5要出现在第四层的第三个位置上。5放到这之后,父元素仍然比他大,所以仍然可以构成堆。

2)当插入20时,同样,要出现在4层的第三个上。但是,这时,7比20小了,那么,就把7拿下来,在判断20是不是可以在原7的位置上,不行的话,往下拉父节点,去侵占父节点的位置。

插入时,每一层操作一次,最多操作height次

于是时间复杂度为O(log2nlog2n)


删除(当然是要删最大的元素了,也就是要删根):

这也是一个最大堆


删掉20时,我们首先由完全树的定义知,第4层的8的未知将消失,我们不妨把8先拿到根上,再进行堆的重构(在左右树都是堆的情况下)。

删除时,每一层最多被修改一次,于是最多修改height次。

时间复杂度时O(log2nlog2n)。

最大堆的初始化(对数组中的元素进行调序):

想一下最大堆,我们要想调序,要从最下层开始,但是,叶子需要调吗?暂时不需要。一个单独的节点就可以看成是一个最大堆了。由最大堆的性质可知,后面⌈n/2⌉⌈n/2⌉个元素是叶子。那么,我们就从第[n/2]个元素开始对其进行调序。

对数组{1,2,3,4,5,6,7,8,9,10,11}进行调序

叶子暂时是不需要重构的。从5开始,对5进行重构和上面的重构方法类似不赘述。对4重构、3、2、1。

分析时间复杂性:

对第i层的某元素进行重构时,最复杂就是 从这个元素到叶子上的一条路径的节点都被修改,时间复杂度为O(height-i+1)。

我们一共需要修改height-1层;且第i层最多有2i−12i−1个元素。

于是,总时间复杂度为

O((height−i+1)∗(height−1)∗2i−1∀i∈1,2,3,...,height−1(height−i+1)∗(height−1)∗2i−1∀i∈1,2,3,...,height−1)=O(2h2h)=O(n)

============================================================================


古人云:“没有代码就是耍流氓。” 代码如下:

/*MaxHeap*/
#include"xcept.h"
#include<iostream>
using namespace std;

template<class T>
class MaxHeap{
public:
    MaxHeap(int _maxsize=10);
    ~MaxHeap(){  delete[] heap; };
    MaxHeap<T>& Insert(T& t);//将元素t插入最大堆
    MaxHeap<T>& Delete(T& t);//将最大堆的根删掉,返回到t中
    void Initialize(T t[], int _currSize, int _maxSize);//初始化最大堆
    void output();
    void Deactive();
private:
    int currentSize;
    int maxSize;
    T *heap;//数组存放
};
template <class T>
MaxHeap<T>::MaxHeap(int _maxsize){//构造函数
    currentSize = 0;
    maxSize = _maxsize;
    heap = new T[maxSize + 1];//第一个元素不使用
}
template <class T>
MaxHeap<T>& MaxHeap<T>::Insert(T& t){//将元素t插入最大堆
    if (currentSize == maxSize) //满了拒绝插入
        throw NoMem();
    int i = ++currentSize;
    while (i != 1 && t > heap[i / 2]){
        heap[i] = heap[i / 2];//拉下父亲来
        i = i / 2;
    }
    heap[i] = t;
    return *this;
}

template <class T>
MaxHeap<T>& MaxHeap<T>::Delete(T& t){//删除最大元素
    if (currentSize == 0){//堆是空的,拒绝
        throw OutofBounds();
    }
    t = heap[1];
    T tail = heap[currentSize--];

    //重构
    int i = 1;
    int ci = 2;//记录i的孩子
    while (ci<=currentSize)
    {
        //找最大的孩子
        if (ci<currentSize && heap[ci+1]>heap[ci]){
            ci = ci + 1;
        }
        //判断是不是可以在当前位置i
        if (tail > heap[ci]){//yes
            break;
        }
        //no
        heap[i] = heap[ci];//把最大的孩子拿上去

        i = ci; //判断位置下移
        ci = i * 2;//仍记录孩子
    }
    heap[i] = tail;
    return *this;
}
template <class T>
void MaxHeap<T>::Initialize(T t[], int _currSize, int _maxSize){
    delete[]heap;
    heap = t;
    maxSize = _maxSize;
    currentSize = _currSize;

    //重构
    for (int i = maxSize / 2; i > 0; i--){
        T data = heap[i];//对第i个元素进行重构
        int ci = i * 2;
        while (ci <= currentSize)
        {
            //找最大的孩子
            if (ci<currentSize && heap[ci + 1]>heap[ci]){
                ci = ci + 1;
            }
            //判断是不是可以在当前位置i
            if (data > heap[ci]){//yes
                break;
            }
            //no
            heap[ci/2] = heap[ci];//把最大的孩子拿上去

            //判断位置下移
            ci = ci * 2;//仍记录孩子
        }
        heap[ci/2] = data;
    }

}
template<class T>
void MaxHeap<T>::Deactive(){
    heap = 0;
}
template<class T>
void MaxHeap<T>::output(){
    for (int  i = 1; i <= currentSize; i++)
    {
        cout << heap[i] << " ";
    }
    cout << endl;
}
class NoMem{
public :
    NoMem(){}
};
class OutofBounds{
public:
    OutofBounds(){

    }
};

这样,我们保证了每次出”队列”的都是最大的元素。即有了优先性。

========================================================================

堆这种基本的出入模式,可以完成一个人人皆知的排序算法—-堆排序。这是一种排序快且稳定的算法。

用堆来实现排序,看下面的代码:

#include"MaxHeap.h"
template <class T>
void heapSort(T arr[],int currSize,int maxSize){
    MaxHeap<T> mh;
    mh.Initialize(arr, currSize, maxSize);
    for (int i = currSize; i >0; i--)
    {
        T temp;
        mh.Delete(temp);
        arr[i] = temp;
    }
    //不让mh删掉arr
    mh.Deactive();
    for (int i = 1; i <= currSize; i++)
    {
        cout << arr[i] << " ";
     }
}

调用

    //第一个位置不放元素
    int x[20] = {0,4,2,3,8,9,1,5};
    heapSort(x, 7, 20);


结果


分析一下时间复杂度:

创建最大堆:n

删掉一个元素:log2nlog2n

删掉全部的元素:n∗log2nn∗log2n

总的操作次数:n+n∗log2nn+n∗log2n

则总的时间复杂度****O(nlognnlogn)(这是最快的排序了吧,好像是)


可视化最小堆算法 Visualizing min-heap algorithms with D3.js

benfrederickson.com/hea

---------------------

编辑于 2019-04-21 09:31