一、原理
逻辑:
1. 准备一个新的数组
=> 遍历原始数组, 把原始数字当做新数组的索引向新数组内填充数据
=> 新数组的数据按照计数排列
2. 遍历新数组
=> 把索引在当做数据放进原始数组内
=> 前提: 把原始数组清空
二、注意点
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
就是遍历数组记录数组下的元素出现过多次,然后把这个元素找个位置先安置下来,简单点说就是以原数组每个元素的值作为新数组的下标,而对应小标的新数组元素的值作为出现的次数,相当于是通过下标进行排序。
因为 JavaScript 的数组下标是以字符串形式存储的,所以计数排序可以用来排列负数,但不可以排列小数。这里引入一个可能会遗忘的知识点,数组可以是负数,但是总数组的长度不会改变。例如
let arr = [3]
arr.length // 1
这个时候,我们数组添加一位负数下标的值,
arr[-100] = 90
arr.length // 这个时候仍旧显示1
然而,我们可以获得arr[-100]的值
arr[-100] // 90
三、动态演示图
例子1:
例子2:
四、代码实现
function countingSort(nums) {
let arr = []; // 定义一个空数组
let max = Math.max(...nums); // 找到这个数组的 最大 最小值
let min = Math.min(...nums);
// 装桶
for(let i=0, len=nums.length; i<len; i++) { // 把传入数组的值作为下标 装入一个长度为数组最大值长度的临时数组
// 比如 第一位是5 传入arr后 arr为[,,,,,1] => [empty × 5, 1]
let temp = nums[i];
arr[temp] = arr[temp] + 1 || 1;
}
let index = 0; // 定义一个索引 这个索引是 后面用来改变原数组的
// 还原原数组
for(let i=min; i<=max; i++) { // 写一个for循环 i=原数组最小值 i>原数组最大值的时候跳出循环 i每次++
while(arr[i] > 0) { // 从arr[i]开始 如果他>0
nums[index++] = i; // 就把原数组的第index位赋值为 i, 这个其实是 nums[index] = i; 和 index++的缩写
arr[i]--; // 每赋值完一次后 临时数组当前位的值就-- 如果=0 就说明这位上没有值了
}
}
}
以上代码是兼容数组里边包含负整数的情况的。如果是不考虑负整数,我们完全不用这么复杂,不用把数组的最大值和最小值拆解出来,代码如下:
// 计数排序
var origin = [ 9, 3, 100, 6, 4, 100, 1, 9, 8, 7, 2, 2, 5, 100, 3, 32, 55 ]
var newarr = []
// 1. 把数据当索引
for (var i = 0; i < origin.length; i++) {
// origin[i] 就是数据
newarr[ origin[i] ] ? newarr[ origin[i] ]++ : newarr[ origin[i] ] = 1
}
// 2. newarr 中的索引当数据
// newarr 中的值当多少个
// 2-1. 清空 origin
origin.length = 0
// 2-2. 填充回去
for (var i = 0; i < newarr.length; i++) {
// i 表示真实数据
// newarr[i] 表示有多少个
if (!newarr[i]) continue
// 填充回去
// 填充 newarr[i] 个 i
for (var k = 0; k < newarr[i]; k++) origin[origin.length] = i
}
console.log('排序之后 : ', origin) // [1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 9, 32, 55, 100, 100, 100]
或者写成以下:
function countingSort(arr, maxValue) {
var bucket = new Array(maxValue+1), //伪造长度为maxValue+1的元素皆为空的数组
sortedIndex = 0; // 该数值为下方循环所用起始值
arrLen = arr.length, // 原始数组的长度
bucketLen = maxValue + 1; // 最大序列号
for (var i = 0; i < arrLen; i++) { // 把新数组的下标对应加上数组,有一个加一次
if (!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}
for (var j = 0; j < bucketLen; j++) {
while(bucket[j] > 0) {// 把新数组下标和数组对应拆解, 当数组被拆解到0的时候,绕开该while循环
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
var arr = [2, 3, 8, 7, 1, 2, 7, 3];
console.log(countingSort(arr,8));//1,2,2,3,3,7,7,8
五、复杂度
最佳情况:T(n) = O(n+k)
最差情况:T(n) = O(n+k)
平均情况:T(n) = O(n+k)
它的时间复杂度为O(n+k), k是最大值与最小值的差值;
计数排序快于任何一种比较排序算法。
这里有一点需要注意,当需要排序的数组中某一个数偏离大部分数据很远的时候,此算法将会耗费比较大的空间和无用循环,该算法对于数量大且数与数之间相比较较大的时候,算法性能较差。
它是以空间换时间的一种算法。这种算法可以继续优化,下次我们讲一下桶排序。