这真的是一种数据结构,尽管名字有点。。。。
前言
昨天(2019/1/19)$Payphone-X$问我堆应该如何实现,当时讲的太仓促,言语特别混乱,于是就有了这篇文章。
二叉堆是什么
二叉堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。 ——百度百科
书面上的定义不太好理解,那我们讲得通俗一点。
实际上二叉堆是一棵完全二叉树,但它比完全二叉树多了以下两个性质:
1、堆中的每一个节点总是不大于(或不小于)其父节点。
2、无论如何调整堆,它总是一棵完全二叉树(
这不是废话吗)。
那么,根据第一条性质,二叉堆就可以分为大根堆和小根堆两种。
我们将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
如图,这就是大根堆和小根堆的一个实例。
这是按根节点的大小来分,如果按操作种类来分,那么常见的堆有二叉堆、斐波那契堆等。
如何存储
堆是线性数据结构,相当于一维数组,有唯一后继。
所以在代码实现时,我们用一个一维数组Heap[]
来存储堆。
学过完全二叉树的同学们都知道,完全二叉树有这样一个性质,即一个非叶节点i
的左儿子的编号为2i
,其右儿子的编号为2i+1
。
那么既然堆是一棵完全二叉树,那么也应该满足这个性质。
所以当一个有$n$个元素的序列${Heap_1,Heap_2,Heap_3,\dots,Heap_n}$满足:
$Heap_i \leq Heap_{2i} \& Heap_i \leq Heap_{2i+1}$,(小根堆)
或者:
$Heap_i \geq Heap_{2i} \& Heap_i \geq Heap_{2i+1}$(大根堆)
时,这个序列称为一个堆。
那么,我们就可以得出,堆顶元素必为这个序列中最大或最小的元素。
操作
一个堆支持以下四个操作。
1、插入一个节点;
2、删除堆顶元素;
3、获取堆顶元素;
4、堆排序。
某些堆实现还支持其他的一些操作,如斐波那契堆支持检查一个堆中是否存在某个元素。
时间复杂度
堆的插入和删除的时间复杂度均为$O(1)-O(\log n)$。
获取堆顶元素为$O(1)$。
堆排序为$O(n \log n)$,和快排的时间复杂度一样,但空间复杂度为$O(2n)$,比快排高了一倍。
操作实现
在插入和删除两个操作中,还嵌套着两个小操作。
1、$shiftUp$上浮。
2、$shiftDown$下沉。
一、上浮
当我们插入一个节点时,(我们现在以小根堆为例)。
根据小根堆的性质,(每一个节点总是不小于其父节点)。
那么首先我们要和它的父节点比较,如果这个节点小于它的父节点,那么交换。
然后继续比较,交换后的节点和它现在的父节点继续比较,如果还小于,那么继续交换。
直到这个节点要比它的父节点大时,或这个节点成为根节点时,停止比较。
这就是上浮操作。
假如我们刚刚插入了一个节点,其点权为2。
那么就要开始上浮操作了:
第一步,交换节点7(点权为2)和它的父亲节点3(点权为4)。
第二步,交换节点3(点权为2)和节点1(点权为7)
最后一步,交换节点3(点权为7)和节点7(点权为4)
至此,这棵完全二叉树就满足堆的性质了。
二、下沉
下沉操作是用来删除堆顶元素的。
当我们删除掉堆顶元素时,把叶子节点中的最后一个拿到根节点上。
当这个节点的左儿子不大于当前堆的大小时,即操作编号不越界时,
找出这个节点的左儿子和右儿子中小的那一个。
再用找出来的这一个和它们的父亲相比较,如果不满足性质,那么交换。
操作位置移至当前节点的左儿子上。
这就是下沉操作。
代码实现
大根堆
1 | /* Headers */ |
小根堆
1 | /* Headers */ |
STL实现
1 | /* Headers */ |
堆排序
本质上就是把一个数组里的所有数插进堆里,然后每一次取出堆顶,就已经排好序列了。
正序:小根堆,逆序:大根堆。
时间复杂度:$O(n\log n)$。
代码实现
1 | inline void Heap_Sort(){ |
警告:
我觉得有一句话说得很好:STL只能是锦上添花,而不能是雪中送炭!