什么是堆?
堆又称为优先队列,但堆并不是队列
优先队列:一种特殊的队列,队列中元素出栈的顺序是按照元素的优先权大小的,而不是元素入队的先后顺序。
堆的特性
- 必须是完全二叉树
- 用数组实现
- 任一节点的值是其子树所有结点的最大值或最小值
- 最大值时,称为"最大堆",也称大根堆
- 最小值时,称为"最小堆",也称小根堆
大根堆
小根堆
对于堆这种数据结构,从根节点到任意结点路径上所有的结点都是有序的
堆的插入和删除
1.堆的插入
根据堆的插入规则,插入的节点只能放在最后,接着节点向上调整
已知关键字序列5,8,12,19,28,20,15,22是最小堆,插入关键字3,调整后得到的最小堆是:
- 将3放入22的后面,为[5,8,12,19,28,20,15,22,3]
- 然后3开始向上调整
2.堆的删除
根据堆的删除规则,删除操作只能在堆顶进行,接着让最后一个节点放在堆顶,做向下调整,让剩余的数组仍然满足最小堆/最大堆
如:最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是
- 删除0后用8填充0的位置,为[8,3,2,5,7,4,6]
- 然后8和其子节点3,2比较,结果2最小,将2和8交换,为:[2,3,8,5,7,4,6]
- 然后8的下标为2,其两个孩子节点下标分别为22+1=5,22+2=6
- 也就是4和6两个元素,经比较,4最小,将8与4交换,为[2,3,4,5,7,8,6]
- 这时候8已经没有孩子节点了,调整完成
筛选法建堆
如果已经存在N个数据元素,如何将这些元素按照堆的要求存储在一个一维数组中呢?这就是堆的建立问题。
首先我们可以想到按照之前的堆的插入算法将这N个元素依次插入一个空堆中,分析时间复杂度,每插入一个元素最多要进行logN(即堆的深度)次比较,所以对于N个元素用插入法建堆的时间复杂度是O(NlogN)
这里要介绍的是筛选法建堆,它可以在线性时间复杂度下完成建堆。以最大堆为例介绍具体操作过程:
- 首先将N个数据元素存入一个一维数组,并把这个数组视作一棵完全二叉树
- 从二叉树的最后一个非叶结点开始用从上向下过滤的方法调整以该非叶结点为根节点的二叉树为最大堆(从上向下过滤是指从二叉树的根结点开始,比较根结点和它的子结点,如果根结点比子结点中最大的那个小的话,就交换父节点和子结点的值,之后对以之前子结点为根节点的二叉树重复执行上述操作)
- 从二叉树的最后一个非叶结点开始用从上向下过滤的方法调整以该非叶结点为根节点的二叉树为最大堆
有n个元素的序列,若使用筛选法建堆,因为从最后一个结点的父亲结点开始,则为位置为n/2取下整的元素开始建堆,所谓开始建堆就是从这里开始调整
堆的实现
堆的经典实现是完全二叉树,这样实现的堆称为二叉堆。
完全二叉树是增加了限定了条件的二叉树。假设一个二叉树的深度为n,为了满足完全二叉树的要求,该二叉树的前n-1层必须填满,第n层也必须按照从左到右的顺序被填满,比如下图:
父节点和左右孩子索引号之间的关系
如果以第 0 个位置开始标记树根节点,则第 i 个结点的左右孩子分别为:
- 2i+1
- 2i+2
反之,如果一个结点的标号为 i,则其父节点为:
- i/2:i 为左孩子结点
- i/2-1:i 为右孩子结点
这是传统常规的做法,可是当我们换个方式存储这些根上的结点的话,比如,根节点在索引为 1,而不是为 0 的地方,这样 结点 i 的左右孩子分别为:
- 2*i
- 2*i+1
这种方式的真正好处在于:当结点为 i 时,不论为左还是右孩子结点,其父节点总是,i/2
插入
每次总是先填满上一层,再在下一层从左往右一次插入,堆的插入步骤:
- 将新的元素增加到堆的末尾
- 按照优先顺序,将新元素与父节点比较,如果新元素大于父节点则两者交换位置
- 不断进行第二步,直到不需要交换新元素和父节点,或者达到堆顶
- 最终得到大根堆
通过将新元素与父节点调整交换的操作叫做上滤
删除
堆的删除与插入操作相反,删除操作从上往下调整堆
- 删除堆顶元素,通常是将堆顶元素放置在数组的末尾
- 比较左右节点,将大的元素上调
- 不断进行步骤二
上述调整方法称为下滤