引言
在上一章中,我们探讨了高效的快速排序及其分治思想。这一次,我们将继续探索两种同样重要的排序算法:归并排序(Merge Sort) 和 堆排序(Heap Sort) 。
它们与快速排序一样,都是**O(n log n)**时间复杂度的排序算法,但实现方式和适用场景却各有不同。归并排序在稳定性和分布式场景中表现优异,而堆排序则在内存有限的情况下表现突出。
接下来,我们将一起深入剖析这两种排序算法的原理、实现与应用。
一、归并排序(Merge Sort)
1.1 算法思想
归并排序是典型的分治法应用,采用"分而治之"的思想,递归地将数组分成两部分,分别排序后再合并。
1.2 算法过程
- 分解:将数组从中间分成两部分,直到每部分只有一个元素(显然是有序的)。
- 合并:将两个有序的子数组合并成一个有序数组。
- 递归:对每个子数组重复上述过程。
1.3 C语言实现
cpp
#include <stdio.h>
#include <stdlib.h>
// 合并两个有序子数组
void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1; // 左子数组的大小
int n2 = right - mid; // 右子数组的大小
// 创建临时数组
int* L = (int*)malloc(n1 * sizeof(int));
int* R = (int*)malloc(n2 * sizeof(int));
// 复制数据到临时数组
for (int i = 0; i < n1; i++) L[i] = arr[left + i];
for (int j = 0; j < n2; j++) R[j] = arr[mid + 1 + j];
// 合并临时数组到原数组
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k++] = L[i++];
} else {
arr[k++] = R[j++];
}
}
// 复制剩余元素
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
// 释放临时数组
free(L);
free(R);
}
// 归并排序
void mergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // 防止溢出
mergeSort(arr, left, mid); // 排序左半部分
mergeSort(arr, mid + 1, right); // 排序右半部分
merge(arr, left, mid, right); // 合并两部分
}
}
int main() {
int arr[] = {38, 27, 43, 3, 9, 82, 10};
int n = sizeof(arr) / sizeof(arr[0]);
mergeSort(arr, 0, n - 1);
printf("排序后的数组: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
1.4 时间和空间复杂度
- 时间复杂度 :
- 每次分解需要
log n
层。 - 每次合并需要
O(n)
时间。 - 总体时间复杂度为O(n log n)。
- 每次分解需要
- 空间复杂度 :
- 由于需要额外的临时数组,空间复杂度为O(n)。
1.5 优点与缺点
- 优点 :
- 时间复杂度始终为O(n log n),无最坏情况。
- 稳定排序,适用于需要保持顺序的场景。
- 缺点 :
- 需要额外的存储空间,空间复杂度较高。
二、堆排序(Heap Sort)
2.1 算法思想
堆排序是基于堆数据结构 的排序算法,通过构建最大堆 或最小堆实现排序。
- 最大堆:堆顶(根节点)是堆中最大的元素。
- 最小堆:堆顶(根节点)是堆中最小的元素。
堆排序的过程可以分为两个步骤:
- 构建堆:将无序数组调整为一个最大堆。
- 排序:依次将堆顶元素(最大值)与最后一个元素交换,并调整堆。
2.2 C语言实现
构建堆的核心:向下调整(Heapify)
cpp
void heapify(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) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest);
}
}
堆排序主函数
cpp
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);
}
}
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
heapSort(arr, n);
printf("排序后的数组: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
2.3 时间和空间复杂度
- 时间复杂度 :
- 构建堆需要
O(n)
时间。 - 排序过程需要
O(n log n)
时间。 - 总体时间复杂度为O(n log n)。
- 构建堆需要
- 空间复杂度 :
- 原地排序,无额外空间需求,空间复杂度为O(1)。
2.4 优点与缺点
- 优点 :
- 时间复杂度为O(n log n),无最坏情况。
- 不需要额外存储空间,适合内存有限的场景。
- 缺点 :
- 不稳定排序。
- 实现相对复杂,调整堆时需要更多代码。
三、对比与总结
特性 | 归并排序 | 堆排序 |
---|---|---|
时间复杂度 | O(n log n) | O(n log n) |
空间复杂度 | O(n) | O(1) |
稳定性 | 稳定 | 不稳定 |
适用场景 | 数据量大,且对稳定性有要求 | 内存有限,且对稳定性无要求 |
四、总结与展望
归并排序和堆排序是经典的高级排序算法,各有优缺点。归并排序因其稳定性和良好的最坏情况表现适用于广泛场景,而堆排序因其原地排序特性在内存有限的情况下更具优势。
在下一篇文章中,我们将深入探讨线性时间排序算法(如计数排序、桶排序、基数排序),感受算法在特定场景下的极致效率,敬请期待!
归并排序与堆排序不仅是经典的排序算法,也是面试中经常被问到的重点。希望通过这篇文章,你能深入理解它们的原理与实现,为掌握高级排序算法迈出扎实的一步。
有疑问或建议?欢迎评论区讨论,我们一起进步!