#C++ 核心知识点汇总(第11日)(排序算法)
引言
排序是算法领域最基础且核心的问题之一,它不仅是独立的考点,更是许多复杂算法(如二分查找、贪心、动态规划)的前置步骤。
一、排序算法的核心评价指标
在学习具体算法之前,我们需要明确几个关键评价维度,这也是竞赛和面试中高频的考点:
| 指标 | 定义 | 意义 |
|---|---|---|
| 时间复杂度 | 算法执行的基本操作次数的上界(通常关注最坏情况) | 衡量算法的运行效率,是选择算法的核心依据 |
| 空间复杂度 | 算法执行过程中需要的额外内存空间 | 原地排序(空间复杂度O(1))适合内存受限场景 |
| 稳定性 | 相等元素在排序后相对顺序是否保持不变 | 处理含附加信息的元素时(如按成绩排序的学生),稳定性至关重要 |
| 适应性 | 对已部分有序的输入是否能自动优化性能 | 插入排序在输入接近有序时,时间复杂度可降至O(n) |
二、基础排序算法(时间复杂度O(n²))
这类算法逻辑简单,易于实现,但效率较低,适合小规模数据(n < 1000)或作为学习算法思想的入门案例。
1. 冒泡排序(Bubble Sort)
核心思想
- 重复地遍历待排序数组,比较相邻元素,若逆序则交换,使较大的元素像"气泡"一样逐步"上浮"到数组的末尾。
- 每一轮遍历后,当前未排序区间的最大元素会被放到正确的位置。
代码实现(C++)
cpp
void bubbleSort(vector<int>& arr) {
int n = arr.size();
for (int i = n - 1; i > 0; --i) {
bool swapped = false; // 优化:若本轮无交换,说明已排序
for (int j = 0; j < i; ++j) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
swapped = true;
}
}
if (!swapped) break;
}
}
关键特性
- 时间复杂度 :
O(n²)(最坏、平均),O(n)(最好,输入已完全有序) - 空间复杂度 :
O(1)(原地排序) - 稳定性:稳定(相等元素不会交换位置)
真题解析
对数组 {5, 3, 8, 1} 执行第一轮冒泡排序:
- 比较
5和3→ 交换 →{3, 5, 8, 1} - 比较
5和8→ 不交换 - 比较
8和1→ 交换 →{3, 5, 1, 8}
第一轮结束后最大元素8已沉底,最终数组为:{3, 5, 1, 8}
2. 插入排序(Insertion Sort)
核心思想
- 将数组分为"已排序"和"未排序"两部分,每次从未排序部分取出第一个元素,插入到已排序部分的合适位置,直到所有元素都被插入。
- 类似整理扑克牌的过程:将每一张牌插入到前面已整理好的牌堆中。
代码实现(C++)
cpp
void insertionSort(vector<int>& arr) {
int n = arr.size();
for (int i = 1; i < n; ++i) {
int base = arr[i]; // 当前待插入的元素
int j = i - 1;
// 在已排序区间中找到插入位置
while (j >= 0 && arr[j] > base) {
arr[j + 1] = arr[j]; // 元素后移
j--;
}
arr[j + 1] = base; // 插入元素
}
}
关键特性
- 时间复杂度 :
O(n²)(最坏、平均),O(n)(最好,输入已完全有序) - 空间复杂度 :
O(1)(原地排序) - 稳定性:稳定(插入时不会改变相等元素的相对顺序)
3. 选择排序(Selection Sort)
核心思想
- 每次从未排序区间中找到最小(或最大)的元素,将其与未排序区间的第一个元素交换位置,逐步扩大已排序区间。
代码实现(C++)
cpp
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;
}
}
swap(arr[i], arr[minIdx]);
}
}
关键特性
- 时间复杂度 :
O(n²)(所有情况,因为无论是否有序都要遍历找最小值) - 空间复杂度 :
O(1)(原地排序) - 稳定性:不稳定(交换操作可能破坏相等元素的相对顺序)
三、高效排序算法(时间复杂度O(n log n))
这类算法是大规模数据排序的首选,通过分治思想或堆结构实现了时间复杂度的突破,也是竞赛和面试的重点。
1. 快速排序(Quick Sort)
核心思想
- 分治思想:选择一个基准元素(Pivot),将数组划分为两部分,左边元素都小于等于基准,右边元素都大于等于基准,然后递归地对左右两部分进行排序。
代码实现(C++)
cpp
// 分区函数:返回基准元素最终的位置
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); // 递归排序右半部分
}
}
关键特性
- 时间复杂度 :
O(n log n)(平均),O(n²)(最坏,如输入已完全有序且选择首尾元素作为基准) - 空间复杂度 :
O(log n)(递归栈深度,平均),O(n)(最坏) - 稳定性:不稳定(交换操作可能破坏相等元素的相对顺序)
优化技巧
- 基准选择:避免选择首尾元素,可采用随机选择、三数取中等策略。
- 尾递归优化:对较长的子数组先递归,减少递归栈深度。
- 小数组切换 :当子数组长度较小时(如
n < 20),切换为插入排序。
2. 归并排序(Merge Sort)
核心思想
- 分治思想:将数组不断地二分,直到每个子数组只有一个元素(天然有序),然后将两个有序的子数组合并成一个更大的有序数组,最终得到完全有序的数组。
代码实现(C++)
cpp
// 合并两个有序数组
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);
}
}
关键特性
- 时间复杂度 :
O(n log n)(所有情况,因为二分和合并的时间复杂度都是稳定的) - 空间复杂度 :
O(n)(需要额外的临时数组存储合并结果) - 稳定性:稳定(合并时遇到相等元素,优先选择左半部分的元素,保持相对顺序)
四、STL 排序工具:std::sort()
在C++开发和竞赛中,我们几乎不需要手动实现排序算法,因为STL提供了高效的std::sort()函数,它是你处理排序问题的首选。
1. 核心特性
- 底层实现 :通常采用快速排序 的优化版本(如IntroSort),当递归深度过大时自动切换为堆排序,保证最坏时间复杂度为
O(n log n)。 - 时间复杂度 :
O(n log n)(平均和最坏) - 空间复杂度 :
O(log n) - 稳定性:不稳定
2. 基本用法
cpp
#include <algorithm>
#include <vector>
int main() {
vector<int> arr = {5, 2, 9, 1, 5, 6};
// 默认升序排序
sort(arr.begin(), arr.end());
// 降序排序
sort(arr.begin(), arr.end(), greater<int>());
// 自定义比较函数(例如:偶数在前,奇数在后)
sort(arr.begin(), arr.end(), [](int a, int b) {
return (a % 2 == 0) && (b % 2 != 0);
});
return 0;
}
五、常见排序算法对比表
| 算法 | 时间复杂度(平均) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) |
O(n²) |
O(1) |
稳定 | 小规模数据、教学演示 |
| 插入排序 | O(n²) |
O(n²) |
O(1) |
稳定 | 小规模数据、输入接近有序 |
| 选择排序 | O(n²) |
O(n²) |
O(1) |
不稳定 | 小规模数据、不要求稳定性 |
| 快速排序 | O(n log n) |
O(n²) |
O(log n) |
不稳定 | 大规模数据、通用场景 |
| 归并排序 | O(n log n) |
O(n log n) |
O(n) |
稳定 | 大规模数据、要求稳定性 |
STL sort() |
O(n log n) |
O(n log n) |
O(log n) |
不稳定 | 工程开发、算法竞赛首选 |
六、高频真题解析(必背)
真题1:快速排序分区函数
在快速排序的分区函数中,横线处应填入:
cpp
if (arr[j] < pivot) {
i++;
swap(arr[i], arr[j]);
}
解析:此逻辑将小于基准的元素交换到左半区,大于等于基准的元素留在右半区,保证分区的正确性。
真题2:归并排序的归并次数
对长度为n的数组,归并排序的merge函数调用次数约为O(n)。
解析 :归并排序的每一层递归都会合并n个元素,总共有log n层,因此总合并次数约为n log n / n = log n次,但合并操作的总元素处理量为O(n log n)。
真题3:稳定性判断
- 稳定:冒泡排序、插入排序、归并排序
- 不稳定:选择排序、快速排序、堆排序
七、总结与学习建议
- 基础优先:先掌握冒泡、插入、选择排序的原理和实现,理解时间复杂度、空间复杂度和稳定性的概念。
- 重点突破:深入理解快速排序和归并排序的分治思想,这是算法学习的基石,也是面试和竞赛的高频考点。
- 实战为王 :在实际开发和竞赛中,优先使用
std::sort(),但要理解其底层原理和局限性。 - 对比分析:学会根据问题的规模、对稳定性的要求和内存限制,选择最合适的排序算法。