回顾计数排序
给定20个随机整数的值如下,如何最快把这些无序的随机整数排序:
9,3,5,4,9,1,2,7,8,1,3,6,5,3,4,0,10,9 ,7,9
1. 由于这些整数的范围是从 0 到 10 这 11 个整数,创建一个长度为 11 的空数组,即统计数组,数组的下标 0 - 10,对于待排序的随机整数 0 -10,
2. 遍历无序的随机数列,每一个值对号入座,对应统计数组下标的元素进行 加 1 的操作,
例如:第一个元素为 9 则对应数组下标为 9 的元素 加 1
第二个元素为 3 则对于数组下标为 3 的元素 加 1
....
最终数列遍历完毕,统计数组如下:
4. 统计数组的下标代表值,value代表该值出现次数,则直接遍历统计数组,输出下标,值为几就输出几次,
0,1,1,2,3,3,3,4,4,5,5,6,7,7,8,9,9,9,9,10
此时,这个数列已经是有序的了
这就是计数排序的朴素版本,为了实现稳定的计数排序(值相同的元素,排序之后顺序不变),可以参考这里(本人的个人博客,需要梯子,没有梯子的可以看这个掘金版本的)
基数排序
从两个特殊的需求看计数排序的局限性:
1. 给定一组手机号排序:
18914021920
13223132981
13566632981
13660891039
13361323035
........
........
按照计数排序的思路,要根据手机号的取值范围创建一个空数组,但是11位的手机号有太多的组合,可能要建立一个大的不可想象的数组,才能装得下所有可能出现的手机号
2. 为一组英文单词排序:
banana
apple
orange
peach
cherry
........
........
计数排序适合的场景是对整数进行排序,如果遇到英文单词就无能为力了
初识基数排序
处理诸如手机号、英文单词等复杂元素的排序,仅靠一次计数排序是很难实现的,
此时,我们不妨把工作拆分成为多个阶段,每个阶段只对一个字符进行排序,
例如:
数组中有若干字符串元素,每个字符串元素都是由三个英文字母组成
bda,cfd,qwe,yui,abc,rrr,uee
由于每个字符的长度是 3,所以可以把排序拆成 3 轮,
第一轮,按照最低位字符进行排序,排序算法使用计数排序(注意如果要保持基数排序是稳定排序,则每一轮使用的计数排序也必须是稳定版),使用 ascii 码作为统计数组的下标,第一轮排序结果如下:
第二轮,在第一轮的基础上,对第二位进行排序
第三轮,在第二轮的基础上,对最高位进行排序
此时,这些字符串的顺序就排好了,
像这样把字符串按位拆分,每一位进行一次计数排序的算法就是基数排序
基数排序既可以从高位到低位(Most Significant Digit first,简称 MSD),也可以从低位到高位(Least Significant Digit first,简称LSD)
针对于位数不同的做法
例如有的元素为 6 位,有的元素为 5 位,
解决方案:以最长位数为基准,不足位数的元素在获取 ascii 码的时候直接返回 0 ,即:为空的元素排在前边
代码实现
// ascii 码取值范围
let ASCII = 128;
function getCharIndex(str:string,index:number):number {
if(str.length <= index) {
return 0
}
return str.charCodeAt(index)
}
function radixSort(arr:Array<string>, maxLength:number) {
let sortedArray = new Array(arr.length);
for (let k = maxLength - 1; k >= 0; k--) {
// 1. 创建统计数组,这里为了简洁,直接以ascii 码范围作为数组长度
let countArray = new Array(ASCII).fill(0);
for (let index = 0; index < arr.length; index++) {
let i = getCharIndex(arr[index], k)
countArray[i]++
}
// 2. 统计数组变形,后边元素等于前边元素之和
for (let index = 1; index < countArray.length; index++) {
countArray[index] += countArray[index-1]
}
// 3. 倒序遍历原始数列,从统计数组找到正确位置,输出到结果数组
for (let index = arr.length - 1; index >= 0; index--) {
let i = getCharIndex(arr[index], k);
let sortedIndex = countArray[i] - 1;
sortedArray[sortedIndex] = arr[index];
countArray[i]--
}
arr=[...sortedArray]
}
return arr
}
function main() {
const arr = ['qd', 'abc', 'qwe', 'hhh', 'a', 'cws', 'ope'];
console.log(radixSort(arr, 3))
}
main()
小结
时间复杂度:
由于计数排序的时间复杂度为 O(n + m),所以基数排序的时间复杂度为 O(k(n + m)),其中 m 为字符取值范围,k为字符最大长度
空间复杂度:
由于结果数组是反复使用,统计数组每轮循环都销毁,所以空间复杂度和计数排序一样为 O(n + m),其中 m 为字符的取值范围
摘要总结自: 漫画算法 小灰的算法之旅