【堆 - 专题】堆排序,大根堆,小根堆

要想了解"堆排序、大根堆、小根堆 "是什么,首先要知道什么是

是一种特殊的 完全二叉树 ,具有堆化的特性。

其存储结构类似于完全二叉树,可以用数组实现。

与一般的排序方式所定义的 有序 不同,看似数组中的数字并未按照 升序降序 排列,但其实这棵树是已经有序的状态了。为什么呢?这就要引入 大、小根堆 的概念了:

大根堆: 父结点的值 大于或等于 其子结点的值

小根堆: 父结点的值 小于或等于 其子结点的值

由此可以看出,在上图所表示的堆中,不论哪一个结点为根,其子结点均大于根结点,因此这是一个 小根堆

因为是一种特殊的完全二叉树,其性质与二叉树类似。它能以 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g N ) O(logN) </math>O(logN) 的时间复杂度完成插入、删除和查找操作,通过调整数组中元素的顺序,维护堆的结构。


下面我们以 大根堆 为例,对堆的两个重要操作: 下调 ( heapfiy ) 和 上调 ( heapInsert ) 进行说明。

若结点数组下标为 i ,则:

父结点数组下标:( i - 1 ) / 2;

左孩子数组下标:2 * i + 1 ;

右孩子数组下标:2 * i + 2 ;

下调 heapfiy

给定一个无序数组,希望调整成为一个大根堆。

最后一个元素开始 向前遍历,比较自己与左右孩子结点的大小,如果小于孩子结点就交换(即:下调 )。下调之后继续与新的左右孩子结点进行比较,能够下调就下调,直到不能下调为止。

向前继续移动,直到所有结点均遍历一遍,所有父结点均大于其孩子结点,便成为了一棵大根堆。

  • 一句话总结:小数往下移

如图所示,6、7、5 是叶子结点,没有孩子。直到遍历到下标为 2 的 2 结点,小于了左子结点 6 ,交换。

接着遍历到了下标为 1 的 9 结点,不小于左右孩子 5 和 7 。因此不需要交换,直接跳过。

接着遍历到了下标为 0 的 4 结点,小于左右孩子 9 和 6 。与较大值 9 进行交换。 继续向下传导 4 结点来到了 1 下标位置,继续与左右孩子 5 和 7 比较,与较大值 7 进行交换。

至此,整个遍历结束。将一个无序数组调整成为了大根堆。**注意:**例子中的数组刚好降序排列,只是个巧合哦!

理解了思路,上代码:

java 复制代码
// 建 大根堆
public static void heapify(int[] arr, int index, int heapSize) {
    int left = index * 2 + 1;
    while (left < heapSize) {
        // 左右子树的最大值下标 largest
        int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
        if (arr[largest] <= arr[index]) {
            break;
        } else {
            swap(arr, largest, index);
            index = largest;
            left = index * 2 + 1;
        }
    }
}

代码解释

因为堆是完全二叉树,所以结点不一定有右子树,因此需要进行越界判断 left + 1 < heapSize,将左右子树的最大值下标赋给 largest。若最大值大于父结点,则进行交换,并继续向下传导:index = largest; left = index * 2 + 1;

上面代码进行了 一次下调 的完整操作即 heapify(), 将数组所有元素倒着遍历一遍即可完成整个大根堆的建立。

java 复制代码
for (int i = arr.length - 1; i >= 0; i--) {
    heapify(arr, i, arr.length);
}

上调 heapInsert

若提前不知道所有的数字(数字一个一个的给出),希望每给出一个数后,都能调整为一个大根堆。

此时我们可以换个方向考虑,从下往上插入

每给到一个数,考察父结点是否小于该结点,若小于该结点就交换。交换到新位置后继续判断,直到不能再往上交换为止。

  • 一句话总结:大数往上移

例如,分别到来的数字序列为:2,5,6,4,9,7。

2,5 分别到来,5 结点往上移。 6 结点往上移 4 结点到来,可以往上移动一次。 9 结点到来,可以往上移动两次。 7 结点到来,可以往上移动一次。 7 不大于 9 ,不移动,最终大根堆建立好了!

代码超级简单:

java 复制代码
public static void heapInsert(int[] arr, int index) {
    while (arr[index] > arr[(index - 1) / 2]) {
        swap(arr, index, (index - 1) / 2);
        index = (index - 1) / 2;
    }
}

上面代码进行了 一次上调 的完整操作即 heapInsert(), 每到来一个数字就调用一次,此时 index 值为 arr.length - 1 ,最终即可完成整个大根堆的建立。

堆排序

有了大根堆之后,我们就能很轻松的进行堆排序了。其思想是将数组划分为 有序无序 的部分,找到未排序部分的最值,放入到已排序部分中,直到未排序的部分为空。

每次将堆顶元素与最后一个元素进行交换,这样最大值就来到了数组的最后。由于堆顶发生了变化,可能不再是一个大根堆,因此进行 heapfiy 操作进行调整。

注意 :此时堆大小减一(最后一个元素已经是有序部分了,不需要参与堆的调整)。

以此往复,直到所有的元素均排好序。

java 复制代码
public static void heapSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    // heapify
    for (int i = arr.length - 1; i >= 0; i--) {
        heapify(arr, i, arr.length);
    }
    int heapSize = arr.length;
    swap(arr, 0, --heapSize);
    while (heapSize > 0) {
        heapify(arr, 0, heapSize);
        swap(arr, 0, --heapSize);
    }

}

// 建 大根堆
public static void heapify(int[] arr, int index, int heapSize) {
    int left = index * 2 + 1;
    while (left < heapSize) {
        int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
        if (arr[largest] <= arr[index]) {
            break;
        } else {
            swap(arr, largest, index);
            index = largest;
            left = index * 2 + 1;
        }
    }
}

public static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

理解了 heapInsert 操作之后,再来看堆排序的代码是不是很轻松呢?

下篇文章我们继续对 做进一步深入的学习 ------ 手写加强堆

~点赞 ~ 关注 ~ 不迷路 ~!!!

-------往期回顾-------

AC 此题,链表无敌!!!

归并排序,也有"套路"?

"二分"一定要有序么

你真的会找链表"中点"么?

相关推荐
zopple2 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001114 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本5 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34165 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan5 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer6 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor3566 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor3566 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer7 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP8 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪