Appearance
8. 线性时间排序
任何比较排序算法(如堆排序、快排、归并排序)在最坏情况下的下界都是
8.1 计数排序
核心思想:不比较、只计数
对于每一个输入元素
适用前提:
- 整数类型:输入的元素必须是整数。
- 范围有限:假设输入的
个数都在 到 的范围内。当 时,排序的复杂度才是线性的 。
步骤:
假设输入数组为
- 初始化:将
数组清零; - 计数:遍历
,统计每个值出现的次数。 保存了值 出现的频率; - 累加(计算前缀和):对
进行累加运算, , 保存了小于或等于 的元素个数; - 反向填充:从后往前遍历
,根据 中的值将 放入 的对应位置,每放一个 中对应的计数 。
cpp
vector<int> countingSort(const vector<int> &A, const int k) {
const int n = A.size();
vector<int> B(n);
vector C(k + 1, 0);
// 1. 频率
for (int j = 0; j < n; j++) {
C[A[j]]++;
}
// 2.累加频率
for (int i = 1; i <= k; i++) {
C[i] += C[i - 1];
}
// 3.反向填充
for (int j = n - 1; j >= 0; j--) {
B[C[A[j]] - 1] = A[j];
C[A[j]]--;
}
return B;
}8.2 基数排序
核心思想:逐位比较
通常采用 LSD(Least Significant Digit,最低有效位)优先的策略;先按个、十、百...位直到最高位排序。在对每一位进行排序时,必须使用稳定的排序算法(例如计数排序)。
从低位开始,只需要对整个数组进行
cpp
// 获取数组中的最大值,确定需要排多少位
int getMax(const vector<int> &A) {
int maxValue = A[0];
for (const int x: A) {
if (x > maxValue) {
maxValue = x;
}
}
return maxValue;
}
// 对于特定的位进行稳定的计数排序(exp,例如:1,10,100,...)
void countingSortForRadix(vector<int> &A, const int exp) {
const int n = A.size();
vector<int> output(n);
int count[10] = {0};
// 1.统计当前位(A[i] / exp) % 10出现的次数
for (int i = 0; i < n; i++) {
count[A[i] / exp % 10]++;
}
// 2.累加频率,确定位置
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 3.反向填充(保持稳定性)
for (int i = n - 1; i >= 0; i--) {
const int digit = A[i] / exp % 10;
output[count[digit] - 1] = A[i];
count[digit]--;
}
//4.写回原数组
for (int i = 0; i < n; i++) {
A[i] = output[i];
}
}
void radixSort(vector<int> &A) {
const int m = getMax(A);
//从个位开始执行
for (int exp = 1; m / exp > 0; exp *= 10) {
countingSortForRadix(A, exp);
}
}时间复杂度:
假设
如果底层使用子排序算法是稳定的(例如计数排序),那么这个算法就是稳定的。
8.3 桶排序
核心思想:
桶排序将
- 分发:根据元素的值,将每个输入值放入到对应桶中。
- 排序:对每个桶中的元素进行排序(可以使用任何稳定的排序算法,通常使用插入排序)。
- 合并:按照桶的顺序,依次列出各个桶的元素。
步骤,假设数组
- 创建一个大小为
的辅助数组 ,其中每个位置 都是一个空链表(桶); - 遍历数组
,将每个元素放入对应的桶 中; - 对每个桶
中的元素进行排序(使用插入排序); - 将每个桶
中的元素依次放回原数组 中。
cpp
void bucketSort(vector<float> &A) {
const int n = A.size();
vector<vector<float> > buckets(n);
// 将元素分发到各个桶
for (int i = 0; i < n; i++) {
const int bucketIndex = n * A[i];
buckets[bucketIndex].push_back(A[i]);
}
// 对每个桶排序
for (int i = 0; i < n; i++) {
ranges::sort(buckets[i]);
}
// 合并桶中的结果
int index = 0;
for (int i = 0; i < n; i++) {
for (const float val: buckets[i]) {
A[index++] = val;
}
}
}时间复杂度:
- 平均情况:
,当输入数据均匀分布在 时,每个桶中的元素个数趋于常数。即使桶内排序使用了 算法,总时间仍是线性的。 - 最坏情况:
,当输入数据集中在某个桶中时,桶内排序的时间复杂度会退化到 。