算法导论读书笔记(5)
堆
(二叉) 堆 数据结构是一种数据结构,它可以被视为一棵完全二叉树。树中每个结点与数组中存放该结点值的那个元素对应。树的每一层都是填满的,最后一层从左子树开始填。表示堆的数组 A 是一个具有两个属性的对象: A.length 是数组中元素的个数, A.heap-size 是存放在 A 中的堆元素个数。此处有 A.heap-size <= A.length 。树的根为 A [ 1 ],给定了某个结点的下标 i ,其父结点 PARENT(i)
,左儿子 LEFT(i)
和右儿子 RIGHT(i)
的下标可以简单的计算出来:
PARENT(i) 1 return FLOOR(i / 2)
LEFT(i) 1 return 2i
RIGHT(i) 1 return 2i + 1
二叉堆有两种: 最大堆 和 最小堆 。这两种堆都要满足各自的堆特性。在最大堆中,最大堆特性是指除了根以外的每个结点 i ,有 A [ PARENT(i)
] >= A [ i ]。即某个结点的值至多和其父结点一样大。这样,堆中的最大元素就存放在根结点中。最小堆的组织方式刚好相反,最小堆特性是指除了根以外的每个结点 i ,有 A [ PARENT(i)
] <= A [ i ],最小堆的最小元素是在根部。
堆可以被看成是一棵树,结点在堆中的高度定义为从本结点到叶子的最长简单下降路径上边的数目;定义堆的高度为树根的高度,因而树的高度为 Θ (lg n )。而堆结构上的一些基本操作的运行时间至多与树的高度成正比,为 O (lg n )。下面将介绍几个基本过程,并说明它们的用法。
MAX-HEAPOFY
过程,运行时间为 O (lg n ),是保持最大堆性质的关键BUILD-MAX-HEAP
过程,以线性时间运行,可以在无序的输入数组上构造出最大堆HEAP-SORT
过程,运行时间为 O ( n lg n ),对一个数组进行原地排序MAX-HEAP-INSERT
,HEAP-EXTRACT-MAX
,HEAP-INCREASE-KEY
,HEAP-MAXIMUM
过程的运行时间为 O (lg n ),可以让堆结构作为优先级队列使用。
保持堆的性质
MAX-HEAPIFY
过程的输入为一个数组 A 和下标 i 。当 MAX-HEAPIFY
被调用时,我们假定以 LEFT(i)
和 RIGHT(i)
为根的两棵二叉树都是最大堆,但这时 A [ i ]可能小于其子女,这样就违反了最大堆特性。 MAX-HEAPIFY
让 A [ i ]在最大堆中“下降”,使以 i 为根的子树成为最大堆。
MAX-HEAPIFY(A, i) 1 l = LEFT(i) 2 r = RIGHT(i) 3 if l <= A.heap-size and A[l] > A[i] 4 largest = l 5 else 6 largest = i 7 if r <= A.heap-size and A[r] > A[largest] 8 largest = r 9 if largest != i 10 exchange A[i] with A[largest] 11 MAX-HEAPIFY(A, largest)
下图描述了 MAX-HEAPIFY
的过程。在算法的每一步里,从元素 A [ i ], A [ LEFT(i)
]和 A [ RIGHT(i)
]中找出最大的,并将其下标保存在 largest 中。如果 A [ i ]是最大的,则以 i 为根的子树已是最大堆,程序结束。否则,交换 A [ i ]和 A [ largest ],从而使 i 及其子女满足堆性质。下标为 largest 的结点在交换后的值为 A [ i ],以该结点为根的子树可能又违反了最大堆性质。因而要对该子树递归调用 MAX-HEAPIFY
。
当 MAX-HEAPIFY
作用在一棵以结点 i 为根的,大小为 n 的子树上时,其运行时间为调整元素 A [ i ], A [ LEFT(i)
]和 A [ RIGHT(i)
]的关系时所用的时间 Θ (1),加上对以 i 为结点的某个子结点为根的子树递归调用 MAX-HEAPIFY
所需的时间。 i 结点的子树大小至多为2 n / 3(最坏情况发生在最底层恰好半满的时候),那么 MAX-HEAPIFY
的运行时间可表示为: T ( n ) <= T (2 n / 3) + Θ (1)。该递归式的解为 T ( n ) = O (lg n )。或者说, MAX-HEAPIFY
作用于一个高度为 h 的结点所需的运行时间为 O ( h )。
建堆
现在可以自底向上地用 MAX-HEAPIFY
来将一个数组 A [ 1 .. n ](此处 n = A.length )变成一个最大堆。又可知子数组 A [ FLOOR(n / 2)
+ 1 .. n ]中的元素都是树中的叶子结点,它们都可以看做是只含一个元素的堆。过程 BUILD-MAX-HEAP
对树中的每一个非叶子结点都调用了一次 MAX-HEAPIFY
。
BUILD-MAX-HEAP(A) 1 A.heap-size = A.length 2 for i = FLOOR(A.length / 2) downto 1 3 MAX-HEAPIFY(A, i)
下图给出了 BUILD-MAX-HEAP
过程的一个例子。
我们可以计算出 BUILD-MAX-HEAP
运行时间的一个简单上界:每次调用 MAX-HEAPIFY
的时间为 O (lg n ),共有 O ( n )次调用,故运行时间是 O ( n lg n )。尽管这个界是对的,但从渐近意义上来说不够紧确。
实际上,我们可以得到一个更加紧确的界。因为,在树中不同高度的结点处运行 MAX-HEAPIFY
的时间不同,而大部分结点的高度都比较小。关于更紧确界的分析依赖于这样的性质:一个 n 元素堆的高度为FLOOR(lg n ),并且,在任意高度 h 上,至多有CEIL( n / 2( h + 1))个结点。
MAX-HEAPIFY
作用在高度为 h 的结点上的时间为 O ( h ),可以将 BUILD-MAX-HEAP
的代价表示为:
最终可以得出 BUILD-MAX-HEAP
过程运行时间的界为
这说明可以在线性时间内,将一个无序数组建成一个最大堆。
堆排序算法
堆排序算法先用 BUILD-MAX-HEAP
将输入数组 A [ 1 .. n ]建成一个最大堆。此时数组中最大元素在根 A [ 1 ],可以通过将它与 A [ n ]互换来达到最终正确的位置。然后通过缩小 A.heap-seize ,可以很容易地将 A [ 1 .. n - 1 ]建成最大堆。不断的重复这一过程,堆的大小由 n - 1一直降到2。
HEAP-SORT(A) 1 BUILD-MAX-HEAP(A) 2 for i = A.length downto 2 3 exchange A[1] with A[i] 4 A.heap-size = A.heap-size - 1 5 MAX-HEAPIFY(A, 1)
HEAP-SORT
过程的时间代价为 O ( n lg n )。其中调用 BUILD-MAX-HEAP
的时间为 O ( n ), n - 1次 MAX-HEAPIFY
调用中每次的时间代价为 O (lg n )。
堆结构和堆排序算法的简单Java实现
/** * 堆支持的公共方法 */ public interface Heap { public int[] toArray(); public int[] toHeapArray(); /** * 优先级队列支持的四个操作 */ public int head(); public int extractHead(); public void changeKey(int i, int key); public void add(int k); }
/** * 堆结构的公共部分实现 */ public abstract class AbstractHeap implements Heap { private static final int DEFAULT_CAPACITY = 10; protected int size; // 堆中元素的个数 protected int length; // 数组中元素的个数 protected int[] elements; protected AbstractHeap() { this(DEFAULT_CAPACITY); } protected AbstractHeap(int len) { this.elements = new int[len]; this.length = len; this.size = 0; } /** * 接受数组类型的参数后直接建堆 */ protected AbstractHeap(int[] array) { this.elements = Arrays.copyOf(array, array.length); this.length = array.length; buildHeap(); } protected int parent(int i) { return (i - 1) >> 1; } protected int left(int i) { return (i << 1) + 1; } protected int right(int i) { return (i + 1) << 1; } /** * 维持堆特性:最大堆与最小堆除了比较部分,其余代码都是相同的,故将比较部分抽取出来留给具体的实现 */ protected void heapify(int i) { int l = left(i); int r = right(i); int index; if (l < size && heapifyLogic(l, i)) index = l; else index = i; if (r < size && heapifyLogic(r, index)) index = r; if (index != i) { swap(index, i); heapify(index); } } protected void buildHeap() { size = length; for (int i = (size >> 1) - 1; i >= 0; i--) heapify(i); } protected abstract boolean heapifyLogic(int a, int b); @Override public int head() { if (size == 0) throw new IndexOutOfBoundsException("heap underflow"); return elements[0]; } @Override public int extractHead() { if (size == 0) throw new IndexOutOfBoundsException("heap underflow"); int head = head(); elements[0] = elements[size - 1]; size--; heapify(0); return head; } @Override public int[] toArray() { return Arrays.copyOf(elements, length); } @Override public int[] toHeapArray() { return Arrays.copyOf(elements, size); } protected void swap(int i, int j) { AlgorithmUtil.swap(elements, i, j); } }
/** * 最大堆 */ public class MaxHeap extends AbstractHeap { public MaxHeap() { super(); } public MaxHeap(int len) { super(len); } public MaxHeap(int[] array) { super(array); } /** * 最大堆特性:要求子结点的值不大于其父结点 */ @Override protected boolean heapifyLogic(int a, int b) { return elements[a] > elements[b]; } @Override public void changeKey(int i, int key) { if (key < elements[i]) throw new IllegalArgumentException("new key is smaller than current key"); elements[i] = key; while (i > 0 && elements[parent(i)] < elements[i]) { swap(i, parent(i)); i = parent(i); } } @Override public void add(int k) { if (size == length) throw new IndexOutOfBoundsException("heap overflow"); size++; elements[size - 1] = Integer.MIN_VALUE; changeKey(size - 1, k); } /** * 堆排序 */ public void heapSort() { for (int i = length - 1; i > 0; i--) { swap(0, i); size--; heapify(0); } } }
/** * 最小堆 */ public class MinHeap extends AbstractHeap { public MinHeap() { super(); } public MinHeap(int len) { super(len); } public MinHeap(int[] array) { super(array); } /** * 最小堆特性:要求子结点的值不小于其父结点 */ @Override protected boolean heapifyLogic(int a, int b) { return elements[a] < elements[b]; } @Override public void changeKey(int i, int key) { if (key > elements[i]) throw new IllegalArgumentException("new key is larger than current key"); elements[i] = key; while (i > 0 && elements[parent(i)] > elements[i]) { swap(i, parent(i)); i = parent(i); } } @Override public void add(int k) { if (size == length) throw new IndexOutOfBoundsException("heap overflow"); size++; elements[size - 1] = Integer.MAX_VALUE; changeKey(size - 1, k); } }
练习
6.2-2
最小堆伪码
MIN-HEAPIFY(A, i) 1 l = LEFT(i) 2 r = RIGHT(i) 3 if l <= A.heap-size and A[l] < A[i] 4 smallest = l 5 else 6 smallest = i 7 if r <= A.heap-size and A[r] < A[smallest] 8 smallest = r 9 if smallest != i 10 exchange A[i] with A[smallest] 11 MIN-HEAPIFY(A, smallest)
6.2-5
使用迭代结构改写 MAX-HEAPIFY
过程
MAX-HEAPIFY-ITERATOR(A, i) 1 largest = -1 2 while largest != i 3 l = left(i) 4 r = right(i) 5 if l <= A.heap-size and A[l] > A[i] 6 largest = l 7 else 8 largest = i 9 if r <= A.heap-size and A[r] > A[largest] 10 largest = r 11 if largest != i 12 exchange A[largest] with A[i] 13 i = largest 14 largest = -1