• 排序算法 —— 堆排序(图文超详细)



    堆排序

    1. 排序思路

    • 将一组数据建成大根堆。
    • 定义一个end来记录此时堆末尾元素的位置。
    • 每排序好一个,end值就减一个。
    • 在排序的过程中要保障堆时刻为大根堆。
    • 开始排序时,也要保证是大根堆。

    待排序的数据:27, 15, 19, 18, 28, 34, 65, 49, 25, 37

    因为堆实际上是在二叉树的基础上做的一些调整。
    在操作堆的时候本质是利用了二叉树的思想。

    2. 如何建成大根堆

    给每一个数据一个下标值。

    2.1. 将待排序的数据看成一个完全二叉树。

    下图是排列好的二叉树。

    但是它此时既不是大根堆也不是小根堆,即不是一个堆。

    要将它变成一个堆,要先将它变成大根堆或者小根堆。

    2.2. 建堆思想

    如果是要建立成大根堆,先将它的子树全部建成大根堆。此时,这棵树整体也就变成了一个大根堆。


    先将图中画红圈的建成大根堆,建成后,整棵树就是一个大根堆了。

    有两个方法:

    1. 从无到有的建立堆,获取一个数据的同时将它建立为一个大根堆。一边获取数据,一边的建堆。
      如果有两个数据,就将两个数据建成堆;若四个数据,就将四个数据建成堆。
    2. 先从最后一棵子树的根节点开始调整,将它建成大根堆;再将它前面的结点逐渐建成大根堆;最后整体就是一个大根堆了。(向下调整)

    2.3. 建堆步骤

    实现向下调整

    2.3.1.先将最后一棵子树建成大根堆
    • 最后一棵子树的孩子结点的下标是数组的长度减1。(10 - 1)
    • 已知孩子结点,求父亲结点的公式:(i - 1) / 2,i 是孩子结点的下标。
    • 现在得到了最后一棵子树的孩子和父亲结点的下标。(孩子:9,父亲:4)
    • 比较两个结点的值,将较大的结点交换到对应父节点的位置。
    • 若父节点为最大值,则不需要交换。

    最后一棵子树建堆后:

    图中的4下标和9下标的结点进行了交换。

    2.3.2.将其余子树建成大根堆

    第1步:

    • r 记录父亲结点的下标;i 记录左右最大值的下标。
    • r - - ,找到新的子树的根节点。
    • 找到了根节点,可以访问到子节点。
      在这里插入图片描述


    第2步:

    • i 结点始终记录左右子节点的最大值。
    • 将这个最大值节点与当前 r 节点交换。


      交换后:

    下图的3下标和7下标交换了。


    第3步:

    • r - -,r 指向了2下标所在根节点的位置。
    • i 指向左右子节点的最大值。



    第4步:

    • 交换 r 和 i 指向的两个结点。

    第5步:

    • r - -,r 指向了1下标所在根节点的位置。
    • i 指向左右子节点的最大值。



    第6步:

    • 交换 r 和 i 指向的两个结点。


    走到这里就会发现,3下标位置的根节点又不是大根堆了。
    要让 r 重新指向3下标,i 再指向左右孩子最大值。


    第7步:

    • r 指向 i ,i 指向(2 * p + 1)的位置(下标7处),p 代表的是父节点下标。
    • i 再指向左右孩子最大值。


    第8步:

    • 比较 r 和 i 之后,交换较大到根节点位置。


    程序并不知道这一步交换完成后,15有没有左右节点了。
    因此并不能确定交换一次 r 和 i 结点之后就为大根堆了。


    解决办法:

    • r 指向 i 的位置,i 指向 (2 * p + 1) 的位置(下标17处),p 代表的是父节点下标。
    • i 再指向左右孩子最大值。


    此时 i 的值大于数据个数(10)的最大下标值(9),此时说明15左右没有孩子。
    此时如果 i 的值乘2加1大于9,就说明此时的 i 指向的就是最后一个结点了。

    2.3.3. 代码分析
     for (int parent = (array.length - 1 - 1) / 2; parent >= 0 ; parent--) {
     }
    
    • 1
    • 2
    • parent 代表的是父节点 r 。
    • 先求出数组长度,减1表示得到末尾元素下标,再减1表示得到每一个父节点。
    • 当 parent 小于0时,调整完毕。
     shiftDown(array, parent, array.length);
    
    • 1
    • 循环调用向下调整方法。
     public static void shiftDown(int[] array, int parent, int len) {
     }
    
    • 1
    • 2
    • 参数 len 是每棵子树调整的结束位置。
    • 结束位置不能大于 len 。
    int child = (2 * parent) + 1;
    
    • 1
    • 求得孩子结点,代表的是 i 。
     while (child < len) {
         if (child + 1 < len && array[child] < array[child + 1]) {
             child++;
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    • child < len 保证有左孩子,len 是数组长度。
    • child + 1 < len 保证不越界。
    • array[child] < array[child + 1] 保证 child 是左右最大值。
    if (array[child] > array[parent]) {
    }
    
    • 1
    • 2
    • 比较孩子结点和父节点的值。
     if (array[child] > array[parent]) {
           swap(array, child, parent);
           parent = child;
           child = 2 * parent + 1;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 如果孩子结点大于父节点就交换这两个结点。
    • 父节点指向子节点。
    • 孩子结点指向新的位置。
    public static void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 交换方法的实现。

    最终实现的大根堆:

    3 如何实现堆排序

    3.1 排序思路

    • 将堆顶元素与堆的最后一个元素交换,此时堆的末尾位置就是最大的元素了。
    • 每一次交换后要保证堆是大根堆。
    • 若堆不是大根堆,就调用向下调整重新建成大根堆。
    • 重新交换堆顶元素与堆的末位置元素。

    3.2 代码实现的思路

    • 定义一个 end 来记录此时堆的末尾位置元素的位置。(元素个数 - 1)
    • 每排序好一个数据,end 就减去1。
    • 若此时的堆不是大根堆,调用向下调整重新实现为大根堆。
    • 若刚开始排序时,堆不为大根堆,则要将它建成大根堆。

    3.3 代码分析

     creatBigHeap(array);
    
    • 1
    • 先调用建堆方法建堆。
    int end = array.length - 1;
    
    • 1
    • 数组长度减1求的是堆末尾位置元素的下标。
     while (end > 0) {
           swap(array, 0, end);
           shiftDown(array, 0, end);
           end--;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 循环调用交换方法交换0下标和end位置的值。
    • 0 下标即使堆顶元素的位置。
    • 循环调用向下调整方法建成大根堆。
    • 每次交换后,end每次减少一个。

    3.4 排序过程

    1. 交换一次后

    2. 此时不为大根堆了,先建成大根堆,再交换。
      在这里插入图片描述

    3. 65 是排序好的了,所以在重新建成大根堆的时候不需要处理65,交换第2次。

    4. 按照上面的步骤会逐渐排成:
      在这里插入图片描述

    4. 整体代码展示

     public static void heapSort(int[] array) {
            creatBigHeap(array);
            int end = array.length - 1;
            while (end > 0) {
                swap(array, 0, end);
                shiftDown(array, 0, end);
                end--;
            }
     }
    
    //建堆
    public static void creatBigHeap(int[] array) {
        for (int parent = (array.length - 1 - 1) / 2; parent >= 0 ; parent--) {
            //调用向下调整
            shiftDown(array, parent, array.length);
        }
    }
    
    //向下调整的方法
    public static void shiftDown(int[] array, int parent, int len) {
        int child = (2 * parent) + 1;
        while (child < len) {
            if (child + 1 < len && array[child] < array[child + 1]) {
                child++;
            }
            if (array[child] > array[parent]) {
                swap(array, child, parent);
                parent = child;
                child = 2 * parent + 1;
            }else {
                break;
            }
        }
    }
    
    //交换的方法
    public static void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    在这里插入图片描述

  • 相关阅读:
    SpringBoot 3.0最低版本要求的JDK 17,这几个新特性不能不知道
    电商风控系统(flink+groovy+flume+kafka+redis+clickhouse+mysql)
    车道线检测-CLRNet-CVPR2022论文学习笔记
    【C++】class的设计与使用(五)静态类成员
    Linux启动过程详解 Xmind导图笔记
    Vue2 .sync修饰符的应用
    android--TextView在刷新时宽度变大的问题排查记录
    如何在CentOS系统中管理Docker容器
    华为Mate 60和iPhone 15选哪个?
    动态 | 10月数据安全重要政策法规、文件汇总
  • 原文地址:https://blog.csdn.net/m0_63033419/article/details/127644252