一篇文章弄懂——堆排序
概述
堆排序是我们常说的十大排序算法中的一种,堆排序也是数据结构中比较重要的一个知识点,我们今天就来好好探究一下堆排序。在说堆排序之前,我们就必须先明白什么是二叉堆,因为堆排序就是在二叉堆的基础上完成的。
什么是二叉堆
话不多说直接上图:
仔细观察上图的特点:
1. 二叉堆是一颗完全二叉树。
2. 二叉堆中父节点的值总是大于获等于(大顶堆)任何一个孩子节点的值。
3. 每一个结点的左子树和右子树都是一个二叉堆。
在了解二叉堆的特性后我们知道,二叉堆其实就是一颗完全二叉树,那么我们如何在程序中表示这个完全二叉树呢?
使用数组表示完全二叉树
使用数组表示一颗完全二叉树的思路是什么呢?
即按照层次遍历的顺序将二叉树的所有节点存入数组中{1,3,7,5,24,41,12,6,5}。既然使用数组保存二叉树,那么访问某个节点的孩子节点,以及父节点的基本需求还是需要满足的:
假设节点的索引为 i,,那么:
该节点的左孩子的数组索引为:2*i+1
右孩子:2*i+2
父节点:(i-1)/2 整数除法
我们可以验算一下,拿键值为5的那个节点来举例,它的索引为3,那么左孩子就应该是2*3+1,也就是索引为7的值.......
上面我们知道了在进行堆排序之前所需要的知识,那么我们就来了解一下堆排序的具体思路吧!
基于二叉堆如何进行排序呢?
第一步:将堆顶元素与最后的叶子节点进行交换
此时会发现堆顶元素(二叉树的根节点)不满足小顶堆的要求(根元素要比两个孩子节点都打小),那么此时就需要将根元素进行“下沉”。
第二步:将根元素进行“下沉”
如果发现粉红色的5依然不满足小顶堆的要求,那么粉色5需要继续下沉,同样是与孩子节点的最小值进行交换。
第三步:将堆顶元素与倒数第二个元素进行交换
第四步:此时堆顶元素还需要进行下沉(下图是经过多次下沉的结果)
注意:标蓝的节点代表已经排好序的元素,所以在下沉的时候就不要考虑它们。
第五步:将堆顶与倒数第三个进行交换
第六步:下沉........
就这么重复“交换”和“下沉”的步骤,直到数组中的所有元素都有序即可完成堆排序。
我想经过上面的图解,你应该对堆排序的的思路有了清晰的认识,到了这里我们还差最后一步,那就是将一个无序数组进行“堆化”。
数组的“堆化”
我们给定一个数组:{7,5,12,6,24,41,1,3,5}。
将数组图形化为二叉树:
从 (数组长度 / 2) - 1 的位置处开始进行下沉,然后依次往后进行下沉:
图中数组的长度为9,所以从9/2-1,也就是索引为3的位置开始,也就是值为“6”的节点,将“6”下沉到合理位置,如下图:
然后在将索引为2,也就是值为“12”的节点,下沉至合理位置,如下图:
然后将索引为1,也就是值为“5”的下沉至合理位置:
最后将索引为0,也就是根节点下沉至合理位置:
这样一个小顶堆就完成了构建,这样堆排序的所有思路都理解了吧。
思路总结
堆排序的第一步就是将无序数组“堆化”,顺序排序则堆化成大顶堆,若想逆序排序则堆化成小顶堆。
然后在小顶堆或大顶堆的基础上进行排序。排序又可总结成两步:
第一步:交换(根节点与未排序的尾节点交换)
第二步:下沉(交换后的节点可能不满足堆的规则,需要下沉到合理位置)
堆排序的实现
import java.util.Arrays;
public class TestHeapSort {
public static void main(String[] args) {
int[] arr = {7, 5, 12, 6, 24, 41, 1, 3, 5};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 进行堆排序
* @param arr
*/
private static void heapSort(int[] arr){
makeHeap(arr);//将数组堆化
sort(arr,arr.length);
}
/**
* 进行无序数组的“堆化操作”
*
* @param arr
*/
private static void makeHeap(int[] arr) {
int k = arr.length / 2 - 1;//获取堆化操作的起始节点
for (int i = k; i >= 0; i--) {
minHeapFixDown(arr, i,arr.length);
}
}
/**
* 判断以索引为k的节点的二叉树是否满足小顶堆堆,如果不满足,则将这个元素下沉,使其满足小顶堆要求
*
* @param arr
* @param k
*/
private static void minHeapFixDown(int[] arr, int k,int length) {
int base = arr[k];
Integer left = null;
Integer right = null;
if (k * 2 + 1 < length) {
left = arr[k * 2 + 1];
}
if (k * 2 + 2 < length) {
right = arr[k * 2 + 2];
}
if (left == null && right == null) {//如果没有孩子节点了,则退出函数,也就是说已经下沉到底部了。
return;
}
if (right != null) {//如果有右孩子
if (base <= left && base <= right) {//如果满足小顶堆,则退出函数
return;
}
if (left < right) {
swap(arr, k, 2 * k + 1);
minHeapFixDown(arr, 2 * k + 1,length);
} else {
swap(arr, k, 2 * k + 2);
minHeapFixDown(arr, 2 * k + 2,length);
}
} else {//只有左孩子时
if (base > left) {
swap(arr, k, 2 * k + 1);
}
}
}
/**
* 在小顶堆的基础上,对数组进行排序,将对顶与最后一个元素进行交换,然后使对顶进行下沉,但下沉的边界不包含刚刚交换的
* 元素的位置。此时又是一个小顶堆,然后将对顶与倒数吧第二个元素进行交换......(递归)
* @param arr
* @param length
*/
private static void sort(int[] arr,int length){
if(length==1){
return;
}
swap(arr,0,length-1);
minHeapFixDown(arr,0,length-1);
sort(arr,length-1);
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
执行结果: