js十大排序算法詳解


js十大排序算法詳解

 原文指路: https://www.cnblogs.com/liyongshuai/p/7197962.html

十大經典算法導圖 

 

圖片名詞解釋:
n: 數據規模
k:“桶”的個數
In-place: 占用常數內存,不占用額外內存
Out-place: 占用額外內存

1.冒泡排序

1.1  原始人冒泡排序

function bubbleSort(arr) {
  var len = arr.length;
  for (var i = 0; i < len; i++) {
    for (var j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j+1]) { //相鄰元素兩兩對比
        var temp = arr[j+1]; //元素交換
        arr[j+ 1] = arr[j];
        arr[j] = temp;
      }
    }
  }
  return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50] ;
這種算法不多說,有點變成基礎的人都能看明白,可以說是“傻瓜排序” 
1.2進化版冒泡排序
function bubbleSort2(arr) {
  console.time('改進后冒泡排序耗時');
  var i = arr.length-1; //初始時,最后位置保持不變  
  while ( i> 0) {
    var pos= 0; //每趟開始時,無記錄交換
    for (var j= 0; j< i; j++){
      if (arr[j]> arr[j+1]) {
        pos= j;  //記錄交換的位置
        var tmp = arr[j]; arr[j]=arr[j+1];arr[j+1]=tmp;
      }
    }
    i= pos; //為下一趟排序作准備
  }
  console.timeEnd('改進后冒泡排序耗時');
  return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort2(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50] ;
“進化版”冒泡排序算法相對於“原始人”冒泡排序有個亮點,就是每一層的循環都記錄上一次排序的位置,這兩種排序算法都是先排最后一位,最后一位是最大的,然后以此類推。細細推敲第二種方法顯然比第一種方法少走了一些冤枉路,也就是說每一層排完序之后,就記錄排到最大的哪一位在什么位置,因為每一層最大的數就是它所在數組的倒數的位數,因此下一次就沒必要再循環一遍,相對於第一種就少進行了很多計算。
1.3.升級版冒泡排序
function bubbleSort3(arr3) {
  var low = 0;
  var high= arr.length-1; //設置變量的初始值
  var tmp,j;
  console.time('2.改進后冒泡排序耗時');
  while (low < high) {
    for (j= low; j< high; ++j) {         //正向冒泡,找到最大者
      if (arr[j]> arr[j+1]) {
        tmp = arr[j]; arr[j]=arr[j+ 1];arr[j+1]=tmp;
      }
    }
    --high;   //修改high值, 前移一位
    for (j=high; j>low; --j) {          //反向冒泡,找到最小者
      if (arr[j]<arr[j-1]) {
        tmp = arr[j]; arr[j]=arr[j -1];arr[j-1]=tmp;
      }
    } 
    ++low;   //修改low值,后移一位
  }
  console.timeEnd('2.改進后冒泡排序耗時');
  return arr3;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort3(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50] ;
這種排序方式也算是錦上添花,因為前兩次的排序都是按最大或者最小方向進行排序,而第三種方法會選擇從兩頭出發一起計算,雙管齊下!
1.4 自創版冒泡排序

function bubbleSort3(arr3) {
  var low = 0;
  var high= arr.length-1; //設置變量的初始值
  var tmp,j;
  console.time('3.改進后冒泡排序耗時');
  while (low < high) {
    var pos1 = 0,pos2=0;
    for (let i= low; i< high; ++i) { //正向冒泡,找到最大者
      if (arr[i]> arr[i+1]) {
        tmp = arr[i]; arr[i]=arr[i+1];arr[i+1]=tmp;
        pos1 = i ;
      }
    }

    high = pos1;// 記錄上次位置

    for (let j=high; j>low; --j) { //反向冒泡,找到最小者
      if (arr[j]<arr[j-1]) {
        tmp = arr[j]; arr[j]=arr[j-1];arr[j-1]=tmp;  
        pos2 = j;
      }
    }   
    
    low = pos2; //修改low值
  }
  console.timeEnd('3.改進后冒泡排序耗時');
  return arr3;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort3(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50] ;

既然每次記錄位置可以減少計算,兩頭算雙管齊下也能減少計算,那么思考,如果每次記錄位置而且還兩頭算是不是會更加省事呢?(根據1.2,1.3自創)

但是冒泡排序也有弊端,就是兩種極端的情況,一種是數據本來就是正序,那做的就是無用功,另外一種就是反序,不想理你。。。具體怎么弊端想想也就知道了

冒泡排序動圖演示

2.選擇排序

 相對於冒泡排序還有一種類似的方法就是選擇排序,顧名思義就是選擇性排序,什么意思呢?
這么來理解,假設在三伏天有一趟室內游泳課,教練說了先在露天場地等着,從你們當中先選取最大個先進去,然后再從剩余的人中選擇最大個進去,依次類推。那么小個的就在想了,教練你TMD的腦子是不是被驢踢了。但是如果是冒泡排序那更有意思了,所有的人先排好隊再進去,這樣還好一點最起碼每個人的心理能平衡一點。簡單理解選擇排序就是從一個未知數據空間,選取數據之最放到一個新的空間。
廢話不多說,看例子:
2.1選擇排序
function selectionSort(arr) {
  var len = arr.length;
  var minIndex, temp;
  console.time('選擇排序耗時');
  for (var i = 0; i < len - 1; i++) {
    minIndex = i;
    for (var j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) { //尋找最小的數
        minIndex = j; //將最小數的索引保存
      }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
  console.timeEnd('選擇排序耗時');
  return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(selectionSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];
經小編測試,選擇排序似乎比冒泡排序的自創版還要省時間,其實選擇排序適合小數據排序,具體這個小數據有多小呢,簡單的測試了一下,在1000條以內的數據,選擇排序更勝1.3冒泡排序。

選擇排序動圖
 
 
3.插入排序

插入排序的原理其實很好理解,可以類比選擇排序。選擇排序時在兩個空間進行,等於說每次從舊的空間選出最值放到新的空間,而插入排序則是在同一空間進行。
可以這么理解,在一個數組中我們不知道哪個是最小值,那么就假定第一個就是最小值,然后取第二個值與第一個值比較產排序后的序列,然后再取第三個值與排序后的序列進行比較插入到對應的位置,依次類推。
打個比方就類比水滸傳一百單八將的排名吧,每個好漢來了不知道自己排老幾,怎么辦,那就和已經排過級別的人比較,然后找到其對應的位置,單八將宋萬、杜遷先上的梁山,先默認杜遷第一來的也是單八將最厲害的,然后宋萬來了,一比較宋萬厲害,那宋萬排第一,杜遷排第二,接下來朱貴來了,朱貴沒他們兩個厲害,那就排第三,再后來林沖來了,林沖比他們三個都厲害,那林沖排第一,宋萬第二,杜遷第三,朱貴第四,依次類推。梁山排名其實就是典型的插入排序。
3.1插入排序
function insertionSort(array) {
  console.time('插入排序耗時:');
  for (var i = 1; i < array.length; i++) {
    var key = array[i];
    var j = i - 1;
    while ( array[j] > key) {
      array[j + 1] = array[j];
         j--;
    }
    array[j + 1] = key;
  }
  console.timeEnd('插入排序耗時:');
  return array;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(insertionSort(arr));
 
3.2升級版  二分法插入排序

function binaryInsertionSort(array) {
  console.time('二分插入排序耗時:');
  for (var i = 1; i < array.length; i++) {
    var key = array[i], left = 0, right = i - 1;
    while (left <= right) {
      var middle = parseInt((left + right) / 2);
      if (key < array[middle]) {
        right = middle - 1;
      } else {
        left = middle + 1;
      }
    }
    for (var j = i - 1; j >= left; j--) {
      array[j + 1] = array[j];
    }
    array[left] = key;
  }
  console.timeEnd('二分插入排序耗時:');
  return array;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(binaryInsertionSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];

二分法插入排序第一遍讀下去,一臉懵逼,寫的是什么鬼,仔細琢磨一下卻別有一番風味,聽小編慢慢講下去,首先外層循環沒什么疑問,就是簡單的遍歷一遍數組,那么先看while循環,left和right兩個變量可以簡單的類比3.1中的已排序的首末兩個位置,然后選取未排序的第一個值和已排序的中間位置的值進行比較,這樣的話也就是在最壞的情況下每層循環也只是計算了已排序的序列長度的一半的次數,簡而言之就是在無限逼近left和right值,找到未排序第一個值應該在的位置。

還是以梁山排名為例子,在宋江沒有到梁上之前,每個上梁上的人跟已經排過名的從大往小進行比較,然后找到自己的位置,在老大宋江來之后,后續人慢慢多了,然后宋老大就訂了條規矩,就是每個新來的人和已排過名次的位於中間名次的好漢進行比較,勝了往前一位比較,敗了往后一位比較,然后找到自己的位置。好了,while循環解釋完畢,那么下面又多了一條for循環,這又是什么鬼?

不要着急,待小編與你慢慢道來,看不懂沒關系,先看循環體,循環體的意思就是把前一個值給后一個,然后看循環條件是從i-1的位置從后往前依次將前一個元素的值給后一個,先不要管i-1是誰,先問 i 是誰,i 不就是未排序的第一個元素么,不就是我們拿來對已進行排序的元素么,簡而言之不就是新上梁山的好漢么,那么從left值開始到 i-1 的位置依次將前一個元素的值給后一個無非就是空出 left 的位置,left 的位置不就是新上梁上好漢的位置!

插入排序法動圖:

 

4.希爾排序

希爾排序,直接上圖;

像這個算法看圖理解起來並不是很難,就像比賽一樣,1-6一組,2-7一組,每差5為一組進行比較,之后再每差2為一組進行比較,最后就是兩兩比較,有點類似冒泡算法,但又比冒泡多了一層增量的概念。起初小編看到這個導圖的時候感覺編程挺簡單的,無非就是改變一下增量,這有何難?人吶,都是眼高手低,廢話不多說直接看代碼:

function shellSort(arr) {
  var len = arr.length,
  temp,
  gap = 1;
  console.time('希爾排序耗時:');
  while(gap < len/5) { //動態定義間隔序列
    gap =gap* 5+1;
  }
  for (gap; gap > 0; gap = Math.floor(gap/5)) {
    for (var i = gap; i < len; i++) {
      temp = arr[i];
      for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
        arr[j+gap] = arr[j];
      }
      arr[j+gap] = temp;
    }
  }
  console.timeEnd('希爾排序耗時:');
  return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(shellSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];
當讀到while循環時,我類個去,什么鬼?什么意思?再往下讀,好了,放棄吧!看起來雲里霧里,變量附來附去,什么意思?
抽象看不明白那就實例化,以arr為例代進去一看究竟,len/5 以五步為增量,這好像是說的通,但是gap = gap *5 +1 ;這是什么鬼?
別着急慢慢讀,len = 15 ,gap = 6 ;while循環結束。再往下讀,var i = gap ;也就是說從i = 6開始循環一直到數組末尾,然后從i=6開始記錄元素,而對於下標為 j 的for循環,則是從0開始,然后以步長為6開始比較,接下來就會發現一個問題,j-=gap ? 又是什么鬼? j=0,j-=gap ,那 j 不就是負的了么?編者這么寫是有他的理由的,對,在下標為6之前的元素之循環了一次,那下邊如果超過6呢,所以小編覺得這個地方也算是個亮點吧!
還沒結束,這層內層循環結束了,跳了出來,gap = Math.floor(gap/5) 又是什么玩意?只是常規的思想局限了創新,這個不就是與while循環的gap = gap *5+1 ;與之相應么?這么做有什么好處呢,也就是說無論數據有多大,最終肯定會走到每隔6步為已增量的循環中,這就是希爾排序的亮點所在,而且前面定義的gap=1;還有gap = gap*5+1 ;這個1不是隨便定義的,因為最終回歸到的就是增量為1的循環當中!
 
5.歸並排序
歸並排序其實可以類比二分法,二分法其實就是二等分的意思,簡而言之就是不斷和新序列的中間值進行比較。歸並排序似乎有異曲同工之妙,什么意思呢,就是將一個原始序對等分為兩部分,然后不斷地對等分新的序列,直至序列的長度為1或者2,那么想,如果一個序列為1,那就沒有比較的意義了,它本身就是之最,如果是兩個呢,那直接比較不就完了,把比較之后的值推送到一個新的數組。就這樣不斷地細分,不斷的產生子序列,然后把穿產生的新序列作為新的父序列,然后同等級的父序列再比較產生新的祖序列,依次類推。
有點抽象了,那就具體化,比如現在有個十萬人的司令部,習大大是首長,習大大跟司令說了,把所有的人按年齡排序,司令想了,讓我一個人也忙活不過來啊,這怎么辦,然后就把任務下達給軍長,軍長下達給師長,依次類推,排長再把一個排分成兩個小隊,小隊再分成兩個小組,最后分成兩個人一組或一人一組,接下來就是組員之間進行比較,完了小隊與小隊比較,排與排之間比較,依次類推,最后軍團和軍團比較,形成最后的序列。
廢話不多說,看代碼:
function mergeSort(arr) { //采用自上而下的遞歸方法
  var len = arr.length;
  if(len < 2) {
    return arr;
  }
  var middle = Math.floor(len / 2),
  left = arr.slice( 0, middle),
  right = arr.slice(middle);
  return merge(mergeSort(left), mergeSort(right));
}
 
function merge(left, right){
  var result = [];
  console.time('歸並排序耗時');
  while (left.length && right.length) {
    if (left[0] <= right[0]) {
      result.push(left.shift());
    } else {
      result.push(right.shift());
    }
  }
 
  while (left.length){
    result.push(left.shift());
  }
  while (right.length){
    result.push(right.shift());
  }
  console.timeEnd('歸並排序耗時');
  return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(mergeSort(arr));

 其實代碼並不難理解,小編就不詳解了。

配上動圖加深印象:

 

6.快速排序

 既然是快速排序,那顧名思義一定很快,快的連小編都被懵逼了好幾圈!建議先不要看動圖,先看第一種寫法:

3.1 抽象版快速排序

function quickSort(array, left, right) {
  console.time('1.快速排序耗時');
  if (left < right) {
    var x = array[right], i = left - 1, temp;
    for (var j = left; j <= right; j++) {
      if (array[j] <= x) {
        i++;
        temp = array[i];
        array[i] = array[j];
        array[j] = temp;
      }
    }
    console.log(array) ;
    console.log(left,i) ;
    quickSort(array, left, i - 1);
    console.log(array)
    console.log(i,right)
    quickSort(array, i + 1, right);
  }
  console.timeEnd('1.快速排序耗時');
  console.log(array)
  return array;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort(arr,0,arr.length-1));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];

看完代碼一臉懵逼,這是人寫的么?瞬間覺得自己弱爆了,連別人代碼都看不懂,更別說自己寫了,別着急,一點點拆分看。

先看一個疑問點,函數中的參數有三個,第一個數組,沒得說;第二個是左值,第三個是右值;好,到這里先分析結束,首先給讀者一種什么感覺,就是這個排序算法是從左右兩端依次逼近完成排序的,那么對於這個猜想對不對呢?

接着看,if條件語句中判斷left < right,這沒得說,就是從左到右排序的,而且if 如果不成立直接結束本層循環了,那如果滿足條件呢,直接進入for循環,而且在進入for循環之前先記錄了一個本次循環的末尾值,又設置一個i ,還有一個空變量,都分別又是什么意思呢?

接着看,for循環遍歷本層循環,然后依次和末尾值進行比較,那么可想而知,這個變量x無非就是個基數,好了,算法的亮點來了,就是 i 值,如果本層循環某個元素大於本層循環的基數,那么置換兩者的位置,那么 i 的作用就是計數的作用,而 temp 就是作為交換暫時存儲的介質,然后這樣下來就是把每次本層循環的最大值放到了最后,這樣下來在quickSort(array, left, i - 1);不斷遞歸循環之后,該數組的右邊最小值大於左邊的最大值(這里的左邊和右邊不一定等分),而且左邊的順序已經排好了,然后同理排右邊的部分,這樣下來函數結束之后就完成了排序。(暫時小編能理解的大概就是這種程度了,不當之處,還望博友指點一二)

3.2 形象版快速排序

var quickSort2 = function(arr) {
  console.time('2.快速排序耗時');
  if (arr.length <= 1) { return arr; }
  var pivotIndex = Math.floor(arr.length / 2);
  var pivot = arr.splice(pivotIndex, 1)[0];
  console.log(pivot)
  var left = [];
  var right = [];
  for (var i = 0; i < arr.length; i++){
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  console.timeEnd('2.快速排序耗時');
  return quickSort2(left).concat([pivot], quickSort2(right));
};
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort2(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];

看完第一種寫法之后,有種放棄的念頭,不要着急,慢慢撥開迷霧你能感受到快速排序的奇特之處。

廢話不多說直接看代碼,第二種開始還能理解,哦,原來和第一種寫法類似,第二種則是選擇中中間數作為基數進行比較,然后再遍歷比較,把比中間值小的放在left數組,把比中間值大的放在right數組中,這種寫法再簡單不過了,而看到后面return quickSort2(left).concat([pivot], quickSort2(right)); 這是什么鬼?是不是寫錯了,怎么感覺那么不對勁呢?不要懷疑經典,拆分代碼看,哦,原來是不斷把數組細分化,分到數組長度為1的最小單位,然后再把左右兩個數組拼湊起來,試想每層基循環都有左右兩個長度為1的數組,且左數組元素比右數組元素值小,而基循環的基數又是兩基數組元素的中間數,那這不就比較完了嗎,把三者拼湊起來不正是排序后的序列么,使用遞歸依次類推形成最后的數組。就是這么簡單,完畢。

配上一個動圖,第一次看可能會很懵逼,配合代碼多看幾遍或許能明白其巧妙之處。

 

7.堆排序

這種排序方式呢,理論性太強,看動圖的時候滿臉寫着懵逼,多看幾遍似乎明白了編者的意圖,但是要把這種理論的概念寫成代碼卻不容易,且看代碼:

function heapSort(array) {
  console.time('堆排序耗時');
  //建堆
  var heapSize = array.length, temp;
  for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {  
    heapify(array, i, heapSize);
  }
  //堆排序
  for (var j = heapSize - 1; j >= 1; j--) {
    temp = array[0];
    array[0] = array[j];
    array[j] = temp;
    console.log(array)
    heapify(array, 0, --heapSize);
  }
  console.timeEnd('堆排序耗時');
  return array;
}
function heapify(arr, x, len) {
  var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
  if (l < len && arr[l] > arr[largest]) {
    largest = l;
  }
  if (r < len && arr[r] > arr[largest]) {
    largest = r;
  }
  if (largest != x) {
    temp = arr[x];
    arr[x] = arr[largest];
    arr[largest] = temp;
    console.log(arr)
    heapify(arr, largest, len);
  }
}
var arr=[91,60,96,13,35,65,46,65,10,30,20,31,77,81,22];
console.log(heapSort(arr));//[10, 13, 20, 22, 30, 31, 35, 46, 60, 65, 65, 77, 81, 91, 96];

這種算法有兩個難點,一是建堆,而是堆排序。首先明白什么是堆,堆其實可以這么理解,類似金字塔,一層有一個元素,兩層有兩個元素,三層有四個元素,每層從數組中取元素,從左到右的順序放到堆相應的位置上,也就是說每一層元素個數為2n-1 ;(n 代表行數),這就完成了建堆。

那么想,堆排序中最后一位不就是2n-m(n代表總行數,m代表差多少位不到完成堆的位數),那該元素的父級是誰,2n-1-m/2,2n-1-m/2是誰?拿總位數除以2就知道了,沒錯就是數組的中間值,這也是編者為什么從中間值入手的原因了。

而對於 l = 2*x +1 與 r = 2*x+2 ,不正是每個父級元素對應的子堆么,每一層的堆排序都能夠把本層的最大值剔除出來,這樣當所有 層循環結束之后,序列也就完成了。

這一點小編覺得和歸並排序有點類似,都是細分到最小單元,從最小單元比較,但是同歸並排序有兩大點不同,一是堆排序並不像歸並那么無序,只是一味的平分數組,而堆排序則是按原始序列排出金字塔式的結構,把最大值一層層往上冒,冒到金字塔最頂端的時候把它踢出來,這樣達到排序的效果。

附動圖,不多看幾遍是看不出來什么門道的:

8.計數排序

計數排序就是遍歷數組記錄數組下的元素出現過多次,然后把這個元素找個位置先安置下來,簡單點說就是以原數組每個元素的值作為新數組的下標,而對應小標的新數組元素的值作為出現的次數,相當於是通過下標進行排序。

看代碼:

function countingSort(array) {
  var len = array.length,
  B = [],
  C = [],
  min = max = array[0];
  console.time('計數排序耗時');
  for (var i = 0; i < len; i++) {
    min = min <= array[i] ? min : array[i];
    max = max >= array[i] ? max : array[i];
    C[array[i]] = C[array[i]] ? C[array[i]] + 1 : 1;
    console.log(C)
  }

  // 計算排序后的元素下標
  for (var j = min; j < max; j++) {
    C[j + 1] = (C[j + 1] || 0) + (C[j] || 0);
    console.log(C)
  }
  for (var k = len - 1; k >= 0; k--) {
    B[C[array[k]] - 1] = array[k];
    C[array[k]]--;
    console.log(B)
  }
  console.timeEnd('計數排序耗時');
  return B;
}
var arr = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log(countingSort(arr)); //[1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9];

這種算法的亮點就是在於利用下標存數據,利用數據存出現的次數。然后這種算法還有一個亮點就是第二個循環,計算排序后的下標,也就是說在這個地方已經把每個元素對應在排序后的數組的位置已經確定了,在第三個循環中只需要安插在對應的位置即可!

其實這里小編還另外一種算法,沒有上面那種復雜,小編感覺更容易理解,僅供參考:

function countingSort(array) {
  var len = array.length,
  B = [],
  C = [],
  min = max = array[0];
  console.time('計數排序耗時');
  for (var i = 0; i < len; i++) {
    min = min <= array[i] ? min : array[i];
    max = max >= array[i] ? max : array[i];
    C[array[i]] = C[array[i]] ? C[array[i]] + 1 : 1;
  }
  for (var k = 0; k <len; k++) {
    var length = C[k];
    for(var m = 0 ;m <length ; m++){
      B.push(k);
    }
  }
  console.timeEnd('計數排序耗時');
  return B;
}
var arr = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log(countingSort(arr)); //[1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9];
思想主要是既然我們已經根據下標進行排序了,C數組的下標對應的數值就是該下標出現的次數,那何不吧該次數作為二層循環的長度遍歷一遍直接推送到新得數組中呢?

附動圖便於理解:

 

9. 桶排序

一看到這個名字就會覺得奇特,幾個意思,我排序還要再准備幾個桶不成?還真別說,想用桶排序還得真准備幾個桶,但是此桶非彼桶,這個桶是用來裝數據用的。其實桶排序和計數排序還有點類似,計數排序是找一個空數組把值作為下標找到其位置,再把出現的次數給存起來,這似乎看似很完美,但也有局限性,不用小編說相信讀者也能明白,既然計數是把原數組的值當做下標來看待,那么該值必然是整數,那假如出現小數怎么辦?這時候就出現了一種通用版的計數排序——桶排序。

小編覺得桶排序可以這么理解,它是以步長為分隔,將最相近數據分隔在一起,然后再在一個桶里排序。好了,現在有個概念,步長是什么玩意?這么來說吧,比如在知道十位的情況下48和36有比較的必要嗎?顯然沒有,十位就把你干下去了,還比什么?那在這里可以簡單的把步長理解為10,桶排序就是這樣,先把同一級別的分到一組,由同一級別的元素進行排序。

代碼實現:

@param array 數組
@param num 桶的數量

function bucketSort(array, num) {
  if (array.length <= 1) {
    return array;
  }
  var len = array.length, buckets = [], result = [], min = max = array[0], space, n = 0;

  var index = Math.floor(len / num) ;
  while(index<2){

    num--;
    index = Math.floor(len / num) ;
  }

  console.time('桶排序耗時');
  for (var i = 1; i < len; i++) {
    min = min <= array[i] ? min : array[i];
    max = max >= array[i] ? max : array[i];
  }
  space = (max - min + 1) / num;  //步長
  for (var j = 0; j < len; j++) {
    var index = Math.floor((array[j] - min) / space);
    if (buckets[index]) { // 非空桶,插入排序
      var k = buckets[index].length - 1;
      while (k >= 0 && buckets[index][k] > array[j]) {
        buckets[index][k + 1] = buckets[index][k];
        k--;
      }
      buckets[index][k + 1] = array[j];
    } else { //空桶,初始化
      buckets[index] = [];
      buckets[index].push(array[j]);
    }
  }
  while (n < num) {
    result = result.concat(buckets[n]);
    n++;
  }
  console.timeEnd('桶排序耗時');
  return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bucketSort(arr,4));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];

但是這邊有個坑點,就是桶的數量不能過多,也就說說至少兩個桶!為什么?你試下就知道了!

附圖理解:

10.基數排序

其實基數排序和桶排序挺類似的,都是找一個容器把屬於同一類的元素裝起來,然后進行排序。可以把基數排序類比成已知該序列的最高位,然后以除去相對來說的最低位(可能是個位,可能是十位)剩余的位數為桶數,這樣一來步長就是10或者100了。但是基數排序相對桶排序又有多了一個亮點,那就是基數排序是先排最低位(個位),把最低位一致的放在一個桶里,然后依次取出,再進一位(十位),把十位相同的再放到一個桶里,然后再取出,這樣經過兩次重排序就能得到百位以內的排序序列了,百位,千位也是如此。

 

 

/**
* 基數排序適用於:
* (1)數據范圍較小,建議在小於1000
* (2)每個數值都要大於等於0
* @author damonare
* @param arr 待排序數組
* @param maxDigit 最大位數
*/
//LSD Radix Sort
 
function radixSort(arr, maxDigit) {
  var mod = 10;
  var dev = 1;
  var counter = [];
  console.time('基數排序耗時');
  for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
    for(var j = 0; j < arr.length; j++) {
      var bucket = parseInt((arr[j] % mod) / dev);
      if(counter[bucket]== null) {
        counter[bucket] = [];
      }
    counter[bucket].push(arr[j]);
    }
    var pos = 0;
    for(var j = 0; j < counter.length; j++) {
      var value = null;
      if(counter[j]!=null) {
        while ((value = counter[j].shift()) != null) {
          arr[pos++] = value;
        }
      }
    }
  }
  console.timeEnd('基數排序耗時');
  return arr;
}
var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log(radixSort(arr,2)); //[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50];
但是基數排序也有個弊端,就是必須知道最高位有多少位。
附動圖:
 
 
 

基數排序 vs 計數排序 vs 桶排序

這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:

  1. 基數排序:根據鍵值的每位數字來分配桶
  2. 計數排序:每個桶只存儲單一鍵值
  3. 桶排序:每個桶存儲一定范圍的數值


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM