一、需要了解的基本概念
- 大顶堆:在一棵二叉树中,根节点一定大于其左右子节点的值
- 小顶堆:在一棵二叉树中,根节点一定小于其左右子节点的值
- 二叉树和数组的转换:假设根节点在数组中的位置为n,那么它的左子节点的位置为2n+1,右子节点的位置为2n+2
- 最后一个非叶子结点的位置:假设数组的长度为len,则最后一个非叶子节点的位置为(len / 2) - 1
二、堆排序的思想
了解了上述的基本概念之后,来说一下堆排序的基本思想顺序,以大顶堆(升序)为例
- 1、从最后一个非叶子结点开始,依次将非叶子结点与它的左右子节点构成的树转化为一个局部的大顶堆,并且将左右子节点也作为根节点进行构建局部大顶堆处理。
- 2、从最后一个非叶子结点到根节点依次执行步骤1之后,最大的数位于 索引0 位置,即树的根节点位置,将 0 位置与数组最后一个位置交换,继续执行大顶堆流程
- 3、反复执行n-1次 构建大顶堆流程,此时可以得到升序的数组
三、图解
如上图,由于小于自己的右子节点,所以需要交换他们的值
而且因为这一层的子节点都是叶子结点,不需要处理叶子结点。
但是到了第二层的非叶子结点,由于在构建完自己的这颗大顶堆树之后,自己子节点的大顶堆会被破坏,所以我们需要再将子节点的结构调整
第一次大顶堆的流程完之后,会将索引0位置的元素与最后一个位置的元素值交换,并且下次构建大顶堆时,数组的长度会减1。
四、代码
4.1 局部大顶堆构建
/**
*
* @param len 当前需要处理的长度
* @param i 当前非叶子结点的索引
* @param arr 数组
*/
public static void heapify(int len, int i, int arr[]){
//获取左子节点的索引
int left = 2 * i + 1;
//获取右子节点的索引
int right = 2 * i + 2;
int largest = i;
//一定注意防止数组越界
if(left < len && arr[left] > arr[largest]){
largest = left;
}
if(right < len && arr[right] > arr[largest]){
largest = right;
}
//当largest 改变,说明需要交换
if(largest != i){
swap(arr, largest, i);
//继续调整被改变了的子节点的大顶堆结构
heapify(len, largest, arr);
}
}
4.2 一次完整的大顶堆构建
/**
* 完成一次大顶堆的构建,将最大数放到根节点
* @param arr
* @param len
*/
public static void buildHeap(int[] arr, int len){
//(len / 2) - 1 获取最后一个非叶子结点
for(int i = (len / 2) - 1; i >= 0; i--){
heapify(len, i, arr);
}
}
为什么是(len / 2) - 1 , 因为是二叉树,所以我们可以知道:比如树高为5的树,每层的节点个数一次为,1、2、4、8、16,总数为 2 * 16 - 1 = 31;满二叉树中,最后一层的节点个数比之前所有层的节点数大1。所以最后一个非叶子结点只要 节点个数 / 2即可,但是因为是映射在数组中的,索引从0开始,所以需要再减1
4.4 len-1次大顶堆的构建
在完成一次大顶堆流程的构建,将最大值交换到数组的最后一位,我们还要对0-(n - 1)位置的数组元素再次进行大顶堆的构建。
public static void heapSort(int[] arr){
int len = arr.length;
for(int i = len ; i > 0; --i){
buildHeap(arr, i);
swap(arr,0,i - 1);
}
}
上述就是堆排序的整个流程,如果想要进行降序排序,只需要将大顶堆换位小顶堆即可,总体的原理和思想是一样的。
由于没有找到合适的画图工具,就偷个懒,用了工具,发现还是不方便,如果想要看完成的堆排序的过程,就跑到下面的链接去找吧
堆排序: 堆排序动图演示
实用工具:基础数据结构动图演示地址
可以看到有很多的数据结构和算法,都是可以动态演示的。
同学们,冲起来,gogogo!!!