排序算法-5(javascript) 計(jì)數(shù)排序的實(shí)現(xiàn)

前面講的都是基于比較交換的算法拂铡,那有沒有不使用比較就能排序的算法呢?它們的復(fù)雜度會(huì)不會(huì)更優(yōu)呢?

接下來在后面的系列文章中芯急,我將一一介紹O(n)線性的排序方法:

  • 計(jì)數(shù)排序
  • 桶排序
  • 基數(shù)排序

這一篇文章我們就先來學(xué)習(xí)什么是計(jì)數(shù)排序:

計(jì)數(shù)排序

計(jì)數(shù)排序是針對(duì)給定范圍的隊(duì)列,創(chuàng)建這個(gè)范圍大小的數(shù)組驶俊,初始值都為0娶耍,然后遍歷隊(duì)列,將數(shù)字放至到對(duì)應(yīng)的下標(biāo)位置饼酿,如果出現(xiàn)1次榕酒,則計(jì)數(shù)+1胚膊,直至遍歷完成,就將所有的數(shù)字都放入這個(gè)數(shù)組里了想鹰。

然后再次遍歷這個(gè)數(shù)組紊婉,按照數(shù)組下標(biāo),將取出對(duì)應(yīng)的值辑舷,如果計(jì)數(shù)為幾喻犁,就按順序出現(xiàn)幾次,將這些值放入一個(gè)新數(shù)組中何缓,遍歷完成后肢础,就可以得到有序隊(duì)列了

下面,按常碌廓,舉個(gè)栗子:
初始的無序數(shù)組传轰,假設(shè)總共有12個(gè)數(shù),假設(shè)所有值都在0-10之間取氓皱,這12個(gè)值分別為:1路召、3、4波材、6股淡、8、9廷区、1唯灵、2、3隙轻、10埠帕、7、5

  1. 創(chuàng)建一個(gè)長(zhǎng)度為11的數(shù)組玖绿,初始值全設(shè)為0:[0,0,0,0,0,0,0,0,0,0,0]
  2. 遍歷這12個(gè)值敛瓷,第1個(gè)為1,將數(shù)組下標(biāo)為1的位置斑匪,計(jì)數(shù)加1 :[0,1,0,0,0,0,0,0,0,0,0]
  3. 第2個(gè)為3呐籽,將數(shù)組下標(biāo)為3的位置,計(jì)數(shù)加1: [0,1,0,1,0,0,0,0,0,0,0,0]
    ...
  4. 直到所有數(shù)字遍歷完成蚀瘸,結(jié)果為:[0,2,1,2,1,1,1,1,1,1,1]
  5. 這時(shí)候狡蝶,遍歷這個(gè)數(shù)組,取出對(duì)應(yīng)下標(biāo)的值贮勃,根據(jù)次數(shù)贪惹,放到一個(gè)新隊(duì)列中
  6. 比如,下標(biāo)為0的計(jì)數(shù)為0寂嘉,代表沒有數(shù)字奏瞬,忽略枫绅;
  7. 下標(biāo)為1的數(shù)字計(jì)數(shù)為2,代表1出現(xiàn)了2次丝格,則新隊(duì)列結(jié)果為:[1,1]
  8. 下標(biāo)為2的數(shù)字計(jì)數(shù)為1撑瞧,代表2出現(xiàn)了1次棵譬,則往新隊(duì)列中添加元素[1,1,2]
    ...
  9. 最后显蝌,可以得到最終的結(jié)果[1,1,2,3,3,4,5,6,7,8,9,10],這就是我們想要的有序隊(duì)列了

計(jì)數(shù)算法的優(yōu)化

如果這個(gè)序列的范圍不是從0開始订咸,而是從某個(gè)值到某個(gè)值之間曼尊,比如是500-600之間取值,這其實(shí)只用了101個(gè)空間脏嚷,這時(shí)候如果從創(chuàng)建從0-600的初始數(shù)組骆撇,這無疑浪費(fèi)了0-499的空間,計(jì)算機(jī)說不要占著茅坑不干事父叙!

所以要優(yōu)化一下神郊,只創(chuàng)建101個(gè)空間大小,來放置500-600間的數(shù)趾唱,那么這個(gè)關(guān)系怎么對(duì)應(yīng)上呢涌乳?
其實(shí)只要下標(biāo) + min就是我們對(duì)應(yīng)的值了,比如500放到下標(biāo)為0的位置甜癞,到時(shí)取出來的值時(shí)夕晓,就下標(biāo)0 + 500,就是我們想到的值了

/**
 * @description 實(shí)現(xiàn)計(jì)數(shù)算法
 * @param arr: 初始的無序隊(duì)列(一般為指定范圍)
 * @return res: 計(jì)數(shù)排序后的有序隊(duì)列
 */

function countSort(arr) {
  // 計(jì)算出原始數(shù)組的最大最小值
  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]
    }
  }
  // 計(jì)算計(jì)數(shù)數(shù)組的容量
  const countLen = max - min + 1
  let countArr = new Array(countLen).fill(0)  // 創(chuàng)建計(jì)數(shù)數(shù)組悠咱,并設(shè)置所有數(shù)的計(jì)數(shù)初始值為0
  
  // 遍歷初始數(shù)組蒸辆,給對(duì)應(yīng)下標(biāo)(arr[j] - min)計(jì)數(shù)加1
  for(let j = 0; j < len; j++ ) {
    countArr[arr[j] - min]++;
  }

  // 結(jié)果隊(duì)列
  let res = []
  // 遍歷結(jié)果隊(duì)列的指針
  let resIndex = 0
  for(let k = 0; k < countLen; k++) {
    // 如果某數(shù)計(jì)數(shù)大于0,就循環(huán)取出值
    while(countArr[k] > 0) {
      // 將此數(shù)放入結(jié)果隊(duì)列中
      res[resIndex] = k + min
      // 隊(duì)列指針右移
      resIndex++;
      // 計(jì)數(shù)減1
      countArr[k]--;
    }
  }

  return res
}
//  使用console.time()和console.timeEnd()這一句語句析既,會(huì)輸出中間程序執(zhí)行的運(yùn)行時(shí)間
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

計(jì)數(shù)算法的再次優(yōu)化

之前計(jì)算排序已經(jīng)實(shí)現(xiàn)排序的基本功能了躬贡?但是,我們會(huì)發(fā)現(xiàn)眼坏,它把相同的部分拂玻,使用計(jì)數(shù)方式存儲(chǔ)后,這幾個(gè)相同部分空骚,我就分不清它在初始隊(duì)列中哪個(gè)先哪個(gè)后出現(xiàn)的了纺讲,是一個(gè)不穩(wěn)定的算法

比如:[3,1,2,1],當(dāng)計(jì)數(shù)統(tǒng)計(jì)后為:[2,1,1]囤屹,那么在1位置上的兩個(gè)1熬甚,最終會(huì)變成:[1,1,2,3]排序后我是分不清之前是哪個(gè)1先哪個(gè)1后,如果我希望排序后肋坚,不要改變相同元素的相對(duì)位置乡括,也就是讓它變成一個(gè)穩(wěn)定的算法肃廓,要怎么做呢?

要實(shí)現(xiàn)這個(gè)功能诲泌,其實(shí)就是把當(dāng)前元素的計(jì)數(shù)改成: 前面所有元素計(jì)數(shù)之和 + 當(dāng)前元素計(jì)數(shù)盲赊,這樣,對(duì)應(yīng)下標(biāo)元素對(duì)應(yīng)的那個(gè)元素的計(jì)數(shù)敷扫,就是它最終序列的位置哀蘑,有一些繞,還是來講栗子吧:
比如:
初始[3,1,2,1,5]

  • 按照之前優(yōu)化1的算法:
    最大值max為5葵第,最小值min為1绘迁,那么設(shè)置空間為3的計(jì)數(shù)數(shù)組[0,0,0,0,0]:

    1. 下標(biāo)0對(duì)應(yīng)元素1 (下標(biāo)+min,就是它所對(duì)應(yīng)的元素值)卒密,下標(biāo)所在位置放置元素1的計(jì)數(shù)2, [2,0,0,0,0]
    2. 下標(biāo)1對(duì)應(yīng)元素2缀台,下標(biāo)所在位置放置元素2的計(jì)數(shù)1, [2,1,0,0,0]
    3. 下標(biāo)2對(duì)應(yīng)元素3,下標(biāo)所在位置放置元素3的計(jì)數(shù)1, [2,1,1,0,0]
    4. 下標(biāo)4對(duì)應(yīng)元素5哮奇,下標(biāo)所在位置放置元素5的計(jì)數(shù)1, [2,1,1,0,1]
      這時(shí)膛腐,遍歷計(jì)數(shù)數(shù)組,根據(jù)下標(biāo)所在得出:
    5. 下標(biāo)0鼎俘,對(duì)應(yīng)元素是1哲身,計(jì)數(shù)為2,所以生成新數(shù)組[1,1]
    6. 下標(biāo)1而芥,對(duì)應(yīng)元素2律罢,計(jì)數(shù)為1,所以生成新數(shù)組[1,1,2]
    7. 下標(biāo)2棍丐,對(duì)應(yīng)元素3误辑,計(jì)數(shù)為1,所以生成新數(shù)組[1,1,2,3]
    8. 下標(biāo)3歌逢,因?yàn)橛?jì)數(shù)為0巾钉,忽略
    9. 下標(biāo)5,對(duì)應(yīng)元素5秘案,計(jì)數(shù)為1砰苍,所以就生成新數(shù)組[1,1,2,3,5]
  • 按照現(xiàn)在優(yōu)化2的算法:
    最大值max為5,最小值min為1阱高,計(jì)數(shù)數(shù)組[0,0,0,0,0]:

    1. 下標(biāo)0對(duì)應(yīng)元素1 (下標(biāo)+min赚导,就是它所對(duì)應(yīng)的元素值),下標(biāo)所在位置放置元素1的計(jì)數(shù)2, [2,0,0]
    2. 下標(biāo)1對(duì)應(yīng)元素2赤惊,下標(biāo)所在位置放置元素2的自身計(jì)數(shù)1 + 前面元素的計(jì)數(shù)2 = 計(jì)數(shù)3, [2,3,0]
    3. 下標(biāo)2對(duì)應(yīng)元素3吼旧,下標(biāo)所在位置放置元素3的自身計(jì)數(shù)1 + 前面元素的計(jì)數(shù)3 = 計(jì)數(shù)4, 最終的計(jì)數(shù)數(shù)組為:[2,3,4]
    4. 下標(biāo)3對(duì)應(yīng)元素4,下標(biāo)所在位置放置元素3的自身計(jì)數(shù)0 + 前面元素的計(jì)數(shù)4 = 計(jì)數(shù)4, 最終的計(jì)數(shù)數(shù)組為:[2,3,4,4]
    5. 下標(biāo)4對(duì)應(yīng)元素5未舟,下標(biāo)所在位置放置元素5的自身計(jì)數(shù)1 + 前面元素的計(jì)數(shù)4 = 計(jì)數(shù)5, 最終的計(jì)數(shù)數(shù)組為:[2,3,4,4,5]

    這時(shí)候圈暗,創(chuàng)建一個(gè)跟原數(shù)組一樣大小的空數(shù)組掂为,從后向前遍歷初始數(shù)組,根據(jù)元素獲取對(duì)應(yīng)計(jì)數(shù)數(shù)組中下標(biāo)(元素值-min)的計(jì)數(shù)值员串,就是它的最終位置勇哗,并更新其計(jì)數(shù)-1,代表下一次相同元素要放置的位置:

    1. 遍歷[3,1,2,1,5]最后一個(gè)數(shù)5,它的計(jì)數(shù)下標(biāo)位置為:5-min=5-1=4,查看計(jì)數(shù)數(shù)組下標(biāo)為4的值寸齐,為5欲诺,代表代表它放置在新數(shù)組的第5位[,,,,5],此時(shí)访忿,計(jì)數(shù)數(shù)組更新5的計(jì)數(shù)為:5-1=4瞧栗,代表下一次相同元素應(yīng)該放置的位置[2,3,4,4,4]斯稳;
    2. 遍歷倒數(shù)第二個(gè)數(shù)1海铆,它的計(jì)數(shù)下標(biāo)位置:1-1=0,查看計(jì)數(shù)數(shù)組下標(biāo)為0的值挣惰,為計(jì)數(shù)2卧斟,代表它放置在新數(shù)據(jù)的第2數(shù),[,1,,,5]憎茂,此時(shí)珍语,計(jì)數(shù)數(shù)組更新1的計(jì)數(shù)為:2-1=1,代表下一次相同元素應(yīng)該放置的位置竖幔,計(jì)數(shù)數(shù)組更新為:[1,3,4,4,4]板乙;
    3. 遍歷倒數(shù)三個(gè)數(shù)2,它的計(jì)數(shù)下標(biāo)位置: 2-1=1拳氢,查看計(jì)數(shù)數(shù)組下標(biāo)為1的值募逞,為計(jì)數(shù)3,代表它放置在新數(shù)據(jù)的第3位馋评,[,1,2,,5]放接,此時(shí),計(jì)數(shù)數(shù)組更新2的計(jì)數(shù)為:3-1=2留特,代表下一次相同元素應(yīng)該放置的位置纠脾,計(jì)數(shù)數(shù)組更新為:[1,2,4,4,4];
    4. 遍歷倒數(shù)第四個(gè)數(shù)1蜕青,它的計(jì)數(shù)下標(biāo)位置1-1=0苟蹈,計(jì)數(shù)數(shù)組下標(biāo)為1的值已經(jīng)在上一次更新為1,所以此時(shí)右核,把它放在新數(shù)組第1的位置[1,1,2,,5],計(jì)數(shù)數(shù)組更新為[0,2,4,4,4]
    5. 遍歷第一個(gè)數(shù)3慧脱,它的計(jì)數(shù)下標(biāo)位置為:3-min = 3-1 = 2,查看計(jì)數(shù)數(shù)組下標(biāo)為2的值蒙兰,為4磷瘤,代表它放置在新數(shù)組的第4位[1,1,2,3,5]芒篷,計(jì)數(shù)數(shù)組更新為[0,2,3,4,4],至此采缚,排序結(jié)束
      這時(shí)候發(fā)現(xiàn)针炉,越往后的出現(xiàn)相同元素的位置越后,這樣就實(shí)現(xiàn)穩(wěn)定的計(jì)數(shù)排序了
   /**
 * @description 優(yōu)化版:實(shí)現(xiàn)穩(wěn)定的計(jì)數(shù)排序
 * @param arr: 初始的無序隊(duì)列(一般為指定范圍)
 * @return res: 計(jì)數(shù)排序后的有序隊(duì)列
 */

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
 // 前面創(chuàng)建計(jì)數(shù)數(shù)組的步驟不變
 let countArr = new Array(countLen).fill(0)
 for(let j = 0; j < len; j++ ) {
   countArr[arr[j] - min]++;
 }
 // 就增加了:從第2個(gè)數(shù)開始扳抽,將后面元素的計(jì)數(shù)變成與前面所有元素的計(jì)數(shù)之和 
 for(let z = 1; z < countLen; z++) {
   // 加上前一位的計(jì)數(shù)次數(shù)(也就是之前所有元素的計(jì)數(shù)之和)  
   countArr[z] += countArr[z -1]
 }
 console.log('2222', countArr); // 此時(shí)篡帕,計(jì)數(shù)數(shù)組:[ 2, 3, 4, 4, 5 ]
 
 // 結(jié)果隊(duì)列  
 let res = new Array(len)
 let countIndex = 0
 // 對(duì)初始序列,進(jìn)行從后往前遍歷  
 for(let k = len - 1; k >= 0; k--) {
   //  獲取元素對(duì)應(yīng)的計(jì)數(shù)
   countIndex = countArr[arr[k] - min]
   // 因?yàn)橄聵?biāo)0占了一位贸呢,所以下標(biāo)要減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 ]

計(jì)數(shù)排序的局限性

雖然計(jì)數(shù)排序的時(shí)間復(fù)雜度只有O(n)镰烧,為什么我們很少用到它呢?因?yàn)樗斜容^大的局限性:

  1. 當(dāng)數(shù)列最大和最小值的差距過大時(shí)楞陷,并不適合怔鳖;過大的差距意思空間上的消耗也要隨之變 大。不但浪費(fèi)空間固蛾,而且時(shí)間復(fù)雜度也會(huì)隨之升高
  2. 當(dāng)數(shù)列元素不是整數(shù)時(shí)结执,也不適合。比如小數(shù)艾凯,就無法根據(jù)元素創(chuàng)建對(duì)應(yīng)的計(jì)數(shù)數(shù)組献幔。

總結(jié)

  • 時(shí)間復(fù)雜度:
    1. 獲取最大最小值O(n)
    2. 獲取計(jì)數(shù)數(shù)組O(n)
    3. 獲取累加計(jì)數(shù)數(shù)組O(n)
    4. 遍歷初始數(shù)組O(n)
      總的是O(4n),還是約等于O(n)
  • 空間復(fù)雜度
    1. 創(chuàng)建計(jì)數(shù)數(shù)組空間O(n)
    2. 結(jié)果隊(duì)列O(n)
      總的是O(2n)趾诗,還是約等于O(n)
  • 是一種典型的空間換時(shí)間的算法
  • 優(yōu)化后的計(jì)數(shù)排序已經(jīng)是穩(wěn)定的排序方法

排序算法系列文章傳送門(未完蜡感,持續(xù)更新中):
排序算法-1(javascript) 冒泡、選擇恃泪、插入郑兴、希爾排序的實(shí)現(xiàn)
排序算法-2(javascript) 快速排序的實(shí)現(xiàn)
排序算法-3(javascript) 堆排序的實(shí)現(xiàn)
排序算法-4(javascript) 歸并排序的實(shí)現(xiàn)
排序算法-5(javascript) 計(jì)數(shù)排序的實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市悟泵,隨后出現(xiàn)的幾起案子杈笔,更是在濱河造成了極大的恐慌,老刑警劉巖糕非,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒙具,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡朽肥,警方通過查閱死者的電腦和手機(jī)禁筏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衡招,“玉大人篱昔,你說我怎么就攤上這事。” “怎么了州刽?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵空执,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我穗椅,道長(zhǎng)辨绊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任匹表,我火速辦了婚禮门坷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘袍镀。我一直安慰自己默蚌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布苇羡。 她就那樣靜靜地躺著绸吸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宣虾。 梳的紋絲不亂的頭發(fā)上惯裕,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音绣硝,去河邊找鬼。 笑死撑刺,一個(gè)胖子當(dāng)著我的面吹牛鹉胖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播够傍,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼甫菠,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了冕屯?” 一聲冷哼從身側(cè)響起寂诱,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎安聘,沒想到半個(gè)月后痰洒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡浴韭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年丘喻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片念颈。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泉粉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嗡靡,我是刑警寧澤跺撼,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站讨彼,受9級(jí)特大地震影響财边,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜点骑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一酣难、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧黑滴,春花似錦憨募、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至晚缩,卻和暖如春尾膊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背荞彼。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工冈敛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鸣皂。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓抓谴,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親寞缝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子癌压,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容