版权声明:本文为博主原创文章,未经博主允许不得转载。
简介
- 堆排序是一种不稳定的排序算法。
- 堆排序的时间复杂度为O(NlogN)。
- 堆排序有两种实现方式。基于递归函数的实现,其额外空间复杂度为O(logN);非递归实现的额外空间复杂度为O(1)。
这里讨论的是非递归的实现方式。堆分为大根堆和小根堆,是完全二叉树。
对于完全二叉树中的任意一个节点,若它存在左孩子和右孩子(右孩子如果缺失可以脑补),你都会发现如图一三角形区域所示的微金字塔结构。
这里说明一点,全篇的关注焦点都应该放在这个微金字塔结构中。复杂的完全二叉树可以将它抽象成多个微金字塔结构的堆叠。堆排序的最小操作单元可以认为是微金字塔结构中的节点。
大根堆和小根堆
大根堆是什么?对于堆中任意一个微金字塔结构内的节点,如果最大值的节点在金字塔顶端,那么它就是大根堆。同理,如果在任意一个微金字塔结构中,最小值的节点在金字塔顶端,那么它就是小根堆。
用公式描述如下:
大根堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小根堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
其中,i为节点所在的位置,2i+1为左孩子的位置,2i+2为右孩子的位置。
排序步骤
这里以大根堆为例。大根堆排序的步骤如下:
1. 将长度为N的待排序数组构造成一个大根堆。
2. 将根节点与尾节点交换。
3. 将去除尾节点后剩余的N -1个节点重新构造成一个大根堆。
4. 重复步骤2,步骤3直至成为一个有序序列。
构造大根堆
排序步骤中的关键点是如何构造一个大根堆。我们从微观层面着眼,构造大根堆就是调节堆中的每一个微金字塔结构,使任意一个微金字塔结构中节点的最大值都在其顶部。
如何调整微金字塔中的节点?找出最大值节点,如果不在微金字塔的顶端,交换位置使它处于顶部。这里有个注意点,如果该微金字塔结构重新调整了,那么它上层的微金字塔结构可能不再符合大根堆的要求,可能需要从该位置起向上层重新调整一遍。
代码实现
- 主函数
public static void heapSort(char[] chas) {
//构造一个大根堆
for (int i = 0; i < chas.length; i++) {
maxHeapFirstBuild(chas, i);
}
for (int i = chas.length - 1; i > 0; i--) {
// 首尾交换
swap(chas, 0, i);
//构造一个新的大根堆
maxHeapify(chas, 0, i);
}
}
- 待排序数组生成大根堆
private static void maxHeapFirstBuild(char[] chas, int i) {
int parent = 0;
while (i != 0) {
parent = (i - 1) / 2;
if (chas[parent] < chas[i]) {
swap(chas, parent, i);
i = parent;
} else {
break;
}
}
}
- 核心代码段
private static void maxHeapify(char[] chas, int i, int size) {
int left = i * 2 + 1;
int right = i * 2 + 2;
int largest = i;
//判断当前节点有没有左孩子
while (left < size) {
if (chas[left] > chas[i]) {
largest = left;
}
if (right < size && chas[right] > chas[largest]) {
largest = right;
}
if (largest != i) {
swap(chas, largest, i);
} else {
break;
}
i = largest;
left = i * 2 + 1;
right = i * 2 + 2;
}
}
while循环内部是微金字塔结构的调整逻辑,如果该微金字塔结构调整过,则回溯。
- 首尾交换
public static void swap(char[] chas, int index1, int index2) {
char tmp = chas[index1];
chas[index1] = chas[index2];
chas[index2] = tmp;
}