数据结构·堆

堆就是一种利用完全二叉树来维护数据的一种数据结构,而当我们实际使用时使用数组来存储时,树中节点与数组中的值相对应,也就是可以灵活运用完全二叉树的性质通过数组下标来维护堆。

想看Stl模板的堆请直达底部

为什么要选择堆?

堆的功能就是保持堆顶的元素最大/最小,本质上是一种排序算法,为什么不用Sort呢?它构建时间一般是复杂度是\(O(n)\),而维护的时间复杂度是\(O(\log_2N)\),如果你用Sort进行排序,时间复杂度是\(O(N\log_2N)\),两者对比,明显堆的时间复杂度优于Sort

但是特殊情况特殊考虑,针对不同题目仍需要使用不同的做法

前置技能点:完全二叉树

何为完全二叉树?

如果一棵深度为K二叉树,1至k-1层的结点都是满的,即满足2i-1,只有最下面的一层的结点数小于2i-1,并且最下面一层的结点都集中在该层最左边的若干位置,则此二叉树称为完全二叉树。

简单来说就是假设一颗树的深度为h,除了最后一层每个节点都有两个子节点,最后一层的结点必须从左向右连续出现。

什么是从从左向右连续出现?如图,a就是完全二叉树,而b不是完全二叉树

完全二叉树.png

此外,如果将完全二叉树按照从上至下从左至右的次序对节点进行编号,则编号为i的节点有以下性质。

  • \(i\leq\lfloor n/2\rfloor\)1,即 \(2i\leq n\) ,则编号为i的节点为分支节点2,否则为叶子节点3

    扫描二维码关注公众号,回复: 7138853 查看本文章
  • 若n为奇数,则树中每个分支节点即有左子节点,又有右子节点;

    若n为偶数,则编号最大的分支节点(编号为n/2)只有左子节点,没有右子节点

  • 若编号为i的节点有左子节点,则左子节点的编号为 \(2i\) ;若编号为i的节点有右子节点,则右子节点编号为 \(2i+1\)

  • 除树根节点外,若一个节点的编号为 \(i\) ,则他的父节点编号为 \(\lfloor i/2\rfloor\)

以上两条性质不理解的可以看一下上面的a图

堆的定义

因为堆是对完全二叉树的一种灵活运用,所以堆的定义很大程度上就是完全二叉树的定义的线性化,只不过堆还需要维护序列数字之间的大小关系

堆的定义:设有n个元素的一个序列,\(A_1,A_2,A_3\ldots\),只有当序列中的数字满足以下其中一种关系时,称为堆。

\(A_i\leq\{{A_{2i} \atop A_{2i+1}}\)\(A_i\geq\{{A_{2i} \atop A_{2i+1}}\)

​ 前面的这个就叫小根堆,后面这个就叫大根堆

堆中序列的数值关系

由完全二叉树的第三和第四条性质我们可以知道,双亲和左右子节点的编号就是 \(i\)\(2i\)\(2i+1\) 的关系,所以判断一个序列是不是堆,可以通过该节点与其左右节点的大小分析。

堆.png

堆的特性:堆顶元素最大或者最小

将大根堆转化为数组储存后,值的对应就如下图

数组堆.png

很明显,在上图中,数组的第一个元素是全堆元素中的最大值

那么如何进行堆的维护呢?

堆的运行与维护

堆只有两个操作:插入和取出

1.假设维护一个大根堆,进行一个插入操作

  • 假设插入一个数据为43,新的数据编号为10,先进行堆长度+1,然后根据堆的性质,我们要维护该节点的父节点大于子节点.

  • 如何寻找这个新的节点的父节点?

    完全二叉树的第四性质,当编号为 \(i\) 时,其父节点编号为 \(\lfloor i/2\rfloor\)

  • 我们将其父节点的值与子节点的值进行比较,如果子节点大于父节点就交换,一直执行到 \(i=0\) ,即找不到父节点,如图

    堆运行1.png

    堆运行2.png

    堆运行3.png

2.假设维护一个大根堆,进行取出操作

  • 堆的取出一般只取出堆的堆顶元素,其他的元素取出并没有什么意义,那么如何将去除后的序列从新维护成一个堆?我们需要从堆顶开始恢复堆的特性。

  • 首先查询堆顶的值后,将堆尾的最后一个元素取出覆盖堆顶,然后整个堆的长度-1

  • 接下来我们需要恢复堆的特性,将堆顶节点的两个子节点与堆顶节点值进行比较,哪个值最大,就将那个值与堆顶进行互换

    如何寻找该节点的子节点?

    完全二叉树的第三性质,若编号为i的节点有左子节点,则左子节点的编号为 \(2i\) ;若编号为i的节点有右子节点,则右子节点编号为 \(2i+1\)

  • 然后对那个被改变值的节点进行如上操作,之后重复进行这个操作,直到改到一个节点的两个子节点的值都比该节点的值小或,该节点没有子节点则停止操作。如图

    堆运行01.png

    堆运行02.png

    堆运行03.png

    而小根堆的维护方式与大根堆大体相同,只是维护的数据关系不同,只要将大于全部改成小于进行操作即可维护小根堆

    堆的模板代码

    废话不多说,直接上代码

    //这是大根堆的操作模板,小根堆只要将大于号全部改成小于即可
    void put(int d)  //该函数是插入函数,heap[1]为堆顶
    {
      int now, mid;
      heap[++Lenth] = d;
      now = Lenth;
      while(now > 1)  //找不到即停止
      {
          mid = now / 2;
          if(heap[now] <= heap[mid]) break;//如果当前节点值小于他的父节点,则不进行操作
          swap(heap[now], heap[next]); //互换
          now = next;     //接着下一个节点进行修改
      }
    }
    int get()  //该函数是取出函数
    {
    int now=1, mid, res= heap[1];
      heap[1] = heap[Lenth--];  //使队尾元素覆盖堆顶元素
      while(now * 2 <= Length)  //没有子节点停止
      {
          mid = now * 2;
          if (mid < Length && heap[mid + 1] < heap[mid]) mid++; //指针还在堆内并且如果当前节点的右子节点比左子节点小,指针+1指向右子节点,否则指向左子节点
          if (heap[now] >= heap[mid]) break; //两个子节点都没有父节点大,可以跳出
          swap(heap[now], heap[next]); //互换
          now = mid; //继续找
      }
      return res;
    }

    堆的应用

  • 例题:最小函数值 Luogu传送门

    大概题意就是求所有给定的二次函数的最小值,然后互相比较下求最小的m个,大暴力显得十分的麻烦,这里我们就可以使用堆这个高级数据结构。

    • 我们先构造一个小根堆,计算出所有函数在x为1时的函数值,将这些函数值全部放入堆中,维护一下堆,此时堆顶的函数值是最小的。
    • 然后因为要求最小的m个函数值,我们每次计算是都取出堆顶的函数,计算它 $ ++x$ 的函数值,然后从堆顶向下维护,保证堆顶的函数值依然是最小的,重复这个步骤,直到计算出m个最小的函数值,即可停止。

    上代码!

    #include <stdio.h>
    #include <algorithm>
    struct INFO{
      int y,x,a,b,c;
    };
    struct INFO info[500005];
    void put(int mid){
      int now;
      while(mid>1){
          now=mid/2;
          if(info[mid].y>=info[now].y) break;
          std::swap(info[now],info[mid]);
          mid=now;
      }
    }
    void pop(int num){
      int mid=1,now;
      while(mid<=num/2){
          now=mid*2;
          if(now+1<=num && info[now].y>=info[now+1].y) now++;
          if(info[mid].y<=info[now].y) break;
          std::swap(info[now],info[mid]);
          mid=now;
      }
    }
    //put与pop函数可以参照上面堆的模板理解,这题并不需要弹出堆顶
    int main(int argc, char const *argv[]){
      int n,m;scanf("%d%d",&n,&m);
      for (register int i = 1; i <= n; ++i){
          int a,b,c;
          scanf("%d%d%d",&info[i].a,&info[i].b,&info[i].c);
          info[i].x=1;  
          info[i].y=info[i].a+info[i].b+info[i].c;//计算当x=1时的初始函数值
          put(i); //放入堆中
      }
      for (register int i = 1; i <= m; ++i){
          printf("%d ",info[1].y); 
          info[1].x++;
          int Va=info[1].a,Vb=info[1].b,Vc=info[1].c,Vx=info[1].x;
          info[1].y=Va*Vx*Vx+Vb*Vx+Vc; //计算函数值
          pop(n); //维护一下堆,不需要弹出,只要保证堆顶最小
      }
      return 0;
    }

当然有许多的人觉得手打堆模板操作起来过于复杂,难记,当然STL库中也提供了一种序列,优先序列。

传送门直达

感谢您的阅读。

点赞+支持是阁下对我最大的鼓励


  1. 这个符号表示向下取整,即当无法整除时,省去小数直接+1

  2. 分支节点指的是除了底层节点的其他节点

  3. 叶子节点指的是最底层节点

猜你喜欢

转载自www.cnblogs.com/lightcoder/p/11437222.html