谈谈「堆排序」

news/2024/5/19 6:47:32 标签: 二叉树, 数据结构, 算法, 堆排序

关于堆

在学习堆排序直线先来看看堆这个数据结构

堆的一个经典的实现是完全二叉树(complete binary tree),这样实现的堆称为二叉堆(binary heap)

二叉树:除了叶子节点,所有的节点的左右孩子都不为空,就是一棵满二叉树,如下图:

可以看出:满二叉树所有的节点都拥有左孩子,又拥有右孩子。

完全二叉树:不一定是一个满二叉树,但它不满的那部分一定在右下侧,如下图:

关于堆详细的描述及实现方式这里不再赘述,网上资源很多,这里重点说一下堆的特性。
堆的特性

  • 必须是完全二叉树
  • 任一结点的值是其子树所有结点的最大值或最小值

最大值时,称为“最大堆”,也称大顶堆;

最小值时,称为“最小堆”,也称小顶堆。

还有一点很重要,那就是如果把一个二叉堆按层次遍历打印出来放在一个数组中,那么对于任意一个有子节点的节点在数组中的索引index会满足下面的关系:

  • leftChild = 2 * index + 1
  • rightChild = 2 * index + 2

比如下面的二叉堆:

关于堆排序

堆排序顾名思义,是利用堆这种数据结构来进行排序的算法

堆是一种优先队列,有两种实现,最大堆和最小堆,由于这里排序按升序排,所以暂且最大堆。

可以把堆看成一棵完全二叉树,位于堆顶的元素总是整棵树的最大值,每个子节点的值都比父节点小,由于堆要时刻保持这样的规则特性,所以一旦堆里面的数据发生变化,必须对堆重新进行一次构建。

既然堆顶元素永远都是整棵树中的最大值,那么我们将数据构建成堆后,只需要从堆顶取元素即可。每次取完根节点,将二叉树的末尾节点移到根节点位置(这里的末尾节点指的是,二叉树按照层序遍历之后的最后一个节点,如上面图中的节点5)。

讲到这里你可能说需要建立一棵真正的二叉树,当然可以,只是没必要,堆排序的精髓并不是真的要用节点类实现一个二叉堆,而是利用堆的思想,通过将数组内部元素进行调整的方式来实现一个虚拟的堆,但是最终达到的效果跟真正通过建立一棵二叉堆来实现排序是一样的。

堆排序其实总的来说就三步:

  1. 待排序数组
  2. 建立二叉堆
  3. 取二叉堆顶元素

这三步都可以通过调整数组中的元素位置来实现!

  1. 待排序数组就是给定的待排序数组

  2. 建立二叉堆的过程有点麻烦,也是堆排序的精髓,其实看代码更好理解一些。

    当我们拿到一个待排序数组后,我们可以把通过一定的规则访问数组元素的方式建立一棵虚拟的二叉树,就是前面所说的leftChild = 2 * index + 1rightChild = 2 * index + 2,有了这个规则,我们可以得到任意一个元素的左右子节点。

    但是现在的二叉树只满足了它是一个完全二叉树的特性,任一结点的值是其子树所有结点的最大值的`特性还需要一定的操作。

    那就是下沉操作,当一个节点小于其中一个子节点时,就将此节点与那个较大的子节点对调。如果这个操作是自顶向下的,那么就比较麻烦,所以这里的实现是自底向上。

  3. 取二叉堆顶元素,就代表将数组中的第一个元素与末尾元素对调。

下面直接上代码,不得不说堆排序的代码真的很巧妙:

public class Solution {

    public static void heapSort(int[] arr) {
        int length = arr.length;
        buildHeap(arr, length);

        for ( int i = length - 1; i > 0; i-- ) {
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            length--;
            sink(arr,0,length);
        }
    }

    private static void buildHeap(int[] arr, int length) {
        for (int i = length / 2; i >= 0; i--) {
            sink(arr,i,length);
        }
    }

    private static void sink(int[] arr, int index, int length) {
        int leftChild = 2 * index + 1; 
        int rightChild = 2 * index + 2; 
        int present = index; 

        if (leftChild < length && arr[leftChild] > arr[present]) {
            present = leftChild;
        }
        if (rightChild < length && arr[rightChild] > arr[present]) {
            present = rightChild;
        }
        if (present != index) {
            int temp = arr[index];
            arr[index] = arr[present];
            arr[present] = temp;

            sink(arr,present,length);
        }
    }
}

解说代码:

package sort.heap;

/**
 * @Author Hory
 * @Date 2020/10/2
 */
public class Solution {

    public static void heapSort(int[] arr) {
        int length = arr.length;
        //构建堆
        buildHeap(arr, length);

        for ( int i = length - 1; i > 0; i-- ) {
            //将堆顶元素与末尾元素调换
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            //数组长度-1 隐藏堆尾元素
            length--;
            //将堆顶元素下沉 目的是将最大的元素浮到堆顶来
            sink(arr,0,length);
        }
    }

  
  	//构建堆的过程就是一个个节点下沉的过程,只不过这个过程是自底向上
  	//为什么是自底向上?
  	//那就要知道sink这个函数的功能:对指定的元素为根节点以下的节点进行下沉,直到遇到一个节点大于其左右子节点才会退出。但是就算退出了,也不能保证当前节点的子节点的子节点是否满足堆的特性,所以,如果我们自底向上的话,每次执行当前节点,那么它下面的节点全是执行过下沉操作的,这就保证了以当前的节点为根节点的整个树都是满足堆特性的
  	//为什么是i = length/2 ?
  	//因为是自底向上,所以arr[length/2]这个节点刚好最后一个节点的父节点
    private static void buildHeap(int[] arr, int length) {
        for (int i = length / 2; i >= 0; i--) {
            sink(arr,i,length);
        }
    }

    /**
     * 下沉调整
     * 该sink函数的功能就是对指定的元素为根节点以下的节点进行下沉,直到遇到一个节点大于其左右子节点才会退出
     * @param arr 数组
     * @param index 调整位置
     * @param length 数组范围
     */
    private static void sink(int[] arr, int index, int length) {
        int leftChild = 2 * index + 1; //左子节点下标
        int rightChild = 2 * index + 2; //右子节点下标
        int present = index; //要调整的节点下标

      	//这里也不用判断左右子节点是否为空,因为如果为空,那么:
      	// leftChild = 2 * index + 1; 这个结果肯定是大于等于length的
        //下沉左边
        if (leftChild < length && arr[leftChild] > arr[present]) {
            present = leftChild;
        }

        //下沉右边
        if (rightChild < length && arr[rightChild] > arr[present]) {
            present = rightChild;
        }

      	//经过两个if判断下来,最终present指向的值肯定是父节点、左右子节点中最大的那个
      	//不相等证明present肯定指向了其中一个子节点
        if (present != index) {
            //交换值
            int temp = arr[index];
            arr[index] = arr[present];
            arr[present] = temp;

            //继续下沉
            sink(arr,present,length);
        }
      	//最后发现sink这个递归函数貌似没有通常的那种递归出口,其实就包含在最后一个if语句中
      	//只要最后一个if语句满足,就会不断执行sink这个函数,直到最终判断一个节点大于其左右子节点,最终才不会进入if中执行sink
    }
}

http://www.niftyadmin.cn/n/1690264.html

相关文章

Java堆实现之「PriorityQueue<>()」

堆简介 Java中PriorityQueue通过二叉小顶堆实现&#xff0c;可以用一棵完全二叉树表示。 前面以Java ArrayDeque为例讲解了Stack和Queue&#xff0c;其实还有一种特殊的队列叫做PriorityQueue&#xff0c;即优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小…

剑指Offer系列之「数据流中的中位数」

如何得到一个数据流中的中位数&#xff1f;如果从数据流中读出奇数个数值&#xff0c;那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值&#xff0c;那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流&#xff0…

单例模式之「双重校验锁」

单例模式 单例即单实例&#xff0c;只实例出来一个对象。 一般在创建一些管理器类、工具类的时候&#xff0c;需要用到单例模式&#xff0c;比如JDBCUtil 类&#xff0c;我们只需要一个实例即可&#xff08;多个实例也可以实现功能&#xff0c;但是增加了代码量且降低了性能&…

剑指Offer系列之「滑动窗口的最大值」

给定一个数组和滑动窗口的大小&#xff0c;找出所有滑动窗口里数值的最大值。例如&#xff0c;如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3&#xff0c;那么一共存在6个滑动窗口&#xff0c;他们的最大值分别为{4,4,6,6,6,5}&#xff1b; 针对数组{2,3,4,2,6,2,5,1}的滑动…

并发系列之「Java中创建线程的三个方式」

Java中有三种线程创建方式&#xff0c;分别为&#xff1a; 继承Thread类并重写run()方法 实现Runnable接口的run()方法 使用FutureTask方式 继承Thread类并重写run()方法 /*** Author Hory* Date 2020/10/5*/ public class ThreadTest {public static void main(String[] ar…

并发系列之「执行run() start()的区别」

执行run()与start()方法的区别&#xff1a; public class MyThread extends Thread{public MyThread(){System.out.println("MyThread构造方法&#xff1a;" Thread.currentThread().getName());}Overridepublic void run(){System.out.println("run方法&#x…

剑指Offer系列之「左旋转字符串」

汇编语言中有一种移位指令叫做循环左移&#xff08;ROL&#xff09;&#xff0c;现在有个简单的任务&#xff0c;就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S&#xff0c;请你把其循环左移K位后的序列输出。例如&#xff0c;字符序列S”abcXYZdef”,要求输出…

剑指Offer系列之「扑克牌顺子」

LL今天心情特别好&#xff0c;因为他去买了一副扑克牌&#xff0c;发现里面居然有2个大王&#xff0c;2个小王(一副牌原本是54张)…他随机从中抽出了5张牌&#xff0c;想测测自己的手气&#xff0c;看看能不能抽到顺子&#xff0c;如果抽到的话&#xff0c;他决定去买体育彩票&…