大顶堆删除最大值_第8篇:C++ 数据结构-最大堆

MaxHeap类接口定义

最大堆的类接口定义,我们命名为MaxHeap,如下代码所示

#ifndef __MAXHEAP_HH__
#define __MAXHEAP_HH__
#include <iostream>
#include <limits>
#include "./Heap.hh"

template<class T>
class MaxHeap:public Heap<T>{

private:
    //泛型T中的最大值
    const T NUM_MAX=std::numeric_limits<T>::max();

    //元素值上浮操作
    void shiftUp(int i);

    //数值
    void increase(int,T);

    //最小堆所有节点重排序
    void max_heapify(int,int);

    void max_heapify(int);

    void init_heap(int);
public:
    MaxHeap();
    MaxHeap(const T*,int);

    void insert(T);

    void removeAt(int);

    T get_max();

    T get_min();

    T extractMax();
    
    template<typename R>
    friend std::ostream &operator<<(std::ostream &out,const MaxHeap<R> &hp);
};
#endif

二叉堆实例化的时间复杂度

我们需要考虑一下创建二叉堆的运行时间。像之前二叉树概念篇一样,这里对n做一些假设简化二叉堆模型。在这种情况下,最方便的假设是 n的形式为

,其中
是二叉堆的高度,并且二叉堆是一颗完美二叉树。初始化二叉堆的运行时间取决于二叉堆元素在heapify终止递归之前其最小值元素下沉的距离。 在最坏的情况下,从根节点到叶子节点的任意路径中的最小值的元素可能会一直下沉到叶子节点所在层。 我们逐层计算二叉堆的时间开销。
  • 在第
    层,有
    个节点,但是我们都不在其中任何一个叶子节点调用max_heapify,因此其开销为0。
  • 在第
    层开始,有
    个节点,每个节点可能会下沉1层,那么最差时间开销的情况
  • 在第
    层开始,有
    个节点,每个节点可能会下沉2层,那么最差时间开销的情况
  • 在第
    层开始,有
    个节点,每个节点可能会下沉3层,那么最差时间开销的情况

.....

那么,泛化为一般情况,在第

层上,有
个节点,每个节点可能会下沉j层。那么最差时间开销的情况

因此,如果我们自下向上逐级计算,那么初始化整个二叉堆的时间复杂度是

,设该表达式A

这是我们分析最糟糕情况下,二叉堆的时间开销。 解上面表达式A的一个近似值,需要用到微积分的数学基础。

首先我们引入

时的无限一般几何级数的积分公式
,求其导数可知

然后对该导数两边乘

,可得
,设为表达式B

非常巧妙的是

,代入表达式B

,即
,和我们上面表达式A形式相近。

二叉堆模型的高度h逻辑上趋于

,从无限级数的积分可以
得到简单的近似值

, 即

因为

,即
,其简化形式如下

二叉堆实例化的时间复杂度

根据其上面的时间复杂度计算是一个接近

。对应的核心算法是max_heapify。MaxHeap构造函数中需要外层一个for循环遍历传入每个节点的索引到max_heapify方法中执行内部的排序操作。如下代码所示
template<class T>
void MaxHeap<T>::init_heap(int n){
    //从最后一个非叶子节点的索引开始遍历
    for (int i = n/2-1; i>=0; i--){
        max_heapify(this->d_len,i);
    }
}
  • 步骤1:max_heapify(7,2):交换索引2和索引5之间的节点值
  • 步骤2:max_heapify(7,5) 立即退出。返回外部for循环。
  • 步骤3:max_heapify(7,1) 交换索引1和索引4之间的节点值
  • 步骤4:max_heapify(7,4) 立即退出。返回外部for循环。
  • 步骤5:max_heapify(7,0) 交换索引0和索引2之间的节点值
  • 步骤6:max_heapify(7,2) 交换索引2和索引6之间的节点值
  • 步骤7:max_heapify(7,4) 立即退出。返回外部for循环。

从上面的max_heapify演算如下图所示,实际上max_heapify在每次递归真正执行交换两个节点值的只有传入非叶子节点索引,例如步骤1、3、5、6。当传入叶子节点索引时函数栈的开销几乎是忽略不计的。换句话说不需要对叶子节点进行max_heapify操作。我们找到二叉堆中最后一个非叶节点(即内部节点)的位置,从第

层以自下向上顺序执行每个内部节点的heapify操作。

03a4c1a506acc4a75e3557566b0665f9.png

那么如何找到树中最后一个内部节点(即非叶子节点)呢?我们不妨来用下图来举例。我们知道图中两个是完整的二叉树,左图中最后一个内部节点是的索引是4,右图的最后一个内部节点是是3

5ca24333ccdfd6ad32fd0fb58b55cbd9.png

如果你自己动笔画出不同的完整二叉树形态,会总结到一个完整二叉树的特殊性质。

性质:完整二叉树的最后一个内部节点始终伴随着树中最后一个叶子节点,当然它可能是左子节点或右子节点

也就是说,在n个节点的完整二叉树中,最后内部节点的索引我们用

表示,那么
表示最后一个节点的索引,那么他们的关系可以表示为

也就是说,我这里解释了为什么for循环为什么总是从n/2-1位置开始遍历二叉堆并调用max_heapify方法。

max_heapify算法实现

我们需要明确的是对于单个内部节点的max_heapify调用的时间复杂度是

,但由于外部循环调用的时间开销
,对于n个节点的二叉堆初始化的时间复杂度实际上也为
template<class T>
void MaxHeap<T>::max_heapify(int n,int i){
    int L_IDX=2*i+1;
    int R_IDX=2*i+2;

    int LARGE_IDX=i;

    //如果左节点大于当前被标记的LARGE_IDX的节点,即更新当前LARGE_IDX为最新的L_IDX
    if(L_IDX<=n-1 && this->d_arr[L_IDX]>this->d_arr[LARGE_IDX])
        LARGE_IDX=L_IDX;
        
    //如果当右节点大于当前被标记的LARGE_IDX的节点,即更新当前LARGE_IDX为最新的R_IDX
    if(R_IDX<=n-1 && this->d_arr[R_IDX]>this->d_arr[LARGE_IDX])
        LARGE_IDX=R_IDX;
    
    if(LARGE_IDX!=i){
        swap(&(this->d_arr[i]),&(this->d_arr[LARGE_IDX]));
        max_heapify(n,LARGE_IDX);
    }
}

假设我们传入如下数组,目标是让其实例化一个最大堆。

d={1, 2, 6, 7, 9, 13, 10, 11, 8, 23, 42}

那么源数组的顺序逐个拷贝入二叉堆的内存会是如下图的起始状态。由于该树总节点数是11。最后一个非叶子节点索引=

。因此,最后一个内部节点的的元素值是9

我们仅对节点索引按逆序即[4、3、2、1、0]执行heapify操作。r如下图所示

51ee075acb576bf89bf4fcca928bd194.gif

构造函数

不论最大堆还是最小堆,完成整个对象的实例化其时间开销是

template<class T>
MaxHeap<T>::MaxHeap(const T* data,int size):Heap<T>::Heap(data,size){
    if(this->d_arr!=nullptr){

        for (size_t i = 0; i < size; i++)
        {
            this->d_arr[i]=data[i];
        }
        this->d_len=size;
        init_heap(this->d_len);
    }
}

其他核心算法

template<class T>
void MaxHeap<T>::shiftUp(int i){
    //最大值上浮
    while(i!=0 && this->d_arr[this->parent(i)]<this->d_arr[i]){
        swap(&(this->d_arr[i]),&(this->d_arr[this->parent(i)]));
        i=this->parent(i);
    }
}

//将索引“i”处的键的值减小为新值。假定新值小于harr[i]
template<class T>
void MaxHeap<T>::increase(int k,T data){
    if(k<this->d_len){
        this->d_arr[k]=data;
        this->shiftUp(k);
    }else{
        std::cout<<""<<std::endl;
    }
}

剩下的算法其核心思想和最小堆是相同的,我就不说废话

插入操作

template<class T>
void MaxHeap<T>::insert(T data){
    if(this->d_len<this->d_capacity){
        this->d_len++;
        int k=this->d_len-1; //插入元素的位置
        this->d_arr[k]=data;
        
        if(k>=1){
            shiftUp(k);
        }
    }else{
        std::cout<<"无法插入新元素"<<std::endl;
    }
}

抽取最大值

template<class T>
T MaxHeap<T>::extractMax(){
    if(this->d_len==0){
        return NUM_MAX;
    }
    if(this->d_len==1){
        this->d_len--;
        return this->d_arr[0];
    }

    T r=this->d_arr[0];
    this->d_arr[0]=this->d_arr[this->d_len-1];
    this->d_len--;

    max_heapify(0);
    return r;
}

删除操作

template<class T>
void MaxHeap<T>::removeAt(int k){
    increase(k,NUM_MAX);
    extractMax();
}

其他辅助操作

template<class T>
T MaxHeap<T>::get_max(){
    return this->d_arr[0];
}

template<class T>
T MaxHeap<T>::get_min(){
    return this->d_arr[this->d_len-1];
}

template<typename R>
std::ostream &operator<<(std::ostream &out,const MaxHeap<R> &hp){
    out<<"[";
    for(auto i=0;i<hp.d_len-1;i++){
        out<<hp.d_arr[i]<<",";
    }

    int e=hp.d_len-1;

    out<<*(hp.d_arr+e)<<"]";
    return out;
}

代码测试:

#include <iostream>
#include "libs/MaxHeap.cpp"

int main()
{
    int a[11]={1,2,6,7,9,13,10,11,8,23,42};
    MaxHeap<int> m=MaxHeap<int>(a,11); 
    std::cout<<"高度:"<<m.height()<<std::endl;
    std::cout<<"节点数:"<<m.length()<<std::endl;
    std::cout<<"最大节点数:"<<m.capacity()<<std::endl;

    std::cout<<m<<std::endl;
    
    for (size_t i =0; i <m.length(); i++)
    {
        std::cout<<"Index:"<<i<<": "<<m[i]<<"  左节点  "<<m[m.lIndex(i)]
             <<" 右节点:"<<m[m.rIndex(i)]<<std::endl;
    }

    std::cout<<"删除节点5:"<<m[5]<<std::endl;
    m.removeAt(5);
    std::cout<<"节点数:"<<m.length()<<std::endl;
    std::cout<<m<<std::endl;

    std::cout<<"删除节点5:"<<m[5]<<std::endl;
    m.removeAt(5);
    std::cout<<"节点数:"<<m.length()<<std::endl;

    for (size_t i =0; i <m.length(); i++)
    {
        std::cout<<"Index:"<<i<<": "<<m[i]<<"  左节点  "<<m[m.lIndex(i)]
             <<" 右节点:"<<m[m.rIndex(i)]<<std::endl;
    }

    std::cout<<"高度:"<<m.height()<<std::endl;
    std::cout<<"节点数:"<<m.length()<<std::endl;
    std::cout<<"最大节点数:"<<m.capacity()<<std::endl;
    std::cout<<m<<std::endl;

    std::cout<<"最大值:"<<m.get_max()<<std::endl;
    std::cout<<"最小值:"<<m.get_min()<<std::endl;

    return 0;
}

程序输出

高度:3
节点数:11
最大节点数:15
[42,23,13,11,9,6,10,7,8,2,1]
Index:0: 42 左节点 23 右节点:13
Index:1: 23 左节点 11 右节点:9
Index:2: 13 左节点 6 右节点:10
Index:3: 11 左节点 7 右节点:8
Index:4: 9 左节点 2 右节点:1
Index:5: 6 左节点 0 右节点:0
Index:6: 10 左节点 0 右节点:0
Index:7: 7 左节点 0 右节点:0
Index:8: 8 左节点 0 右节点:0
Index:9: 2 左节点 0 右节点:0
Index:10: 1 左节点 0 右节点:0
删除节点5:6
节点数:10
[42,23,13,11,9,1,10,7,8,2]
删除节点5:1
节点数:9
Index:0: 42 左节点 23 右节点:13
Index:1: 23 左节点 11 右节点:9
Index:2: 13 左节点 2 右节点:10
Index:3: 11 左节点 7 右节点:8
Index:4: 9 左节点 0 右节点:0
Index:5: 2 左节点 0 右节点:0
Index:6: 10 左节点 0 右节点:0
Index:7: 7 左节点 0 右节点:0
Index:8: 8 左节点 0 右节点:0
高度:3
节点数:9
最大节点数:15
[42,23,13,11,9,2,10,7,8]
最大值:42
最小值:8

总结:

我们用了3篇的来讨论了二叉堆的几乎所有特性,还有一个优先队列的数据结构,它是基于最大堆或最小堆构建的。笔者没兴趣再写。有兴趣的读者请自行实现。

下一篇,有空时在更新二叉查找树(Binary Search Tree)的有关话题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值