这里的堆并不是JVM中堆栈的堆,而是一种特殊的二叉树,通常也叫作二叉堆。它具有以下特点:
1)它是完全二叉树
2)每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆。每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。代码中只体现大顶堆。
堆排序的基本思想是:.
1)将待排序序列构造成一个大顶堆。实现方式。从最后一个非叶子节点(arr.length / 2 - 1 ) 开始,直到根节点0,是一个树结构上从右到左,从下而上构建大顶堆的过程。此时,整个序列的最大值就是堆顶的根节点。
2)将其与末尾元素进行交换,此时末尾就为最大值。因为末尾元素和堆顶元素交换了位置,正好被破坏了大顶堆的结构,现在只需要从上而下,将交换过来的堆顶元素放到一个合适位置就好了。从堆顶元素开始找到左右子节点中较大的数,和堆顶元素比较,如果比堆顶元素大,就替换元素,
如果没有堆顶元素大,正好符合大顶堆的要求。
3)然后将剩余n-1 个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行2)-3),便能得到一个有序序列了。
建堆的时间复杂度为O(n)(调用一次);调整堆的时间复杂度为lgn,其中调用了n-1次,因此堆排序的时间复杂度为O(n)+O(nlgn) ~ O(nlgn)。
代码如下。请好好看注释!
public class HeapSort {
public static void main(String[] args) {
// 要求将数组进行升序排序
int arr[] = { 7, 4, 3, 8, 9, 10, -1, 99 };
System.out.println("排序前=" + Arrays.toString(arr));
heapSort(arr);
System.out.println("最终结果=" + Arrays.toString(arr));
}
public static void heapSort(int[] arr) {
if (arr == null || arr.length == 0) {
System.out.println("数组不能为空");
return;
}
/*
* 大顶堆的是一个完全二叉树,所以最后的非叶子节点开始到根节点,每个节点都是非叶子节点。
* 下面for循环将一个无序的二叉树构建成了大顶堆。从最后一个非叶子节点(arr.length / 2 - 1 )
* 开始,直到根节点0,是一个树结构上从右到左,从下而上构建大顶堆的过程。
* 因为自下而上构建,所以,当父节点比左右子节点大的时候,不用再遍历左右子节点的子节点了。
*/
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
System.out.println("排序后=" + Arrays.toString(arr));
/*
* 注意下面这个循环每次调用adjustHeap()时,因为末尾元素和堆顶元素交换了位置,正好被破坏了,
* 千万注意,目前的这个被破快的结构是除了堆顶元素下面的分支都是符合大堆顶的二叉树。
* 现在只需要从上而下,将交换过来的栈顶元素放到一个合适位置就好了。
* adjustHeap()的做法就是,从堆顶元素开始找到左右子节点中较大的数,和堆顶元素比较,如果比堆顶元素大,就替换元素,
* 如果没有堆顶元素大,正好符合大顶堆的要求(解释了方法中break的由来),如此循环,不断调整结构,使其满足堆定义,
* 给堆顶元素找到合适的位置。
*/
for (int n = arr.length - 1; n > 0; n--) {
System.out.println("剩余数据量为" + (n + 1));
int temp = arr[n];
arr[n] = arr[0];
arr[0] = temp;
System.out.println("排序后=" + Arrays.toString(arr));
adjustHeap(arr, 0, n);
System.out.println("调整后=" + Arrays.toString(arr));
}
}
//将一个根节点索引为i的子二叉树,调整成一个大顶堆
/**
* @param arr 待调整的数组
* @param i 表示非叶子结点在数组中索引
* @param lenght 表示对多少个元素继续调整, length 是在逐渐的减少
*/
private static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];
//1. k = i * 2 + 1 k 是 i结点的左子结点
for (int j = i * 2 + 1; j < length; j = j * 2 + 1) {
//说明左子结点的值小于右子结点的值。j指向右节点。本质就是找到左右节点中的较大值
if (j + 1 < length && arr[j] < arr[j + 1]) {
j++;
}
//如果子结点大于父结点
if (arr[j] > temp) {
//把较大的值赋给当前结点
arr[i] = arr[j];
//!!! i 指向 k,继续循环比较
i = j;
} else {
/*
* 下面break的理解至关重要。上面调用方法两次提到这里!注意看上面解释!
*/
break;
}
}
//当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)。并且给temp找到了合适为位置(当前i的位置)
arr[i] = temp;
}
}