【数据结构与算法】十大经典排序算法-堆排序

个人博客:www.hellocode.top
Java知识导航:Java-Navigate
CSDN:HelloCode.
知乎:HelloCode
掘金:HelloCode
⚡如有问题,欢迎指正,一起学习~~


堆排序是一种高效的排序算法,基于堆数据结构实现。堆是一种特殊的树状结构,具有以下特点:父节点的值大于等于(或小于等于)其子节点的值。堆排序利用堆的性质,将数组看作一个完全二叉树,通过构建最大堆(或最小堆),实现对数组的排序。

基本思想

这里采用五分钟学算法大佬的图解,十分清晰

  1. 构建初始堆:将待排序数组视为一个完全二叉树,从最后一个非叶子节点开始,逐步将树调整为大顶堆(或小顶堆)。
  2. 排序过程:将堆顶元素与最后一个叶子节点交换,然后将堆大小减一,继续调整堆结构,使其重新成为大顶堆(或小顶堆)。
  3. 重复步骤 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]

优化

堆排序的核心是构建堆和调整堆,可以通过一些优化来提升性能。

  • 在构建堆的时候,有自顶向上和自底向下两种,不同的场景使用不同的方法性能也有不同,这里可以去了解了解,对对应的场景进行优化。

总结

优点

  1. 高效性:堆排序的时间复杂度为 O(n log n),在大规模数据下表现优异。
  2. 不占用额外空间:堆排序是原地排序算法,不需要额外的存储空间。

缺点

  1. 不稳定性:堆排序是不稳定的排序算法,相等元素的相对顺序在排序后可能发生变化。

复杂度

  • 时间复杂度
    • 平均时间复杂度:O(n log n)
    • 最好情况时间复杂度:O(n log n)
    • 最坏情况时间复杂度:O(n log n)
  • 空间复杂度:原地排序,空间复杂度为 O(1)。

使用场景

堆排序适用于大规模数据的排序,尤其在需要稳定排序时(如堆顶元素是最大值时)。虽然实现相对复杂,但其高效性使其成为处理大量数据的有力工具。在实际应用中,堆排序在需要高性能排序时可能是一个不错的选择。

当使用堆排序时,应特别注意其时间和空间复杂度的说明是基于固定的数据集。在实际情况中,堆排序的性能可能因为一些特定因素而有所不同,因此在特定情况下堆排序可能表现更好。

你可能感兴趣的:(数据结构与算法,排序算法,算法,java)