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的形式为
- 在第
层,有
个节点,但是我们都不在其中任何一个叶子节点调用max_heapify,因此其开销为0。
- 在第
层开始,有
个节点,每个节点可能会下沉1层,那么最差时间开销的情况
。
- 在第
层开始,有
个节点,每个节点可能会下沉2层,那么最差时间开销的情况
。
- 在第
层开始,有
个节点,每个节点可能会下沉3层,那么最差时间开销的情况
。
.....
那么,泛化为一般情况,在第
因此,如果我们自下向上逐级计算,那么初始化整个二叉堆的时间复杂度是
即
这是我们分析最糟糕情况下,二叉堆的时间开销。 解上面表达式A的一个近似值,需要用到微积分的数学基础。
首先我们引入
然后对该导数两边乘
非常巧妙的是
二叉堆模型的高度h逻辑上趋于
因为
二叉堆实例化的时间复杂度
根据其上面的时间复杂度计算是一个接近
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操作。我们找到二叉堆中最后一个非叶节点(即内部节点)的位置,从第
![03a4c1a506acc4a75e3557566b0665f9.png](https://i-blog.csdnimg.cn/blog_migrate/332fd9ce5cdec8c130169e0740d2f375.jpeg)
那么如何找到树中最后一个内部节点(即非叶子节点)呢?我们不妨来用下图来举例。我们知道图中两个是完整的二叉树,左图中最后一个内部节点是的索引是4,右图的最后一个内部节点是是3
![5ca24333ccdfd6ad32fd0fb58b55cbd9.png](https://i-blog.csdnimg.cn/blog_migrate/bbf14e4c4e56105f673683977052a43a.jpeg)
如果你自己动笔画出不同的完整二叉树形态,会总结到一个完整二叉树的特殊性质。
性质:完整二叉树的最后一个内部节点始终伴随着树中最后一个叶子节点,当然它可能是左子节点或右子节点。
也就是说,在n个节点的完整二叉树中,最后内部节点的索引我们用
也就是说,我这里解释了为什么for循环为什么总是从n/2-1位置开始遍历二叉堆并调用max_heapify方法。
max_heapify算法实现
我们需要明确的是对于单个内部节点的max_heapify调用的时间复杂度是
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。最后一个非叶子节点索引=
我们仅对节点索引按逆序即[4、3、2、1、0]执行heapify操作。r如下图所示
![51ee075acb576bf89bf4fcca928bd194.gif](https://i-blog.csdnimg.cn/blog_migrate/94bbc1194a2aaa8a2bfc94035891283d.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)的有关话题。