堆树
堆是一种特殊类型的二叉树,具有以下两个性质:
- 每个节点的值大于(或小于)等于其每个子节点的值。
- 该树完全平衡,最后一层的叶子都处于最左侧位置。
违反以上两个性质之一的树都是非堆树。当父节点的值总是大于或小于其子节点的时候,则称为最大堆和最小堆。
图示最大堆:
可以用数组来实现堆,根据最大堆的性质:
heap[i] ≥ heap[2i+1]
heap[i] ≥ heap[2i+2] ,注意2i+2不能超过项数。
根据堆树的性质,其是一个完全二叉树,所以i可以视为父节点,而2i+1和2i+2视为其左右子节点,对于最大堆来说,上面两个公式始终成立,因此可以将一个无序数组转变为堆树。
将堆作为优先队列
之前的文章栈和队列当中描述了用链表实现优先队列的方式,其结构复杂度是
O
(
n
)
O(n)
O(n)或
O
(
n
)
O(\sqrt n)
O(n),对于n较大的时候,效率极其低下,而堆是完全平衡树,
O
(
log
n
)
O(\log n)
O(logn)次查找可以到达叶节点。为了达到这个目的,可以向堆中添加或删除元素。为了保持堆的性质,在添加元素的时候可以将最后一个叶子节点向根部移动。
上述算法的中心思想是在堆尾添加新的元素,并一直往根节点移动直至父节点比该节点的值大,此时堆树的结构未被打破。
由于堆的性质,删除的一定是根节点(FIFO)并且将原来的尾节点逐步下移直至满足一个新的堆。
用数组实现堆
堆可以用数组实现,但数组并不是堆。某些情况下,数组需要转变为堆(堆排序)。进行堆转化有多种方式,最简单的就是从空堆,按顺序添加心得元素,这是一个自顶向下的方法。
通过对之前描述的堆插入的描述,我们可以总结出这种算法的最坏情况得出复杂度。当每个新添加的元素都需要从堆尾移动到堆顶,这种情况需要的步骤最多,第k个节点,移动到堆顶需要
⌊
lg
k
⌋
\lfloor \lg k \rfloor
⌊lgk⌋次交换操作,所以总的计算公式为:
∑
i
=
1
n
⌊
lg
k
⌋
≤
∑
i
=
1
n
lg
k
=
lg
1
+
lg
2
+
.
.
.
+
lg
n
=
l
g
(
1
∗
2
∗
.
.
.
∗
n
)
=
lg
(
n
!
)
=
O
(
n
lg
n
)
{\displaystyle\sum_{i=1}^n }\lfloor \lg k \rfloor \le \displaystyle{\sum_{i=1}^n \lg k}=\lg1+\lg2+...+\lg n=lg(1*2*...*n)=\lg (n!)=O(n\lg n)
i=1∑n⌊lgk⌋≤i=1∑nlgk=lg1+lg2+...+lgn=lg(1∗2∗...∗n)=lg(n!)=O(nlgn)
该式的证明过程略,百度可以搜到。
而常用另一种堆排序算法分为堆创建和堆重构两个部分,简单说明,堆创建的时间复杂度为O(n),堆重构的复杂度为O(nlgn)。
treap树
堆的性质非常有趣,它是完全平衡树,且允许立即访问最大或者最小节点,无法立即访问其他元素。二叉树的查找非常高效,但取决于插入与删除操作的顺序以及树的形状,畸形树会趋近于链表的复杂度。这里我们将堆与查找树的概念结合,形成treap树,这种结构抛弃了树完全平衡的要求,以及叶子节点应该在靠左位置的特征,尽管堆本身会尽可能平衡。