排序算法-5(javascript) 计数排序的实现

前面讲的都是基于比较交换的算法,那有没有不使用比较就能排序的算法呢?它们的复杂度会不会更优呢?

接下来在后面的系列文章中,我将一一介绍O(n)线性的排序方法:

  • 计数排序
  • 桶排序
  • 基数排序

这一篇文章我们就先来学习什么是计数排序:

计数排序

计数排序是针对给定范围的队列,创建这个范围大小的数组,初始值都为0,然后遍历队列,将数字放至到对应的下标位置,如果出现1次,则计数+1,直至遍历完成,就将所有的数字都放入这个数组里了。

然后再次遍历这个数组,按照数组下标,将取出对应的值,如果计数为几,就按顺序出现几次,将这些值放入一个新数组中,遍历完成后,就可以得到有序队列了

下面,按常,举个栗子:
初始的无序数组,假设总共有12个数,假设所有值都在0-10之间取,这12个值分别为:1、3、4、6、8、9、1、2、3、10、7、5

  1. 创建一个长度为11的数组,初始值全设为0:[0,0,0,0,0,0,0,0,0,0,0]
  2. 遍历这12个值,第1个为1,将数组下标为1的位置,计数加1 :[0,1,0,0,0,0,0,0,0,0,0]
  3. 第2个为3,将数组下标为3的位置,计数加1: [0,1,0,1,0,0,0,0,0,0,0,0]
    ...
  4. 直到所有数字遍历完成,结果为:[0,2,1,2,1,1,1,1,1,1,1]
  5. 这时候,遍历这个数组,取出对应下标的值,根据次数,放到一个新队列中
  6. 比如,下标为0的计数为0,代表没有数字,忽略;
  7. 下标为1的数字计数为2,代表1出现了2次,则新队列结果为:[1,1]
  8. 下标为2的数字计数为1,代表2出现了1次,则往新队列中添加元素[1,1,2]
    ...
  9. 最后,可以得到最终的结果[1,1,2,3,3,4,5,6,7,8,9,10],这就是我们想要的有序队列了

计数算法的优化

如果这个序列的范围不是从0开始,而是从某个值到某个值之间,比如是500-600之间取值,这其实只用了101个空间,这时候如果从创建从0-600的初始数组,这无疑浪费了0-499的空间,计算机说不要占着茅坑不干事!

所以要优化一下,只创建101个空间大小,来放置500-600间的数,那么这个关系怎么对应上呢?
其实只要下标 + min就是我们对应的值了,比如500放到下标为0的位置,到时取出来的值时,就下标0 + 500,就是我们想到的值了

/**
 * @description 实现计数算法
 * @param arr: 初始的无序队列(一般为指定范围)
 * @return res: 计数排序后的有序队列
 */

function countSort(arr) {
  // 计算出原始数组的最大最小值
  let max = arr[0]
  let min = arr[0]
  const len = arr.length
  for(let i = 0; i< len; i++) {
    if(arr[i] > max) {
      max = arr[i]
    }
    if(arr[i] < min) {
      min = arr[i]
    }
  }
  // 计算计数数组的容量
  const countLen = max - min + 1
  let countArr = new Array(countLen).fill(0)  // 创建计数数组,并设置所有数的计数初始值为0
  
  // 遍历初始数组,给对应下标(arr[j] - min)计数加1
  for(let j = 0; j < len; j++ ) {
    countArr[arr[j] - min]++;
  }

  // 结果队列
  let res = []
  // 遍历结果队列的指针
  let resIndex = 0
  for(let k = 0; k < countLen; k++) {
    // 如果某数计数大于0,就循环取出值
    while(countArr[k] > 0) {
      // 将此数放入结果队列中
      res[resIndex] = k + min
      // 队列指针右移
      resIndex++;
      // 计数减1
      countArr[k]--;
    }
  }

  return res
}
//  使用console.time()和console.timeEnd()这一句语句,会输出中间程序执行的运行时间
console.time()
const arr = [544,522,522,533,511,522,551,552,553,510,557,555]
let res = countSort(arr)
console.log('res:', res); // [ 510, 511, 522, 522, 522, 533, 544, 551, 552, 553, 555, 557 ]
console.timeEnd()  // default: 4.135ms

计数算法的再次优化

之前计算排序已经实现排序的基本功能了?但是,我们会发现,它把相同的部分,使用计数方式存储后,这几个相同部分,我就分不清它在初始队列中哪个先哪个后出现的了,是一个不稳定的算法

比如:[3,1,2,1],当计数统计后为:[2,1,1],那么在1位置上的两个1,最终会变成:[1,1,2,3]排序后我是分不清之前是哪个1先哪个1后,如果我希望排序后,不要改变相同元素的相对位置,也就是让它变成一个稳定的算法,要怎么做呢?

要实现这个功能,其实就是把当前元素的计数改成: 前面所有元素计数之和 + 当前元素计数,这样,对应下标元素对应的那个元素的计数,就是它最终序列的位置,有一些绕,还是来讲栗子吧:
比如:
初始[3,1,2,1,5]

  • 按照之前优化1的算法:
    最大值max为5,最小值min为1,那么设置空间为3的计数数组[0,0,0,0,0]:

    1. 下标0对应元素1 (下标+min,就是它所对应的元素值),下标所在位置放置元素1的计数2, [2,0,0,0,0]
    2. 下标1对应元素2,下标所在位置放置元素2的计数1, [2,1,0,0,0]
    3. 下标2对应元素3,下标所在位置放置元素3的计数1, [2,1,1,0,0]
    4. 下标4对应元素5,下标所在位置放置元素5的计数1, [2,1,1,0,1]
      这时,遍历计数数组,根据下标所在得出:
    5. 下标0,对应元素是1,计数为2,所以生成新数组[1,1]
    6. 下标1,对应元素2,计数为1,所以生成新数组[1,1,2]
    7. 下标2,对应元素3,计数为1,所以生成新数组[1,1,2,3]
    8. 下标3,因为计数为0,忽略
    9. 下标5,对应元素5,计数为1,所以就生成新数组[1,1,2,3,5]
  • 按照现在优化2的算法:
    最大值max为5,最小值min为1,计数数组[0,0,0,0,0]:

    1. 下标0对应元素1 (下标+min,就是它所对应的元素值),下标所在位置放置元素1的计数2, [2,0,0]
    2. 下标1对应元素2,下标所在位置放置元素2的自身计数1 + 前面元素的计数2 = 计数3, [2,3,0]
    3. 下标2对应元素3,下标所在位置放置元素3的自身计数1 + 前面元素的计数3 = 计数4, 最终的计数数组为:[2,3,4]
    4. 下标3对应元素4,下标所在位置放置元素3的自身计数0 + 前面元素的计数4 = 计数4, 最终的计数数组为:[2,3,4,4]
    5. 下标4对应元素5,下标所在位置放置元素5的自身计数1 + 前面元素的计数4 = 计数5, 最终的计数数组为:[2,3,4,4,5]

    这时候,创建一个跟原数组一样大小的空数组,从后向前遍历初始数组,根据元素获取对应计数数组中下标(元素值-min)的计数值,就是它的最终位置,并更新其计数-1,代表下一次相同元素要放置的位置:

    1. 遍历[3,1,2,1,5]最后一个数5,它的计数下标位置为:5-min=5-1=4,查看计数数组下标为4的值,为5,代表代表它放置在新数组的第5位[,,,,5],此时,计数数组更新5的计数为:5-1=4,代表下一次相同元素应该放置的位置[2,3,4,4,4];
    2. 遍历倒数第二个数1,它的计数下标位置:1-1=0,查看计数数组下标为0的值,为计数2,代表它放置在新数据的第2数,[,1,,,5],此时,计数数组更新1的计数为:2-1=1,代表下一次相同元素应该放置的位置,计数数组更新为:[1,3,4,4,4];
    3. 遍历倒数三个数2,它的计数下标位置: 2-1=1,查看计数数组下标为1的值,为计数3,代表它放置在新数据的第3位,[,1,2,,5],此时,计数数组更新2的计数为:3-1=2,代表下一次相同元素应该放置的位置,计数数组更新为:[1,2,4,4,4];
    4. 遍历倒数第四个数1,它的计数下标位置1-1=0,计数数组下标为1的值已经在上一次更新为1,所以此时,把它放在新数组第1的位置[1,1,2,,5],计数数组更新为[0,2,4,4,4]
    5. 遍历第一个数3,它的计数下标位置为:3-min = 3-1 = 2,查看计数数组下标为2的值,为4,代表它放置在新数组的第4位[1,1,2,3,5],计数数组更新为[0,2,3,4,4],至此,排序结束
      这时候发现,越往后的出现相同元素的位置越后,这样就实现稳定的计数排序了
   /**
 * @description 优化版:实现稳定的计数排序
 * @param arr: 初始的无序队列(一般为指定范围)
 * @return res: 计数排序后的有序队列
 */

function stableCountSort(arr) {
  let max = arr[0]
  let min = arr[0]
  const len = arr.length
  for(let i = 0; i< len; i++) {
    if(arr[i] > max) {
      max = arr[i]
    }
    if(arr[i] < min) {
      min = arr[i]
    }
  }
 const countLen = max - min + 1
 // 前面创建计数数组的步骤不变
 let countArr = new Array(countLen).fill(0)
 for(let j = 0; j < len; j++ ) {
   countArr[arr[j] - min]++;
 }
 // 就增加了:从第2个数开始,将后面元素的计数变成与前面所有元素的计数之和 
 for(let z = 1; z < countLen; z++) {
   // 加上前一位的计数次数(也就是之前所有元素的计数之和)  
   countArr[z] += countArr[z -1]
 }
 console.log('2222', countArr); // 此时,计数数组:[ 2, 3, 4, 4, 5 ]
 
 // 结果队列  
 let res = new Array(len)
 let countIndex = 0
 // 对初始序列,进行从后往前遍历  
 for(let k = len - 1; k >= 0; k--) {
   //  获取元素对应的计数
   countIndex = countArr[arr[k] - min]
   // 因为下标0占了一位,所以下标要减1   
   res[countIndex - 1] = arr[k]
   console.log(res)
   countArr[arr[k] - min]--;
 }

 return res
}
const arr1 = [3,1,2,1,5]
res = stableCountSort(arr1)
console.log('res2:', res); // [ 1, 1, 2, 3, 5 ]

计数排序的局限性

虽然计数排序的时间复杂度只有O(n),为什么我们很少用到它呢?因为它有比较大的局限性:

  1. 当数列最大和最小值的差距过大时,并不适合;过大的差距意思空间上的消耗也要随之变 大。不但浪费空间,而且时间复杂度也会随之升高
  2. 当数列元素不是整数时,也不适合。比如小数,就无法根据元素创建对应的计数数组。

总结

  • 时间复杂度:
    1. 获取最大最小值O(n)
    2. 获取计数数组O(n)
    3. 获取累加计数数组O(n)
    4. 遍历初始数组O(n)
      总的是O(4n),还是约等于O(n)
  • 空间复杂度
    1. 创建计数数组空间O(n)
    2. 结果队列O(n)
      总的是O(2n),还是约等于O(n)
  • 是一种典型的空间换时间的算法
  • 优化后的计数排序已经是稳定的排序方法

排序算法系列文章传送门(未完,持续更新中):
排序算法-1(javascript) 冒泡、选择、插入、希尔排序的实现
排序算法-2(javascript) 快速排序的实现
排序算法-3(javascript) 堆排序的实现
排序算法-4(javascript) 归并排序的实现
排序算法-5(javascript) 计数排序的实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容