数据结构·堆

孤人 提交于 2019-11-28 22:53:58

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

前置技能点:完全二叉树

何为完全二叉树?

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

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

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

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

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

  • 若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\) 的关系,所以判断一个序列是不是堆,可以通过该节点与其左右节点的大小分析。

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

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

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

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

堆的运行与维护

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

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

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

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

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

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

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

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

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

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

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

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

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

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

堆的模板代码

废话不多说,直接上代码

//这是大根堆的操作模板,小根堆只要将大于号全部改成小于即可 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; }

未完待续……


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

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

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!