目录
前言
建议
简介
A.建堆:
B.排序
一、代码实现
二、时空复杂度
A.时间复杂度
B.空间复杂度
三、稳定性
四、现实中的应用
1.学习算法最重要的是理解算法的每一步,而不是记住算法。
2.建议读者学习算法的时候,自己手动一步一步地运行算法。
tips:文中的对数均以2为底数
堆排序是一种基于堆数据结构的排序算法。它分为两个主要步骤:建堆和排序。
建堆的过程是将一个无序的数组构建成一个堆,通常采用最大堆(Max Heap)的形式。最大堆是一种特殊的二叉树,其中每个节点的值都大于或等于其子节点的值。下面是建堆的简单步骤:
1.从最后一个非叶子节点开始: 最后一个非叶子节点的索引可以通过 (n/2 - 1) 计算,其中 n 是数组的长度。非叶子节点是指有子节点的节点。
2.进行堆调整(Heapify): 对于每个非叶子节点,进行堆调整,将当前节点与其子节点比较,确保当前节点是最大的。如果子节点的值较大,则交换节点值,然后递归地调整子树。
3.逐层向上重复: 重复步骤2,逐层向上处理非叶子节点,直到根节点。这样就确保了整个数组构成了一个最大堆。
排序的过程是将一个无序的数据集合按照一定规则重新排列成有序的序列。以下是一般排序算法的简单过程:
1.比较元素: 排序算法的第一步是通过比较元素来确定它们的相对顺序。比较可以根据不同的规则进行,例如升序或降序。
2.交换元素: 根据比较的结果,可能需要交换元素的位置以达到正确的顺序。这是排序算法中一个关键的步骤。
3.重复步骤1和2: 排序算法会持续比较和交换元素,直到整个数据集合中的元素都按照规则有序排列。
#include
// 交换数组中两个元素的值
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 调整堆,保持最大堆性质
void maxHeapify(int arr[], int n, int i) {
int largest = i; // 初始化最大值的索引为根节点
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) {
swap(&arr[i], &arr[largest]);
maxHeapify(arr, n, largest);
}
}
// 建立最大堆
void buildMaxHeap(int arr[], int n) {
// 从最后一个非叶子节点开始,逐个调整子树使其满足最大堆性质
for (int i = n / 2 - 1; i >= 0; i--) {
maxHeapify(arr, n, i);
}
}
// 堆排序主函数
void heapSort(int arr[], int n) {
// 建立最大堆
buildMaxHeap(arr, n);
// 从最后一个元素开始,逐个将当前最大元素交换到数组末尾
for (int i = n - 1; i > 0; i--) {
swap(&arr[0], &arr[i]); // 将当前最大元素移动到数组末尾
maxHeapify(arr, i, 0); // 调整堆,保持最大堆性质
}
}
// 打印数组
void printArray(int arr[], int size) {
for (int i = 0; i < size; 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),使得整个数组构成一个最大堆。建堆的时间复杂度是,其中n是数组的长度。
排序时间复杂度: 在建堆完成后,堆的根节点是数组中的最大元素。将根节点与数组末尾的元素交换,然后重新调整堆,这样就把最大的元素放到了数组末尾。重复这个过程,每次交换后堆的大小减一,直到整个数组有序。由于进行n次交换和调整,排序的时间复杂度也是。
因此,堆排序的总时间复杂度是建堆和排序时间复杂度的和:。
堆排序的空间复杂度主要包括两个方面:原地排序所需的空间和递归调用的栈空间。
原地排序: 堆排序是一种原地排序算法,它不需要额外的辅助数组来存储数据。排序过程中主要是通过在输入数组上进行元素交换,而不需要额外的存储空间。因此,原地排序的空间复杂度为 O(1)。
递归调用的栈空间: 在堆排序的建堆过程中,通常会使用递归调用或迭代方式来实现堆调整。递归调用会在调用栈中占用一定的空间,其空间复杂度与递归深度相关。最坏情况下,堆排序的递归深度为堆的高度,即,其中 n 是输入数组的长度。因此,递归调用的空间复杂度为 。
因此,原地排序和递归调用的空间开销,堆排序的总体空间复杂度为 :。这表明堆排序的空间需求相对较低,是一个空间效率较好的排序算法。
在排序过程中,可能会交换相同值的元素的相对位置,导致相等元素之间的原始相对顺序被打乱。具体来说,在建堆和排序的过程中,会涉及到元素的交换操作。如果两个相等元素的顺序在排序之前是相对的,那么在排序之后,它们的相对顺序可能会发生变化。由于堆排序是通过不断交换堆顶元素和数组末尾元素,并调整堆的过程来实现的,这种交换可能破坏相同元素的相对顺序。因此,堆排序不是稳定的排序算法。
堆排序虽然在某些方面不如一些简单的排序算法直观,但在现实中仍然有一些应用场景,其中它的优势得到充分发挥:
优先队列: 堆排序常用于实现优先队列,其中最大堆或最小堆的性质使得可以高效地找到最大或最小元素。这在任务调度、事件模拟等领域有广泛应用。
堆作为数据结构的一部分: 堆结构本身作为一种数据结构,可以被应用于图算法中的最短路径算法(如Dijkstra算法)和最小生成树算法(如Prim和Kruskal算法)等。堆排序作为堆结构的一种附带应用,用于维护和操作堆。
外部排序: 在需要对大规模数据进行排序的情况下,堆排序相对于其他算法的稳定的时间复杂度使得它成为外部排序算法的一种选择。外部排序涉及到将大量数据从外部存储(例如硬盘)加载到内存中进行排序,然后再写回外部存储。
不稳定排序的需求: 在某些情况下,不需要保持相同值元素的相对顺序,而更关注排序算法的性能,此时堆排序可以是一个合适的选择。