排序算法 - 图文解析堆排序

堆(heap)是一类特殊的数据结构,通常可以被看做一棵树的数组对象,也就是顺序存储的树
不清楚顺序存储的可以看:数据结构 - 解析二叉树的顺序存储

将树结构用数组存储:
在这里插入图片描述

堆是具有以下性质的完全二叉树:

  • 每个结点的值都大于或等于左右子结点的值称为大顶堆,根据完全二叉树的性质,可以推出arr[i]>=arr[i*2+1],arr[i]>=arr[i*2+2]
  • 每个结点的值都小于或等于左右子结点的值称为小顶堆,根据完全二叉树的性质,可以推出arr[i]<=arr[i*2+1],arr[i]<=arr[i*2+2]

注意:堆并没有限制左右子结点的大小关系,是简化版的二次查找树


Java解析实现大顶堆

这样一个堆的树结构:

在这里插入图片描述

对应的数组对象(层次遍历):

在这里插入图片描述

如何实现大顶堆:

  1. 找到最后一个非叶子结点arr.length/2-1,当前为3,即元素8,从左至右,从下至上进行调整,也就是调整该非叶子结点为父结点的子树

在这里插入图片描述

在这里插入图片描述

  1. 先比较左右子结点,找到最大的结点,再比较该结点与父结点的值的大小,如果该结点大,交换位置

在这里插入图片描述

  1. 指针前移,调整结点6所在子树

在这里插入图片描述
在这里插入图片描述

明显,也是需要交换值的:

在这里插入图片描述

  1. 指针前移,调整结点3所在子树,因为我们是从下到上调整,实际上也只是比较[3,66,10]的值

在这里插入图片描述

明显,也需要调整:将66与3交换位置,我们会发现交换完位置后,结点3所在子树是不符合大顶堆要求的

在这里插入图片描述

还需要交换结点3与13,这在我们的代码中同样需要考虑到

  1. 指针前移,调整结点1所在子树

在这里插入图片描述

这已经是最后的非叶子结点,最终大顶堆为:其中同样遇到了上面的情况,需要多次调整

在这里插入图片描述

Java实现:

    /**
     * @param arr 待排序的数组
     * @param length 数组长度
     * @param i 表示非叶子节点在数组中的索引
     * 完成将对应的非叶子结点调整成大顶堆,递归完成大顶堆
     */
    public static void adjustHeap(int[] arr,int length,int i){
        //取出当前元素值,保存
        int temp = arr[i];
        //k = i * 2 +1说明k是i结点的左子结点
        for (int k = i * 2 +1;k < length;k = k * 2 +1){
            if (k + 1 < length && arr[k] < arr[k + 1]){
                //左子结点小于右子结点的值,指向右子结点
                k++;
            }
            if (arr[k] > temp){
                //子结点大于父结点,将值赋值给父结点
                arr[i] = arr[k];
                //将k赋值给i,用于后续交换值
                i = k;
            }else {
                break;
            }
        }
        //当for循环结束,以i为父结点的子树的最大值已经在i点
        //如果运行了for循环内的赋值,将父结点值赋值给对应的子结点
        arr[i] = temp;
    }

其中在for循环内,将子结点值赋值给父结点,并移动指针i,直到for循环结束才将父结点的值赋值给arr[i]这样就可以处理步骤4、5的多次调整

接下来就需要设计堆排序


堆排序

具体的思路: 对于已经调整好的大顶堆、小顶堆,我们可以知道根结点是最大值(最小值),且在数组的首位,将堆顶元素与末尾元素交换,使末尾元素最大,然后重新调整堆,再将堆顶元素与末尾元素交换,得到第二大的数据,循环直到数组完成

在这里插入图片描述


完整堆排序Java代码

堆排序的实现:每次对于调整好后的大顶堆,交换堆顶与末尾元素,然后继续调整堆

package com.company.sort;

import java.util.Arrays;
import java.util.Date;

/**
 * @author zfk
 * 堆排序
 */
public class HeapSort{

    public static void main(String[] args) {
        //假设数组升序排序:大顶堆
        int[] arr = {1,3,6,8,10,14,88,66,13};
        System.out.println("初始数组:"+Arrays.toString(arr));
        heapSort(arr);
    }

    /**
     * @param arr 要排序的数组
     * 堆排序方法
     */
    public static void heapSort(int[] arr){
        System.out.println("=== 堆排序 ===");

        int length = arr.length;
        int temp;

        //调整得到大顶堆
        for (int i = length / 2 - 1;i >= 0;i--){
            adjustHeap(arr,length,i);
        }

        for (int j = length - 1;j > 0;j--){
            //交换大顶堆最大值与最后的结点
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            //进行调整大顶堆
            //因为是取走最大值,并将数组末尾值放到首位,再次调整大顶堆从首位开始
            adjustHeap(arr,j,0);
        }

        System.out.println("排序后数组:" + Arrays.toString(arr));
    }

    /**
     * @param arr 待排序的数组
     * @param length 数组长度
     * @param i 表示非叶子节点在数组中的索引
     * 完成将对应的非叶子结点调整成大顶堆,递归完成大顶堆
     */
    public static void adjustHeap(int[] arr,int length,int i){
        //取出当前元素值,保存
        int temp = arr[i];
        //k = i * 2 +1说明k是i结点的左子结点
        for (int k = i * 2 +1;k < length;k = k * 2 +1){
            if (k + 1 < length && arr[k] < arr[k + 1]){
                //左子结点小于右子结点的值,指向右子结点
                k++;
            }
            if (arr[k] > temp){
                //子结点大于父结点,将值赋值给父结点
                arr[i] = arr[k];
                //将k赋值给i,用于后续交换值
                i = k;
            }else {
                break;
            }
        }
        //当for循环结束,以i为父结点的子树的最大值已经在i点
        //如果运行了for循环内的赋值,将父结点值赋值给对应的子结点
        arr[i] = temp;
    }
}

在这里插入图片描述

这里完成了大顶堆排序,小顶堆大致相同


速度测试

将80000个随机0~80000大小的数据进行堆排序

 public static void main(String[] args) {

        int[] arr2 = new int[80000];
        for (int i = 0; i < arr2.length; i++) {
            //随机生成80000内的整数
            arr2[i] = (int) (Math.random() * 80000);
        }
        Date dataBefore = new Date();

        heapSort(arr2);
        Date dateAfter = new Date();
        System.out.println("消耗了:"+(dateAfter.getTime()-dataBefore.getTime())+"ms");

    }

可以看到很快:
在这里插入图片描述

时间复杂度

堆排序的时间复杂度,主要在初始化堆交换最大值后重建堆

  • 初始化堆的时间复杂度为O(n)

初始化堆也就是heapSort方法中的for循环adjustHeap方法:从倒数第一个非叶子结点,从下而上建堆,倒数第二层的结点在调整的过程中只需要比较一次,倒数第三层需要比较两次。。。
n为堆结点数,k为堆层数,堆是完全二叉树
只有根结点需要比较 logn 次(logn 向下取整),层数k的满二叉树总结点数为
2^k -1,近似计算

在这里插入图片描述

堆排序中建堆过程时间复杂度O(n)怎么来的? - 吴献策的回答 - 知乎
https://www.zhihu.com/question/20729324/answer/509924802

  • 交换最大值后重建堆

循环n-1次建堆,每次都是从根结点开始,且每次堆大小-1,最多交换k(层数)-1次,即每次调整时间为logn

总时间:log n*(n-1) = nlogn - logn 约为 nlogn

堆排序:O(n)+O(nlogn) 约为 O(nlogn)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值