排序-10-基数排序

199 阅读4分钟

回顾计数排序

给定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 为字符的取值范围

摘要总结自: 漫画算法 小灰的算法之旅