计数排序
思路
很简单,就是设一个哈希表来统计数组各元素的个数,统计完毕后计算前缀和,将各元素统计数转化为应该放的下标位置,最后遍历原数组各元素,将元素放到哈希表映射的位置即可。(注意每次放一个元素对应下标位置计数减1)
实现
class Solution {
public:
// 数据范围 -50000 <= A[i] <= 50000, 故数组要容纳100000个数
int SIZE = 100001;
// 设置偏移量的目的是使得所有值都为正值,防止数组溢出
int OFFSET = 50000;
vector<int> countingSort(vector<int>& nums) {
vector<int> count(SIZE,0);
int n = nums.size();
for(int i = 0; i < n; i++){
count[nums[i] + OFFSET]++;
}
for(int i = 1; i < SIZE; i++){
count[i] += count[i - 1];
}
vector<int> temp(nums.begin(),nums.end());
for(int i = n - 1; i >= 0; i--){
int idx = count[temp[i] + OFFSET] - 1;
count[temp[i] + OFFSET]--;
printf("idx=%d nums[%d]=%d\n",idx,i,temp[i]);
nums[idx] = temp[i];
}
return nums;
}
};
分析
时间复杂度: O ( N + k ) O(N+k) O(N+k) , 其中N为数组的长度,k为哈希表的长度
空间复杂度: O ( N + k ) O(N+k) O(N+k), 需要额外用到一个哈希表和一个额外数组
稳定性:稳定,如果你最后遍历数组是后往前遍历的话。
最后遍历数组时一定要从后往前遍历,因为如果一个相同值在数组中出现了多次,统计这些元素是按从前往后的顺序来叠加计数的,通过前缀和换算以后,值最初的下标位置对应就是最后出现的相同元素,排定一个元素下标就减1,指向这个值在原数组中的前一个相同元素,因此我们也要按计数顺序来做出调整,使得同一个值最后出现的元素进入排定数组最后的位置,如果弄成从前往后遍历,那就是先把最后出现的元素反而先放进排定数组最前的位置了,丧失稳定性。
初始次序:
移动次数无关:和归并一样,排定来源来自于额外的数组,有不有序都要移动,一次排定。
比较次数无关:非比较算法
时间复杂度无关: 最好最坏都是O(N+k)
排序趟数无关:O(N) 算法,有不有序都是一趟过
基数排序
(别和计数混淆,计数是Counting Sort基数是Radix Sort)
计数排序的缺点是,计数器的大小严重依赖于原始序列,当元素值极大时将造成极大的空间损耗(数少值大的情况下就是极大浪费空间),改进方法是按各元素位数自底向上来排序,计数器的大小也就被固定在0-9的范围内,显然要付出时间换空间的代价。
一般来说,当待排序序列值域小,数量大
时使用计数排序
,而当待排序序列值域大,数量小
时使用基数排序。
class Solution {
public:
int OFFSET = 50000; // -50000 <= A[i] <= 50000
vector<int> RadixSort(vector<int>& nums) {
int n = nums.size(),maxlen = 0;
for(int i = 0; i < n; i++){
nums[i] += OFFSET; // 加上偏移量,防止负数影响
maxlen = std::max(maxlen,nums[i]); // 取最大元素的位数做循环量
}
vector<int> cnt(10,0),tmp(nums.begin(),nums.end());
int div = 1;
while(div <= maxlen){
for(int i = 0; i < n; i++){
int bit = (nums[i] / div) % 10; //从元素的个位一直取到顶位
cnt[bit]++;
}
for(int i = 1; i < 10; i++){
cnt[i] += cnt[i - 1];
}
for(int i = n - 1; i >= 0; i--){
int bit = (tmp[i] / div) % 10;
int idx = cnt[bit] - 1;
cnt[bit]--;
nums[idx] = tmp[i];
}
// 清空计数器, 更新tmp,进行下一位数的排序
cnt = vector<int>(10,0);
tmp = vector<int>(nums.begin(),nums.end());
div *= 10;
}
for(int i = 0; i < n; i++){
nums[i] -= OFFSET; //排完序别忘了还原原值
}
return nums;
}
int getElemLen(int digit){
int cnt = 0;
while(digit){
digit /= 10;
cnt++;
}
return cnt;
}
};
分析
时间复杂度: O ( N ∗ k ) O(N*k) O(N∗k), 其中N为数组的长度,k为序列中最大值的位数
空间复杂度: O ( N ) O(N) O(N) , 计数器长度固定为10,可以忽略, 主要看额外数组的长度
稳定性: 稳定,理由同计数排序
初始序列:
移动次数无关:同计数排序,排序依靠额外数组一次排定,无需移动
比较次数无关:同计数排序,排序依靠额外数组一次排定,无需比较
时间复杂度无关:最好最坏情况都是 O ( N ∗ k ) O(N*k) O(N∗k)
排序趟数无关:外循环次数取决于最大元素的位数而不是初始序列
桶排序
将原序列按照值域分为多个等长区间的“桶”,将序列各元素分类到对应的桶中并排序,装桶结束后将各桶内元素按顺序逐一“倒回”原数组:
class Solution {
public:
int OFFSET = 50000; // -50000 <= A[i] <= 50000
vector<int> sortArray(vector<int>& nums) {
buckSort(nums);
return nums;
}
void buckSort(vector<int> &nums){
int n = nums.size(),maxi = 0,mini = 0;
for(int i = 0; i < n; i++){
nums[i] += OFFSET; // 加上偏移量,防止负数影响
maxi = std::max(maxi,nums[i]);
mini = std::min(mini,nums[i]);
}
int gap = 2; // 设各桶的区间长,区间设置为左闭右开
int buckAmount = (maxi - mini) / gap + 1; //桶个数
vector<vector<int>> buckets(buckAmount);
//元素入桶,每次更新桶时都需要排序,这里采用插入排序
for(int i = 0; i < n; i++){
int idx = (nums[i] - mini) / gap;
buckets[idx].push_back(nums[i]);
insertionSort(buckets[idx]);
}
int k = 0;
//将排序后的元素逐一出桶
for(int i = 0; i < buckAmount; i++){
int buckLoad = buckets[i].size();
for(int j = 0; j < buckLoad; j++){
nums[k++] = buckets[i][j];
}
}
//恢复数组原值
for(int i = 0; i < n; i++){
nums[i] -= OFFSET;
}
}
void insertionSort(vector<int> &nums){
int n = nums.size();
for(int i = 1; i < n; i++){
int tmp = nums[i];
int j = i;
while(j > 0 && nums[j - 1] > tmp){
nums[j] = nums[j - 1];
j--;
}
nums[j] = tmp;
}
}
};
分析
时间复杂度: 取决于桶的数量k和采用的排序算法。设数组长为n, 最好情况下,各元素被均匀的分到各桶中且都有序,时间复杂度达 O ( N ) O(N) O(N) , 最坏情况下所有元素都挤在一个桶内, 时间复杂度为 O ( N 2 ) O(N^2) O(N2)
本例中采用的排序算法是插入排序,如果采用其他排序算法则需要另行讨论,例如:
排序算法 | 最终复杂度 |
---|---|
插入 | 最好$ O(n/k)$ -> O ( N ) O(N) O(N), 最坏O( (n/k)^2 ) $-> $ O ( N 2 ) O(N^2) O(N2) |
快排 | 最好 O ( ( n / k ) ∗ l o g ( n / k ) ) O( (n/k)*log(n/k) ) O((n/k)∗log(n/k)) -> O ( N l o g N ) O( NlogN) O(NlogN), 最坏 O((n/k)^2) -> O ( N 2 ) O(N^2) O(N2) |
不过在桶的数量合适的情况上,O(NlogN) 约等于 O(N)。
空间复杂度:取决于桶数组的实现方式,本例采用了vector来动态插入数组,类似于链表,空间复杂度为O(n+k), 也就是数组长度+桶数, 如果采用数组固定分配,则空间复杂度为O(nk)
稳定性:装桶的逻辑类似于计数排序,所以过程是稳定的,千万别用非稳定的排序算法来排序桶。
初始次序:
移动次数:无关,排序源来自于桶数组,一次排定,无需移动
比较次数:无关,非比较算法
时间复杂度:取决于排序桶的排序算法
排序趟数:取决于排序桶的排序算法