🌟个人博客:www.hellocode.top
🏰Java知识导航:Java-Navigate
🔥CSDN:HelloCode.
🌞知乎:HelloCode
🌴掘金:HelloCode
⚡如有问题,欢迎指正,一起学习~~
堆排序是一种高效的排序算法,基于堆数据结构实现。堆是一种特殊的树状结构,具有以下特点:父节点的值大于等于(或小于等于)其子节点的值。堆排序利用堆的性质,将数组看作一个完全二叉树,通过构建最大堆(或最小堆),实现对数组的排序。
基本思想
这里采用五分钟学算法大佬的图解,十分清晰
- 构建初始堆:将待排序数组视为一个完全二叉树,从最后一个非叶子节点开始,逐步将树调整为大顶堆(或小顶堆)。
- 排序过程:将堆顶元素与最后一个叶子节点交换,然后将堆大小减一,继续调整堆结构,使其重新成为大顶堆(或小顶堆)。
- 重复步骤 2,直到堆大小为 1,排序完成。
需要掌握的部分知识:
- 完全二叉树:指除了最后一层外,其他层的节点都被完全填满,最后一层的节点都靠左排列,并且不存在不规则的空缺。这意味着从根节点到倒数第二层都是满的,最后一层从左到右有可能存在空缺,但不能跳过空缺。
- 堆:堆是一种基于完全二叉树的数据结构,可分为大顶堆和小顶堆。在大顶堆中,父节点的值大于等于其子节点的值;在小顶堆中,父节点的值小于等于其子节点的值。堆的性质使其适合用来进行排序和实现优先队列等数据结构。
- 堆的构建和调整:在堆排序中,我们主要关注构建初始堆和调整堆的过程。构建初始堆的目标是将一个无序数组调整为一个最大堆或最小堆。调整堆的目标是保持堆的性质,确保父节点的值大于等于(或小于等于)子节点的值。
- 完全二叉树中相关计算:对于任意节点来说,左子节点索引计算公式为
2*i + 1
,右子节点为:2*i + 2
,最后一个非叶子节点计算公式为n/2 - 1
(n为节点总数,i为当前节点索引)
这里的难点就是对应的概念和计算,拿到待排序数组后,首先需要将其构建为大顶堆(或小顶堆),然后需要进行相应的节点交换,并继续调整结构使其保持大顶堆(或小顶堆)特性,代码层面还需要配合动画多多理解
代码实现
相比之前的几种排序,堆排序就相对复杂一些,需要用到递归的思想。遇到递归,还是要考虑递归的出口,避免无休止的递归,这里使用递归就是不断调整来维持堆结构,自上而下递归,那么递归的出口就是到叶子节点(不能超出数组范围)。
主要分为三个方法:heapSort(对外提供的堆排序方法)、buildMaxHeap(构建初始堆结构方法)、heapify(真正调整堆结构、维持堆规则的方法)
package top.hellocode;
import java.util.Arrays;
/**
* @author HelloCode
* @blog https://www.hellocode.top
* @date 2023年08月13日 20:01
*/
public class HeapSort {
public static void main(String[] args) {
int[] arr = {19, 23, 13, 7, 84, 66, 98, 78, 54, 32, 23, 77, 88, 17};
System.out.println("排序前:" + Arrays.toString(arr));
heapSort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
public static void heapSort(int[] arr) {
// 构建初始堆结构(默认大顶堆,实现升序排序)
buildMaxHeap(arr, arr.length);
// 开始排序
// 从堆顶取出元素,并和最后一个元素进行交换,并不断调整维持堆结构
for (int i = arr.length - 1; i > 0; i--) {
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
heapify(arr, 0, i);
}
}
// 构建初始最大堆
private static void buildMaxHeap(int[] arr, int length) {
// 构建大顶堆,从最后一个非叶子节点开始((length - 1) / 2)
for (int i = length / 2 - 1; i >= 0; i--) {
heapify(arr, i, length);
}
}
/**
* 调整堆结构,使其成为最大堆
* int[] arr:待调整数组
* int index:子树根节点(从哪里开始向下调整)
* int length:数组长度,主要用来判断子树递归是否到达了叶子节点(超出数组长度)
*/
private static void heapify(int[] arr, int index, int length) {
// 默认假设当前子树根节点为最大值,寻找左右子节点是否有更大值
int max = index;
int left = 2 * index + 1;
int right = 2 * index + 2;
// 递归终止条件(出口)
if (left >= length || right >= length) {
return;
}
// 开始比较左右子节点
if (arr[left] > arr[max]) {
max = left;
}
if (arr[right] > arr[max]) {
max = right;
}
// 如果最大值发生了变化,则进行交换
// 只要是有交换发生,就需要对其子树继续进行递归调整
if (max != index) {
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
// 继续对其子树递归调整
heapify(arr, max, length);
}
}
}
测试:
排序前:[19, 23, 13, 7, 84, 66, 98, 78, 54, 32, 23, 77, 88, 17]
排序后:[7, 13, 17, 19, 23, 23, 32, 54, 66, 77, 78, 84, 88, 98]
优化
堆排序的核心是构建堆和调整堆,可以通过一些优化来提升性能。
- 在构建堆的时候,有自顶向上和自底向下两种,不同的场景使用不同的方法性能也有不同,这里可以去了解了解,对对应的场景进行优化。
总结
优点
- 高效性:堆排序的时间复杂度为 O(n log n),在大规模数据下表现优异。
- 不占用额外空间:堆排序是原地排序算法,不需要额外的存储空间。
缺点
- 不稳定性:堆排序是不稳定的排序算法,相等元素的相对顺序在排序后可能发生变化。
复杂度
- 时间复杂度
- 平均时间复杂度:O(n log n)
- 最好情况时间复杂度:O(n log n)
- 最坏情况时间复杂度:O(n log n)
- 空间复杂度:原地排序,空间复杂度为 O(1)。
使用场景
堆排序适用于大规模数据的排序,尤其在需要稳定排序时(如堆顶元素是最大值时)。虽然实现相对复杂,但其高效性使其成为处理大量数据的有力工具。在实际应用中,堆排序在需要高性能排序时可能是一个不错的选择。
当使用堆排序时,应特别注意其时间和空间复杂度的说明是基于固定的数据集。在实际情况中,堆排序的性能可能因为一些特定因素而有所不同,因此在特定情况下堆排序可能表现更好。