要想了解“堆排序、大根堆、小根堆”是什么,首先要知道什么是 堆。
其存储结构类似于完全二叉树,可以用数组实现。
与一般的排序方式所定义的 有序 不同,看似数组中的数字并未按照 升序 或 降序 排列,但其实这棵树是已经有序的状态了。为什么呢?这就要引入 大、小根堆 的概念了:
大根堆: 父结点的值 大于或等于 其子结点的值
小根堆: 父结点的值 小于或等于 其子结点的值
由此可以看出,在上图所表示的堆中,不论哪一个结点为根,其子结点均大于根结点,因此这是一个 小根堆。
因为是一种特殊的完全二叉树,其性质与二叉树类似。它能以 O ( l o g N ) O(logN) O(logN) 的时间复杂度完成插入、删除和查找操作,通过调整数组中元素的顺序,维护堆的结构。
下面我们以 大根堆 为例,对堆的两个重要操作: 下调 ( heapfiy ) 和 上调 ( heapInsert ) 进行说明。
若结点数组下标为 i ,则:
父结点数组下标:( i - 1 ) / 2;
左孩子数组下标:2 * i + 1 ;
右孩子数组下标:2 * i + 2 ;
从 最后一个元素开始 向前遍历,比较自己与左右孩子结点的大小,如果小于孩子结点就交换(即:下调 )。下调之后继续与新的左右孩子结点进行比较,能够下调就下调,直到不能下调为止。
向前继续移动,直到所有结点均遍历一遍,所有父结点均大于其孩子结点,便成为了一棵大根堆。
如图所示,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);
}
若提前不知道所有的数字(数字一个一个的给出),希望每给出一个数后,都能调整为一个大根堆。
此时我们可以换个方向考虑,从下往上插入。
每给到一个数,考察父结点是否小于该结点,若小于该结点就交换。交换到新位置后继续判断,直到不能再往上交换为止。
例如,分别到来的数字序列为: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 此题,链表无敌!!!
归并排序,也有“套路”?
“二分”一定要有序么
你真的会找链表“中点”么?