堆(C++) --优先级队列的代理者

堆(C++) --优先级队列的代理者

  聊堆不能不聊优先级队列,优先级队列就是决定哪个任务优先执行的队列,通常会有一个优先级的数据,通过数据的大小来判断优先级,实现优先级队列其实有三种方式:

  • 第一种:无序数组队列,这种在入队时的时间复杂度为O(1),但是出队时的时间复杂度是O(n);
  • 第二种:有序数组队列,这种在入队时的时间复杂度为O(n),但是出队时的时间复杂度是O(1);
  • 第三种:堆,最大堆或最小堆,入队出队都是O(logn),虽然相较于第一种第二种时会在入队或出队时较慢,但是当频繁进行操作,数据量大时,动态进行时,堆所需要的平均时间远远少于第一种和第二种

  通过上面三种优先级队列的比较就体现出了堆的优势,所以称堆为优先级队列的代理者,另外需要意识到的是,堆有两种基本形态最大堆和最小堆,而不管是哪种形态其本质都是一棵树,而且是一棵完全二叉树,最大堆的性质是父节点的值一定大于字节点,最小堆则是父节点的值一定小于子节点,兄弟节点大小无所谓~~
  以下演示的是一个最大堆的实现,对于最大堆的存储用的还是一个数组,只是这个数组中的元素有些讲究;元素的讲究主要体现在对这个堆数组插入(insert方法)元素和弹出根节点元素(popTheTop)的实现上:

  • insert方法:这个方法是对元素的插入,而因为要形成最大堆,在元素插入时就进行了最大堆的维护,维护的方法时insert方法中调用的spinUp方法;思路是在当前数组已有元素的最后一个元素的右边将元素插入到数组中,这是这个元素就相当于放在了最大堆最下面最后的一个子节点,插入的元素可能比上面的节点大,所以在spinUp中就是不断进行与父节点比较,如果比父节点大则交换,不断进行,这个过程需要O(long(n))的时间复杂度,比父节点小说明已经在正确位置,最大堆已维护好;
  • popTheTop方法:弹出根节点元素,在最大堆中也就是弹出最大值;弹出的方式是通过获取并返回根节点的元素,获取到了最大值,因为是弹出,所以这个最大值应该在最大堆中去掉,实际实现时是将堆的尾节点的值替换了根节点的值,然后堆的数据量减1;之所以要用尾节点替换掉根节点是因为,当根节点的最大值去掉之后,堆就不是一个最大堆了,要进行最大堆的维护了,取尾节点到根节点能确保维护时父节点比子节点大这一性质,维护的过程其实就是从根节点开始对左右子节点的大小进行比较,较大的再与父节点比较,如果比父节点大则该子节点和父节点换位,比父节点小,说明最大堆已维护好,最大堆的维护在spinDown方法中实现;

最大堆

// 最大堆类实现
template <typename T>
class MaxHeap {

private:
    // 存放堆数组
    T* heapList;
    // 容量
    int capacity;
    // 当前堆元素个数,也是当前元素下标位置,从1开始,0位不放,这样方便
    int count;
    // 插入元素时调用,将插入元素上调到合适位置
    // 这里如果出现插入元素与某个元素相同时,不会换位,插入元素会作为与之相同元素的子节点
    void spinUp(int size) {
        while(size/2 >= 1 && heapList[size]>heapList[size/2]) {
            swap(heapList[size],heapList[size/2]);
            size /= 2;
        }
    }

    void spinDown(int p) {
        // 父节点
        int flag = p;
        // 考虑有左右子节点的情况
        while(flag*2+1<=count) {
            int largerIndex;
            if(heapList[flag*2] > heapList[flag*2+1]) {
                flag = flag*2;
            } else {
                flag = flag*2+1;
            }
            if(heapList[p] < heapList[flag]) {
                swap(heapList[p],heapList[flag]);
                p = flag;
                // 如果元素比其左右子节点都大则结束循环
            } else {
                break;
            }
        }
        // 考虑只有左节点的情况,因为堆必定为完全二叉树,所以可能出现最后的元素为父节点的左子节点,没有右子节点
        if(flag*2<=count) {
            if(heapList[p] < heapList[flag*2]) {
                swap(heapList[p],heapList[flag*2]);
            }
        }
    }

public:
    // 构造方法,常规,创建的是一个相应容量的空堆
    MaxHeap(int capacity) {
        heapList = new T[capacity+1];
        // 注意容量不要+1,上面+1是因为下标从1开始
        this->capacity = capacity;
        count = 0;
    }
    // 构造方法,与堆的实现无关,可以跳过,
    // 给堆排序的第二种玩法用的构造方法,创建一个存储了数组中各元素的堆,然后以heapify的方式进行最大堆化
    // 主要思路是,所有叶节点都是一个最大堆,如果叶节点在k层,往上一级k-1层进行最大堆变换,那么k-1层往下都是最大堆了,再往上层走,
    // 直到k=1,整个堆就是最大堆了,之所以逐层往上的过程中只需要比较当前节点的两个子节点,这两个子节点肯定是其各自分支中最大的
    MaxHeap(T arr[], int capacity) {
        this->capacity = capacity;
        count = capacity;
        heapList = new T[capacity+1];
        // 堆的数组中的元素是从下标1开始存放的
        for(int i=1;i<=capacity;i++) {
            heapList[i] = arr[i-1];
        }
        // 从capacity/2开始逐个堆化,capacity/2就是为了获取上一级,但是因为这里还要进行赋值
        // 因为堆中的元素下标是从1开始的,所以这里直接用capacity
        for(int i=capacity/2;i>=1;i--) {
            spinDown(i);
        }
    }
    // 析构方法
    ~MaxHeap() {
        delete [] heapList;
    }

    // 获取当前堆中元素个数
    int getSize() {
        return count;
    }

    T* getHeap() {
        return heapList;
    }

    // 插入元素,元素在底部插入,然后上调到合适位置
    void insert(T element) {
        assert(count < capacity);
        heapList[++count] = element;
        spinUp(count);
    }

    // 取出最大的元素,也就是根节点元素
    T popTheTop() {
        assert(count > 0);
        // 因为从下标1开始记录,所以下标1为根节点
        T maxElement = heapList[1];
        // 还有元素时,要进行元素调整,将最后一个元素调到根节点,然后进行下调到合适位置,维护最大堆
        // count--是因为,最后一个元素调上根的位置了,堆的元素数减1
        heapList[1] = heapList[count--];
        spinDown(1);

        return maxElement;
    }

};

  最小堆的实现大同小异,需要注意的是这里的最大堆实现为了方便是从数组下标1开始,而不是0开始

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值