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

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

是一种特殊的 完全二叉树,具有堆化的特性。
【堆 - 专题】堆排序,大根堆,小根堆_第1张图片

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

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

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

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

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

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


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

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

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

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

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

下调 heapfiy

给定一个无序数组,希望调整成为一个大根堆。
【堆 - 专题】堆排序,大根堆,小根堆_第2张图片

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

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

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

【堆 - 专题】堆排序,大根堆,小根堆_第3张图片

如图所示,6、7、5 是叶子结点,没有孩子。直到遍历到下标为 2 的 2 结点,小于了左子结点 6 ,交换。
【堆 - 专题】堆排序,大根堆,小根堆_第4张图片

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

【堆 - 专题】堆排序,大根堆,小根堆_第5张图片
接着遍历到了下标为 0 的 4 结点,小于左右孩子 9 和 6 。与较大值 9 进行交换。
继续向下传导
【堆 - 专题】堆排序,大根堆,小根堆_第6张图片
4 结点来到了 1 下标位置,继续与左右孩子 5 和 7 比较,与较大值 7 进行交换。

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

理解了思路,上代码:

// 建 大根堆
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(), 将数组所有元素倒着遍历一遍即可完成整个大根堆的建立。

for (int i = arr.length - 1; i >= 0; i--) {
    heapify(arr, i, arr.length);
}

上调 heapInsert

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

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

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

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

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

2,5 分别到来,5 结点往上移。
【堆 - 专题】堆排序,大根堆,小根堆_第7张图片
6 结点往上移
【堆 - 专题】堆排序,大根堆,小根堆_第8张图片
4 结点到来,可以往上移动一次。
【堆 - 专题】堆排序,大根堆,小根堆_第9张图片
9 结点到来,可以往上移动两次。
【堆 - 专题】堆排序,大根堆,小根堆_第10张图片
【堆 - 专题】堆排序,大根堆,小根堆_第11张图片
7 结点到来,可以往上移动一次。
【堆 - 专题】堆排序,大根堆,小根堆_第12张图片
7 不大于 9 ,不移动,最终大根堆建立好了!
【堆 - 专题】堆排序,大根堆,小根堆_第13张图片

代码超级简单:

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 操作进行调整。

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

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

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 此题,链表无敌!!!

归并排序,也有“套路”?

“二分”一定要有序么

你真的会找链表“中点”么?

你可能感兴趣的:(数据结构,算法,java)