学习了一下堆排序的思想,分享一下我的理解。
首先介绍一些概念。
堆(heap),最大堆(max heap),最小堆(min heap)
堆是一种特别的树状结构,普通的树结构,没有对子节点也特别的规定,但堆是一颗完全的树,除了最底层,上面的每一层都是满的。
如果一个堆中所有的节点,它有用子节点的话,并且这个节点大于它的子节点,那么这个堆就是最大堆。
如果一个堆中所有的节点,它有用子节点的话,并且这个节点小于它的子节点,那么这个堆就是最小堆。
堆的数据结构与特征
堆是一种树结构,但是我们却可以使用数组来表示,因为它是一个完全二叉树。所以我们给定一个数组,比如:[1, 2, 3, 4, 5, 6, 7, 8],就可以构造一个堆。
构造的方法,在图纸上,很简单,按顺序,以树的结构书写数字就好了。
1
/ \
2 3
/ \ / \
4 5 6 7
/
8
因此,它可以直接用数组来表示。
用数组表示的时候,index有一些特征。
节点index = i
,它的左子节点的index = 2 * i + 1
,它的右子节点的index = 2 * i + 2
。
如果数组长度为length
,那么,它最后一个拥有子节点的节点index = length / 2 - 1
堆排序的基本流程
1.将给定的数组按照堆去理解(或者说构造成堆也行)
2.将这个堆进行调整,调整为大根堆或者小根堆,接下来,我们将堆的顶端的元素拿走放进一个新队列中保存起来,将最后一个元素放在堆的根节点上。
3.重复2
在每次对堆进行调整后,都可以得到最大值or最小值,每次取走这个值,堆会越变越小,最后也就排好了。
算法的实现
注释我写的非常清楚了,大家自己看下理解一下吧。
enum SortType {
Increase, Decrease
}
/**
* 堆排序
* @param input 输入数据
* @param sortType 排序类型 升序还是降序
* */
public static int[] heapSort(int[] input, SortType sortType) {
int[] heap = input;
int heapLength = input.length; // 初始化堆的大小,每次找到一个数后,堆的大小都减少1,不去操作最后一个已经排好的数
for (int i = 0; i < input.length; i++) {
heap = maxHeapify(heap, heapLength, sortType); // 进行堆调整,只调整heapLength的区域
swap(heap, 0, heapLength - 1); // 调整完成后,将第一个元素后最后一个元素交换
heapLength--; // 堆的大小减一
}
return heap;
}
/**
* 堆调整
* */
public static int[] maxHeapify(int[] heap, int heapLength, SortType sortType) {
if (heap == null) {
return null;
}
if (heap.length == 1) {
return heap;
}
// 标记是否改变过,如果改变了,需要再次执行本方法,直到没有变化为止
boolean isChange = false;
for (int i = heapLength / 2 - 1; i >= 0; i--) { // 从最后一个有children的节点开始,向前遍历
int leftIndex = i * 2 + 1; // left child index
int rightIndex = i * 2 + 2; // right child index
if (heapLength > leftIndex) {
if (sortType == SortType.Increase) {
if (heap[i] < heap[leftIndex]) { // 判断当前节点和左节点的大小,进行交换
swap(heap, i, leftIndex);
isChange = true; // 每次交换,需要标记,发生了改变,这个过程需要再来一次
}
} else if (sortType == SortType.Decrease) {
if (heap[i] > heap[leftIndex]) {
swap(heap, i, leftIndex);
isChange = true;
}
}
if (heapLength > rightIndex) { // 处理右节点,如果存在的话
if (sortType == SortType.Increase) {
if (heap[i] < heap[rightIndex]) {
swap(heap, i, rightIndex);
isChange = true;
}
} else if (sortType == SortType.Decrease) {
if (heap[i] > heap[rightIndex]) {
swap(heap, i, rightIndex);
isChange = true;
}
}
}
}
}
if (isChange) {
// 如果改变了,需要再来一次
return maxHeapify(heap, heapLength, sortType);
} else {
// 如果每个节点都OK了,返回
return heap;
}
}
public static void swap(int[] arr, int i, int j) {
int v = arr[i];
arr[i] = arr[j];
arr[j] = v;
}
最后测试一下效果:
用了三组数据来测试,一组最差的情况,一组最好的情况,另外一组随机的情况
public static void main(String[] args) {
int loopCount = 100000;
int[] result = null;
long t = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
result = heapSort(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, SortType.Decrease);
}
System.out.println(System.currentTimeMillis() - t);
System.out.println(Arrays.toString(result));
t = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
result = heapSort(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, SortType.Increase);
}
System.out.println(System.currentTimeMillis() - t);
System.out.println(Arrays.toString(result));
int[] arr = new int[10];
for (int i = 0; i < 10; i++) {
arr[i] = (int) (Math.random() * 100);
}
t = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
result = heapSort(arr, SortType.Increase);
}
System.out.println(System.currentTimeMillis() - t);
System.out.println(Arrays.toString(result));
}
运行结果:
可以看到,随机的情况,耗时最少,最差的情况,耗时也很少,最好的情况,反而耗时最多。大概和调整为最大堆和最小堆的时候有关。当我需要从小到大排列的时候,我反而需要将排好的数据构造成最大堆,然后再取出最大的数据放到数组最后。
36 // 最差的情况
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
49 // 最好的情况
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
20 // 随机生成的数据
[4, 5, 11, 21, 22, 44, 66, 79, 93, 99]
如果使用额外的空间来存储,不关心最大堆还是最小堆,会怎么样呢?
总是得到最小堆,然后再用额外的空间来存储结果。
/**
* 堆排序
* @param input 输入数据
* @param sortType 排序类型 升序还是降序
* */
public static int[] heapSort(int[] input, SortType sortType) {
if (input == null || input.length == 0) {
return null;
}
int[] heap = input;
int[] result = new int[input.length];
int heapLength = input.length; // 初始化堆的大小,每次找到一个数后,堆的大小都减少1,不去操作最后一个已经排好的数
for (int i = 0; i < input.length; i++) {
heap = maxHeapify(heap, heapLength, SortType.Decrease); // 进行堆调整,只调整heapLength的区域
// Decrease 排序的话,总是得到最小堆
if (sortType == SortType.Increase) {
result[i] = heap[0];
} else {
result[input.length - i - 1] = heap[0];
}
swap(heap, 0, heapLength - 1); // 调整完成后,将第一个元素后最后一个元素交换
heapLength--; // 堆的大小减一
}
return result;
}
运行结果:
33
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
32
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
23
[12, 13, 18, 21, 38, 72, 73, 86, 91, 97]
最好情况所花的时间确实减少了,但对平均情况来说却没什么影响的,总得来说平均情况是非常稳定的。