基于Javascript的排序算法

寫(xiě)在前面

個(gè)人感覺(jué):javascript對(duì)類似排序查找這樣的功能已經(jīng)有了很好的封裝,以致于當(dāng)我們想對(duì)數(shù)組排序的時(shí)候只需要調(diào)用arr.sort()方法,而查找數(shù)組元素也只需要調(diào)用indexOf()方法或lastIndexOf()方法,我們忽略了其內(nèi)部的實(shí)現(xiàn)。而今,js能開(kāi)發(fā)的項(xiàng)目越來(lái)越龐大,對(duì)性能和效率要求也越來(lái)越高,雖然眾多的庫(kù)和框架也可以幫我們應(yīng)付這些問(wèn)題,但小編覺(jué)得框架過(guò)眼云煙,把握程序開(kāi)發(fā)的基礎(chǔ),才能在飛速的更新?lián)Q代中應(yīng)對(duì)自如。因此我們不妨也研究一下這些算法,其中的思路有助于我們自身的提高。

聲明:本文章中的部分圖片來(lái)自百度搜索,如侵刪。

冒泡排序

這個(gè)是最簡(jiǎn)單的排序,就像氣泡從水里冒出來(lái)。
它每執(zhí)行一次外層循環(huán),就會(huì)將最小數(shù)(或最大的)放到數(shù)組最后,然后再尋找剩余部分的最小數(shù)(或最大的)放在這一部分的最后,以此類推。
每一個(gè)外層循環(huán)的過(guò)程可以用一下圖來(lái)描述:

冒泡排序

冒泡排序的時(shí)間復(fù)雜度為O(n^2),空間復(fù)雜度為O(1),屬于 穩(wěn)定 排序。適用于數(shù)據(jù)比較少或基本有序的情況。

//冒泡排序
bubbleSort = function(arr){
  var len = arr.length;
  for (var i = 0; i < len; i++){
    for (var j = 0; j < len - i - 1; j++){
      if (arr[j] > arr[j + 1])
        [arr[j + 1], arr[j]] = [arr[j],arr[j + 1]];
    }
  }
}

選擇排序

選擇排序也很簡(jiǎn)單。它每一次從待排序的數(shù)據(jù)元素中選出最?。ɑ蜃畲螅┑囊粋€(gè)元素,存放在序列的起始位置,直到全部待排序的數(shù)據(jù)元素排完。下面是完整的選擇排序過(guò)程:

選擇排序

選擇排序的時(shí)間復(fù)雜度為O(n^2),空間復(fù)雜度為O(1),屬于 穩(wěn)定 排序。適用于數(shù)據(jù)比較少的情況,綜合各種情況來(lái)講還是這個(gè)最慢。

//選擇排序
 selectionSort = function(arr){
  var len = arr.length;
  var min, min_index;//min每次找到的最小值,min_index最小值在無(wú)序序列的位置
  for (var i = 0; i < len - 1; i++){
    min = arr[i];
    for (var j = i + 1; j < len; j++){//找到最小值
      if (arr[j] < min){
        min = arr[j];//找到的最小值
        min_index = j;//找到的最小值索引
      }
    }
    if (min != arr[i])
      [arr[min_index], arr[i]] = [arr[i], arr[min_index]];
  }
}

插入排序

這個(gè)要略微復(fù)雜一點(diǎn)了。它的思路就是將一個(gè)數(shù)據(jù)插入到已經(jīng)排好序的有序數(shù)據(jù)中,依然保持有序。實(shí)現(xiàn)過(guò)程把數(shù)組看作2部分,一部分是有序的,一部分是無(wú)序的,每次大循環(huán)將無(wú)序數(shù)組的第一個(gè)元素插入到有序的數(shù)組中。

插入排序

插入排序時(shí)間復(fù)雜度為O(n^2),空間復(fù)雜度為O(1),屬于 穩(wěn)定 排序。算法適用于少量數(shù)據(jù)的排序。

<small>注:二分插入和直接插入各種情況復(fù)雜度一樣</small>

//直接插入排序
insertionSort = function (arr){
  var len = arr.length;
  var temp;//temp每次要執(zhí)行插入的值
  var index;//index插入值在有序序列的位置
  for (var i = 1; i < len; i++){
    temp = arr[i];
    for (var j = 0; j < i; j++){//找到插入位置
      index = i;
      if (arr[j] > temp){
        index = j;//找到的插入點(diǎn)索引
        break;
      }
    }
    if (i != index){
      for (var j = i; j > index; j--)//插入該值
        [arr[j - 1], arr[j]] = [arr[j],arr[j - 1]];
    }
    arr[index] = temp;
  }
}

快速排序

這個(gè)想必大家都耳熟能詳,20世紀(jì)十大經(jīng)典算法之一。主要原因還是它極大的推動(dòng)了信息技術(shù)的發(fā)展,可惜它不是穩(wěn)定算法。
這個(gè)算法比較就比較難理解了,它通過(guò)一趟排序?qū)⒁判虻臄?shù)據(jù)分割成獨(dú)立的兩部分,其中一部分的所有數(shù)據(jù)都比另外一部分的任一數(shù)據(jù)都要小,然后再按此方法對(duì)這兩部分?jǐn)?shù)據(jù)分別進(jìn)行快速排序,整個(gè)排序過(guò)程可以遞歸進(jìn)行,以此達(dá)到整個(gè)數(shù)據(jù)變成有序序列。這里面包含的分治的思想。
下面一個(gè)圖表現(xiàn)了函數(shù)的一次執(zhí)行過(guò)程:

快速排序思路

而這個(gè)圖表現(xiàn)了整個(gè)排序過(guò)程:

快速排序過(guò)程

快速排序時(shí)間復(fù)雜度為O(nlogn),空間復(fù)雜度為O(logn),屬于 不穩(wěn)定 排序。

////快速排序(前軸)
function quickSort(arr){
  qSort(0, arr.length - 1);
  return arr;

  function qSort(left, right){
    if (left >= right)//兩個(gè)數(shù)相遇則結(jié)束該輪排序
      return;
    var key = arr[left];//取最左邊的元素作為標(biāo)識(shí)數(shù)
    var i = left;
    var j = right;
    while (i != j){//兩個(gè)數(shù)相遇則結(jié)束該輪排序
      while (i != j && arr[j] >= key) j--;//j前移
      [arr[j], arr[i]] = [arr[i], arr[j]];
      while (i != j && arr[i] <= key) i++;//i后移
      [arr[j], arr[i]] = [arr[i], arr[j]];
    }
    qSort(left, j - 1);//對(duì)標(biāo)識(shí)數(shù)前面的數(shù)繼續(xù)該方法排序
    qSort(j + 1, right);//對(duì)標(biāo)識(shí)數(shù)后面的數(shù)繼續(xù)該方法排序
  }
}

這里補(bǔ)充一下:快速排序由于其實(shí)軸的選擇不同,分為前軸、中軸、后軸快速排序,上面的例子是前軸快速排序,下文比較算法的時(shí)候也才用上述代碼。不過(guò),這里補(bǔ)充另外2種代碼:

//中軸快速排序
quickSortM: function(arr){
  qSort(arr, 0, arr.length - 1);
  return arr;

  function qSort(arr, left, right){
    if (left < right){
      var index = Math.floor((left + right) / 2);
      var key = arr[index];
      var i = left - 1;
      var j = right + 1;
      while (true){
        while (arr[++i] < key); // 向右找大于軸的數(shù)
        while (arr[--j] > key); // 向左找小于軸的數(shù)
        if (i >= j)//兩索引相同結(jié)束排序
          break;
        [arr[i], arr[j]] = [arr[j],arr[i]];//交換找到的數(shù)
      }
      qSort(arr, left, i - 1); // 繼續(xù)這樣對(duì)軸前面的排序
      qSort(arr, j + 1, right); // 繼續(xù)這樣對(duì)軸后面的排序
    }
  }
}

//后軸快速排序
function quickSortB(arr){
  qSort(0, arr.length - 1);
  return arr;

  function qSort(left, right){
    if (left >= right)//兩索引相同結(jié)束排序
      return;
    var key = arr[right];
    var i = left - 1;//s是最右邊的軸
    for (var j = left; j < right; j++){ //將數(shù)據(jù)分成大于軸和小于軸兩部分
      if (arr[j] <= key){
        i++;
        [arr[i], arr[j]] = [arr[j],arr[i]];
      }
    }
    i++;
    [arr[right], arr[i]] = [arr[i],arr[right]];//將軸插入到大于軸和小于軸兩部分的中間
    qSort(left, i - 1);//繼續(xù)這樣對(duì)軸前面的排序
    qSort(i, right);//繼續(xù)這樣對(duì)軸后面的排序
  }
}

歸并排序

這個(gè)排序在小編眼里用的是最廣泛的,很多函數(shù)封裝內(nèi)部都才用這個(gè)排序,包括數(shù)據(jù)庫(kù)在內(nèi)的排序也采用了歸并排序或紅黑樹(shù)的形式。這個(gè)排序也用到了分治的思想:它將一個(gè)序列逐級(jí)拆分成小序列,將小序列排序后合并,得到完全有序的序列。若每次將序列分成2個(gè)子序列,再依此合并,稱為二路歸并。
沒(méi)理解?看圖:

歸并排序

歸并排序時(shí)間復(fù)雜度為O(nlogn),空間復(fù)雜度為O(n),屬于 穩(wěn)定 排序。

//歸并排序
mergeSort = function(arr){
  var temp = [];
  merge(arr, 0, arr.length - 1, temp);
  return arr;

  function merge(arr, left, right, temp){//temp是臨時(shí)空間,存放排序過(guò)程中間結(jié)果
    var mid;//該部分中間位置
    if (left >= right)//分組小于等于1時(shí)歸并結(jié)束
      return;
    mid = Math.floor((left + right) / 2);
    merge(arr, left, mid, temp);//對(duì)中間位置之前部分繼續(xù)該方法排序
    merge(arr, mid + 1, right, temp);//對(duì)中間位置之后部分繼續(xù)該方法排序
    var i = left, j = mid + 1, k = left;
    while (i != mid + 1 && j != right + 1)//比較兩部分每個(gè)值,把較小的放入temp中,并后移該指針,直到某部分全部遍歷
      temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
    //將未全部遍歷部分?jǐn)?shù)據(jù)順次放入temp中
    while (i != mid + 1)
      temp[k++] = arr[i++];
    while (j != right + 1)
      temp[k++] = arr[j++];
    //將temp復(fù)制會(huì)a中
    for (i = left; i <= right; i++)
      arr[i] = temp[i];
  }
}

希爾排序

這是惟一一個(gè)用人名命名的排序算法。它把數(shù)據(jù)按下標(biāo)的一定增量分組,對(duì)每組使用直接插入排序算法排序;隨著增量逐漸減少,每組包含的關(guān)鍵詞越來(lái)越多,當(dāng)增量減至1時(shí),整個(gè)文件恰被分成一組,算法便終止。
這個(gè)估計(jì)最不好理解了,看看圖吧:

希爾排序

希爾排序時(shí)間復(fù)雜度為O(n^\frac1{3}),空間復(fù)雜度為O(1),屬于 不穩(wěn)定 排序。

//希爾排序
shellSort = function(arr){
  var len = arr.length;
  var index = Math.floor(len / 2);//得到比較步長(zhǎng)
  var j, temp;
  while (index > 0){
    for (var i = index; i < len; i++){//遍歷起點(diǎn)后移,保證每個(gè)數(shù)在該步長(zhǎng)下參與且只參與1此排序
      temp = arr[i];
      for (j = i; j >= index && arr[j - index] > temp;){//等步長(zhǎng)數(shù)列執(zhí)行插入排序
        arr[j] = arr[j - index];
        j -= index;
        arr[j] = temp;
      }
    }
    index = Math.floor(index / 2);//步長(zhǎng)減半
  }
}

堆排序

首先說(shuō)一下一個(gè)名詞:大根堆。大根堆的要求是每個(gè)節(jié)點(diǎn)的值都不大于其父節(jié)點(diǎn)的值,即A[PARENT[i]] \geq A[i], 屬于完全二叉樹(shù)。
根據(jù)大根堆的要求可知,最大的值一定在堆頂,根據(jù)其特點(diǎn),如果要求每個(gè)節(jié)點(diǎn)的左孩子小于右孩子,得到的就是數(shù)據(jù)從小到大的排列。反之從大到小排列應(yīng)該使用小根堆。
如果你對(duì)二叉樹(shù)熟悉的話,可以簡(jiǎn)單用圖理解一下

堆排序

堆排序時(shí)間復(fù)雜度為O(nlogn),空間復(fù)雜度為O(1),屬于 不穩(wěn)定 排序。
下面利用數(shù)組快速定位指定索引的元素模擬堆操作,并沒(méi)有實(shí)際建立二叉樹(shù)。

//堆排序
heapSort = function(arr){
  var len = arr.length;
  for (var i = len / 2 - 1; i >= 0; i--)//反向遍歷數(shù)組,將數(shù)組調(diào)整為大根堆
    heapAdjust(arr, i, len);
  for (var i = len - 1; i > 0; i--){
    [arr[0], arr[i]] = [arr[i], arr[0]];//將無(wú)需部分最大數(shù)放在最后,即構(gòu)成有序部分
    heapAdjust(arr, 0, i);//將剩余無(wú)需部分調(diào)整為大根堆,直到該部分只有一個(gè)元素為止
  }
  return arr;

  function heapAdjust(arr, i, len){//二叉堆調(diào)整函數(shù),負(fù)責(zé)將堆調(diào)整成大根堆(因?yàn)槭窃鲂蚺帕校?    var child;//根孩子的索引
    var temp;
    //以等倍數(shù)間隔,調(diào)整堆為大根堆
    for (; 2 * i + 1 < len; i = child){
      child = 2 * i + 1;  //定位其左孩子
      if (child < len - 1 && arr[child + 1] > arr[child])//從其左右孩子中選擇最大的孩子
        child++;
      if (arr[i] < arr[child])//如果自己比最大的孩子小,和該孩子交換
        [arr[child], arr[i]] = [arr[i], arr[child]];
      else
        break;
    }
  }
}

基數(shù)排序(桶排序)

這個(gè)排序是對(duì)費(fèi)空間的,不過(guò)這個(gè)思想有點(diǎn)像哈希表的意思。顧名思義,它是透過(guò)鍵值的部份資訊,比如每個(gè)數(shù)的最高位(如果位數(shù)不同在前方補(bǔ)零),將要排序的元素分配至某些“桶”中,依次從低位到高位執(zhí)行,然后再把每個(gè)桶的數(shù)據(jù)順序綜合起來(lái),以達(dá)到排序的作用。就像下圖這樣,可以理解桶的意思:

基數(shù)排序

下圖是整個(gè)排序過(guò)程示意圖:

排序過(guò)程

基數(shù)排序時(shí)間復(fù)雜度為O(d(r+n)),空間復(fù)雜度為O(rd+n),屬于 穩(wěn)定 排序。(其中r為基數(shù),n為數(shù)據(jù)總數(shù),d為桶數(shù);也有書(shū)得到其平均時(shí)間復(fù)雜度為O(nlog_{r}d)

//基數(shù)排序(桶排序)
radixSort = function(arr){
  var len = arr.length;
  var bullet= [];
  var k=1, temp;//k是處理數(shù)字的權(quán)重,k=1表示處理個(gè)位數(shù),k=10表示處理十位數(shù),以此類推
  for (var i = 0; i < 10; i++)//為每個(gè)桶分配內(nèi)存空間
    bullet[i] = [];
  while (true){
    var num = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];//num用來(lái)統(tǒng)計(jì)0~9每個(gè)桶里面現(xiàn)有數(shù)字個(gè)數(shù)
    for (var i = 0; i < len; i++){//統(tǒng)計(jì)分配每個(gè)數(shù)字到桶里面,并統(tǒng)計(jì)每個(gè)桶數(shù)字個(gè)數(shù)
      temp = Math.floor(arr[i] / k) % 10;
      bullet[temp][num[temp]++] = arr[i];
    }
    if (num[0] == len) break;//當(dāng)全部數(shù)字都在編號(hào)為0的桶中,排序結(jié)束
    //將桶里的數(shù)依次放回a數(shù)組中
    for (var i = 0; i < len; i++){
      for (var j = 0; j < 10; j++){
        for (var r = 0; r < num[j]; r++)
          arr[i++] = bullet[j][r];
      }
    }
    k *= 10;//k增加10倍,從右至左處理下一位數(shù)字
  }
  return arr;
}

排序?qū)Ρ?/h2>

以上是常見(jiàn)的8種排序算法,小編也把結(jié)果寫(xiě)出來(lái)把。下面是10個(gè)隨機(jī)數(shù)的排序效果:

數(shù)據(jù)結(jié)果

當(dāng)然還有算法速度,小編用了2萬(wàn)個(gè)均勻分布在0到10000的隨機(jī)數(shù),得到如下結(jié)果:

使用時(shí)間

不過(guò)實(shí)際使用中,并不是越快越好,而且即便是追求快也和數(shù)據(jù)本身的質(zhì)量有關(guān)系。就像下面這個(gè)表中的:

算法 時(shí)間復(fù)雜度(最好) 時(shí)間復(fù)雜度(最好) 時(shí)間復(fù)雜度(最壞) 空間復(fù)雜度 穩(wěn)定性
插入排序 O(n^2) O(n) O(n^2) O(1) 穩(wěn)定
希爾排序 O(n^(1.3)) O(n) O(n^2) O(1) 不穩(wěn)定
選擇排序 O(n^2) O(n^2) O(n^2) O(1) 不穩(wěn)定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩(wěn)定
冒泡排序 O(n^2) O(n) O(n^2) O(1) 穩(wěn)定
快速排序 O(nlogn) O(nlogn) O(n^2) O(nlogn) 不穩(wěn)定
歸并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩(wěn)定
基數(shù)排序 O(d(r+n)) O(d(n+rd)) O(d(r+n)) O(n+rd) 穩(wěn)定

注:

  1. 基數(shù)排序的復(fù)雜度中,r 代表關(guān)鍵字基數(shù),d 代表長(zhǎng)度,n 代表關(guān)鍵字個(gè)數(shù)
  2. 排序算法的穩(wěn)定性指在原序列中,ri=rj,且 ri 在 rj 之前,而在排序后的序列中,ri 仍在 rj 之前,則稱這種排序算法是穩(wěn)定的;否則稱為不穩(wěn)定的。

開(kāi)發(fā)的時(shí)候應(yīng)該綜合排序原始數(shù)據(jù)狀態(tài),需求,穩(wěn)定性,系統(tǒng)資源等諸多因素來(lái)確定使用哪種排序方式,也可一將幾種排序組合使用以提高性能,比如小編就發(fā)現(xiàn)在快速排序中,當(dāng)每個(gè)部分?jǐn)?shù)據(jù)數(shù)量小于8時(shí),對(duì)每個(gè)部分用插入排序就比一直使用快速排序更快。小編在找到一個(gè)動(dòng)圖,十分生動(dòng)形象的表現(xiàn)了不同算法的速度上的差異。

動(dòng)畫(huà)示意圖

本章js源碼可以 點(diǎn)此去下載

排序算法就寫(xiě)這么多,有什么不足還請(qǐng)指點(diǎn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概述 排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    蟻前閱讀 5,301評(píng)論 0 52
  • 概述:排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,825評(píng)論 0 15
  • 11月9日,特意起了一個(gè)早床,早上五點(diǎn)半到國(guó)皇賓館。幾天前同事趙對(duì)我說(shuō),她要請(qǐng)一位已退休的老領(lǐng)導(dǎo)L去邵陽(yáng)崀山玩,順...
    大尾巴狗閱讀 637評(píng)論 0 0
  • 1. 使用方法 第一個(gè)參數(shù)是組件,第二個(gè)參數(shù)是動(dòng)畫(huà)操作方式,有以下幾種: 平移 translationX,tran...
    八月之雨閱讀 541評(píng)論 1 14
  • 張健,我個(gè)人認(rèn)為好的題目能夠激發(fā)大家從不同的角度去闡述一件事情,是非常好的。目前不準(zhǔn)備參加的原因是和自己的計(jì)劃有沖...
    一路向南007閱讀 274評(píng)論 0 0

友情鏈接更多精彩內(nèi)容