1. 归并排序(Merge Sort)
核心思想(分治):
- 分解:把数组从中间分成左右两半。
- 解决:递归地对左右两半进行归并排序。
- 合并:把两个已排序的子数组合并成一个有序数组。
归并排序是稳定的,时间复杂度非常稳定。
完整实现
cpp
#include <iostream>
#include <vector>
using namespace std;
// 合并两个有序子数组 arr[left..mid] 和 arr[mid+1..right]
void merge(vector<int>& arr, int left, int mid, int right) {
vector<int> temp(right - left + 1);
int i = left; // 左半部分起始
int j = mid + 1; // 右半部分起始
int k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) { // 加等号保证稳定性
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 把剩余元素拷贝进去
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
// 拷贝回原数组
for (int p = 0; p < temp.size(); ++p) {
arr[left + p] = temp[p];
}
}
void mergeSort(vector<int>& arr, int left, int right) {
if (left >= right) return;
int mid = left + (right - left) / 2; // 防止溢出
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
// 对外接口
void MergeSort(vector<int>& arr) {
if (arr.size() <= 1) return;
mergeSort(arr, 0, arr.size() - 1);
}
复杂度:
- 时间复杂度:最好、平均、最坏都是 O(n log n)
- 空间复杂度:O(n)(需要额外数组)
- 稳定性:稳定
2. 快速排序(Quick Sort)
核心思想(分治 + 填坑/交换):
- 选取一个基准值(pivot)。
- 把比基准小的元素放到左边,比基准大的放到右边(分区)。
- 递归地对左右两部分进行快速排序。
快速排序平均性能最优,是实际使用最多的排序算法之一。
完整实现(挖坑法 + 随机基准优化)
cpp
#include <cstdlib> // rand
#include <ctime>
// 分区函数(挖坑法)
int partition(vector<int>& arr, int left, int right) {
// 随机选择基准,降低最坏情况概率
int randomIndex = left + rand() % (right - left + 1);
swap(arr[randomIndex], arr[left]);
int pivot = arr[left]; // 基准值
int i = left, j = right;
while (i < j) {
// 从右向左找小于基准的
while (i < j && arr[j] >= pivot) --j;
if (i < j) arr[i] = arr[j];
// 从左向右找大于基准的
while (i < j && arr[i] <= pivot) ++i;
if (i < j) arr[j] = arr[i];
}
arr[i] = pivot; // 基准归位
return i;
}
void quickSort(vector<int>& arr, int left, int right) {
if (left >= right) return;
int pivotPos = partition(arr, left, right);
quickSort(arr, left, pivotPos - 1);
quickSort(arr, pivotPos + 1, right);
}
// 对外接口
void QuickSort(vector<int>& arr) {
if (arr.size() <= 1) return;
srand(time(nullptr)); // 设置随机种子
quickSort(arr, 0, arr.size() - 1);
}
复杂度:
- 平均时间复杂度:O(n log n)
- 最坏时间复杂度:O(n²)(已通过随机基准大幅降低概率)
- 空间复杂度:O(log n)(递归栈)
- 稳定性:不稳定
3. 归并 vs 快速 对比
| 对比维度 | 归并排序 | 快速排序 |
|---|---|---|
| 时间复杂度 | 稳定 O(n log n) | 平均 O(n log n),最坏 O(n²) |
| 空间复杂度 | O(n) | O(log n) |
| 稳定性 | 稳定 | 不稳定 |
| 是否原地排序 | 否 | 基本原地 |
| 实际运行速度 | 较慢(常量较大) | 通常最快 |
| 适用场景 | 需要稳定排序、链表排序 | 大多数通用场景 |
| 优化手段 | 小数组转插入排序 | 随机基准、三数取中、小数组优化 |
4. 完整测试代码
cpp
void printArray(const vector<int>& arr) {
for (int x : arr) cout << x << " ";
cout << endl;
}
int main() {
vector<int> arr1 = {49, 38, 65, 97, 76, 13, 27, 49, 55, 4};
vector<int> arr2 = arr1;
cout << "原数组:";
printArray(arr1);
MergeSort(arr1);
cout << "归并排序后:";
printArray(arr1);
QuickSort(arr2);
cout << "快速排序后:";
printArray(arr2);
return 0;
}
5. 总结与思考
归并排序:分治思想的经典体现,稳定且时间复杂度有保障,但需要额外空间。
快速排序:平均性能之王,通过基准分区实现高效排序,实际工程中使用最多。
结合《C++ Primer Plus》的思考:
- 归并排序中的临时数组体现了"空间换时间"。
- 快速排序的递归分区过程,与书中讲解的递归思想高度契合。
- 随机基准的使用,是对"避免最坏输入"这一工程思维的体现。
到这里,第9章常用排序算法已经基本覆盖。下一篇可以补充基数排序或做一次整体排序算法总结对比。
6. 基数排序(Radix Sort)
核心思想(LSD 低位优先):
- 按位分配:从最低位(个位)开始,依次对每一位进行排序。
- 稳定收集:每一轮使用稳定的计数排序,保证相同位值元素的相对顺序不变。
- 逐位推进:从低位到高位逐轮处理,最终得到整体有序序列。
基数排序不基于比较,而是通过"分配 + 收集"的方式,在特定数据范围内能达到线性时间复杂度。
完整实现(LSD + 计数排序子过程)
cpp
#include <vector>
#include <algorithm> // max_element
using namespace std;
// 计数排序子过程:按第 exp 位(10 的幂)对数组排序
void countingSortByDigit(vector<int>& arr, int exp) {
int n = arr.size();
vector<int> output(n);
int count[10] = {0};
// 统计每个数字(0-9)出现的次数
for (int i = 0; i < n; ++i) {
int digit = (arr[i] / exp) % 10;
++count[digit];
}
// 累加计数,得到每个数字在输出数组中的位置
for (int i = 1; i < 10; ++i) {
count[i] += count[i - 1];
}
// 从后向前遍历,保证稳定性
for (int i = n - 1; i >= 0; --i) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
--count[digit];
}
// 拷贝回原数组
for (int i = 0; i < n; ++i) {
arr[i] = output[i];
}
}
// 基数排序(LSD 低位优先)
void RadixSort(vector<int>& arr) {
if (arr.size() <= 1) return;
// 找到最大值,确定最大位数
int maxVal = *max_element(arr.begin(), arr.end());
// 从个位开始,逐位排序
for (int exp = 1; maxVal / exp > 0; exp *= 10) {
countingSortByDigit(arr, exp);
}
}
复杂度:
- 时间复杂度:O(d * (n + k)),其中 d 为最大位数,k 为基数(十进制下 k = 10)
- 空间复杂度:O(n + k)(需要额外输出数组和计数数组)
- 稳定性:稳定
与归并排序、快速排序的对比
| 对比维度 | 归并排序 | 快速排序 | 基数排序 |
|---|---|---|---|
| 排序方式 | 比较排序(分治) | 比较排序(分治) | 非比较排序(分配 + 收集) |
| 时间复杂度 | 稳定 O(n log n) | 平均 O(n log n),最坏 O(n²) | O(d * (n + k)) |
| 空间复杂度 | O(n) | O(log n) | O(n + k) |
| 稳定性 | 稳定 | 不稳定 | 稳定 |
| 是否原地排序 | 否 | 基本原地 | 否 |
| 适用场景 | 需要稳定排序、链表排序 | 大多数通用场景 | 整数、固定长度字符串等可拆位数据 |
| 局限 | 额外空间开销较大 | 最坏情况退化 | 依赖数据范围,不适合浮点或任意长度数据 |
基数排序在数据位数较少、范围可控时表现优异,但通用性不如归并排序和快速排序。三者各有适用场景,实际工程中应根据数据特征和稳定性要求综合选择。