目录
💯引言
💯堆的概念
(一)什么是堆
(二)堆的表示
💯堆排序原理
(一)建堆
(二)排序
💯代码实现
💯代码分析
(一)heapify函数
(二)heapSort函数
(三)printArray函数
(四)main函数
💯性能分析
(一)时间复杂度
(二)空间复杂度
(三)稳定性
💯应用场景
💯总结
💯引言
在计算机科学领域,排序算法是非常重要的基础算法之一,它们被广泛应用于各种场景,如数据库查询、数据处理、算法设计等。之前的关于堆的文章👉【数据结构】堆(Heap)详解
堆排序作为一种高效的排序算法,具有时间复杂度为的优异性能,并且在空间复杂度上也有较好的表现。本文将深入解析堆排序的原理、实现过程以及其性能特点,并使用 C 语言进行代码实现。
💯堆的概念
(一)什么是堆
堆是一种特殊的数据结构,它通常可以被看作是一棵完全二叉树。在这棵完全二叉树中,每个节点都满足特定的堆性质。对于大根堆,每个节点的值都大于或等于其左右子节点的值;对于小根堆,每个节点的值都小于或等于其左右子节点的值。
(二)堆的表示
在实际的程序实现中,堆通常用数组来表示。因为完全二叉树的性质,对于数组中索引为i
的节点,其左子节点的索引为2i + 1
,右子节点的索引为2i + 2
,父节点的索引为(i - 1) / 2
(向下取整)。这种表示方式使得在操作堆时,可以方便地通过数组索引来访问和修改节点的值,而无需显式地构建二叉树的节点结构和指针。
例如,假设有一个数组arr = [10, 5, 8, 3, 2]
,它可以表示为如下的完全二叉树(以大根堆为例):
10
/ \
5 8
/ \ /
3 2
在这个例子中,数组索引 0
处的元素10
是根节点,它的左子节点是数组索引1
处的元素5
,右子节点是数组索引2处的元素8
;节点5
的左子节点是数组索引3
处的元素3
,右子节点是数组索引4
处的元素2。
💯堆排序原理
(一)建堆
⭐升序:建大堆
⭐降序:建小堆
堆排序的第一步是将待排序的数组构建成一个堆。从数组的中间位置开始,向前遍历每个节点,对每个节点进行堆化操作(调整节点及其子树,使其满足堆性质)。
以大根堆为例,如果一个节点的值小于其较大的子节点的值,就将它们交换,然后继续对交换后的子节点进行堆化操作,直到该节点及其子树满足大根堆性质。这个过程确保了数组构建成的完全二叉树满足大根堆的要求,即根节点是数组中的最大元素。
“为什么从数组中间位置开始向前遍历进行堆化呢?”这是因为数组后半部分的节点都是叶子节点,它们本身已经满足堆的性质(没有子节点),所以只需要对非叶子节点进行堆化操作。
例如,对于数组{1, 5, 3, 8, 7, 6}
,中间位置的索引为(6 / 2 - 1) = 2
,先对索引为2
的节点(值为3)进行堆化,然后依次向前对索引为1
和0
的节点进行堆化,最终构建成大根堆。
⭐图解如下:
(二)排序
建堆完成后,数组中的最大元素位于根节点(即数组的第一个位置)。将根节点与数组的最后一个元素交换,此时最大元素被放置到了正确的位置(数组的末尾)。然后对除了最后一个元素之外的数组进行堆化操作,使其再次成为一个大根堆。重复这个过程,每次将根节点与当前未排序部分的最后一个元素交换,然后对剩余部分进行堆化,直到整个数组有序。
比如,在第一次交换后,数组的最后一个元素是当前最大的,然后对剩下的元素重新堆化,找到次大的元素放在根节点,再与剩下未排序部分的最后一个元素交换,以此类推,逐步将数组排序。
💯代码实现
#include <stdio.h>
// 调整堆,使其满足大根堆性质
void heapify(int arr[], int n, int i) {
int largest = i; // 初始化 largest 为当前节点索引
int left = 2 * i + 1; // 左子节点索引
int right = 2 * i + 2; // 右子节点索引
// 如果左子节点大于根节点,更新最大索引
if (left < n && arr[left] > arr[largest])
largest = left;
// 如果右子节点大于当前最大节点,更新最大索引
if (right < n && arr[right] > arr[largest])
largest = right;
// 如果最大节点不是根节点,交换并递归调整
if (largest!= i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest); // 递归调整以 largest 为根的子树
}
}
// 堆排序函数
void heapSort(int arr[], int n) {
// 建堆
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 排序
for (int i = n - 1; i > 0; i--) {
// 将根节点与最后一个元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 对剩余元素进行堆化
heapify(arr, i, 0);
}
}
// 打印数组函数
void printArray(int arr[], int n) {
for (int i = 0; i < n; ++i)
printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组: ");
printArray(arr, n);
heapSort(arr, n);
printf("排序后的数组: ");
printArray(arr, n);
return 0;
}
💯代码分析
(一)heapify
函数
// 调整堆,使其满足大根堆性质
void heapify(int arr[], int n, int i) {
int largest = i; // 初始化 largest 为当前节点索引
int left = 2 * i + 1; // 左子节点索引
int right = 2 * i + 2; // 右子节点索引
// 如果左子节点大于根节点,更新最大索引
if (left < n && arr[left] > arr[largest])
largest = left;
// 如果右子节点大于当前最大节点,更新最大索引
if (right < n && arr[right] > arr[largest])
largest = right;
// 如果最大节点不是根节点,交换并递归调整
if (largest!= i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest); // 递归调整以 largest 为根的子树
}
}
- 函数首先确定当前节点
i
及其左右子节点的索引left
和right
。 - 然后通过比较左右子节点与当前节点的值,找到三者中的最大值,并将其索引存储在
largest
变量中。 - 如果
largest
不等于i
,说明当前节点不满足大根堆性质,需要将当前节点与largest
指向的子节点交换值。然后递归地对largest
索引处的子树进行heapify
操作,以确保交换后子树仍然满足大根堆性质。
例如,在对某个节点进行堆化时,如果该节点的值小于其左子节点的值,就将它们交换,然后再检查交换后的左子节点及其子树是否满足大根堆性质,如果不满足,继续递归调整。
(二)heapSort
函数
// 堆排序函数
void heapSort(int arr[], int n) {
// 建堆
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 排序
for (int i = n - 1; i > 0; i--) {
// 将根节点与最后一个元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 对剩余元素进行堆化
heapify(arr, i, 0);
}
}
- 首先进行建堆操作。从数组中间位置开始向前遍历,对每个节点调用
heapify
函数,构建大根堆。 - 建堆完成后,开始排序过程。每次将根节点(即当前最大元素)与数组的最后一个未排序元素交换,然后对除最后一个元素外的数组进行
heapify
操作,使剩余部分再次成为大根堆。重复这个过程,直到整个数组有序。
(三)printArray
函数
// 打印数组函数
void printArray(int arr[], int n) {
for (int i = 0; i < n; ++i)
printf("%d ", arr[i]);
printf("\n");
}
用于简单地打印数组中的元素,方便查看排序前后的数组状态。
(四)main
函数
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组: ");
printArray(arr, n);
heapSort(arr, n);
printf("排序后的数组: ");
printArray(arr, n);
return 0;
}
- 定义了一个测试数组
arr
并初始化,同时计算数组的长度n
。 - 打印原始数组,然后调用
heapSort
函数对数组进行排序,最后打印排序后的数组。
⭐全过程图解如下:
💯性能分析
(一)时间复杂度
- 建堆时间复杂度:建堆过程从数组的中间位置开始,对每个节点进行堆化操作。对于一个包含
n
个元素的数组,建堆的时间复杂度约为。更准确地分析,建堆的时间复杂度为,但是由于堆是一种近似完全二叉树的结构,其高度为,且在调整堆的过程中,每个节点的调整时间与树的高度相关,因此可以近似看作是线性时间复杂度。因此:建堆的时间复杂度为O(N)。 - 排序时间复杂度:在排序阶段,每次将根节点与未排序部分的最后一个元素交换,然后对剩余元素进行堆化,需要进行次操作。每次堆化的时间复杂度为,所以排序的时间复杂度为。
综合来看,堆排序的时间复杂度为,无论是最好、最坏还是平均情况,时间复杂度都是稳定的,这使得堆排序在处理大规模数据时具有很好的性能表现。
(二)空间复杂度
堆排序的空间复杂度为。因为在整个排序过程中,只需要常数级别的额外空间来进行元素的交换和临时变量的存储,不需要像一些其他排序算法(如归并排序)那样需要额外的数组来存储中间结果。
(三)稳定性
堆排序是一种不稳定的排序算法。这是因为在交换根节点和最后一个元素时,可能会改变相同值元素的相对顺序。例如,如果数组中有两个相等的元素,一个在较大元素的子树中,另一个在较小元素的子树中,在排序过程中它们的相对位置可能会发生改变。
💯应用场景
- 海量数据排序:由于堆排序具有较好的时间复杂度和相对较低的空间复杂度,适用于对大规模数据进行排序。在内存有限的情况下,堆排序可以有效地处理大量数据,而不需要过多的额外存储空间。
- 优先级队列:堆可以很方便地实现优先级队列。在优先级队列中,元素根据其优先级进行排序,堆的性质使得插入和删除最大(或最小)优先级元素的操作可以在的时间内完成。例如,操作系统中的任务调度、网络数据包的优先级处理等都可以使用基于堆的优先级队列来实现。
💯总结
堆排序是一种高效的排序算法,通过利用堆这种特殊的数据结构,能够在的时间复杂度内对数据进行排序,并且具有较好的空间性能。理解堆排序的原理和实现对于深入掌握算法和数据结构知识具有重要意义。在实际应用中,我们可以根据具体的需求和场景选择合适的排序算法,而堆排序在处理大规模数据和需要实现优先级队列等方面具有独特的优势。通过对堆排序的深入学习,我们也可以更好地理解和应用其他相关的数据结构和算法,提高程序的性能和效率。
💝💝💝如果你对堆排序还有其他疑问或者想要进一步探讨相关内容,欢迎随时交流~
感谢你看到最后,制作不易,点个赞再走吧!我的主页👉【A charmer】