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

是一种特别的 彻底二叉树,具有堆化的特性。

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

存储结构类似于彻底二叉树,能够用数组实现。

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

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

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

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

因为是一种特别的彻底二叉树,其性质与二叉树类似。它能以 O(logN)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 进行交流。

至此,整个遍历结束。将一个无序数组调整成为了大根堆。**留意:**例子中的数组刚好降序摆放,仅仅个偶然哦!

理解了思路,上代码:

// 建 大根堆
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 结点往上移。

【堆 - 专题】堆排序,大根堆,小根堆
6 结点往上移
【堆 - 专题】堆排序,大根堆,小根堆
4 结点到来,能够往上移动一次。
【堆 - 专题】堆排序,大根堆,小根堆
9 结点到来,能够往上移动两次。
【堆 - 专题】堆排序,大根堆,小根堆
【堆 - 专题】堆排序,大根堆,小根堆
7 结点到来,能够往上移动一次。
【堆 - 专题】堆排序,大根堆,小根堆
7 不大于 9 ,不移动,终究大根堆树立好了!
【堆 - 专题】堆排序,大根堆,小根堆

代码超级简单:

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

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

“二分”一定要有序么

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