数据结构--堆(Heap)

堆(Heap)

 本文主要介绍以下内容:
  • Heap的实现
  • HeapSort(堆排序)
  • 完善各种堆的函数接口
  • TopK经典问题

堆就是一棵完全二叉树。因为它的某些性质,我们可以用数组存储。

  • 堆的性质
    1 完全二叉树的性质。
    (1)将根节点(树的最上面的结点)序号设为0,然后由左至右,由上至下编号。
    (2)父节点编号若为i,左孩子编号为(21+1),右孩纸为(21+2);
    (3)孩子的编号为i,父亲的编号则为 (i-1)/2 ;
    堆的性质

2 父节点大于等于(或者小于等于)所有的子孙。
父节点大于等于子孙的堆叫大堆,反之叫做小堆。
大堆
小堆
不写代码的程序员不是好程序员,接下来,我们来实现Heap。

Heap的实现

//我们需要大量的函数接口
typedef int HeapDataType;  //增加代码的移植性
typedef struct Heap{      //堆的结构
    HeapDataType* _a;    //数组
    int _size;          //堆的实际数据个数
    int _capacity;      //数组容量
}Heap;
   //初始化堆
  //hp是Heap结构体变量, a是数组   n是数组大小
 void HeapCreat(Heap* hp ,HeapDataType* a ,int n);
 //入堆
 void HeapPush(Heap* hp ,HeapDataType x);
 //从堆中删除
 void HeapPop(Heap* hp);
 //为了减少大家的恐惧,我们先写这三个(后面多着呢)。

首先大家应该思考一个问题,我随意给你一个数组,你怎样实现完全二叉树到堆的建立?即,怎样建堆?
这里,我们给大家来一个算法,向下调整算法,我们以建立一个小堆为例。假设有这样一个完全二叉树:
完全二叉树
显然这不是一个小堆,因为0号(即编号为0的结点,为了方便叙述)大于1号,所以我将他俩换下位置。
第一次交换
我们先不管其他的,先盯着值为20的结点。我们发现,这时候1号比3和4都大,那么换谁呢?显然换3号,因为3号比4号小!
所以有,
第二次交换
同样的,3号大于7号和8号,交换7号(较小的)。
第三次交换
好了,经过这几步交换,看看我们得到了什么。
好像什么都没得到?不,我们将一个比较大的数字移到了下边,我们用的方法也值得我们将它写下来。

//向下调整算法
//三个参数:数组  ,数组大小  ,  根节点
void AdjustDown(HeapDataType* a, int n, int root)
{
     int parent = root;
     int child = parent*2 + 1; //二叉树的性质
     while(child < n)
     if(child+1 < n && a[child] > a[child+1])//找出小的孩子
      {
            ++child;
     }
     if(a[parent] > a[child])  //如果父亲大于孩子,则交换
     {
         int temp = a[parent];    
          a[parent] = a[child];
          a[child] = temp;
  
          parent = child;   //迭代
          child = parent*2 + 1;
     }
     else {           //否则任务完成,直接跳出循环。
            break;
     }
}

我们在写的时候巧妙运用了 ++child,避免分左右孩子,接下来我们将用这个算法建堆。
我们意识到,如果从根节点向下调整不能建成小堆,但是,可以移动一个较大的数下沉,那么如果我从下开始用这个算法呢?
初始图像
我们从最后一个有孩子的父节点(3号)开始会怎么样呢?
第一步
然后我们到第二个父节点,2号。
第二部
接着是1号父节点。
3
最后0号结点。
4
我们发现,成了!!!!
这就是我们想要的结果!
代码如下:

void HeapCreat(Heap* hp ,HeapDataType* a ,int n)
{
      hp->_size = hp->_capacity = n;
      hp->_a = (HeapDataType*)malloc(sizeof(HeapDataType));
      if(hp->_a == NULL) return ;  //防止开辟失败
      memcpy(ph->_a,a,sizeof(HeapDataType)*n);//拷贝数据
    //n-1是最后一片叶子的下标,然后利用性质求父节点,依次遍历前面的父节点。
      for(int i = (n-1-1)/2 ; i >= 0; --i){
           AdjustDown(hp->_a,hp->_size,i);
      }
}

好了,小堆也已经建好,那么大堆呢? 显然易得,只需改变两个符号即可。

//   a[child] < a[child+1];
//   a[child] > a[parent] ;

好了,接下来,我们来看看堆能干什么呢?找最值!而且是连续的找!
那么这不就是排序吗?
所以接下来为大家介绍堆排序。

HeapSort ( 堆排序 )

我们以降序来举栗子。
首先有一个大问题摆在我们面前,降序使用大堆还是小堆?
这里给大家思考时间。

1

2

3

4

5

我就知道你会选择用大堆!降序不就是每次将最大的值选出来,排在数组头不久ok了?but,真是这样吗?
大
如果拿走100,然后呢?我们肯定想要那第二大的数作为根,于是70被拿了上来,
But,这样关系全乱了!!!
1
所以我们还得用向下调整法,但是时间复杂度相当高!
所以,我们用小堆。

我们先排成小堆,
s
ss
然后怎么做呢?我们只知道4是最小的,它又要求降序,所以我们拿走4,
与最后一个结点交换。
就是

20 7 25 8 14 30 40 10 4

这时候最后一个数最小,然后我们当然要找第二小的。所以我们对除最后一个数进行重建小堆,然后再将根拿到倒数第二个位置,以此类推。堆排序就完成了。

//代码实现
void HeapSort(HeapDataType* hp->_a,int hp->_size)
{
      for(int i = (hp->_size-2)/2;i>=0;i--)
      {
          AdjustDown(hp->_a,hp->_size,i);   //建小堆
      }
      int end = hp->_size - 1;
      while(end > 0){              
          Swap(&hp->_a[0],&hp->_a[end]);  //交换函数 (我懒得写了(嘻嘻))
          AdjustDown(hp->_a,end,0);     //重建小堆
             --end;     
      }
}

jie
result。此时时候已经不早了,是时候来完善我们接下来的接口。

//void HeapSort(HeapDataType* a, int len);堆排
//void AdjustDown(HeapDataType* a, int n, int root);  向下调整
void AdjustUp(HeapDataType* a, int child);  //向上调整
//void HeapCreat(Heap* hp, HeapDataType* a, int n);  建堆  
void HeapPush(Heap* hp, HeapDataType x);  //入堆
void HeapPop(Heap* hp);   //从堆中移除
HeapDataType HeapTop(Heap* hp);  //返回堆的根值
int HeapEmpty(Heap* hp);   //判断堆是否为空

注释掉的是我们已经KO掉的,接下来我们挨个解决剩下的。
首先先来入堆。
怎么入堆呢?从根结点上入吗?那么原来的根结点放在哪里呢?
这里我们再来一种算法,向上调整法。

我们将x插入到堆的尾部,然后逐层向上调整。
111
结果:
234

//向上调整法
void AdjustUp(hp->_a,child)
{
    int parent = (child - 1)/2;
    while(child > 0) {
        if(a[child] < a[parent]){
        Swap(&hp->_a[child],&hp->_a[parent]);
        child = parent;
        parent = (child - 1)/2;
        }
         else break;
    }
}
  //我们想要将x插入到堆中,使得插入后还是一个堆。
    //同样的,假设之前是一个小堆。
void HeapPush(Heap* hp, HeapDataType x)
{
     //首先一个小问题,万一hp->_a 没空间怎么办?
     if(hp->_size == hp->_capacity){  //空间满了。需要calloc
          int newcapacity = hp->_capacity*2;
         HeapDataType* HP = (HeapDataType*)calloc(hp->_a,sizeof(HeapDataType)*newcapacity);
         if(HP == NULL){    //开辟失败
             return}
         hp->_a = HP;
         hp->_capacity = newcapacity;   //开辟完成
     }

        hp->_a[hp->_size - 1] = x;  //放入数据
        hp->_size++;        //size加一
        AdjustUp(hp->_a,hp->_size-1);
}

出堆就比较简单,我们利用堆排的思想,将根节点与尾结点交换,然后向下调整即可。

void HeapPop(Heap* hp)
{
    Swap(&hp->_a[0],&hp->_a[hp->_size-1]);   //交换根和尾元素
    hp->_size--;   //干掉尾元素
    AdjustDown(hp->_a,hp->_size,0); //向下调整。
}

剩下2个更是简单的一批。

HeapDataType HeapTop(Heap* hp)
{
    return hp->_a[0];
}
int HeapEmpty(Heap* hp)    
{
    return hp->_size == 0 ? 1 : 0;
}

最后我们用堆来解决一道经典问题,TopK问题。
何为TopK问题? 从一堆数(大于k)中选出前k个大数(或小数)。
比如英雄联盟有一天说要给南京市第一亚索,一直到南京市第10亚索,给这10个玩家颁发奖励,他们先用一套算法算出每个压缩玩家的得分,然后怎么选出前k个呢?

自然而然的想法

  • 排序
    我不管你要求什么,只要是数组,排序可以解决大部分问题。实际上这题确实可以排序,但时间复杂度不尽人意,还有更优的解吗?能用堆吗?
  • 堆的性质
    我先建一个大堆,找出最高分,然后再找次小的,知道k个玩家找完。这已经不错了,但是还有没有更优的解?或者说,我能不能建造小点的堆(堆的建立算法时间复杂度不是很easy)?
  • 更优的解法

任意取k个玩家的分数建造一个k的小堆,用其他玩家与根节点比较,如果比根节点大,则换掉根节点。然后向下调整成小堆,知道所有玩家比较完毕。

这里要注意的是,我只是找出前k个,并不知道这k个玩家谁更厉害(我没有给这k个玩家排序)。
而且,这并不是绝对的公平,想一想,如果所有人的分数都相同,这个算法不就是随机抽10个人吗?
这就是堆的大部分问题,由于是晚上写的博客,加上对截图工具使用实在粗糙,图有点模糊,如有问题,希望不要介意。
(全文完)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值