CSDN新人:爱和九九
哈哈哈,接触CSDN已经有一年多了吧,今天才开始写第一篇博客,说来真的惭愧呢!本来以为自己在电脑上做好笔记,就够了,不用发到网上来,没想到,写博客这么重要!
反正,以后还请各位大佬多多指教哇~
如何写出一个完整的带索引的二叉堆类,以及用它实现堆排序。
首先呢,我们先声明这个堆:
#include<bits/stdc++.h>
using namespace std;
template<typename T>
class IndexMaxHeap
如上,为了使这个类应用更加广泛,我们用template模板写这个类,并将变量名命名为大众变量名T。
对于这个类,我需要三个变量数组: a(用来存储数据)、indexes(用来存储a的地址)、rev(用来存储indexes的地址,即indexes的反向指针)
另外我还规定了这个堆的容量和堆内当前的数目,分别表示为capacity和count。
class IndexMaxHeap{
private:
T *a;
int count;
int capacity;
int *indexes;
int *rev;
}
那么,我们接下来的任务是处理a的同时,维护好indexes和rev。
实际上,对于我所介绍的这个堆,真正的堆是indexes,而a数组是不会变的。因为indexes永远都是int类型的,交换、赋值等操作都比较方便,而a可能是一个存储了几百万字符的变量,处理起来很麻烦。
以最大堆为例:
indexes(堆)的特性:
最上面的元素应该是整个数组中最大的一个元素,从这个元素下分,每一个元素应该有两个子节点,并且子节点的元素应该比父节点要大。对于最后一层没有子节点的“树叶”,如果能完全填满最后一层,则填满,否则的话,就集中在最左侧。
本来我们应该处理数组a,来使数组a具有堆的特性。但是考虑到a的复杂和未知,所以我们维护充当a的指针的数组indexes来确保a满足堆的特性,而对于a,除了存储和删除,我们不做任何处理。
rev的特性:
和indexes相反,即通过indexes的值找到indexes在数组中相应的位置。
例如:
如果indexes[i]=j;
那么rev[j]=i;
我们设置rev,是方便后面一个change操作(往下看吧骚年)。
对于一个类,我们第一补当然是实现它的构造函数啦~
public:
IndexMaxHeap(int n){
indexes=new int[n+1];
a=new T[n+1];
rev=new int[n+1];
for(int i=0;i<=n;i++){
indexes[i]=0;
rev[i]=0;
}
count=0;
capacity=n;
}
在这里我只实现创建一个空的堆的构造函数,你们还可以写一个直接把数组存进去的构造函数。
刚刚建立一个大小为n的堆(n的大小由用户来控制)。
相应的,我们来开辟a、rev、indexes的空间。因为一开始没有元素,所以indexes和rev都为0。
接下来我们实现析构函数,释放掉三个数组开辟的空间:
~IndexMaxHeap(){delete[] a,indexes,rev;}
那么,接下来最基本的,存入一个数据:
我们把它存到树的最下层的从左往右数第一个空的树叶里,然后,我们进行shiftup处理。
shiftup,顾名思义,就是把它相应地往上调啦。我们顺便实现这个函数如下:
void Insert(int i,T t){
i+=1;
indexes[count+1]=i;
rev[i]=count+1;
a[i]=t;
count++;
ShiftUp(count);
}
void ShiftUp(int c){
while(c>1&&a[indexes[c/2]]<a[indexes[c]]){
swap(indexes[c/2],indexes[c]);
rev[indexes[c/2]]=c/2;
rev[indexes[c]]=c;
c/=2;
}
}
因为我们不对a数组进行操作,所以我们进行shiftup的时候,仅仅是将存入的indexes的子节点与父节点交换——记得,我们的indexes才是真正的那个堆!
需要上移的我们向上移动,知道它不能再移动,shiftup函数结束。同时,每移动一次,我们都要更新一下rev,来维护它作为反向指针的特性。
到此,这个堆的插入操作就完成啦。
下面来介绍删除定点并弹出的操作:
T ExtractMax(){
assert(count>0);
swap(indexes[1],indexes[count]);
rev[indexes[1]]=1;
rev[indexes[count]]=0;
count--;
ShiftDown(1);
return a[indexes[count+1]];
}
int ExtractMaxIndex(){
assert(count>0);
return indexes[1];
}
void ShiftDown(int m){
while(m*2<=count){
int k=m*2;
if(k+1<=count&&a[indexes[k+1]]>a[indexes[k]]){
k++;
}
if(a[indexes[k]]<=a[indexes[m]])break;
swap(indexes[k],indexes[m]);
rev[indexes[k]]=k;
rev[indexes[m]]=m;
m=k;
}
}
我们先说extractmax操作:即弹出顶端元素并删除。我们将顶端元素和最末端元素交换,然后弹出这个最末端元素,最后再让count减少1,那么最末端的这个元素就已经不在我们堆的范围了,也就删除了。同时,弹出的元素在更新rev的时候,直接让rev等于0即可,因为我们是不会访问0这个地址的,所以也就相当于没有这个地址啦~
而extractmaxindex 操作,即弹出顶端元素的位置,不需要进行删除。
对于辅助函数shiftdown,更是和shiftup如出一辙,只不过操作方向完全相反而已。
我们在进行以上操作的时候,维护好indexes和rev,就已经完全实现了一个堆的插入和删除操作啦~
接下来还有一点——我们只能删除顶点元素,只能在树的末尾添加元素,那么,能不能修改某一个节点的元素呢?当然可以辣~这也是我写rev的原因所在。
我们先写一个确保修改位置有元素的函数:
bool contain(int i){
assert(i>0&&i+1<=capacity);
return rev[i]!=0;
}
接下来,我们实现这个change函数:
change(int i,T newT){
assert(contain(i));
i++;
a[i]=newT;
int j=rev[i];
ShiftDown(j);
ShiftUp(j);
return;
}
如上。
我们要修改某个节点的值,得首先确保这个节点有值哇。其实在其他更复杂的操作中,都需要保证这一点。
可以看到,在change函数里面,我们直接让int j=rev[i];这就是rev的作用所在——根据indexes指向的位置,就能够找到indexes在数组中的位置,从而对它进行调整来维护堆的性质。如果没有rev的话,我们就只能遍历数组了。而如果数组很大,比如说我们这个堆有999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999个元素,这时候,遍历一遍需要好多好多好多好多年,而有rev数组的话,就不用担心这个问题啦~
还有一点就是,在写类的时候,对于副主函数contain、shiftdown、shiftup,最好写在private里面,因为这些函数本身是不应该被用户调用的。
还有,我写的这个堆,是从1开始计数的,大家也可以写一个从0开始计数的堆。
接下来,我利用这个思想,实现一个堆排序。
template<typename T>
void __shiftDown(T a[],int n,int mm){
int m=mm;
int count=n;
while(m*2+1<count){
int k=m*2+1;
if(k+1<count&&a[k+1]>a[k]){
k++;
}
if(a[k]<=a[m])break;
swap(a[k],a[m]);
m=k;
}
}
template<typename T>
void heapSort(T arr[],int n){
for(int i=(n-1)/2;i>=0;i--){
__shiftDown(arr,n,i);
}
for(int i=n-1;i>0;i--){
swap(arr[0],arr[i]);
__shiftDown(arr,i,0);
}
}
因为我们处理的通常是数组,所以我就直接交换啦~其实有的时候需要一个indexes来做堆排序,比方说牛客网第九次训练某题……因为牛客不让透题,我就不多说了233333333.
而对于那个堆的类,这个高级的数据结构本身不是用来排序的。因为你会发现,用这个堆排序,还不如直接快速排序或者归并排序来的效率高。
堆可以处理这种问题:
给你999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999个数据,让你找到最大的五个数并输出。
想一想,对999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
个数字进行排序,是不是很可怕?!就连快速排序,估计也得上亿年下不来,nlogn已经是个天文数字了。
然而这个题只要求找5个数字,并没有要求排序。
而我们如果使用一个大小为5的堆,将这999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
个数字遍历,结束后,存在堆里面的元素就是最大的五个元素。因此,只需要O(n)的时间复杂度,我们就能解决这个问题,这个时候,O(n)和O(nlogn)的差距就会是个天文数字。
最后,把类的完整代码年在这,感谢观看,一起进步呦~
#include<bits/stdc++.h>
using namespace std;
template<typename T>
class IndexMaxHeap{
private:
T *a;
int count;
int capacity;
int *indexes;
int *rev;
void ShiftUp(int c){
while(c>1&&a[indexes[c/2]]<a[indexes[c]]){
swap(indexes[c/2],indexes[c]);
rev[indexes[c/2]]=c/2;
rev[indexes[c]]=c;
c/=2;
}
}
void ShiftDown(int m){
while(m*2<=count){
int k=m*2;
if(k+1<=count&&a[indexes[k+1]]>a[indexes[k]]){
k++;
}
if(a[indexes[k]]<=a[indexes[m]])break;
swap(indexes[k],indexes[m]);
rev[indexes[k]]=k;
rev[indexes[m]]=m;
m=k;
}
}
bool contain(int i){
assert(i>0&&i+1<=capacity);
return rev[i]!=0;
}
public:
IndexMaxHeap(int n){
indexes=new int[n+1];
a=new T[n+1];
rev=new int[n+1];
for(int i=0;i<=n;i++){
indexes[i]=0;
rev[i]=0;
}
count=0;
capacity=n;
}
IndexMaxHeap(T arr[],int n){
capacity=n;
indexes=new int[n+1];
a=new T[capacity+1];
rev=new int[n+1];
for(int i=0;i<n;i++){
a[i+1]=arr[i];
}
count=n;
for(int i=count/2;i>=1;i--)
ShiftDown(i);
}
~IndexMaxHeap(){delete[] a,indexes,rev;}
int size(){return count;}
bool isEmpty(){return count==0;}
T ExtractMax(){
assert(count>0);
swap(indexes[1],indexes[count]);
rev[indexes[1]]=1;
rev[indexes[count]]=0;
count--;
ShiftDown(1);
return a[indexes[count+1]];
}
int ExtractMaxIndex(){
assert(count>0);
return indexes[1]-1;
}
void Insert(int i,T t){
i+=1;
indexes[count+1]=i;
rev[i]=count+1;
a[i]=t;
count++;
ShiftUp(count);
}
T GetItem(int i){
assert(contain(i));
return a[i];
}
change(int i,T newT){
assert(contain(i));
i++;
a[i]=newT;
int j=rev[i];
ShiftDown(j);
ShiftUp(j);
return;
}
};