堆排序-使用java代码实现

堆排序

堆排序 是利用这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。

根据尚硅谷韩顺平老师的数据结构与算法摘录笔记。



在这里插入图片描述
给树中的每个结点按顺序编号,并映射到数组对应的索引上。
在这里插入图片描述

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

① 每个结点的值都大于或等于其左右子结点的值,称为大顶堆。
② 每个结点的值都小于或等于其左右子结点的值,称为小顶堆。

用公式来定义:

①大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
②小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

思路

堆排序的基本思想

① 将待排序的数组,构造成一个大顶堆。
此时,整个数组的最大值就是堆顶的根节点。
② 然后,将根节点末尾元素交换,此时末尾即成为了最大值。
③ 然后将剩余的 n - 1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。
如此反复执行,最终会得到一个有序数列了。

步骤说明

首先我先给一个数组,比如 : int[] arr = {5, 7, 1, 3, 8, 4, 2};
在这里插入图片描述
将它转为顺序存储二叉树,如下所示。
在这里插入图片描述

步骤一

1.我们从最后一个非叶子结点开始( arr.length/2-1=7/2-1=2,也就是下面的值为 1 的结点),从右往左,从下往上进行调整。
比较{1, 4, 2} 三个值,发现 1 的左子节点 4 最大,对两者进行调换

在这里插入图片描述
2.然后找到刚才的位置索引即2,再-1,让现在的位置变为值为 7 的节点,比较{7, 3, 8}三个值,发现7的右子节点8大,则调换7 和 8 的位置。
在这里插入图片描述
3.然后找到刚才的位置索引即1,再-1,让现在的位置变为值为 5 的节点,现在已经是根节点了,比较{5, 8, 4}的值,同理,父结点和子结点调换。
在这里插入图片描述
4. 这是导致子根{5, 3, 7}结构混乱,继续调整{5, 3, 7} 中 7最大,调换7 和 5。
在这里插入图片描述
此时我们将这个无序的数列变成了一个大顶堆。

步骤二

① 将堆顶元素末尾元素进行交换,使末尾元素最大。
② 然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。
如此反复进行交换、重建、交换。

1.那我们将8 与 2 交换,使末尾的元素最大。我们在接下来的排序中就不考虑这个最大的元素了。
在这里插入图片描述

2.再走一遍排序,这次就从堆顶开始调整,因为下面的结构都符合大顶堆,所以从上往下开始排序。
{2, 7, 4} 中 7最大, 7和2调换;然后{2, 3, 5}中 5最大, 5和 2调换。
在这里插入图片描述
在这里插入图片描述
3.又成为了一个大顶堆,此时调换根节点 7末尾节点 1。继续反复
在这里插入图片描述
4下面就是重复的按照步骤二反复交换首尾、重建大顶堆、交换…
在这里插入图片描述

在这里插入图片描述
最后排序完成。
在这里插入图片描述

根据图解编写代码

堆排序的基本思路

I. 将无序数列构建成一个,根据升序降序需求选择大顶堆小顶堆;

II. 将堆顶元素末尾元素交换,将最大元素"沉"到数组末端;

III. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

代码实现

首先编写一个方法,大致理解为以 i 对应索引的非叶子结点,将它包括下面的左子节点和右子节点进行重构,让它们三个符合大顶堆的规则,即 i 节点 大于 左子节点和右子节点

/**
     * 功能 : 完成 将 以 i 对应的非叶子结点的树,调整成大顶堆
     * 举例 : int[] arr = {4, 6, 8, 5, 9}; ==> i = 1 ==> adjustHeap ==> {4, 9, 8, 5, 6}
     * 如果我们再次调用adjustHeap, 传入的应该是i = 0 ==> {9, 6, 8, 5, 4}
     * @param arr 待调整的数组
     * @param i 表示非叶子结点在数组中索引
     * @param length 表示对多少个元素进行调整,length 是在逐渐的减少
     */
    public static void adjustHeap(int[] arr, int i, int length) {

        int temp = arr[i]; // 先取出当前元素的值,保存在临时变量
        // 开始调整
        /**
         * 说明
         *      1. 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++; // 就让k指向右子结点
            }
            if (arr[k] > temp) { // 此时的 arr[k] 指向的是左右结点中最大的值, 如果子结点 > 父结点
                arr[i] = arr[k]; // 把较大的值赋给当前结点
                i = k; // 让 i 指向 k,继续循环比较
            } else {
                break; // 这里break是因为下面的都已经是有序的了,直接跳出循环。
            }
        }
        // 当 for 循环结束后,我们已经将以 i 为父结点的树的最大值,放在了 最顶上
        arr[i] = temp; // 将temp值放到调整后的位置
    }

再编写一个方法实现全部排序的逻辑:
第一个循环是为了构建好初始的大顶堆,将最大的值移到根节点,并使下面的所有节点满足大顶堆的规则。
第二个循环就是首先将根节点和末尾节点交换,然后进行重构堆,重构完后再进行根节点和末尾节点交换,然后再进行重构堆,然后再交换…

	// 编写一个堆排序的方法
    public static void heapSort(int[] arr) {
        int temp = 0;
        System.out.println("-----堆排序-----");

        // 将无序序列构建成一个大顶堆,根据升序降序需求选择大顶堆或小顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--) { // arr.length / 2 - 1 就是有几个非叶子结点
            adjustHeap(arr, i, arr.length);
        }
        System.out.println("数组 = " + Arrays.toString(arr));

        /**
         * 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端
         * 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整 + 交换步骤,直到整个序列有序
         */
        for (int j = arr.length - 1; j > 0; j--) {
            // 交换
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            adjustHeap(arr, 0, j);
        }
        System.out.println("数组 = " + Arrays.toString(arr));
    }

打印结果:
第一遍是构建大顶堆。
第二遍是完成堆排序。

-----堆排序-----
数组 = [8, 7, 4, 3, 5, 1, 2]
数组 = [1, 2, 3, 4, 5, 7, 8]

结语

堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舍其小伙伴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值