这是小密圈《进击的Java新人》第十一周第二课。
我们今天接着昨天讲的内容把选择排序的相关知识都讲完。选择排序中有一类特殊的排序算法,叫做堆排序。在介绍堆排序之前,我们必须先来讲明白堆是什么。
堆是一种非常灵活的数据结构,我们可以单独地使用它来解决很多有趣的问题。而且,由于堆的定义本来就有最优的含义,所以它与贪心算法有着天然的联系。在后面的讲解中,我们会不断地遇到以堆做为基本的数据结构进行贪心求解的例子。
堆这种数据结构本质是一个完全二叉树,如图 4.1 所示,但是通过分析,我们可以使用数组来实现它。由于堆本质上是一棵完全二叉树,因此,这里可以直接使用二叉树的相关概念,例如高度等等。在实现二叉树的时候,树结点通常这样定义:
public class Node {
public T data;
public Node left;
public Node right;
public Node parent;
public int state;
public Node(T d) {
this.data = d;
}
}
就是说,一个结点通常会有左,右孩子结点指针和父结点指针。但在完全二叉树中,是可以省略掉这三个指针的,因为完全二叉树的结点编号都是有规律的。给定某一个结点,假设它的下标为i,那么它的左孩子结点的下标就是2i + 1,右孩子结点的下标就是2i + 2,它的父结点为(i−1)/2。这样,我们就把可以省略去这些指针,直接将堆中的结点存储在数组中了。请对照示意图理解一下这段话。
堆又分为最大堆和最小堆。堆的性质非常简单,如果是最大堆,对于每个结点,都有结点的值大于两个孩子结点的值。如果是最小堆,那么对于每个结点,都有结点的值小于孩子结点的值。由此,可以得到一个推论,那就是,最大堆的根结点,必然是堆中的最大值,同理,最小堆的根结点,也必然是堆中的最小值。
建堆
对于一个堆,如果除了堆顶元素不满足结点大于孩子结点的条件,它的两个子树已经是符合条件的最大堆,我们很容易就可以将其再维护成一个符合条件的最大堆。将堆顶元素与两个孩子结点中最大的那个进行交换,然后再对互换的子树递归地进行维护。
public static void maxHeapify(int arr[], int length, int root) {
if (root >= length) {
return;
}
int largest = root;
int left = root * 2 + 1;
int right = root * 2 + 2;
if (left < length && arr[left] < arr[largest]) {
largest = left;
}
if (right < length && arr[right] < arr[largest]) {
largest = right;
}
if (largest != root) {
int t = arr[root];
arr[root] = arr[largest];
arr[largest] = t;
maxHeapify(arr, length, largest);
}
}
如图 4.2 所示,除了堆顶元素不满足最大堆的条件外,根结点的两棵子树已经分别是两个最大堆。使这个堆规范化的过程实际上就是堆顶元素13不断地向下降的过程。
有了规范化最大堆的函数以后,我们就可以轻松地从把一个无序数组规范化成一个堆。首先,如果堆中只含有一个元素,那么它必然是一个规范的最大堆,也就是说,如果把一个数组表示成完全二叉树,那么树上的每一个叶子结点都是一个规范的最大堆。这样,我们可以不断地自底向上规范化子堆,直到整个堆已经全部规范化。
public static void buildUpHeap(int[] arr) {
if (arr.length <= 1)
return;
int n = (arr.length - 2) / 2;
while (n >= 0) {
maxHeapify(arr, arr.length, n);
n--;
}
}
好了。今天的课程就到这里了。我们今天讲的方法是自底向上地建堆。这种方法应用于堆排序可以减少空间的使用,但其实不利于扩展。明天我们再讲一下自上而下的建堆方法。然后,我们再讲一下堆排序。
堆的内容还没讲解完,今天不留作业,因为好几道题目都是要求自上而下建堆的。今天只要对照图和代码把堆的性质牢牢掌握就可以了。