【算法】排序(4),堆排序

大家好,我是被白菜拱的猪。

一个热爱学习废寝忘食头悬梁锥刺股,痴迷于girl的潇洒从容淡然coding handsome boy。

一、写在前言

四五天没有写博客了,怎么越接近开学的日子越懒惰了呢。哈希表,二叉树,线索二叉树的博客没有写,心里一直想着补,补了半天也没动笔,这不行,不能在拖了,以前的东西既往不咎,今天学了堆排序,那么就先把堆排序的东西写完,这样拖啊拖,真的无穷尽也。

二、排序

(一)堆排序

堆排序是对之前讲的排序的补充,是一种选择排序,因为涉及二叉树,所以当时没有讲。

堆排序涉及到大顶堆和小顶堆,升序(从小到大)的时候使用大顶堆,降序的时候使用小顶堆。那么下面就以升序为例进行讲解。

什么是大顶堆呢?大顶堆是一种特殊的完全二叉树,其结点大于等于左右孩子的值,注意这里左右孩子谁大谁小无所谓,不作要求。这就保证了根节点是这颗树的最大值,然后把他与数组的最后一位进行交换。然后在对剩下的元素调整为大顶堆,然后在那走根节点。下面看看具体的思路。

1、思路讲解

堆排序运用了顺序存储二叉树,假如结点在数组中的索引为n(数组从0开始),那么其左孩子的索引为2n+1,右孩子的索引为2n+2,。

具体步骤:

  • 将数组构造为大顶堆
  • 此时根节点就是数组中最大的元素
  • 将其与末尾元素交换,此时数组的末尾元素就为最大值
  • 然后将剩下的元素的继续构造为大顶堆,然后在交换
  • 如此反复执行,就得到了一个有序序列

堆排序的难点是如何将数组中的元素构造成大顶堆,尤其是初始化这个阶段,我们采取从第一个非叶子结点出发,由下向上,由左向右,使用adjustHeap这个方法,先将以非叶子结点为根结点的树构造成大顶堆,这里会出现一种情况,也是这段代码的难点,是如何给该节点找一个合适的位置,保证其大于左右孩子,比如这种情况。
在这里插入图片描述
首先我们保证了90,40,80这三个元素为大顶堆,现在要让整个树为大顶堆,构造大顶堆的原理是先将左孩子与右孩子比较,找出最大的那一个,然后将最大值与其父结点比较,假如父结点大的换就退出比较,我们脑海里想小的话就交换。其实这是错的,因为我们不能保证交换过后,其下面的子树是否也构成大顶堆,比如这里50与90交换的话,50,40,80则不会构成大顶堆,所以我们继续向下查找,找出一个合适的位置来放50(这里指树节点)。我们一开始就要设置一个变量,来存储要构成大顶堆的父节点的值,还要用一个值来记录要交换位置的索引。这里需要好好思考,我也是思考了好久才想明白。

那么有人就有疑惑了,为什么在一开始我们说要从下往上从左往右,然后上面又说向下查找,这不是矛盾吗?其实这不矛盾,从下往上是指从最后一个非叶子几点出发,然后一步步往上,在往上的过程中,这就会使下面的已经是大顶堆,假如那个结点大于左右孩子,则就无须在给他找位置,因为左右孩子都大于了,说明也就大于了左右子树。往下查找是为了结点小于左右孩子,交换位置会破坏下面的大顶堆,所以要给结点找一个合适的位置。

2、代码实现及测试

package com.codingboy.sort;

import java.util.Arrays;

/**
 * @author: ljl
 * @date: 2020/8/21 11:02
 * @description: 堆排序
 */
public class HeapSort {
    public static void main(String[] args) {
        int[] arr1 = {5, 10,2,-1, 3, 2};
        System.out.println(Arrays.toString(arr1));
        heapSort(arr1);
        System.out.println(Arrays.toString(arr1));

        //下面对八百万条数据进行测试
        int[] arr = new int[8000000];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int)(Math.random() * 8000000); //Math.random [0,1)
        }
        long before = System.currentTimeMillis();
        heapSort(arr);
        long after = System.currentTimeMillis();
        System.out.println("八百万条数据测试用时(s):" + (after - before)/1000.0);


    }

    public static void heapSort(int[] arr) {
        //首先初始化大顶堆,从最后一个非叶子节点开始
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            adjustHeap(arr, i, arr.length);
        }

        int temp = 0;//用来交换的辅助值
        //首元素与尾元素进行交换,继续构造大顶堆,只不过这次是直接从根结点开始
        //因为前面初始化大顶堆之后以及保证了下面都是大顶堆
        //这里i指尾元素
        for (int i = arr.length - 1; i > 0; i--) {
            temp = arr[i];
            arr[i] = arr[0];
            arr[0] = temp;
            //交换过后继续构造,这里i也指数组的长度,因为最后一位已经确定,所以是之前的length - 1即i,下一轮则i--
            adjustHeap(arr, 0, i);
        }

    }

    //传入父结点,以该结点为根结点的数构造大顶堆,length在减小
    public static void adjustHeap(int[] arr, int index, int length) {
        //首先记录当前结点的值
        int temp = arr[index];
        //从上往下查找,给父结点找一个合适的位置
        for (int i = 2 * index + 1; i < length; i = 2 * index + 1) {
            //首先找到左右孩子最大值的一个,这里是从左往右查找
            if (i + 1 < length && arr[i] < arr[i + 1]) {
                i++;
            }
            //最后所有为i的值是左右孩子最大的一个,假如temp(我们是在给temp找位置)比他大,说明已经是大顶堆了,直接退出
            //注意:这里要仔细思考为什么可以直接退出?
            //因为外层我们是从下往上开始,下面的已经是大顶堆,假如比左右孩子还打的话就已经保证了是大顶堆
            if (temp >= arr[i]) {
                break;
            } else {
                //假如小于则交换位置,然后考虑交换位置之后是否会破坏下面的大顶堆的状态,则继续判断
                //注意这里要理解,index是指什么,i指的是什么,
                //i是最后要交换的位置,然后index = i,是继续判断以index为父节点是否为大顶堆
                arr[index] = arr[i];
                index = i;
            }
        }
        //最后索引为index的就是temp放的地方
        arr[index] = temp;
    }
}

最后结果八百万用了两秒多,八千万在我的电脑上用了30秒左右。
在这里插入图片描述

3、难点分析

说实在话,堆排序相较于其他排序还是有些难以理解的,不仅仅是思路上,在敲代码的过程中有些变量所代表的意义还是要也多加思考,多加琢磨,比如代码中index和i的含义,不单单指堆排序,像我们敲其他代码也是一样,不能模棱两可,每个变量有每个变量的作用,在敲之前理解变量的意思再去敲就会容易许多,index是指父节点,i是指其左右孩子,交换之后,要去判断是否破坏了下面的大顶堆的状态,所以这时候就要改变index的值,其左右孩子还要改变。

还要为什么在循环开始之前我们要记录index的值,然后在循环的最后来确定该值的位置,我们发现每次需要交换时只需要将父节点的值替换成孩子的值,而之前的父节点还没有确定在那,所以我们需要确定好了合适的位置再去交换。其实在里面频繁交换也可以,但是效率没有最后一次性交换高,这里类似于快速排序,给pivot找位置。

一定要把握好细节上的东西,问问自己为什么要这么写,每一行代码多代表的意思,这样下来脑海中的思路会更加清晰,锻炼自己独自敲代码的能力。

三、结束语

写完这篇给我的最大一个感觉时,我的语言表达能力实在太差,有些地方表达的不是很清晰,这造成“只可意会不可言传”的现状,另外思路讲解的地方没有配有插图步骤,这算是不太完美的地方,因为配图过于耗费时间,又想想写博客的目的是让自己更深刻的了解知识点及其精髓,更重要的效率,所以对于广大博友是在抱歉,可能这篇讲解不太完美多多包涵。

离开学还有一周,虽然学习效率不高,我也不奢求了,能学一点是一点,总比啥都不学强啊,欠的账真不想补啊。想想之前寒暑假作业没怎么写,开学胆战心惊,我最喜欢老师说的一句话之前的就让他过去了,既往不咎,新的学期新的开始,大家努力加油,悬着的心总算放了下来。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值