C++算法详解 - 模块三:排序与离散化
文章目录
- [C++算法详解 - 模块三:排序与离散化](#C++算法详解 - 模块三:排序与离散化)
-
- [🎯 模块目标](#🎯 模块目标)
- [📚 核心内容](#📚 核心内容)
-
- 排序算法全面对比表
- 第一部分:比较排序算法
-
- [3.1 基础排序算法:理解排序思想](#3.1 基础排序算法:理解排序思想)
-
- [1. 冒泡排序 --- O ( n 2 ) O(n²) O(n2) 稳定](#1. 冒泡排序 --- O ( n 2 ) O(n²) O(n2) 稳定)
- [2. 选择排序 --- O ( n 2 ) O(n²) O(n2) 不稳定](#2. 选择排序 --- O ( n 2 ) O(n²) O(n2) 不稳定)
- [3. 插入排序 - O(n²) 稳定,对几乎有序数据高效](#3. 插入排序 - O(n²) 稳定,对几乎有序数据高效)
- [3.2 希尔排序:插入排序的改进](#3.2 希尔排序:插入排序的改进)
- [3.3 堆排序:利用完全二叉树](#3.3 堆排序:利用完全二叉树)
- [3.4快速排序: 分治与排序的结合](#3.4快速排序: 分治与排序的结合)
- 3.5归并排序:另一种分治于排序中的策略
- 第二部分:非比较排序
-
- [3.4 计数排序:适用于有限范围整数](#3.4 计数排序:适用于有限范围整数)
- [3.5 桶排序:均匀分布数据的高效排序](#3.5 桶排序:均匀分布数据的高效排序)
- [3.6 基数排序:多关键字排序](#3.6 基数排序:多关键字排序)
- 第三部分:排序算法应用
-
- [3.7 第K大/小元素问题](#3.7 第K大/小元素问题)
- [3.8 区间合并问题](#3.8 区间合并问题)
- 第四部分:离散化技术
-
- [3.9 有序离散化:将稀疏数据映射到密集索引](#3.9 有序离散化:将稀疏数据映射到密集索引)
- [3.10 离散化应用场景](#3.10 离散化应用场景)
- [🎯 学习要点总结](#🎯 学习要点总结)
- [⚠️ 常见问题与解决方案](#⚠️ 常见问题与解决方案)
- [🚀 实践路径建议](#🚀 实践路径建议)
🎯 模块目标
掌握数据预处理的核心技术,理解不同排序算法的原理与适用场景,学会离散化方法处理大规模稀疏数据。
📚 核心内容
排序算法全面对比表
| 算法 | 类型 | 平均时间复杂度 | 最坏时间复杂度 | 最好时间复杂度 | 空间复杂度 | 稳定性 | 原地排序 | 核心思想 | 适用场景 |
|---|---|---|---|---|---|---|---|---|---|
| 冒泡排序 | 比较排序 | O ( n 2 ) O(n²) O(n2) | O ( n 2 ) O(n²) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 | 是 | 相邻元素比较交换,最大元素"冒泡"到末尾 | 教学演示,小规模数据 ( n ≤ 50 ) (n ≤ 50) (n≤50) |
| 选择排序 | 比较排序 | O ( n 2 ) O(n²) O(n2) | O ( n 2 ) O(n²) O(n2) | O ( n 2 ) O(n²) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 | 是 | 每次选择最小/最大元素放到已排序末尾 | 交换次数最少的情况,小规模数据 |
| 插入排序 | 比较排序 | O ( n 2 ) O(n²) O(n2) | O ( n 2 ) O(n²) O(n2) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 | 是 | 构建有序序列,逐个插入未排序元素 | 小规模或几乎有序数据 |
| 希尔排序 | 比较排序 | O ( n l o g 2 n ) − O ( n 2 ) O(n log_2 n) -O(n²) O(nlog2n)−O(n2) | O ( n 2 ) O(n²) O(n2) | O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) | O ( 1 ) O(1) O(1) | 不稳定 | 是 | 改进的插入排序,按间隔分组排序 | 中等规模数据,对缓存友好 |
| 堆排序 | 比较排序 | O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) | O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) | O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) | O ( 1 ) O(1) O(1) | 不稳定 | 是 | 构建二叉堆,反复提取最大/最小元素 | 需要原地排序且保证 O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) |
| 归并排序 | 比较排序 | O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) | O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) | O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) | O ( n ) O(n) O(n) | 稳定 | 否 | 分治法,递归排序后合并有序数组 | 需要稳定性,链表排序,外部排序 |
| 快速排序 | 比较排序 | O ( n l o g 2 n ) O(n log_2n) O(nlog2n) | O ( n 2 ) O(n²) O(n2) | O ( n l o g 2 n ) O(n log_2 n) O(nlog2n) | O ( l o g 2 n ) − O ( n ) O(log_2 n) - O(n) O(log2n)−O(n) | 不稳定 | 是 | 分治法,选择基准分区,递归排序 | 通用排序,实际性能最好 |
| 计数排序 | 非比较排序 | O ( n + k ) O(n + k) O(n+k) | O ( n + k ) O(n + k) O(n+k) | O ( n + k ) O(n + k) O(n+k) | O ( n + k ) O(n + k) O(n+k) | 稳定 | 否 | 统计每个值出现次数,累加确定位置 | 整数,值范围 k k k较小 ( k ≤ 5 n ) (k ≤ 5n) (k≤5n) |
| 桶排序 | 非比较排序 | O ( n + k ) O(n + k) O(n+k) | O ( n 2 ) O(n²) O(n2) | O ( n ) O(n) O(n) | O ( n + k ) O(n + k) O(n+k) | 稳定(取决于内排序) | 否 | 将数据分到有限数量的桶,分别排序 | 数据均匀分布,浮点数排序 |
| 基数排序 | 非比较排序 | O ( d ( n + k ) ) O(d(n + k)) O(d(n+k)) | O ( d ( n + k ) ) O(d(n + k)) O(d(n+k)) | O ( d ( n + k ) ) O(d(n + k)) O(d(n+k)) | O ( n + k ) O(n + k) O(n+k) | 稳定 | 否 | 按位排序,从最低位到最高位 | 整数、字符串等多关键字排序 |
第一部分:比较排序算法
3.1 基础排序算法:理解排序思想
虽然这些算法在实际中很少直接使用,但它们是理解排序思想的基础:
1. 冒泡排序 --- O ( n 2 ) O(n²) O(n2) 稳定
思路:
1.比较相邻的元素。如果第一个比第二个大,就交换它们两个;
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素就是最大的数;
3.排除最大的数,接着下一轮继续相同的操作,确定第二大的数...
4.重复步骤1-3,直到排序完成。
动画演示:

实现代码:
cpp
void bubbleSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) {
// 每次冒泡将最大元素"浮"到最后
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]); // 相邻元素比较交换
}
}
// 第i次循环后,arr[n-i-1...n-1]已有序
}
}
2. 选择排序 --- O ( n 2 ) O(n²) O(n2) 不稳定
思路:
1.第一轮,找到最小的元素,和数组第一个数交换位置。
2.第二轮,找到第二小的元素,和数组第二个数交换位置...
3.直到最后一个元素,排序完成。
动画演示:

实现代码:
cpp
void selectionSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
// 找到[i+1, n-1]中的最小值
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
swap(arr[i], arr[minIdx]); // 将最小值放到位置i
}
}
3. 插入排序 - O(n²) 稳定,对几乎有序数据高效
思路:
1.从第一个元素开始,该元素可以认为已经被排序;
2.取出下一个元素,在前面已排序的元素序列中,从后向前扫描;
3.如果该元素(已排序)大于新元素,将该元素移到下一位置;
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5.将新元素插入到该位置后;
6.重复步骤2~5。
动画演示:

实现代码:
cpp
void insertionSort(vector<int>& arr) {
int n = arr.size();
for (int i = 1; i < n; i++) {
int key = arr[i]; // 当前要插入的元素
int j = i - 1;
// 将比key大的元素后移
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key; // 插入key到正确位置
}
}
算法对比分析:
| 算法 | 最好情况 | 平均情况 | 最坏情况 | 空间 | 稳定 | 特点 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n²) O(n2) | O ( n 2 ) O(n²) O(n2) | O ( 1 ) O(1) O(1) | 是 | 简单,适合教学 |
| 选择排序 | O ( n 2 ) O(n²) O(n2) | O ( n 2 ) O(n²) O(n2) | O ( n 2 ) O(n²) O(n2) | O ( 1 ) O(1) O(1) | 否 | 交换次数最少 |
| 插入排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n²) O(n2) | O ( n 2 ) O(n²) O(n2) | O ( 1 ) O(1) O(1) | 是 | 几乎有序时高效 |
3.2 希尔排序:插入排序的改进
希尔排序通过分组插入排序 来提升效率:
思路:
把数组分割成若干( h h h)个小组(一般数组长度 l e n g t h 2 \frac{length}{2} 2length),然后对每一个小组分别进行插入排序。每一轮分割的数组的个数逐步缩小, h 2 − > h 4 − > h 8 \frac{h}{2}->\frac{h}{4} ->\frac{h}{8} 2h−>4h−>8h ,并且进行排序,保证有序。当 h = 1 h=1 h=1时,则数组排序完成。
动画演示:

实现代码:
cpp
void shellSort(vector<int>& arr) {
int n = arr.size();
// 使用Knuth序列生成间隔:h = 3*h + 1
int h = 1;
while (h < n / 3) {
h = 3 * h + 1; // 1, 4, 13, 40, 121...
}
while (h >= 1) {
// 对每个间隔h进行插入排序
for (int i = h; i < n; i++) {
// 将arr[i]插入到arr[i-h], arr[i-2h], ...中
int key = arr[i];
int j = i;
while (j >= h && arr[j - h] > key) {
arr[j] = arr[j - h];
j -= h;
}
arr[j] = key;
}
h /= 3; // 缩小间隔
}
}
希尔排序特点:
- 时间复杂度:取决于间隔序列,最好可达O(n log² n)
- 不稳定排序:相同元素可能改变相对顺序
- 原地排序:空间复杂度O(1)
3.3 堆排序:利用完全二叉树
堆排序基于二叉堆 数据结构,是高效的原地排序算法:
大顶堆:
每个节点的值都大于或者等于它的左右子节点的值,所以顶点的数就是最大值。
思路:
1.对原数组构建成大顶堆。
2.交换头尾值,尾指针索引减一,固定最大值。
3.重新构建大顶堆。
4.重复步骤2~3,直到最后一个元素,排序完成。 注:构建大顶堆的思路见代码注释。
动画演示:

实现代码:
cpp
// 调整堆,使以i为根的子树满足堆性质
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();
// 步骤1:构建最大堆(从最后一个非叶子节点开始)
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 步骤2:逐个提取最大元素
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]); // 将当前最大值移到末尾
heapify(arr, i, 0); // 调整剩余堆
}
}
优先队列应用:
cpp
// 使用STL优先队列(默认最大堆)
priority_queue<int> maxHeap; // 最大堆
priority_queue<int, vector<int>, greater<int>> minHeap; // 最小堆
// 处理数据流中的Top K问题
vector<int> getTopK(vector<int>& nums, int k) {
// 使用最小堆保持最大的k个元素
priority_queue<int, vector<int>, greater<int>> minHeap;
for (int num : nums) {
minHeap.push(num);
if (minHeap.size() > k) {
minHeap.pop(); // 移除最小的,保持堆中只有k个元素
}
}
// 提取结果
vector<int> result;
while (!minHeap.empty()) {
result.push_back(minHeap.top());
minHeap.pop();
}
return result;
}
3.4快速排序: 分治与排序的结合
快排 ,最常用的排序算法之一。是运用分治法的一种排序算法。
思路:
1.从数组中选一个数做为基准值,一般选第一个数,或者最后一个数。
2.采用双指针(头尾两端)遍历,从左往右找到比基准值大的第一个数,从右往左找到比基准值小的第一个数,交换两数位置,直到头尾指针相等或头指针大于尾指针,把基准值与头指针的数交换。这样一轮之后,左边的数就比基准值小,右边的数就比基准值大。
3.对左边的数列,重复上面1,2步骤。对右边重复1,2步骤。
4.左右两边数列递归结束后,排序完成。
动画演示:

基础实现
cpp
// 分区函数:将数组分为小于pivot和大于pivot的两部分
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为pivot
int i = low - 1; // 小于pivot的元素边界
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
swap(arr[i], arr[j]); // 将小于pivot的元素移到左边
}
}
// 将pivot放到正确位置
swap(arr[i + 1], arr[high]);
return i + 1;
}
void quickSort(vector<int>& arr, int low, int high) {
if (low < high) {
// 分区操作,pivotIndex已在正确位置
int pivotIndex = partition(arr, low, high);
// 递归排序左右子数组
quickSort(arr, low, pivotIndex - 1); // 左半部分
quickSort(arr, pivotIndex + 1, high); // 右半部分
}
}
2. 优化版本:防止最坏情况
cpp
// 优化1:三数取中法选择pivot
int medianOfThree(vector<int>& arr, int low, int high) {
int mid = low + (high - low) / 2;
// 对arr[low], arr[mid], arr[high]排序
if (arr[low] > arr[mid]) swap(arr[low], arr[mid]);
if (arr[low] > arr[high]) swap(arr[low], arr[high]);
if (arr[mid] > arr[high]) swap(arr[mid], arr[high]);
// 返回中位数的索引
return mid;
}
// 优化2:三路快速排序(处理大量重复元素)
void quickSort3Way(vector<int>& arr, int low, int high) {
if (low >= high) return;
// 随机选择pivot
int pivotIdx = low + rand() % (high - low + 1);
swap(arr[low], arr[pivotIdx]);
int pivot = arr[low];
// 三路分区:lt - 小于pivot的边界,gt - 大于pivot的边界
int lt = low; // arr[low+1..lt] < pivot
int gt = high; // arr[gt..high] > pivot
int i = low + 1; // arr[lt+1..i-1] == pivot
while (i <= gt) {
if (arr[i] < pivot) {
swap(arr[lt++], arr[i++]);
} else if (arr[i] > pivot) {
swap(arr[i], arr[gt--]);
} else {
i++; // 等于pivot,继续前进
}
}
// 现在:arr[low..lt-1] < pivot,arr[lt..gt] == pivot,arr[gt+1..high] > pivot
quickSort3Way(arr, low, lt - 1);
quickSort3Way(arr, gt + 1, high);
}
3. 迭代版本:避免递归栈溢出
cpp
void quickSortIterative(vector<int>& arr) {
int n = arr.size();
if (n <= 1) return;
// 使用栈模拟递归调用
stack<pair<int, int>> stk;
stk.push({0, n - 1});
while (!stk.empty()) {
auto [low, high] = stk.top();
stk.pop();
if (low >= high) continue;
int pivotIndex = partition(arr, low, high);
// 将子数组范围入栈(先处理较小的子数组,减少栈深度)
if (pivotIndex - low < high - pivotIndex) {
stk.push({low, pivotIndex - 1});
stk.push({pivotIndex + 1, high});
} else {
stk.push({pivotIndex + 1, high});
stk.push({low, pivotIndex - 1});
}
}
}
3.5归并排序:另一种分治于排序中的策略
归并排序是采用分治法的典型应用,而且是一种稳定的排序方式,不过需要使用到额外的空间。
思路:
1.把数组不断划分成子序列,划成长度只有2或者1的子序列。
2.然后利用临时数组,对子序列进行排序,合并,再把临时数组的值复制回原数组。
3.反复操作1~2步骤,直到排序完成。
归并排序的优点在于最好情况和最坏的情况的时间复杂度都是O(nlogn),所以是比较稳定的排序方式。
动画演示:

1. 标准实现
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);
// 拷贝数据到临时数组
copy(arr.begin() + left, arr.begin() + mid + 1, L.begin());
copy(arr.begin() + mid + 1, arr.begin() + right + 1, R.begin());
// 合并两个有序数组
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) return; // 基本情况:单个元素
int mid = left + (right - left) / 2;
// 递归排序左右两部分
mergeSort(arr, left, mid); // 排序左半部分
mergeSort(arr, mid + 1, right); // 排序右半部分
// 合并已排序的两部分
merge(arr, left, mid, right);
}
2. 优化版本
cpp
// 优化1:小数组使用插入排序
void insertionSort(vector<int>& arr, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int key = arr[i];
int j = i - 1;
while (j >= left && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
void optimizedMergeSort(vector<int>& arr, int left, int right) {
const int INSERTION_THRESHOLD = 16;
// 小数组使用插入排序
if (right - left + 1 <= INSERTION_THRESHOLD) {
insertionSort(arr, left, right);
return;
}
int mid = left + (right - left) / 2;
optimizedMergeSort(arr, left, mid);
optimizedMergeSort(arr, mid + 1, right);
// 如果已经有序,无需合并
if (arr[mid] <= arr[mid + 1]) return;
merge(arr, left, mid, right);
}
// 优化2:自底向上的归并排序(迭代版本)
void mergeSortBottomUp(vector<int>& arr) {
int n = arr.size();
vector<int> temp(n);
// 从大小为1的子数组开始,每次翻倍
for (int size = 1; size < n; size *= 2) {
for (int left = 0; left < n - size; left += 2 * size) {
int mid = left + size - 1;
int right = min(left + 2 * size - 1, n - 1);
// 合并 arr[left..mid] 和 arr[mid+1..right]
if (arr[mid] > arr[mid + 1]) {
// 使用临时数组合并
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right) {
temp[k++] = (arr[i] <= arr[j]) ? arr[i++] : arr[j++];
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
// 拷贝回原数组
copy(temp.begin() + left, temp.begin() + right + 1,
arr.begin() + left);
}
}
}
}
第二部分:非比较排序
3.4 计数排序:适用于有限范围整数
计数排序不是基于比较 ,而是通过计数实现排序:
思路:
1.整个序列 A A A,获取最小值 m i n min min 和最大值 m a x max max
2.开辟一块新的空间创建新的数组 B B B,长度为 ( m a x − m i n + 1 ) ( max - min + 1) (max−min+1) 。数组 B B B 中 i n d e x index index 的元素记录的值是 A A A 中某元素出现的次数
4.最后输出目标整数序列:遍历数组 B B B,输出相应元素以及对应的个数
动画演示(来源于五分钟学算法,侵删):

实现代码:
cpp
void countingSort(vector<int>& arr) {
if (arr.empty()) return;
// 步骤1:找到最大值和最小值
int maxVal = *max_element(arr.begin(), arr.end());
int minVal = *min_element(arr.begin(), arr.end());
int range = maxVal - minVal + 1;
// 步骤2:创建计数数组并统计频率
vector<int> count(range, 0);
for (int num : arr) {
count[num - minVal]++; // 偏移,支持负数
}
// 步骤3:累加计数(计算每个元素的最终位置)
for (int i = 1; i < range; i++) {
count[i] += count[i - 1];
}
// 步骤4:反向填充结果数组(保证稳定性)
vector<int> output(arr.size());
for (int i = arr.size() - 1; i >= 0; i--) {
int index = arr[i] - minVal;
output[count[index] - 1] = arr[i];
count[index]--;
}
// 步骤5:拷贝回原数组
arr = output;
}
计数排序特点:
- 时间复杂度:O(n + k),k为数据范围
- 空间复杂度:O(n + k)
- 稳定性:稳定排序
- 限制条件:数据必须为整数,且范围不能太大
3.5 桶排序:均匀分布数据的高效排序
桶排序假设数据均匀分布 在一定范围内:
思路:
1.找出最大值,最小值。
2.根据数组的长度,创建出若干个桶。
3.遍历数组的元素,根据元素的值放入到对应的桶中。
4.对每个桶的元素进行排序(可使用快排,插入排序等)。
5.按顺序合并每个桶的元素,排序完成。
为什么是均匀分布数据的高效排序:
对于数组中的元素分布均匀的情况,数据较均匀在多个桶中分布,排序效率较高。相反的,如果分布不均匀,则会导致大部分的数落入到同一个桶中,使效率降低。
动画演示(来源于五分钟学算法,侵删):

实现代码
cpp
void bucketSort(vector<float>& arr) {
int n = arr.size();
if (n <= 1) return;
// 步骤1:创建n个空桶
vector<vector<float>> buckets(n);
// 步骤2:将元素放入对应桶中
for (float num : arr) {
int bucketIndex = n * num; // 假设数据在[0,1)范围内
buckets[bucketIndex].push_back(num);
}
// 步骤3:对每个桶内部排序(使用插入排序)
for (auto& bucket : buckets) {
sort(bucket.begin(), bucket.end()); // 或其他排序算法
}
// 步骤4:合并所有桶
int index = 0;
for (const auto& bucket : buckets) {
for (float num : bucket) {
arr[index++] = num;
}
}
}
桶排序适用场景:
- 数据均匀分布在某个范围内
- 数据量较大
- 需要稳定排序
3.6 基数排序:多关键字排序
基数排序按照从低位到高位 的顺序逐位排序:
思路
1.将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零
2.从最低位开始,依次进行一次排序
3.从最低位排序一直到最高位
4.排序完成以后, 数列就变成一个有序序列
动画演示(来源于五分钟学算法,侵删):

实现代码
cpp
// 获取数字的第d位(从个位开始为第1位)
int getDigit(int num, int d) {
for (int i = 1; i < d; i++) {
num /= 10;
}
return num % 10;
}
void radixSort(vector<int>& arr) {
if (arr.empty()) return;
// 找到最大数确定位数
int maxVal = *max_element(arr.begin(), arr.end());
int maxDigits = to_string(maxVal).length();
// 从个位开始,逐位进行计数排序
for (int digit = 1; digit <= maxDigits; digit++) {
// 使用计数排序对当前位排序
vector<int> count(10, 0);
vector<int> output(arr.size());
// 统计频率
for (int num : arr) {
int d = getDigit(num, digit);
count[d]++;
}
// 计算累积频率
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 反向填充(保证稳定性)
for (int i = arr.size() - 1; i >= 0; i--) {
int d = getDigit(arr[i], digit);
output[count[d] - 1] = arr[i];
count[d]--;
}
// 更新数组
arr = output;
}
}
基数排序特点:
- 时间复杂度:O(d(n + k)),d为位数,k为基数(通常10)
- 稳定排序:相同元素保持原顺序
- 适用场景:整数或字符串排序,位数不多的情况
第三部分:排序算法应用
3.7 第K大/小元素问题
快速选择算法是解决Top K问题的高效方法:
cpp
int partition(vector<int>& arr, int left, int right) {
int pivot = arr[right];
int i = left;
for (int j = left; j < right; j++) {
if (arr[j] <= pivot) {
swap(arr[i], arr[j]);
i++;
}
}
swap(arr[i], arr[right]);
return i;
}
// 快速选择:找到第k小的元素(k从0开始)
int quickSelect(vector<int>& arr, int left, int right, int k) {
if (left == right) return arr[left];
int pivotIndex = partition(arr, left, right);
if (k == pivotIndex) {
return arr[k]; // 找到目标
} else if (k < pivotIndex) {
return quickSelect(arr, left, pivotIndex - 1, k); // 在左边找
} else {
return quickSelect(arr, pivotIndex + 1, right, k); // 在右边找
}
}
// 使用示例:找到第k大的元素
int findKthLargest(vector<int>& nums, int k) {
// 第k大 = 第(n-k)小(索引从0开始)
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
快速选择复杂度:
- 平均情况:O(n)
- 最坏情况:O(n²)(可以通过随机化pivot避免)
3.8 区间合并问题
排序是解决区间问题的关键预处理步骤:
cpp
vector<vector<int>> mergeIntervals(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
// 步骤1:按区间起点排序
sort(intervals.begin(), intervals.end(),
[](const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
});
// 步骤2:合并重叠区间
vector<vector<int>> merged;
merged.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
vector<int>& last = merged.back();
vector<int>& current = intervals[i];
if (current[0] <= last[1]) { // 有重叠
last[1] = max(last[1], current[1]); // 合并
} else {
merged.push_back(current); // 新区间
}
}
return merged;
}
第四部分:离散化技术
离散化是程序设计中通过映射无限空间的有限个体至有限空间以提高算法效率的技术,其核心在保持数据相对大小的前提下压缩规模,应用于坐标处理、大数据缩减等领域。该技术通过排序、去重、索引实现数据压缩,常用
STL算法的lower_bound和upper_bound函数操作,也可采用分桶法(等频、等宽或人工指定)进行数值转换
通俗理解"离散化",就是把杂乱无章的大范围数据进行精简压缩,使其变得规整易处理,同时保持数据间的相对大小关系不变。
举个例子:某班30名学生的月考成绩分布在0-100分之间。如果直接统计,需要创建100个存储单元。但经过离散化处理后,仅需30个单元就能完成统计。
离散化的具体操作步骤
以学生成绩为例:
- 提取所有不重复的分数:
cpp
[12, 34, 45, 55, 67, 76, 89, 98]
- 为每个分数分配唯一编号:
cpp
12 → 1
34 → 2
45 → 3
55 → 4
67 → 5
76 → 6
89 → 7
98 → 8
实际应用时:
- 只需用长度为8的数组统计各编号出现次数
- 最后通过映射关系还原为原始分数
核心优势:
- 大幅压缩数据范围
- 完美保持原始数据的大小顺序
- 显著提升后续统计、查询等操作的效率
3.9 有序离散化:将稀疏数据映射到密集索引
离散化是处理值域很大但数量不多的数据的关键技术:
1.基本离散化:将数值映射到排名(从0开始)
cpp
vector<int> discretize(vector<int>& nums) {
// 步骤1:去重排序
vector<int> sorted = nums;
sort(sorted.begin(), sorted.end());
sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
// 步骤2:建立映射
vector<int> result(nums.size());
for (int i = 0; i < nums.size(); i++) {
// 使用二分查找确定排名
result[i] = lower_bound(sorted.begin(), sorted.end(), nums[i])
- sorted.begin();
}
return result;
}
- 带权离散化:保留原值信息的版本
cpp
struct DiscretizedValue {
int index; // 离散化后的索引
int original; // 原始值
int count; // 该值出现次数
};
vector<DiscretizedValue> discretizeWithInfo(vector<int>& nums) {
// 步骤1:排序并统计频率
sort(nums.begin(), nums.end());
vector<DiscretizedValue> result;
int current = nums[0];
int count = 1;
int index = 0;
// 步骤2:遍历并创建离散化信息
for (int i = 1; i <= nums.size(); i++) {
if (i == nums.size() || nums[i] != current) {
result.push_back({index++, current, count});
if (i < nums.size()) {
current = nums[i];
count = 1;
}
} else {
count++;
}
}
return result;
}
3.10 离散化应用场景
场景1:区间统计
cpp
// 离散化后使用树状数组进行区间统计
class FenwickTree {
vector<int> tree;
public:
FenwickTree(int n) : tree(n + 1, 0) {}
void update(int idx, int delta) {
for (int i = idx + 1; i < tree.size(); i += i & -i) {
tree[i] += delta;
}
}
int query(int idx) {
int sum = 0;
for (int i = idx + 1; i > 0; i -= i & -i) {
sum += tree[i];
}
return sum;
}
};
// 离散化后统计区间和
vector<int> rangeSumQueries(vector<int>& nums, vector<pair<int, int>>& queries) {
// 离散化所有出现的数值
vector<int> allValues = nums;
for (auto& q : queries) {
allValues.push_back(q.first);
allValues.push_back(q.second);
}
vector<int> discretized = discretize(allValues);
// 使用树状数组
FenwickTree ft(discretized.size());
vector<int> result;
for (auto& q : queries) {
// 将原始值转换为离散化索引
int leftIdx = lower_bound(discretized.begin(), discretized.end(), q.first)
- discretized.begin();
int rightIdx = lower_bound(discretized.begin(), discretized.end(), q.second)
- discretized.begin();
// 查询区间和
int sum = ft.query(rightIdx) - ft.query(leftIdx - 1);
result.push_back(sum);
}
return result;
}
场景2:坐标压缩
cpp
// 处理二维平面上的点,压缩坐标范围
struct Point {
int x, y;
int compressedX, compressedY;
};
vector<Point> compressCoordinates(vector<Point>& points) {
// 分别收集x和y坐标
vector<int> xs, ys;
for (auto& p : points) {
xs.push_back(p.x);
ys.push_back(p.y);
}
// 分别离散化
vector<int> compressedX = discretize(xs);
vector<int> compressedY = discretize(ys);
// 更新点的坐标
for (int i = 0; i < points.size(); i++) {
points[i].compressedX = compressedX[i];
points[i].compressedY = compressedY[i];
}
return points;
}
🎯 学习要点总结
排序算法选择指南
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 小规模数据(n ≤ 50) | 插入排序 | 实现简单,常数因子小 |
| 几乎有序数据 | 插入排序 | 接近O(n)性能 |
| 需要稳定排序 | 归并排序、插入排序 | 保持相同元素顺序 |
| 内存有限 | 堆排序、快速排序 | 原地排序 |
| 整数数据,范围小 | 计数排序 | O(n + k)线性时间 |
| 多关键字排序 | 基数排序 | 逐位稳定排序 |
| 通用排序 | 快速排序、归并排序 | 平均O(n log n) |
离散化技巧总结
- 去重排序 :使用
sort+unique组合 - 二分查找映射 :
lower_bound快速查找索引 - 离线处理:一次性收集所有值进行离散化
- 保持映射关系:可能需要双向映射(原始值↔离散值)
性能优化要点
cpp
// 优化1:内联比较函数
sort(arr.begin(), arr.end(), [](int a, int b) {
return a > b; // 降序排序
});
// 优化2:使用移动语义减少拷贝
vector<int> optimizedSort(vector<int>&& arr) {
sort(arr.begin(), arr.end());
return arr; // RVO优化
}
// 优化3:部分排序
partial_sort(arr.begin(), arr.begin() + k, arr.end()); // 只排前k个
// 优化4:第n个元素定位
nth_element(arr.begin(), arr.begin() + k, arr.end()); // 第k小元素放在位置k
⚠️ 常见问题与解决方案
排序相关问题
- 稳定性要求:选择归并、插入等稳定排序算法
- 内存限制:使用原地排序算法(堆排序、快速排序)
- 浮点数排序:注意精度问题,避免使用==比较
- 自定义类型排序:重载<运算符或提供比较函数
离散化注意事项
- 二分查找边界:确保使用正确的查找函数(lower_bound/upper_bound)
- 去重处理:离散化前一定要去重
- 映射一致性:查询和更新使用相同的离散化映射
- 值域考虑:考虑是否需要处理负数或大数值
🚀 实践路径建议
学习路线
- 理解原理:手动模拟每种排序算法的执行过程
- 实现算法:亲手实现冒泡、插入、选择、归并、快速、堆排序
- 分析比较:对不同规模、不同分布数据测试各算法性能
- 应用实践:用排序解决第K大、区间合并等问题
- 掌握离散化:在树状数组、线段树等问题中应用离散化
练习题目推荐
基础:
中等:
- P1152 欢乐的跳
- P1104 生日
- P1093 [NOIP 2007 普及组] 奖学金
- P1068 [NOIP 2009 普及组] 分数线划定
- P1116 车厢重组
- P2676 [USACO07DEC] Bookshelf B
- P1923 【深基9.例4】求第 k 小的数
进阶:
模块三核心价值 :排序是算法的"基本功",离散化是处理大规模稀疏数据的"利器"。掌握这些技术,你将能高效处理各种数据预处理任务,为后续复杂算法打下坚实基础。记住:没有最好的排序算法,只有最适合场景的算法。