堆排序,实际是将数组转化成堆,利用堆的结构,保存每一次的对比结果,加快排序的过程。
一、什么是堆
堆可以简单理解为是一种有一定顺序的完全二叉树。
而完全二叉树就是中间没有空节点的二叉树。
完全二叉树图示:
(PS:完全二叉树定义:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。)
堆也是一种完全二叉树,但是它有一个性质:堆中某个节点的值总是不大于或不小于其父节点的值。所以,堆简单记为有一定顺序的完全二叉树。
根据堆的性质,分为两种堆。
1、根节点是最大值的堆叫做大根堆;
2、根节点是最小值的堆叫做小根堆;
堆的图示:
二、堆排序实现过程
根据完全二叉树结构,设当前位置为 i ,则
当前位置左节点为 (2 * i + 1),
当前位置右节点为 (2 * i + 2),
当前位置父节点为 ((i - 1) / 2)。
堆排实现步骤:
1、先将数组转化为大根堆,转化思路:将数组0位定为堆的根节点,数组后续的数依次插入这个大根堆,插入规则是,当前插入的值比父节点大时,则当前值与父节点值交换,确保大根堆的结构;
2、形成大根堆后,取出该堆的根节点,即数组中最大值,排到数组最后一位,由此,数组中最大值已排好位置。剩下的数继续调整为大根堆,继续取出根节点,直至整个数组排好序。
3、上述过程,我们可以不用辅助数组,直接在原数组上操作,作法是,将堆的根节点与堆的最后一个节点交换,并把堆容量-1,即数组0位与数组最后一位交换值,如:原始数组arr是0-9位,取出一次根节点后,数组arr[9]就是最大值,剩下的0-8位继续组成大根堆,继续取出根节点,直到0-0位时,则整个数组有序。
过程图示:
java代码实现:
// 堆排
public class HeapSort {
public static void main(String[] args) {
Integer[] arr = DataStore.getData();
String source = "";
for(int i = 0; i < arr.length; i++) {
source += arr[i] + " ";
}
System.out.println("原数组:" + source);
HeapSort.sort(arr);
String res = "";
for(int i = 0; i < arr.length; i++) {
res += arr[i] + " ";
}
System.out.println("排序后:" + res);
}
// 堆排
public static void sort(Integer[] arr) {
// 将数组转化为大根堆
for(int i = 0; i < arr.length; i++) {
HeapSort.headInsert(arr, i);
}
// 堆容量
int headSize = arr.length - 1;
while(headSize > 0) {
// 取出最大值,即将堆的根节点与最后一个堆值交换,并让堆容量-1
DataStore.swap(arr, 0, headSize);
headSize = headSize - 1;
// 调整堆顺序为大根堆
HeapSort.headIfy(arr, 0, headSize);
}
}
// 向堆中插入新值
public static void headInsert(Integer[] arr, int i) {
if(i >= 0) {
// 堆结构固定,arr[(i - 1) / 2]得到的是父节点
// 如果当前位置比父节点大,则与父节点交换值,并把当前位置i指向父节点
// 循环这一过程,直到当前位置的值不比父节点大,或者当前位置已到堆顶
while(arr[i] > arr[(i - 1) / 2]) {
DataStore.swap(arr, i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
}
// 调整堆顺序为大根堆
public static void headIfy(Integer[] arr, int i, int headSize) {
if(i >= 0) {
// 堆结构固定,(2 * i + 1) 得到的是左子节点
// 左子节点小于等于堆容量时,即当前位置仍然有子节点时,继续循环
while((2 * i + 1) <= headSize) {
// 左子节点
int l = 2 * i + 1;
// 堆结构固定,(2 * i + 2) 得到的是右子节点
int r = 2 * i + 2;
// 获取左右节点中的最大值
int large = r <= headSize && arr[r] > arr[l] ? r : l;
// 获取当前节点与左右子节点间的最大值
large = arr[large] > arr[i] ? large : i;
// 当最大值就是当前位置,说明已经是大根堆,跳出循环
if(large == i) {
return;
}
// 当前位置的值与最大值交换
DataStore.swap(arr, i, large);
// 当前位置指向最大值节点
i = large;
}
}
}
}
DataStore工具类
public class DataStore {
// 随机获取10个整数
public static Integer[] getData() {
List<Integer> list = new ArrayList<Integer>();
for(int i = 0; i < 10; i++) {
double d = Math.random();
list.add((int)(d*10));
}
Integer[] arrays = new Integer[list.size()];
list.toArray(arrays);
return arrays;
}
// 数组中两数交换
public static void swap(Integer[] array, int i, int j) {
if(array != null && i < array.length && j< array.length) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
输出:
原数组:5 1 7 7 3 6 5 8 6 8
排序后:1 3 5 5 6 6 7 7 8 8
容易看到,堆排将一个数组看成是一个完全二叉树的结构去处理,完全二叉树的层级是logN,每个数最多需要对比的次数也是logN,所以,堆排序时间复杂度是O(N*logN),并且,空间复杂度只有O(1)。
堆排序真是计算机先驱的一大发明,一个巧妙的排序过程。