关于二叉索引堆的使用、堆排序

15 篇文章 0 订阅
8 篇文章 0 订阅

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;
    }

};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值