Java算法之选择排序 以小见大的堆排序(含图示讲解)


前情提要

下面我们会介绍一种速度非常快的排序算法:堆排序, 它的核心步骤主要是构建大小顶堆实施排序.

在实施排序的过程中, 同样也是通过循环调用堆排序来完成的, 因此,最为核心的内容是构建顶堆

建议配合哔哩哔哩<<Java数据结构与算法(尚硅谷)>>,p107-p111,视频演示理解更直观

提示:以下是本篇文章正文内容,下面案例可供参考

一、以大顶堆为例讲讲基本思路和注意点

1.这几点必看👇

前面我们已经提到过,构建顶堆是整个排序的核心,在构建顶堆的时候,我们需要注意以下几点:

  1. 二叉树可以顺序存储,那么以此 ,我们在对数组进行排序的过程中,也是在对二叉树进行排序. 正是因为这一点 , 我们可以利用二叉树和数组相互辅助理解堆排序
    2.几个关键公式我们需要回顾 :
    (1)第n个元素的左子结点 : 2n+1
    (2)右子节点 : 2n+2
    (3)父节点 : (n-1/2) 向下取整
    (4)第一个非叶子结点在数组中的索引值:length / 2 -1

2.基本思路

(1) 构建大顶堆

(2)二叉树的堆顶元素, 在与其对应数组中 , 与数组的最后一个元素交换,也就是将堆顶元素沉入的数组的最末端

(3) 重复(1) , (2) ,以此类推,直到数组有序为止

二、构建大顶堆思路图解

下图与下面的代码思路一致,要是觉得看代码太抽象了,可以回过头来看看这里

在这里插入图片描述

这里我只画出了二叉树第一个非叶子结点的子树进行构建大顶堆的演示,能以小见大

二、代码

1.先将数组调成大顶堆

代码如下(示例):

	//先将数组调成大顶堆
    /**
     * /**+enter 自动生成方法注解信息
     * @param arr 要调整的数组
     * @param i 要调整的元素索引
     * @param length 要调整的范围
     * 这么看是非常难理解的,结合下面的排序方法就容易多了
     */
    public static void adjustHeap(int[]arr,int i ,int length) {
        //先保存指定的元素,因为下面有可能会覆盖掉它
        int temp = arr[i];
        for (int k = i*2+1 ; k<length ;k++){
            //k作为一个游标使用,初始值是指向当前元素的左子结点
            if(k+1<length && arr[k]<arr[k+1]){ //左子结点大于右
                //把游标k指向右子结点
                k++; //根据公式,我们只需+1即可
            }
            if(arr[k] > temp){ //右子结点大于父节点
                arr[i] = arr[k];
                i = k; //游标指向i
            }else{ //如果不是,就终止循环
                break;
            }
        arr[i] = temp; //这一行与上面的arr[i] = arr[k];一起作用相当于交换了右子结点与父节点 
        }
    }

注解里已经讲的非常详细了,下面结合上图再理一理思路

整个流程,从头到尾相当于一个狸猫换太子的过程

(1) 以上图为例 , 假设i为1 , 我们先用一个临时变量 ,保存值为4的这个节点,因为在后面我们有可能会覆盖掉它

(2) 根据顺序存储二叉树的公式,为了能取到4这个节点的左右子结点,在循环的一开始就把游标k的值设为2*i+1 ,即左子节点6

(3) 下面我们开始判断 , 先比较同一层级上的结点, 我们发现兄弟节点中 , 6比9小,于是我们把游标k 指向右结点(k++即可). 这同样是根据二叉树的左右子节点公式得到的

(4) 再比较右节点(此时它在这颗子树中最大) 与父节点的大小,如图, 9大于4 ,因此我们将9赋值给4,即arr[i] = arr[k]; , 同时游标k 指向i

(5) 如果父节点大于右子结点的值 , 那么我们直接跳出循环.

这里有个不好理解的地方,为什么直接跳出循环呢?
解答: 如果父节点大于右子结点(也就是当前子树中最大的那个元素),那么此时该子树中的大顶堆结构已经构建完成了,直接结束循环即可

最后,我们将最开始保存的临时变量赋给arr[i] 此时 , arr[i]表示的是当前子树的右结点(如图所示) , 狸猫换太子完成

以上流程只是构建大顶堆过程中,一颗子树的顶堆构建过程,接下来的操作将完成整棵子树的顶堆构建以及排序

2.循环构建顶堆与"沉入"

我们需要根据公式 , 找到第一个非叶子结点在数组中的位置,循环构建

代码如下(示例):

public static void heapSort(int[]arr){
        for (int i = arr.length/2-1;i>=0;i--){
            adjustHeap(arr,i,arr.length);
        }
    }

其实到这里 , 我们的排序就已经结束了 , 如果想变换升降序 ,还需要加上沉入的步骤

代码如下(示例):

public static void heapSort(int[]arr){

        int temp = 0; //临时变量

        for (int i = arr.length/2-1;i>=0;i--){
            adjustHeap(arr,i,arr.length);
        }
        for(int j = arr.length-1;j >0; j--) {
            //交换
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            adjustHeap(arr, 0, j);
        }
    }

三、时间复杂度

我们在开头讲过 , 堆排序的速度非常快(时间复杂度约为o(nlogn)),我们下面来测试一下

public static void main(String[] args) {
        final int SIZE = 80000000;
        int[] arr = new int[SIZE];
        for (int i = 0; i < SIZE; i++) {
            arr[i] = (int) (Math.random() * SIZE);
        }

        System.out.println("排序前");
        Date data1 = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date1Str = simpleDateFormat.format(data1);
        System.out.println("排序前的时间是=" + date1Str);

        heapSort(arr);

        Date data2 = new Date();
        String date2Str = simpleDateFormat.format(data2);
        System.out.println("排序前的时间是=" + date2Str);

    }

总结

完整代码后序会放到GitHub上 , 关注微信公众号获取更多技术分享

本文参考资料为尚硅谷数据结构视频(前文有提)

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值