一、思想
堆排序是一种基于比较的排序算法,且使用了堆的数据结构来辅助进行排序。其思想是利用堆的特性,即在每个节点都保证是最大(大顶堆)或者最小(小顶堆)的关键码。具体原理和步骤如下:
- 构建初始堆:将待排序的数组构造成一个最大堆(升序排序时使用)或最小堆(降序排序时使用)。这通过从最后一个非叶子节点开始,依次向上对每个父节点进行调整来完成。调整的方式是,如果父节点的值小于其子节点的值(对于最大堆),则将它们交换,直到该父节点的值大于其所有子节点的值,或已达到数组的开头。
- 堆顶元素交换:将堆顶元素(即当前最大的元素)与数组的最后一个元素交换,此时最后一个元素放置了当前的最大值。
- 调整堆结构:由于交换了堆顶元素,堆的结构被破坏,需要对剩下的元素再次进行调整以维持堆的性质。这一过程是从新的堆顶元素开始向下进行的,确保父节点的值大于或等于其子节点的值。
- 重复操作:重复执行步骤2和步骤3,每次减少一个元素进行堆调整,直至整个数组有序。每轮排序后,数组的末尾都会增加一个排序好的元素,直到所有元素都排好序。
总的来说,堆排序是一个较为高效的排序算法,其时间复杂度为O(nlogn)。尽管它在最坏、平均和最好情况下的时间复杂度都一样,但是常数因子较小,因此在实践中通常比其他O(nlogn)算法更快。此外,堆排序也不需要额外的存储空间,是一种原地排序算法
二、图解
1.图解堆化
首先我们要了解一个数组如何调整为一个堆,首先有这样一个数组,我们如何让他调整成为大根堆(所有的父亲节点比孩子节点的元素值大)
我们只需要从后往前依次对子堆进行向下调整即可。具体实现如下:首先我们需要从最后一个孩子所在的子堆进行调整,最好一个孩子就是数组长度-1的位置,那么我们如何根据孩子节点的下标去求出父亲节点的下标呢,我们只需要让孩子节点的下标减去1的差然后除2就能得到父亲节点的下标,同样我们知道父亲节点的下标同样除2在加1就能求出孩子节点的下标。接着进行向下调整:首先对于这个堆如何进行向下调整呢?
首先我们需要找到孩子节点中的最大值,然后拿他与父亲节点比较,如果他比父亲节点大,那么就进行交换,然后依次向下进行调整(调整孩子节点的子堆),如果比父亲节点小说明这个堆已经调整完成我们就可以结束向下
此时交换完成后,继续向下进行调整
这个时候发现已经没有孩子节点了于是结束这个堆的调整开始调整下一个堆
这个时候孩子节点比父亲节点值大,于是进行交换,交换后继续向下进行调整parent = child; child = 2 * parent + 1,重复上述操作找到孩子的最大值继续与父亲节点进行比较
孩子节点最大值比父亲节点值大继续进行交换与向下调整
这个时候孩子节点下标越界也就意味着没有孩子,于是结束调整,下面是堆化代码
2.图解堆排
在将数组堆化以后,我们就可以根据堆的特性进行排序,由于堆顶的元素一定是堆中最大的值。我们可以将他与最后一个元素交换,然后调整堆结束的下标防止最大元素在进行堆维护时重新回到堆顶。再将堆维护为大根堆重复这个操作,知道堆结束下标为0时
重新调整堆,在0位置继续向下调整维护大根堆
重复此操作将最大值交换到末尾
继续调整
交换到末尾
重复上述步骤知道结束下标指向0
三、代码实现
void heap_sort(vector<int>& arr) {
// 1. 堆化
for (int parent = (arr.size() - 1 - 1) / 2; parent >= 0; parent--) {
int child = 2 * parent + 1;
while (child < arr.size()) {
if (child + 1 < arr.size() && arr[child] < arr[child + 1]) child++;
if (arr[child] > arr[parent]) {
swap(arr[child], arr[parent]);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
// 2. 开始堆排
int len = arr.size() - 1;
while (len) {
swap(arr[0], arr[len]);
int parent = 0;
int child = 2 * parent + 1;
while (child < len) {
if (child + 1 < len && arr[child] < arr[child + 1]) child++;
if (arr[child] > arr[parent]) {
swap(arr[child], arr[parent]);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
len--;
}
}
private static void siftDown(int[] arr, int parent, int len) {
int child = 2 * parent + 1;
while (child < len) {
// 寻找两孩子节点中最大得
if (child + 1 < len && arr[child + 1] > arr[child]) {
child++;
}
// 孩子节点与父亲节点进行比较
if (arr[child] > arr[parent]) {
// 进行交换
swap(arr, child, parent);
// 继续向下调整
parent = child;
child = 2 * parent + 1;
} else {
// 父亲节点比孩子节点大, 满足大根堆结束调整
break;
}
}
}
public static void heapSort(int[] arr) {
// 堆化
for (int parent = (arr.length - 1 - 1) / 2; parent >= 0; parent--) {
siftDown(arr, parent, arr.length);
}
// 排序
int len = arr.length - 1;
while (len >= 0) {
// 交换
swap(arr, 0, len);
// 调整
siftDown(arr, 0, len);
// 修改结束下标
len--;
}
}