目录
概述
与以前我们遇到的所有排序都不同的是,今天我们介绍的计数排序|桶排序|基数排序是非比较排序。
非比较排序用一种独到的方式来进行排序,但并不进行元素间的比较。
那是怎么排序的呢?
它预先设置了一些已经排好序的辅助空间,将待排序元素发配到其中,然后再依次取出。
简单来说,这种排序只关心元素的绝对特征,而不该关心元素的相对特征。
1.计数排序
思路
利用cnt数组对原数组进行元素统计。
定义长度为待排序数组arr最大元素值+1的辅助数组cnt[],遍历待排序数组arr[i],进行cnt[arr[i]]++操作。
意为:cnt[x]记录了原数组中x的出现次数。
将cnt中的数据依次释放回原数组,即完成排序。
复杂度
时间复杂度: O(w)
空间复杂度: O(w)
w:arr中的最大元素值。
看起来效率很高,但真的是这样吗?
事实上,计数排序只能用于稠密的小范围非负整数元素。
一旦arr中的最大值极大且元素分布及其稀疏,都会造成极大量的空间占用和时间占用。
如果元素不能作为cnt数组下标(如负数或浮点数),计数排序都会直接失效。
(你可能会想到用哈希表代替cnt数组,但哈希值的生成也会占用时间,并有可能触发哈希碰撞)
Code
cpp
#include <algorithm>
void count_sort(int arr[], int len) {
int size = *max_element(arr, arr + len) + 1;
int* cnt=new int[size]();
for (int i = 0; i < len; i++)cnt[arr[i]]++;
for (int j = 0, k = 0; j < size; j++)
while(cnt[j])arr[k++] = cnt[j]--;
delete[] cnt;
}
2.桶排序
思路
桶排序是优化过的计数排序,它在大范围内进行非比较类排序,小范围内进行比较类排序。
我们知道计数排序的痛点在于数组下标和数据范围。那怎么对此调整优化呢?
你可以认为计数排序也是一种特殊的桶排序,只不过每个桶里都只有一个元素。
桶排序是一类广义概念,它只是对元素进行了范围划分,并以此分装在不同的桶中,保证每个桶都代表一个数据范围,前一个桶的所有元素小于后一个桶的所有元素。在桶里则采用比较型排序(如插入排序),排序后,依次将桶中元素释放回原数组。
一般,我们定义原数组元素最大值为max_limit,最小值为min_limit。
cpp
int num_of_buckets = (max_limit - min_limit) / len + 1;
int size_of_bucket = (max_limit - min_limit) / num_of_buckets + 1;
桶数量的计算方式是人为规定,你也可以选择其他的方式计算,或直接使用固定数量,无需纠结。
桶范围的计算方式则是固定的。
*注意*:+1是为了规避整除结果取0。
复杂度
时间复杂度: O(n+n²/k+k)
空间复杂度: O(n)
k:桶数量
复杂度分析:
时间分析:
元素依次入桶,复杂度为n。
桶内排序对n/k个元素进行插入排序,小范围插入排序复杂度为n,n*n/k=n²/k。
各个桶内元素释放回原数组时使用memcpy函数,k个桶的整体复杂度为k。
事实上,桶排序只能用于具有范围概念的元素。
并且,对于我们提供的桶数量计算方式,我们期望桶排序的对象是稠密且均匀的,这样分桶数量几乎可以到达计数排序级别,否则会被动地分出大量空桶。
但是好在桶排序解决了计数排序的最大范围限制和元素类型限制。
Code
cpp
#include <algorithm>
#include <vector>
void bucket_sort(int arr[], int len) {
using bucket = vector<int>;
int max_limit = *max_element(arr, arr + len);
int min_limit = *min_element(arr, arr + len);
int num_of_buckets = (max_limit - min_limit) / len + 1;
int size_of_bucket = (max_limit - min_limit) / num_of_buckets + 1;
vector<bucket>buckets(num_of_buckets);
for (int i = 0; i < len; i++)buckets[(arr[i] - min_limit) / size_of_bucket].push_back(arr[i]);
for (bucket& b : buckets)insertion_sort(b.data(), b.size());
int k = 0;
for (const bucket& b : buckets) {
memcpy(arr + k, b.data(), b.size() * sizeof(int));
k += b.size();
}
}
cpp
void insertion_sort(int arr[], int len) {
for (int i = 1; i < len; i++) {
int temp = arr[i], j = i - 1;
for (; j >=0; j--) {
if (temp<arr[j])arr[j + 1] = arr[j];
else break;
}
arr[j+1] = temp;
}
}
3.基数排序
思路
基数排序是另一种计数排序的优化方案,它规避了计数排序对稠密小范围数据的要求,但是它对元素类型的要求同样严苛,必须是整数,因为它是基于整数特征实现的算法。
计数排序只有十个桶buckets[10],表示[0,9]数字,将数据按位统计,即:
先对元素的个位进行计数排序,将个位同为x的元素放入buckets[x]。
将数据释放回原数组,现在所有元素的个位有序;
先对元素的十位进行计数排序,将十位同为x的元素放入buckets[x],十位为0则放入buckets[0]。
将数据释放回原数组,现在所有元素的十位有序;
...
直到所有元素的最高位有序,则整体有序。
例如:
cpp
i 0 1 2 3 4 5 6
nums[i] 1 22 31 45 671 398 6
按个位排序:
nums[i] 1 31 671 22 45 6 398
* * * * * * *
按十位排序;
nums[i] 1 6 22 31 45 671 398
* * * * *
按百位排序:
nums[i] 1 6 22 31 45 398 671
* *
复杂度
时间复杂度: O(n*w)
空间复杂度: O(n)
w:最大元素的数量级+1
事实上,基数排序只能用于整数元素,但不对数据范围有任何要求。
Code
*注意*:理论上,基数排序支持负整数排序,但我们并未在以下code中提供。
cpp
#include <algorithm>
void radix_sort(int arr[], int len) {
using bucket = vector<int>;
bucket buckets[10];
int limit = *max_element(arr, arr + len);
for(int mask=1;mask<limit;mask*=10){
for (int i = 0; i < len; i++)buckets[arr[i] / mask % 10].push_back(arr[i]);
for (int j = 0, k = 0; j < 10; j++) {
memcpy(arr + k, buckets[j].data(), buckets[j].size() * sizeof(int));
k += buckets[j].size();
buckets[j].clear();
}
}
}
总结
以上三种排序都是非比较类排序,它们只关注元素的绝对特征,而忽视相对特征,同时,也被称为时间换空间类型算法,虽然适用范围较为狭窄,但是在适用范围内时间效率较高。