「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」
今天学习计数排序(Counting Sort).
之前我们学习的冒泡、选择、插入、归并、快速、希尔、堆排序,都是基于比较的排序,平均时间复杂度目前最低是O(nlogn).
今天要学的计数排序,不是基于比较的排序,它们是典型的用空间换时间,在某些时候,平均时间复杂度可以比O(nlongn)更低。
本篇文章中的代码在这里.
计数排序(Counting Sort)
核心思想
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。一句话概括就是: 统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引。
时间复杂度
O(n + k)
[当输入的元素是 n 个0到k之间的整数时]
算法步骤
- 找出待排序数组中的最大值和最小值
- 统计数组中每个值为
i
的元素出现的次数,存入数组C的第i
项 - 对所有的计数累加(从 C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素
i
放在新数组的第C(i)
项,每放一个元素就将C(i)
减去1
动图演示
代码实现
简单版
直接开辟从0到max的数组
比如 对于 arr = [7, 3, 5, 8, 6, 7, 4, 5]
元素 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
次数 | 0 | 0 | 0 | 1 | 1 | 2 | 1 | 2 | 1 |
function getRandomArr() {
const res = []
for (let i = 0; i < Math.floor(Math.random() * 15) + 5; i++) {
res.push(Math.floor(Math.random() * 10) + 1)
}
return res
}
function countSort(arr) {
const counts = []
for (let i = 0; i < arr.length; i++) {
if (!counts[arr[i]]) {
counts[arr[i]] = 1;
continue;
}
counts[arr[i]]++
}
let index = 0 // 数组中的位置
for (let j = 0; j < counts.length; j++) {
while (counts[j] && counts[j]--) {
arr[index++] = j
}
}
return arr
}
const countArr = getRandomArr()
console.log('before count sorting ===>', countArr)
countSort(countArr)
console.log('after count sorting ===>', countArr)
这个版本的实现存在一下问题
- 无法对负整数进行排序
- 及其浪费内存空间
- 是个不稳定的排序
改进版
要针对上面的三个问题进行改进
- 从索引0开始依次存放min~max出现的次数
- 每个次数累加上其前面的所有次数得到的就是元素在有序序列中的位置信息
- 有序数组的填充顺序为从右到左
比如 对于 arr =[7, 3, 5, 8, 6, 7, 4, 5]
元素 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
次数 | 1 | 2 | 4 | 5 | 7 | 8 |
可以看出,这样做的好处是 假设array 中的最小值是 min 则
-
array 中的元素 k 对应的 counts 索引是
k - min
-
array 中的元素 k 在有序序列中的索引为
counts[k-min]-p
, 其中 p代表着倒数第几个k。 比如 元素 7 中倒数第一个在有序序列 中的index为 counts[7 -3] - 1 = 6, 倒数第二个 7 在有序序列中 index = counts[7-3] -2 = 5;这也是为啥要填充的顺序为从右到左,是为了保持排序为稳定排序。
function countSort2(arr) {
let min = arr[0],
max = arr[0]
for (let i = 0; i < arr.length; i++) {
if (arr[i] > max) max = arr[i]
if (arr[i] < min) min = arr[i]
}
const counts = new Array(max - min + 1).fill(0);
// 和简单版一样,找出对应元素的次数
for (let i = 0; i < arr.length; i++) {
counts[arr[i] - min]++
}
// 累加次数,让元素的个数为叠加式
for (let i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1]
}
// 从后往前遍历元素,把它放到有序数组中的合适位置
const sortedArr = []
for (let i = arr.length - 1; i >= 0; i--) {
// -- 放前面 是模拟 counts[k-min]-p,且同时把 对应 counts中的记数次数-1;这样方便遇到后面相同的元素执行操作相同
sortedArr[--counts[arr[i] - min]] = arr[i]
}
// 将有序数组值赋值到arr, 可做可不做,具体看是要改变arr还是不改变arr返回新数组
console.log('this is sorted Arr', sortedArr)
for (let i = 0; i < sortedArr.length; i++) {
arr[i] = sortedArr[i]
}
return sortedArr
}
const countArr2 = getRandomArr()
console.log('before count sorting 2 ===>', countArr2)
countSort2(countArr2)
console.log('after count sorting 2 ===>', countArr2)
好了,今天的算法学习就到这里吧。你学到了吗?