一、堆排序概述
作为选择排序的改进版,堆排序可以把每一趟元素的比较结果保存下来,以便我们在选择最小/大元素时对已经比较过的元素做出相应的调整。
堆排序是一种树形选择排序,在排序过程中可以把元素看成是一颗完全二叉树,每个节点都大(小)于它的两个子节点,当每个节点都大于等于它的两个子节点时,就称为大顶堆,也叫堆有序; 当每个节点都小于等于它的两个子节点时,就称为小顶堆。
算法思想(以大顶堆为例):
- 将长度为n的待排序的数组进行堆有序化构造成一个大顶堆
- 将根节点与尾节点交换并输出此时的尾节点
- 将剩余的n -1个节点重新进行堆有序化
- 重复步骤2,步骤3直至构造成一个有序序列
二、构造堆
假设待排序数组为[20,50,10,30,70,20,80]
在构造有序堆时,我们开始只需要扫描一半的元素(n/2-1 ~ 0)即可,为什么?
因为(n/2-1)~0的节点才有子节点,如下图,n=8,(n/2-1) = 3 即3 2 1 0这个四个节点才有子节点
第一步从3开始,比较3与其子节点32+1和32=2,把最大的和3位置上的数字互相交换;
然后从下到上,从右到左,依次调整所有的非叶子结点;
这里注意图四,80和20交换后,由于交换前80有子节点,交换可能打破平衡,所以对于有子节点的节点替换后应该再次调整它使堆保持平衡。
至此有序堆已经构造好了!如上图四。
三、调整堆
(1)堆顶元素80和尾40交换后–>调整堆
(2)堆顶元素70和尾30交换后–>调整堆
(3)堆顶元素60尾元素20交换后–>调整堆
(4)其他依次类推,最终已排好序的元素如下:
四、代码实现
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] arr = new int[]{20, 50, 21, 40, 70, 10, 80, 30, 60};
//最后一位下标
int lastIndex = arr.length - 1;
//arr.length / 2 - 1;堆特性只有这个数量的节点有子节点
for (int i = lastIndex / 2 - 1; i >= 0; i--) {
constructHeap(arr, i, lastIndex);
}
System.out.println(Arrays.toString(arr));
//[80, 70, 21, 60, 50, 10, 20, 30, 40]
while (lastIndex > 0) {
//把堆顶的元素换到末尾
swap(arr, 0, lastIndex);
//末尾不参与置换
lastIndex--;
//对堆结构进行调整,最后一位不参与调整
constructHeap(arr, 0, lastIndex);
}
System.out.println(Arrays.toString(arr));
//[10, 20, 21, 30, 40, 50, 60, 70, 80]
}
/**
* 对数组的某个父节点进行操作,比较其与子节点的大小,将最大的置换到父节点位置
* @param arr 给定数组
* @param i 处理哪一个父节点
* @param lastIndex 参与交换的数组尾标。可以指定小于数组长度,代表后面的几个数不参与交换
*/
private static void constructHeap(int[] arr, int i, int lastIndex) {
if (lastIndex <= 0) {
return;
}
//堆特性,左节点右节点index
int left = 2 * i + 1;
int right = 2 * i + 2;
if (right > lastIndex) {
//只比较left和父节点哪个大放到父节点
if (arr[left] > arr[i]) {
swap(arr, left, i);
//与父节点交换的节点是否有子节点,如果有的话需要递归
if (left <= lastIndex / 2 - 1) {
constructHeap(arr, left, lastIndex);
}
}
} else {
//比较三个最大的和父节点交换
int sonMaxIndex = arr[left] >= arr[right] ? left : right;
if (arr[sonMaxIndex] > arr[i]) {
swap(arr, sonMaxIndex, i);
//与父节点交换的节点是否有子节点,如果有的话需要递归
if (sonMaxIndex <= lastIndex / 2 - 1) {
constructHeap(arr, sonMaxIndex, lastIndex);
}
}
}
}
private static void swap(int[] arr, int son, int father) {
int temp = arr[father];
arr[father] = arr[son];
arr[son] = temp;
}
}
五、topN实践
大多数的选择排序算法在topn应用中性能都不错,因为topn只需要取出最大或最小的n个数就可以停止了。
这里举一个曾优化过的案例分享给大家:
- 需求:某电商app需要实时统计每天的topn热销商品,要求就是每天、实时。老的项目组使用Flink WindowAll所有数据放到一个集合然后使用jdk自带的排序实现topn计算,初期还好,后来订单量激增后,产生背压,mq积压等问题。
- 原因:之后分析其使用的Trigger为每条数据都会触发计算,jdk的sort方法性能一般,在数据量并发量大时执行缓慢
- 优化:trigger指定n条数据触发一次窗口计算,排序算法优化为堆排序。优化后性能提高一个数量级以上。