堆排序
从算法概述,算法思想,算法实现与分析展开阐述
如有疏漏,欢迎指出
博主空间
https://blog.csdn.net/JOElib?spm=1011.2266.3001.5343BST
https://blog.csdn.net/JOElib/article/details/124116947?spm=1001.2014.3001.5501AVL
https://blog.csdn.net/JOElib/article/details/124072495?spm=1001.2014.3001.5501
目录
算法概述🐼
堆排序的定义🦁
- 堆排序是根据堆中树这种数据结构推演出来的一个排序算法,在实际应用中,我们会通过顺序存 储二叉树来替代真实的二叉树,且堆排序是一种不稳定的排序算法
堆排序所需要的条件🦁
大顶堆🐨
- 大顶堆即父节点的值要比其子节点的值都要大(左右子节点的值没有做出要求)
- 大顶堆适用于升序排序
小顶堆🐨
- 小顶堆即父节点的值要比其子节点的值都要小(左右子节点的值没有做出要求)
- 小顶堆适用于降序排序
堆排序的算法思想🐼
- 构建一个大顶堆,此刻根节点的值为最大值
- 将根节点与最后一个节点进行交换,最后一个节点的值最大且确定
- 将剩余的节点再构建成一个大顶堆,重复以上操作,直到排序完成
即:构建大顶堆 -> 排序 -> 构建大顶堆 -> 排序.....
注意:大顶堆其实是从构建局部大顶堆层层递进从而逐步构建的
堆排序的算法实现与分析🐼
1.构建大顶堆🐻
//array代表的是顺序存储二叉树的数组,
//i是当前节点对应的下标,
//length是调整元素的长度
public static void adjustHeap(int[] array,int i,int length) {
var temp = array[i];
for (int k = 2*i+1; k < length; k = k*2+1) {
if (k+1 < length && array[k+1] > array[k]) {
k++;
}
if (array[k] > temp) {
array[i] = array[k];
i = k;
}else {
break;
}
}
array[i] = temp;
}
代码分析与图解:😶🌫️
- 先将array[i]即当前节点的值存放到临时变量中
- 循环的详解:
- k = 2i + 1因为我们想要从当前节点作为根节点去构建一个局部大顶堆,所以我比较当前节点与其左子节点或右子节点的值,根据顺序存储二叉树得,其左子节点是2i+1
- 因为两个子节点的值的大小不做要求,所以我们判断两个字节点的较大值,与父节点交换,这样才能保证父节点绝对大于子节点
- 因此,k + 1即右子节点的下标,首先,我们要保证有这个节点,即 k + 1 < length(最大的节点的下标是length-1)
- 其次,如果右子节点的值array[k+1]比左子节点的值要大,我们就让k++,指向右子节点
- 此刻,k指向了我们子节点中最大的值,可以跟父节点进行比较即与temp进行比较
- 如果发现子节点的值比父节点的值要大,直接将array[k]赋值给array[i],并把i赋值给k
- 否则,直接退出循环,最后将temp赋值给array[i]
- 原因是:更高效的进行交换.如果我们每次发现子节点的值大于父节点的值都要交换以下,那么就要交换很多次,效率大大降低
- 我们通过k = i,将i不断后移,i到了最后就是最后一个被交换的子节点的位置,直接赋值即可
- 我么发现,通过这个方法,我们只需要覆盖,真正交换一次,会比频繁交换的效率高得多
- 此刻,有一个问题,为什么要有这个循环?
- 我们可以知道,我们构建大顶堆是从构建局部小顶堆开始的,那么说明,我们的局部小顶堆会越来越大,此刻,到达了一定程度的时候,对于某一个节点值的交换会导致其子树的变化,即有可能一个改变导致了树不再成为大顶堆,原理如图所示
- 通过下面两张图我们可以清晰的了解到,改变了2与25之后不再是大顶堆,所以要通过循环一层一层的往下去再次核实覆盖,这样才可以构建完整大顶堆
2.算法主体🐻
public static void heapSort(int[] array) {
var temp = 0;
for (int i = array.length/2-1; i >= 0; i--) {
adjustHeap(array,i,array.length);
}
for (int j = array.length-1; j > 0; j--) {
temp = array[j];
array[j] = array[0];
array[0] = temp;
for (int i = array.length/2-1; i >= 0; i--) {
adjustHeap(array,i,j);
}
}
}
代码分析与图解:😶🌫️
- temp临时变量的建立是用于最后一个节点和第一个节点交换
- 第一个for循环
- i = array.length/2 -1的原因是,该下标是最后一棵子树的根节点的下标(通过上图再加上该表达式可得,结果是4,刚好是最后一棵子树的下标)
- 为什么这样干?
- 由算法思想可知,我们是由局部大顶堆从而构建大顶堆的,所以,我们应该从最后一个子树去弄,从最底层干起,否则无法构建大顶堆
- i>=0,为了防止越界且根节点的下标是0,根节点对应的树是大顶堆则完成
- i--,从图中可以看出,当i--时,我们会跳跃到另一棵子树,循环的调用构建大顶堆的方法,最终可以得到大顶堆
- 第二个for循环
- j = array.lenth-1表示最后一个节点
- j > 0,原因是,自己和自己交换无意义
- j--,原因是每一次都能确定一个最大的元素,调整元素-1
- 因为我们已经构建好了大顶堆,根节点一定是最大的,所以将array[0]与array[j]进行交换,我们就能确定最后一个元素,且是最大的元素
- 我们再调用调整大顶堆的方法,此时,length应该是j了,因为最后一个元素已经确定了,没必要再去调整位置了,所以调整的元素变成了j
- 通过以上的往复操作,就可以完成排序了
完整代码🐼
package datastructure.chapter04.tree.heapsorting;
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: asus
* Date: 2022-03-27
* Time: 8:46
*/
public class HeapSortingDemo {
public static void main(String[] args) {
var array = new int[] {3,2,1,6,5};
heapSort(array);
System.out.println(Arrays.toString(array));
}
public static void heapSort(int[] array) {
var temp = 0;
for (int i = array.length/2-1; i >= 0; i--) {
adjustHeap(array,i,array.length);
}
for (int j = array.length-1; j > 0; j--) {
temp = array[j];
array[j] = array[0];
array[0] = temp;
for (int i = array.length/2-1; i >= 0; i--) {
adjustHeap(array,i,j);
}
}
}
public static void adjustHeap(int[] array,int i,int length) {
var temp = array[i];
for (int k = 2*i+1; k < length; k = k*2+1) {
if (k+1 < length && array[k+1] > array[k]) {
k++;
}
if (array[k] > temp) {
array[i] = array[k];
i = k;
}else {
break;
}
}
array[i] = temp;
}
}
结论🐼
对于快速排序和归并排序来说,堆排序还是较为简单的,只要掌握了大顶堆与顺序存储二叉树等概念就可以快速写出代码,我来总结以下必须掌握的几点:
1.大顶堆的定义与小顶堆的定义
2.大顶堆构建时为什么要循环
3.交换后调整元素如何改变
数据结构与算法到站啦,接下来我会来个站点大总结哦!