1. 引言
排序算法是计算机科学中最基础也最重要的算法之一。无论是数据库索引、搜索引擎结果排序,还是日常开发中的数据处理,排序都无处不在。掌握排序算法的原理与实现,不仅能帮助我们写出更高效的代码,更是理解算法设计与复杂度分析的最佳入门途径。
本文将以 C++ 代码为例,详细讲解七大经典排序算法:选择排序、冒泡排序、插入排序、计数排序、堆排序、快速排序和归并排序。我们将从算法思想、代码实现、复杂度分析三个维度逐一展开,并在最后给出横向对比与选型建议。
2. 选择排序(Selection Sort)
2.1 算法思想
选择排序的核心思想非常直观:每一轮从未排序区间中选出最小(或最大)的元素,放到已排序区间的末尾。经过 n-1 轮选择后,整个数组就有序了。
它的特点是交换次数少,每轮最多交换一次,但比较次数固定为 n(n-1)/2,与初始数据顺序无关。
2.2 C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
void selectionSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; ++i) {
int minIdx = i;
for (int j = i + 1; j < n; ++j) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
if (minIdx != i) {
swap(arr[i], arr[minIdx]);
}
}
}
int main() {
vector<int> arr = {64, 25, 12, 22, 11};
selectionSort(arr);
for (int x : arr) cout << x << " ";
return 0;
}
2.3 复杂度分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n²) |
| 最坏时间复杂度 | O(n²) |
| 平均时间复杂度 | O(n²) |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
3. 冒泡排序(Bubble Sort)
3.1 算法思想
冒泡排序通过相邻元素的比较和交换,让较大的元素像气泡一样逐渐"浮"到数组末尾。每一轮结束后,当前未排序区间的最大值就位。通过设置标志位,可以在数组已经有序时提前退出,这是它相对选择排序的一个优势。
3.2 C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
void bubbleSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; ++i) {
bool swapped = false;
for (int j = 0; j < n - i - 1; ++j) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
swapped = true;
}
}
if (!swapped) break; // 提前退出
}
}
int main() {
vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
bubbleSort(arr);
for (int x : arr) cout << x << " ";
return 0;
}
3.3 复杂度分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n) |
| 最坏时间复杂度 | O(n²) |
| 平均时间复杂度 | O(n²) |
| 空间复杂度 | O(1) |
| 稳定性 | 稳定 |
4. 插入排序(Insertion Sort)
4.1 算法思想
插入排序的思想类似整理扑克牌:将数组分为已排序和未排序两部分,每次从未排序部分取出第一个元素,在已排序部分从后向前扫描,找到合适位置插入。对于近乎有序的数据,插入排序的效率非常高。
4.2 C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
void insertionSort(vector<int>& arr) {
int n = arr.size();
for (int i = 1; i < n; ++i) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
--j;
}
arr[j + 1] = key;
}
}
int main() {
vector<int> arr = {12, 11, 13, 5, 6};
insertionSort(arr);
for (int x : arr) cout << x << " ";
return 0;
}
4.3 复杂度分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n) |
| 最坏时间复杂度 | O(n²) |
| 平均时间复杂度 | O(n²) |
| 空间复杂度 | O(1) |
| 稳定性 | 稳定 |
5. 计数排序(Counting Sort)
5.1 算法思想
计数排序是一种非比较型排序算法,适用于数据范围较小且已知的整数序列。它通过统计每个元素出现的次数,再根据计数信息将元素放回正确位置。时间复杂度可以达到 O(n+k),其中 k 是数据范围。
5.2 C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
void countingSort(vector<int>& arr) {
if (arr.empty()) return;
int maxVal = *max_element(arr.begin(), arr.end());
int minVal = *min_element(arr.begin(), arr.end());
int range = maxVal - minVal + 1;
vector<int> count(range, 0);
for (int x : arr) count[x - minVal]++;
int idx = 0;
for (int i = 0; i < range; ++i) {
while (count[i] > 0) {
arr[idx++] = i + minVal;
count[i]--;
}
}
}
int main() {
vector<int> arr = {4, 2, 2, 8, 3, 3, 1};
countingSort(arr);
for (int x : arr) cout << x << " ";
return 0;
}
5.3 复杂度分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n+k) |
| 最坏时间复杂度 | O(n+k) |
| 平均时间复杂度 | O(n+k) |
| 空间复杂度 | O(k) |
| 稳定性 | 稳定 |
6. 堆排序(Heap Sort)
6.1 算法思想
堆排序利用二叉堆(通常用数组实现)的性质进行排序。首先将数组构建成一个大顶堆,此时堆顶元素就是最大值;将堆顶与末尾元素交换,然后对剩余元素重新调整堆,重复此过程即可完成排序。堆排序的时间复杂度稳定为 O(n log n)。
6.2 C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
void heapify(vector<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]);
heapify(arr, n, largest);
}
}
void heapSort(vector<int>& arr) {
int n = arr.size();
// 构建大顶堆
for (int i = n / 2 - 1; i >= 0; --i) {
heapify(arr, n, i);
}
// 逐个取出堆顶
for (int i = n - 1; i > 0; --i) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}
int main() {
vector<int> arr = {12, 11, 13, 5, 6, 7};
heapSort(arr);
for (int x : arr) cout << x << " ";
return 0;
}
6.3 复杂度分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n log n) |
| 最坏时间复杂度 | O(n log n) |
| 平均时间复杂度 | O(n log n) |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
7. 快速排序(Quick Sort)
7.1 算法思想
快速排序采用分治策略:选择一个基准元素(pivot),将数组划分为小于基准和大于基准的两部分,然后递归地对两部分进行排序。快速排序的平均性能非常优秀,是实际应用最广泛的排序算法之一。为了避免最坏情况,通常会采用随机选择基准或三数取中法。
7.2 C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; ++j) {
if (arr[j] < pivot) {
++i;
swap(arr[i], arr[j]);
}
}
swap(arr[i + 1], arr[high]);
return i + 1;
}
void quickSort(vector<int>& arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
int main() {
vector<int> arr = {10, 7, 8, 9, 1, 5};
quickSort(arr, 0, arr.size() - 1);
for (int x : arr) cout << x << " ";
return 0;
}
7.3 复杂度分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n log n) |
| 最坏时间复杂度 | O(n²) |
| 平均时间复杂度 | O(n log n) |
| 空间复杂度 | O(log n) |
| 稳定性 | 不稳定 |
8. 归并排序(Merge Sort)
8.1 算法思想
归并排序同样采用分治策略:将数组不断二分,直到每个子数组只有一个元素,然后两两合并,在合并过程中完成排序。归并排序是稳定的排序算法,时间复杂度稳定为 O(n log n),但需要额外的 O(n) 空间。
8.2 C++ 代码实现
cpp
#include <iostream>
#include <vector>
using namespace std;
void merge(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
vector<int> L(n1), R(n2);
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++];
}
void mergeSort(vector<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() {
vector<int> arr = {38, 27, 43, 3, 9, 82, 10};
mergeSort(arr, 0, arr.size() - 1);
for (int x : arr) cout << x << " ";
return 0;
}
8.3 复杂度分析
| 指标 | 值 |
|---|---|
| 最好时间复杂度 | O(n log n) |
| 最坏时间复杂度 | O(n log n) |
| 平均时间复杂度 | O(n log n) |
| 空间复杂度 | O(n) |
| 稳定性 | 稳定 |
9. 七大排序算法对比与选型建议
9.1 综合对比
| 算法 | 最好时间复杂度 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
9.2 选型建议
面对不同场景,应结合数据规模、有序程度、取值范围和稳定性要求来选择合适的排序算法。
当数据量较小时,插入排序凭借简单的实现和较低常数开销往往表现最佳;
若数据近乎有序,插入排序可达到接近 O(n) 的线性效率,是理想选择。
当数据范围有限且为整数时,计数排序能以 O(n+k) 的线性时间完成排序,远优于比较型算法。
若对稳定性有严格要求,归并排序是稳妥之选,它能在 O(n log n) 时间内保持相等元素的相对顺序,代价是需要 O(n) 的额外空间。
对于通用的大规模数据排序,快速排序平均性能最优,是实际应用最广泛的选择;
若需兼顾最坏情况下的性能保证且空间受限,堆排序的 O(n log n) 稳定上界更具优势。
10. 实战应用场景与选型建议
在实际项目中,排序算法的选择往往不是追求理论上的最优复杂度,而是结合数据规模、数据特征和稳定性要求综合权衡。下面针对每种排序算法给出典型应用场景,并说明选型思路。
10.1 各排序算法的典型应用场景
选择排序:适用于数据量很小且对稳定性没有要求的场景,例如对几十个元素进行简单排序;也常用于教学演示,帮助理解最基础的排序思想。由于比较次数固定,它不适合大规模数据。
冒泡排序:适合数据量小且近乎有序的场景,借助提前退出标志可以在有序时达到 O(n) 的线性效率;也常用于算法入门教学。在实际工程中,冒泡排序更多作为理解相邻交换思想的示例,较少直接用于生产环境。
插入排序:非常适合近乎有序的小数据集,例如在线插入维护有序列表、处理实时到达的少量数据;同时它也是快速排序等高级算法在小规模子数组上的常用优化手段,能显著降低常数开销。
计数排序:适用于数据范围有限且为整数的场景,例如对学生成绩(0-100 分)排序、统计年龄分布、对固定范围内的 ID 或状态码排序。当 k 远小于 n 时,计数排序能以 O(n+k) 的线性时间完成排序,远优于比较型算法。
堆排序:适合需要稳定 O(n log n) 最坏时间复杂度的场景,且对额外空间敏感,例如嵌入式系统或内存受限环境中的排序;也常用于实现优先队列、Top-K 问题(如从海量数据中找出最大的 K 个元素)等场景。
快速排序:是通用大规模数据排序的首选,广泛应用于标准库和数据库索引构建中,例如 C++ 的 sort 函数、文件系统排序等。配合随机基准或三数取中法,快速排序在绝大多数场景下都能发挥出色的平均性能。
归并排序:适合对稳定性有严格要求且内存充足的场景,例如对链表排序、外部排序(处理无法一次性载入内存的大文件)、多路归并等。它能在 O(n log n) 时间内保持相等元素的相对顺序,代价是需要 O(n) 的额外空间。
10.2 实际项目中的选型思路
在实际项目中,可以按照以下思路快速确定排序算法:
首先判断数据规模,若数据量很小(如几十个元素),优先选择插入排序,其实现简单且常数开销低;若数据量较大,则进入下一步。
其次分析数据特征,若数据近乎有序,插入排序可达到接近 O(n) 的效率;若数据是范围有限的整数,计数排序能以线性时间完成排序。
再次考虑稳定性要求,若业务需要保持相等元素的相对顺序(如按多个字段排序时),应优先选择归并排序或插入排序等稳定算法。
最后权衡空间约束,若内存受限且需要最坏情况下的性能保证,堆排序是稳妥之选;若内存充足且追求平均性能,快速排序通常是最佳选择。