目录
- 堆的概念和特征
- 父子关系:`(i-1)/2`
- 堆的构造过程
- 自底向上堆化(Bottom-up Heapify)
- 举例
- 自顶向下堆化(Top-down Heapify)
- 插入操作
- 举例
- 删除操作
- 举例
- 堆结构的价值
- 口诀
堆的概念和特征
堆是一个很大的概念,不一定是完全二叉树。下面说的用完全二叉树是因为这个很容易被数组储存,但是除了这种二叉堆之后,还有二项堆、斐波那契堆,这些堆就不属于二叉树。
堆是将一组数组按照完全二叉树的存储顺序,将数据存储在一个一维数组中的结构。
完全二叉树:每一层都是满的,除了最底层,最底层的节点都靠左排列。
堆有两种结构:大顶堆、小顶堆。(有些地方也叫大根堆、小根堆、最大堆、最小堆… )
- 小顶堆:任意节点的值均 <= 它的左右孩子,并且最小值位于堆顶,即根节点处。
- 大顶堆:任意节点的值均 >= 它的左右孩子,并且最大值位于堆顶,即根节点处。
父子关系:(i-1)/2
假设一个节点的下标是i
。
- 当
i=0
时,为根节点; - 当
i>=1
时,父节点索引是(i-1)/2
- 下标
i
的节点,其左子节点索引是2*i+1
,右子节点索引是2*i+2
堆的构造过程
构造堆的过程被称为"堆化",它可以分为两种不同的方式:自底向上和自顶向下。
在构造堆的过程中,堆化操作会反复执行,直到整个堆满足堆的性质。
构造堆的时间复杂度通常是 O ( n ) O(n) O(n),其中n是堆中元素的数量。
在堆排序等算法中,首先通过自底向上堆化将数组构造成堆,然后依次取出堆顶元素进行排序,最终实现高效的排序操作。
自底向上堆化(Bottom-up Heapify)
这种方法从最后一个非叶子节点开始,逐步向上调整节点,使得以该节点为根的子树满足堆的性质。具体步骤如下:
- 从最后一个非叶子节点开始,向前遍历所有非叶子节点。
- 对于每个非叶子节点,比较它与其子节点的值,如果不满足堆的性质,则交换节点与较大(或较小,根据最大堆或最小堆)子节点的位置。
- 继续向前遍历,直到根节点,这样整个堆就满足了堆的性质。
举例
下面看一下如何建立一个大堆:
将元素依次排到完全二叉树节点上去,如下左图所示。
int i = (size - 2)/2 = 4
(思考一下这里为什么是size-2而不是size-1)。找到数组中的4号下标。65大于其孩子,满足大堆性质,所以不用交换。如下右图
解答:因为数组中最后一个元素的索引是
size-1
,所以最后一个非叶子节点的索引(其父节点)是(size-2)/2
-
然后
i= i-1;
然后用2和其孩子比较,2和204交换。交换之后204所在的子树满足大堆,如下左图。 -
54和其孩子比较,54和92交换。此时92所在子树满足大堆,如下右图。
-
继续,23和其孩子比较,23和204交换,交换完之后,23的子树却不满足了,所以还需调整它的子树。 如下两图所示。
-
12和204交换,仍然出现不平衡的情况,以此类推,直到根节点也满足要求就完毕了。
这样我们就建好了一个大顶堆,从图中可以看到,根元素是整个树中值最大的那个,而第二大和第三大就是其左右子树,具体是哪个更大则是未知的,需要比较一下才知道。
另外,对于同一组数据,如果输入的序列不一样,那最终构造的树是否也会不一样呢?非常有可能,那这样的树有什么意义呢?我们后面再看,这里先理解堆是这么构建的就行了。
自顶向下堆化(Top-down Heapify)
这种方法是从根节点开始,逐步向下调整节点,使得以该节点为根的子树满足堆的性质。具体步骤如下:
- 从根节点开始,比较它与其子节点的值,如果不满足堆的性质,则与较大(或较小,根据最大堆或最小堆)子节点交换位置。
- 交换后,继续对被交换的子节点进行堆化,直到叶子节点,或者子节点满足堆的性质为止。
插入操作
从上面可以看到根节点和其左右子节点是堆里的老大老二和老三,其他结点则没有太明显的规律,那如果要插入一个新元素,该怎么做呢?
直接说规则:插入操作会首先将新元素添加到堆的末尾,然后通过与其父节点比较并可能进行交换,以维持堆的性质。
举例
如下图,要插入300, 我们将其插入到31的右孩子位置,然后不断向上爬,31<300,所以两者要交换,再向上发现300比65大,所以两者要交换。最后300比根元素204大,两者也交换。最后就得到了新的堆。完整过程如下所示:
删除操作
堆本身比较特殊,一般对堆中的数据进行操作都是针对堆顶的元素,即每次都从堆中获得最大值或最小值,其他的不关心,所以我们删除的时候,也是删除堆顶。如果直接删掉堆顶,整个结构被破坏了,群龙无首就不易管理了。
所以实际策略是:先将堆中最后一个元素(假如为A)和堆顶元素进行替换,然后删除堆中最后一个元素。之后再从根开始逐步与左右比较,谁更大谁上位。然后A再继续与子树比较,如果有更大的继续交换,直到自己所在的子树也满足大顶堆。
上面的过程可以理解为皇上突然驾崩了,这时候先找个顾命大臣维持局面,大臣先看左右两个皇子谁更强谁就是老大。然后大臣自己再逐步隐退,直到找到属于自己的位置。
举例
最后新的堆结构如下:
堆结构的价值
说了这么多,你觉得这东西的价值在哪里呢?
价值就在于大顶堆的根节点是整个树最大的那个,增加时会根据根的大小来决定要不要加,而删除操作只删除根元素。这个特征可以在很多场景下有奇妙的应用,后面的算法题全都基于这一点。
总结下就是:堆是一种优秀的数据结构,它能够在维护性质的同时,提供高效的操作。适用于一些需要高效插入、删除、查找最大(或最小)元素的场景,比如优先队列、堆排序等。
这里可能有些人还有疑问,感觉不管插入还是删除,堆的操作都不简单,那为什么还说堆的效率比较高呢?
这是因为堆元素的数量是有限制的,一般不用将所有的元素都放到堆里。后面题目中可以看到,在序列中找K大,则堆的大小就是K。如果K个链表合并,那么堆就是K。
口诀
关于堆的问题,记住口诀:
查找:找大用小,大的进;找小用大,小的进。
排序:升序用小,降序用大。
查找的口诀解释一下就是:是找K大,则用小堆,后续数据只有比根元素更大时才允许进入堆。如果是找K小,则对应反过来。