以一个初学者的角度去了解堆排序,这或许能帮助大家更好的去理解堆排序。
一、什么是堆排序?
堆排序(Heapsort)是指利用堆这种数据结构来设计的一种排序算法。那么堆是什么:堆是一个近似完全二叉树的非线性结构但同时又满足堆积的性质:即子结点的键值或索引小于(或者大于)它的父节点。堆排序的时间复杂度O(N*logN),空间复杂度O(1),这是个不稳定性的排序算法。
二、堆的分类:
堆分为大顶堆(大根堆)和 小顶堆(小根堆)那么下面我们来认识这两个堆结构
大顶堆(大根堆):简单来说就是堆顶(根节点)的元素是所有元素中最大的那个,每个节点的元素都要大于或等于左右的子节点。
小顶堆(小根堆)就与之相反:堆顶(根节点)的元素是所有元素中最小的那个,每个节点的元素都要小于或等于左右的子节点。
如果文字难以理解那咱直接上图片!
三、基本思想:
堆排序的基本思想:先将需要排序的堆结构转换为数组结构形成大根堆,用现在堆顶的元素与左右子节点进行比较后交换(子节点大于父节点就进行交互,两种比较方案往下会解释),比较交换完成后这时堆顶的元素应该是数组中最大值,把堆顶元素和堆结构的最后一个子节点进行交换来表示该元素已经是有序的了,然后继续进行比较交互在比较交互这里可以排除最后一个子节点。依次重复逐渐达到排序目的。
四、代码的逐步实现:
在上图我们给每个元素都标注了下标(索引)这看起来是不是有点收悉,堆就类似数组(我是这么理解的)所以我们给它像数组一样排个序。
这就完成堆排序的第一步了!
1)第一步:堆结构转换到数组结构。
因为堆结构与数组结构类似所以可以不用转换直接进行排序。
2)第二步:将堆结构不断调整成大顶堆。
这一步是堆排序的难点,我们可以提取成一个方法方便调用。在该方法中涉及到一个公式
[(构建大根堆的最小下标-最大下标) / 2]向上取整数再-1。用该公式帮我们找到堆结构的最后一个父节点的数组下标方便我们交互
public void maxHeap(int[] array,int end) {
//根据公式计算出堆结构中最后一个父节点的下标
//公式:lastFather = [(start + end) / 2]向上取整 - 1;(array.length - 2 ) / 2
int lastFather = (0 + end) % 2 != 0 ? (0 + end)/2 : (0 + end)/2-1;
//从最后一个父节点开始向前不断减一(循环),使得每个父节点能够与左右子节点进行比较
for(int father = lastFather;father >= 0;father--) {
//根据父节点的下标推算出左右孩子的数组下标
//左子节点:2n+1
//右子节点:2n+2
int left = father * 2 + 1;
int right = father * 2 + 2;
//需要保证右子节点下标在没有越界的情况下,如果越界就会出现异常
//如果右子节点 > 父节点的,则进行交换
if(right <= end && array[right] > array[father]) {
int temp = array[right];
array[right] = array[father];
array[father] = temp;
}
//使用左子节点和父节点进行比较,这里不用考虑下标越界问题,因为有父节点就必须有左子
//节点要不然就没有父节点
//如果左子节点 > 父节点,则进行交换
//这里间接进行了左右子节点的大小比较
if(array[left] > array[father]) {
int temp = array[left];
array[left] = array[father];
array[father] = temp;
}
}
}
3)第三步:将堆顶元素和堆的最后一个字节点进行交换,表示这个元素已经有序了。
4)第四步:需要把第二步还有第三步放在一个循环里让每一次让每一个元素变得有序。
for(int end = array.length-1; end > 0; end--) {
maxHeap(array,end);
int temp = array[0];
array[0] = array[end];
array[end] = temp;
}
五、完整代码:
public class HeapSort {
public void heapSort(int[] array) {
//利用循环,让每一次每一个元素变得有序,每次都将堆排序的数组中最大下标减一
for(int end = array.length-1; end > 0; end--) {
//调用newHeap()方法,将堆结构重构为大根堆
newHeap(array,end);
//将堆顶元素(数组中最大的元素)和堆的最后一个子节点进行交换,表示该元素已经有序
int temp = array[0];
array[0] = array[end];
array[end] = temp;
}
}
/*
* 重构大根堆的方法
* end:数组元素的最大下标,相当于堆中最后一个节点在数组中对应的下标
*
*/
private void newHeap(int[] array,int end) {
//用公式计算出堆结构中最后一个父节点的下标
//公式:lastFather = [(start + end) / 2]向上取整 - 1;(array.length - 2 ) / 2
int lastFather = (0 + end) % 2 != 0 ? (0 + end)/2 : (0 + end)/2-1;
//从最后一个父节点开始向前推,使得每个父节点能够实现左右比较交换
for(int father = lastFather;father >= 0;father--) {
//可根据父节点的下标推算出左右子节点的数组下标
//左子节点:2n+1
//右子节点:2n+2
int left = father * 2 + 1;
int right = father * 2 + 2;
//需要保证右子节点下标没有越界,使用右子节点与父节点进行比较
//如果右子节点 > 父节点的,则进行交换
if(right <= end && array[right] > array[father]) {
int temp = array[right];
array[right] = array[father];
array[father] = temp;
}
//使用左子节点和父节点进行比较,如果左子节点 > 父节点,则进行交换
//相当于间接进行了左右子节点的大小比较
if(array[left] > array[father]) {
int temp = array[left];
array[left] = array[father];
array[father] = temp;
}
}
}
//测试
public static void main(String[] args) {
int[] array = new int[] {1,8,2,6,0,3,5,9,7,4};
HeapSort he = new HeapSort();
he.heapSort(array);
System.out.println(Arrays.toString(array));
}
}
总结:堆排序应该是数据结构中特别难的难点了,尤其是像我这样的初学者(本人才学习半年多的JAVA语言)当面对这些比较难的难点时我们应该先去打好基础,在面对这类题目时首先应该考虑平时日常生活中常用的数学思维去思考,手写出题目的数学逻辑在一点一点的转换到代码层面。这点我个人认为是比较难的地方这需要做很多题目才能训练出来。像在堆排序中我们可以先理解它的基本思想在草稿纸上进行演示,在转换到代码层面从而更深刻的理解堆排序。以上就是我关于堆排序的一些了解。如果有错误望指正。
//本篇代码学习自B站:写Bug的拉哥。如果侵权请联系我会及时删除。