堆的概念以及实现堆

  普通队列就是遵循时间原则,先进先出,后进后出的一种队列结构;而优先队列则是按照优先级来决定出队的次序,与何时进入队列无关,实际生活中有很多优先队列的例子:医院候诊病人中,急诊病人优先看病,而不是按照病人到达医院的先后次序来,这就是一个优先队列的例子。

  优先队列适合应用于动态场景中,比如游戏中自动作战,当存在多个敌人时,不是按照敌人出现的顺序进行攻击,而是根据某个优先级(比如先攻击血量最大的)进行攻击,为什么说是动态的?是因为不是在攻击完血量最大的敌人之后就接着攻击剩余的敌人,实际上还会有其他的敌人陆续出现,这时候再在所有敌人中选出优先级最高的进行攻击。

  实际上,除了在动态数据的处理上优先队列很有优势,优先队列在处理静态数据时有时也是很有优势的。

  优先队列的实现方式有几种:



  当总共需要进行N次时,采用普通数组和顺序数组实现的优先队列的时间复杂度最差为O(n^2),而采用堆实现的优先队列时间复杂度为O(Nlogn)。

  接下来介绍堆这种数据结构。

  什么是堆?经典的是二叉堆,对应的就是一棵二叉树,二叉堆又分为最大堆和最小堆。最大堆:堆中每个节点的值都不大于其双亲节点的值,根结点为最大值;最小堆:堆中每个节点的值都不小于其双亲节点的值,根结点为最小值。二叉堆总是一棵完全二叉树。

  二叉堆的经典存储方式是使用数组来存储。



  根据这种存储方式,对数组中下标为 i 的元素,其双亲结点就对应数组中下标为 i/2 的元素,其左孩子为数组中下标为 2i 的元素,其右孩子为数组中下标为 2i+1 的元素。

  接下来以最大堆为例,用C++实现最大堆:

#include <iostream>
#include <cassert>
#include <algorithm>
#include <cstdlib>
#include <ctime>

using namespace std;

template<typename Item>
class MaxHeap
{
private:
    Item *data;
    int count;
    int capacity;
    void shiftUp(int k)
    {
        //凡是涉及到数组下标的,都要限定它不出界
        while(k > 1 && data[k] > data[k / 2])
        {
            swap(data[k], data[k / 2]);
            k /= 2;
        }
    }
    void shiftDown(int k)
    {
        //当k有孩子的情况下开始shiftDown操作
        while( 2*k <= count )
        {
            int j = 2*k;  //j就是要和data[k]交换的元素的位置下标
            //判断是否有右孩子
            if( j+1 <= count && data[j+1] > data[j] )
            {
                j = j + 1;
            }
            if(data[k] >= data[j])
            {
                break;
            }
            swap(data[k], data[j]);
            k = j;
        }
    }

public:
    MaxHeap(int capacity)
    {
        data = new Item[capacity + 1];  //从1号单元开始存放
        count = 0;
        this->capacity = capacity;
    }
    ~MaxHeap()
    {
        delete[] data;
    }
    //返回堆现在的大小
    int size() const
    {
        return count;
    }
    //判断堆是否为空堆
    bool isEmpty() const
    {
        return count == 0;
    }
    //向堆中插入一个元素
    void insert(Item item)
    {
        //判断count+1是否出界
        assert(count + 1 <= capacity);

        data[count + 1] = item;
        count++;
        //从count位置处开始向上调整堆
        shiftUp(count);  //这个函数不需要被用户调用,所以设为私有
    }
    //取出堆中的一个元素(只能取出根结点)
    Item extractMax()
    {
        //首先判断堆是否为空
        assert(count > 0);

        Item ret = data[1];
        data[1] = data[count];
        count--;
        shiftDown(1);
        return ret;
    }
    //打印堆中内容
    void printData() const
    {
        for(int i = 1; i <= count; i++)
        {
            cout << data[i] << " ";
        }
        cout << endl;
    }
};

int main()
{
    MaxHeap<int> maxheap = MaxHeap<int>(100);  //利用构造函数初始化
    cout << maxheap.size() << endl;

    //随机插入几个元素
    srand(time(0));
    while(maxheap.size() < 10)
    {
        int item = rand() % 100 + 1;
        maxheap.insert(item);
    }
    cout << maxheap.size() << endl;
    maxheap.printData();

    //取出堆中元素(实际上就是将堆中元素从大到小排序的过程)
    while(!maxheap.isEmpty())
    {
        cout << maxheap.extractMax() << " ";
    }
    cout << endl;
    return 0;
}
  我们将最大堆声明为一个类,因为堆的大小是由用户定义的,所以使用指针 Item *data 来动态分配内存(使用构造函数 MaxHeap(int capacity)来实现),在构造函数中使用 new[] 动态分配内存后,相应的就需要在析构函数中 delete[] 内存。

  接下来分析向堆中插入元素的过程:只能在数组的最后一个位置插入元素,此时的完全二叉树不满足堆的定义,所以需要进行向上调整堆的过程,若新插入节点值大于其双亲节点的值,则交换两者位置,重复此步骤直至根结点(由于用户只需要调用插入元素的函数,不需要调用向上调整的函数,所以将shiftUp(int k)定义在私有成员中)。

  接下来分析取出堆中元素的过程:只能取出根节点的值(最大值),此时根节点的值用堆中最后一个元素的值代替,此时新的完全二叉树不满足堆的定义,所以需要向下调整堆。若根节点的值小于其左右孩子中的较大值,则交换两者位置,重复此步骤,直到其没有孩子为止(由于用户只需要调用取出元素的函数,不需要调用向下调整的函数,所以将shiftDown(int k)定义在私有成员中)。

  在main函数中,最后的while(!maxheap.isEmpty())循环按从大到小的顺序打印出堆中所有元素,实际上就是一个排序的过程,打印结果如下:



  在上述代码中,需要多次进行swap操作,这是一项耗时的操作,最好使用赋值操作来取代之:即先找到要交换的位置,然后赋值。优化后的 shiftUp() 和 shiftDown() 代码如下:

    void shiftUp(int k)
    {
        //凡是涉及到数组下标的,都要限定它不出界
        //优化:用赋值取代交换
        Item item = data[k];
        while( k>1 && data[k/2] < item )
        {
            data[k] = data[k/2];
            k /= 2;
        }
        data[k] = item;
    }
    void shiftDown(int k)
    {
        //当k有孩子的情况下开始shiftDown操作
        //优化:用赋值取代交换
        Item item = data[k];
        while( 2*k <= count )
        {
            int j = 2*k;
            if(j + 1 <= count && data[j + 1] > data[j])
            {
                j = j + 1;
            }
            if(data[j] <= item)
            {
                break;
            }
            data[k] = data[j];
            k = j;
        }
        data[k] = item;
    }

  到这里,如何实现最大堆就讲完了,下一节将实现堆排序以及其优化。




  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值