分类目录:《算法设计与分析》总目录
相关文章:
·堆(一):基础知识
·堆(二):维护堆的性质
·堆(三):建堆
·堆(四):优先队列
·排序算法:堆排序
·利用heapq模块实现堆
如下图,堆是一个数组,它可以被看成一个近似的完全二叉树,树上的每一个结点对应数组中的一个元素。除了最底层外,该树是完全充满的,而且是从左向右填充。表示堆的数组 A A A包括两个属性: A . l e n g t h A. length A.length给出数组元素的个数, A . h e a p − s i z e A.heap-size A.heap−size表示有多少个堆元素存储在该数组中。也就是说,虽然 A [ 1 ⋯ A . l e n g t h ] A[1\cdots A.length] A[1⋯A.length]可能都存有数据,但只有 A [ 1 ⋯ A . h e a p − s i z e ] A[1\cdots A.heap-size] A[1⋯A.heap−size]中存放的是堆的有效元素,这里, 0 ≤ A . h e a p − s i z e ≤ A . l e n g t h 0\leq A.heap-size\leq A.length 0≤A.heap−size≤A.length。树的根结点是 A [ 1 ] A[1] A[1],这样给定一个结点的下标 i i i,我们很容易计算得到它的父结点、左孩子和右孩子的下标:
def parent(i):
return int(i/2)
def left(i):
return 2i
def right(i):
return 2i+1
在大多数计算机上,通过将
i
i
i的值左移一位,left(i)
过程可以在一条指令内计算出
2
i
2i
2i,采用类似方法,在right(i)
过程中也可以通过将
i
i
i的值左移1位并在低位加1,快速计算得到
2
i
+
1
2i+1
2i+1。
至于parent(i)
过程,则可以通过把
i
i
i的值右移1位计算得到
⌊
i
2
⌋
\lfloor\frac{i}{2}\rfloor
⌊2i⌋。在堆排序的好的实现中,这个函数通常是以“宏”或者“内联函数”的方式实现的二叉堆可以分为两种形式:最大堆和最小堆。
在这两种堆中,结点的值都要满足堆的性质,但一些细节定义则有所差异。在最大堆中,最大堆性质是指除了根以外的所有结点 i i i都要满足: A [ p a r e n t ( i ) ] ≥ A [ i ] A[parent(i)] \geq A[i] A[parent(i)]≥A[i]也就是说,某个结点的值至多与其父结点一样大。因此,堆中的最大元素存放在根结点中;并且,在任一子树中,该子树所包含的所有结点的值都不大于该子树根结点的值。
最小堆的组织方式正好相反:最小堆性质是指除了根以外的所有结点 i i i都有 A [ p a r e n t ( i ) ] ≤ A [ i ] A[parent(i)] \leq A[i] A[parent(i)]≤A[i]最小堆中的最小元素存放在根结点中。在堆排序算法中,我们使用的是最大堆。最小堆通常用于构造优先队列,在后面的文章中我们会再具体讨论。对于某个特定的应用来说,我们必须明确需要的是最大堆还是最小堆;而当某一属性既适合于最大堆也适合于最小堆的时候,我们就只使用“堆”这一名词。
如果把堆看成是一棵树,我们定义一个堆中的结点的高度就为该结点到叶结点最长简单路径上边的数目;进而我们可以把堆的高度定义为根结点的高度。既然一个包含 n n n个元素的队可以看做一棵完全二又树,那么该堆的高度是 O ( lg n ) O(\lg n) O(lgn)。我们会发现,堆结构上的一些基本操作的运行时间至多与树的高度成正比,即时间复杂度为 O ( lg n ) O(\lg n) O(lgn)。在后续的文章中,我们将介绍一些基本过程,并说明如何在排序算法和优先队列中应用它们。