「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。
有趣之处就在于,计数排序不是基于原址排序的,需要“数”出数组中每个元素的个数,然后按照出现的次数依次添加到一个新的数组中。
- 时间复杂度:扫描一次source,扫描一个helper,复杂度为N+K
- 空间复杂度:辅助空间 k ,k =maxOf(source) - minOf(source) +1
- 非原址排序
- 稳定性:相同元素不会出现交叉,非原址都是拷来考去
- 计数有缺点,数据密集或者范围小时,适用
- 而且当 O(k)>O(nlog(n)) 的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n)), 如归并排序,堆排序)
动画演示
分析
- 找到数组中最大的元素
- 使用一个数组,长度等于
最大值max+1(数字从0开始),记录每个元素出现的次数 - 根据计数数组,依次输出到新的数组中
简单版代码实现
const arr = [0, 2, 5, 3, 7, 9, 10, 3, 7, 6]
// 简单版
function countSort(arr) {
let max = -Infinity // 记录数组中最大的元素
for (let v of arr) {
max = Math.max(max, v)
}
// 记录每个元素出现次数
const count = new Array(max + 1).fill(0)
for (let v of arr) {
count[v]++
}
const res = [] //结果数组
for (let i = 0; i < count.length; i++) {
let tmp = count[i]
while (tmp--) {
res.push(i)
}
}
return res
}
console.log(countSort(arr)) //[0, 2, 3, 3, 5, 6, 7, 7, 9, 10]
到这里聪明的你可能已经发现问题了,计数数组的长度和数组中的最大值max有关。如果数组中的元素所属区间比较集中,最大值又非常大,将导致计数数组的空间浪费。比如范围在[100,200],计数数组的长度将达到201,[0,99]的空间就全都浪费了。
为了优化,可以再找到数组中的最小值min,则可以将计数数组的长度缩小为max-min+1,输出的时候做相应的移位加上最小值min
这样做还有一个好处,就是可以处理负数数组下标的情况,但是任然会有空间浪费的情况,比如元素区间较大的情况,例如[-101,200]。
优化版本
// 优化空间浪费及有负数情况
const arr2 = [-101, 109, 107, 103, 108, 102, 103, 110, 107, 103]
function countSort_v2(arr) {
let max = -Infinity,
min = Infinity //额外记录最小值
for (let v of arr) {
max = Math.max(max, v)
min = Math.min(min, v)
}
const count = new Array(max - min + 1).fill(0) //数组长度最大值-最小值,最小值变成下标0,
// 如果最大值和最小值差距很大,仍然存在空间浪费的情况
for (let v of arr) {
count[v - min]++ //需要减去最小值
}
const res = []
for (let i = 0; i < count.length; i++) {
let tmp = count[i]
while (tmp--) {
res.push(i + min) //这里要加回来
}
}
return res
}
console.log(countSort_v2(arr2)) //[-101, 102, 103, 103, 103, 107, 107, 108, 109, 110]
希望有大佬能指导如何进一步优化
参考自 https://blog.csdn.net/weixin_44491927/article/details/105120985