JavaScript:十大排序的算法思路和代碼實現(xiàn)

冒泡排序

通過相鄰元素的比較和交換,使得每一趟循環(huán)都能找到未有序數(shù)組的最大值或最小值殿雪。

最好:O(n)暇咆,只需要冒泡一次數(shù)組就有序了。
最壞:O(n2)
平均:O(n2)

單向冒泡

function bubbleSort(nums) {
  for(let i=0, len=nums.length; i<len-1; i++) {
    // 如果一輪比較中沒有需要交換的數(shù)據(jù)丙曙,則說明數(shù)組已經(jīng)有序爸业。主要是對[5,1,2,3,4]之類的數(shù)組進行優(yōu)化
    let mark = true;
    for(let j=0; j<len-i-1; j++) {
      if(nums[j] > nums[j+1]) {
        [nums[j], nums[j+1]] = [nums[j+1], nums[j]];
        mark = false;
      }
    }
    if(mark)  return;
  }
}

雙向冒泡

普通的冒泡排序在一趟循環(huán)中只能找出一個最大值或最小值,雙向冒泡則是多一輪循環(huán)既找出最大值也找出最小值亏镰。

function bubbleSort_twoWays(nums) {
  let low = 0;
  let high = nums.length - 1;
  while(low < high) {
    let mark = true;
    // 找到最大值放到右邊
    for(let i=low; i<high; i++) {
      if(nums[i] > nums[i+1]) {
        [nums[i], nums[i+1]] = [nums[i+1], nums[i]];
        mark = false;
      }
    }
    high--;
    // 找到最小值放到左邊
    for(let j=high; j>low; j--) {
      if(nums[j] < nums[j-1]) {
        [nums[j], nums[j-1]] = [nums[j-1], nums[j]];
        mark = false;
      }
    }
    low++;
    if(mark)  return;
  }
}

選擇排序

和冒泡排序相似扯旷,區(qū)別在于選擇排序是將每一個元素和它后面的元素進行比較和交換。

最好:O(n2)
最壞:O(n2)
平均:O(n2)

function selectSort(nums) {
  for(let i=0, len=nums.length; i<len; i++) {
    for(let j=i+1; j<len; j++) {
      if(nums[i] > nums[j]) {
        [nums[i], nums[j]] = [nums[j], nums[i]];
      }
    }
  }
}

插入排序

以第一個元素作為有序數(shù)組索抓,其后的元素通過在這個已有序的數(shù)組中找到合適的位置并插入钧忽。

最好:O(n),原數(shù)組已經(jīng)是升序的逼肯。
最壞:O(n2)
平均:O(n2)

function insertSort(nums) {
  for(let i=1, len=nums.length; i<len; i++) {
    let temp = nums[i];
    let j = i;
    while(j >= 0 && temp < nums[j-1]) {
      nums[j] = nums[j-1];
      j--;
    }
    nums[j] = temp;
  }
}

快速排序

選擇一個元素作為基數(shù)(通常是第一個元素)耸黑,把比基數(shù)小的元素放到它左邊,比基數(shù)大的元素放到它右邊(相當于二分)篮幢,再不斷遞歸基數(shù)左右兩邊的序列大刊。

最好:O(n * logn),所有數(shù)均勻分布在基數(shù)的兩邊三椿,此時的遞歸就是不斷地二分左右序列缺菌。
最壞:O(n2),所有數(shù)都分布在基數(shù)的一邊搜锰,此時劃分左右序列就相當于是插入排序伴郁。
平均:O(n * logn)

參考學習鏈接:
算法 3:最常用的排序——快速排序
三種快速排序以及快速排序的優(yōu)化

快速排序之填坑

從右邊向中間推進的時候,遇到小于基數(shù)的數(shù)就賦給左邊(一開始是基數(shù)的位置)蛋叼,右邊保留原先的值等之后被左邊的值填上焊傅。

function quickSort(nums) {
  // 遞歸排序基數(shù)左右兩邊的序列
  function recursive(arr, left, right) {
    if(left >= right)  return;
    let index = partition(arr, left, right);
    recursive(arr, left, index - 1);
    recursive(arr, index + 1, right);
    return arr;
  }
  // 將小于基數(shù)的數(shù)放到基數(shù)左邊剂陡,大于基數(shù)的數(shù)放到基數(shù)右邊,并返回基數(shù)的位置
  function partition(arr, left, right) {
    // 取第一個數(shù)為基數(shù)
    let temp = arr[left];
    while(left < right) {
      while(left < right && arr[right] >= temp)  right--;
      arr[left] = arr[right];
      while(left < right && arr[left] < temp)  left++;
      arr[right] = arr[left];
    }
    // 修改基數(shù)的位置
    arr[left] = temp;
    return left;
  }
  recursive(nums, 0, nums.length-1);
}

快速排序之交換

從左右兩邊向中間推進的時候狐胎,遇到不符合的數(shù)就兩邊交換值鹏倘。

function quickSort1(nums) {
  function recursive(arr, left, right) {
    if(left >= right)  return;
    let index = partition(arr, left, right);
    recursive(arr, left, index - 1);
    recursive(arr, index + 1, right);
    return arr;
  }
  function partition(arr, left, right) {
    let temp = arr[left];
    let p = left + 1;
    let q = right;
    while(p <= q) {
      while(p <= q && arr[p] < temp)  p++;
      while(p <= q && arr[q] > temp)  q--;
      if(p <= q) {
        [arr[p], arr[q]] = [arr[q], arr[p]];
        // 交換值后兩邊各向中間推進一位
        p++;
        q--;
      }
    }
    // 修改基數(shù)的位置
    [arr[left], arr[q]] = [arr[q], arr[left]];
    return q;
  }
  recursive(nums, 0, nums.length-1);
}

歸并排序

遞歸將數(shù)組分為兩個序列,有序合并這兩個序列顽爹。

最好:O(n * logn)
最壞:O(n * logn)
平均:O(n * logn)

參考學習鏈接:
圖解排序算法(四)之歸并排序

function mergeSort(nums) {
  // 有序合并兩個數(shù)組
  function merge(l1, r1, l2, r2) {
    let arr = [];
    let index = 0;
    let i = l1, j = l2;
    while(i <= r1 && j <= r2) {
      arr[index++] = nums[i] < nums[j] ? nums[i++] : nums[j++];
    }
    while(i <= r1)  arr[index++] = nums[i++];
    while(j <= r2)  arr[index++] = nums[j++];
    // 將有序合并后的數(shù)組修改回原數(shù)組
    for(let t=0; t<index; t++) {
      nums[l1 + t] = arr[t];
    }
  }
  // 遞歸將數(shù)組分為兩個序列
  function recursive(left, right) {
    if(left >= right)  return;
    // 比起(left+right)/2纤泵,更推薦下面這種寫法,可以避免數(shù)溢出
    let mid = parseInt((right - left) / 2) + left;
    recursive(left, mid);
    recursive(mid+1, right);
    merge(left, mid, mid+1, right);
    return nums;
  }
  recursive(0, nums.length-1);
}

桶排序

取 n 個桶镜粤,根據(jù)數(shù)組的最大值和最小值確認每個桶存放的數(shù)的區(qū)間捏题,將數(shù)組元素插入到相應的桶里,最后再合并各個桶肉渴。

最好:O(n)公荧,每個數(shù)都在分布在一個桶里,這樣就不用將數(shù)插入排序到桶里了(類似于計數(shù)排序以空間換時間)同规。
最壞:O(n2)循狰,所有的數(shù)都分布在一個桶里。
平均:O(n + k)券勺,k表示桶的個數(shù)绪钥。

參考學習鏈接:
拜托,面試別再問我桶排序了9亓丁3谈埂!

function bucketSort(nums) {
  // 桶的個數(shù)儒拂,只要是正數(shù)即可
  let num = 5;
  let max = Math.max(...nums);
  let min = Math.min(...nums);
  // 計算每個桶存放的數(shù)值范圍寸潦,至少為1,
  let range = Math.ceil((max - min) / num) || 1;
  // 創(chuàng)建二維數(shù)組社痛,第一維表示第幾個桶见转,第二維表示該桶里存放的數(shù)
  let arr = Array.from(Array(num)).map(() => Array().fill(0));
  nums.forEach(val => {
    // 計算元素應該分布在哪個桶
    let index = parseInt((val - min) / range);
    // 防止index越界,例如當[5,1,1,2,0,0]時index會出現(xiàn)5
    index = index >= num ? num - 1 : index;
    let temp = arr[index];
    // 插入排序蒜哀,將元素有序插入到桶中
    let j = temp.length - 1;
    while(j >= 0 && val < temp[j]) {
      temp[j+1] = temp[j];
      j--;
    }
    temp[j+1] = val;
  })
  // 修改回原數(shù)組
  let res = [].concat.apply([], arr);
  nums.forEach((val, i) => {
    nums[i] = res[i];
  })
}

基數(shù)排序

使用十個桶 0-9斩箫,把每個數(shù)從低位到高位根據(jù)位數(shù)放到相應的桶里,以此循環(huán)最大值的位數(shù)次凡怎。但只能排列正整數(shù)校焦,因為遇到負號和小數(shù)點無法進行比較赊抖。

最好:O(n * k)统倒,k表示最大值的位數(shù)。
最壞:O(n * k)
平均:O(n * k)

參考學習鏈接:
算法總結系列之五: 基數(shù)排序(Radix Sort)

function radixSort(nums) {
  // 計算位數(shù)
  function getDigits(n) {
    let sum = 0;
    while(n) {
      sum++;
      n = parseInt(n / 10);
    }
    return sum;
  }
  // 第一維表示位數(shù)即0-9氛雪,第二維表示里面存放的值
  let arr = Array.from(Array(10)).map(() => Array());
  let max = Math.max(...nums);
  let maxDigits = getDigits(max);
  for(let i=0, len=nums.length; i<len; i++) {
    // 用0把每一個數(shù)都填充成相同的位數(shù)
    nums[i] = (nums[i] + '').padStart(maxDigits, 0);
    // 先根據(jù)個位數(shù)把每一個數(shù)放到相應的桶里
    let temp = nums[i][nums[i].length-1];
    arr[temp].push(nums[i]);
  }
  // 循環(huán)判斷每個位數(shù)
  for(let i=maxDigits-2; i>=0; i--) {
    // 循環(huán)每一個桶
    for(let j=0; j<=9; j++) {
      let temp = arr[j]
      let len = temp.length;
      // 根據(jù)當前的位數(shù)i把桶里的數(shù)放到相應的桶里
      while(len--) {
        let str = temp[0];
        temp.shift();
        arr[str[i]].push(str);
      }
    }
  }
  // 修改回原數(shù)組
  let res = [].concat.apply([], arr);
  nums.forEach((val, index) => {
    nums[index] = +res[index];
  }) 
}

計數(shù)排序

以數(shù)組元素值為鍵房匆,出現(xiàn)次數(shù)為值存進一個臨時數(shù)組,最后再遍歷這個臨時數(shù)組還原回原數(shù)組。因為 JavaScript 的數(shù)組下標是以字符串形式存儲的浴鸿,所以計數(shù)排序可以用來排列負數(shù)井氢,但不可以排列小數(shù)

最好:O(n + k)岳链,k是最大值和最小值的差花竞。
最壞:O(n + k)
平均:O(n + k)

function countingSort(nums) {
  let arr = [];
  let max = Math.max(...nums);
  let min = Math.min(...nums);
  // 裝桶
  for(let i=0, len=nums.length; i<len; i++) {
    let temp = nums[i];
    arr[temp] = arr[temp] + 1 || 1;
  }
  let index = 0;
  // 還原原數(shù)組
  for(let i=min; i<=max; i++) {
    while(arr[i] > 0) {
      nums[index++] = i;
      arr[i]--;
    }
  }
}

計數(shù)排序優(yōu)化

把每一個數(shù)組元素都加上 min 的相反數(shù),來避免特殊情況下的空間浪費掸哑,通過這種優(yōu)化可以把所開的空間大小從 max+1 降低為 max-min+1约急,maxmin 分別為數(shù)組中的最大值和最小值。

比如數(shù)組 [103, 102, 101, 100]苗分,普通的計數(shù)排序需要開一個長度為 104 的數(shù)組厌蔽,而且前面 100 個值都是 undefined,使用該優(yōu)化方法后可以只開一個長度為 4 的數(shù)組摔癣。

function countingSort(nums) {
  let arr = [];
  let max = Math.max(...nums);
  let min = Math.min(...nums);
  // 加上最小值的相反數(shù)來縮小數(shù)組范圍
  let add = -min;
  for(let i=0, len=nums.length; i<len; i++) {
    let temp = nums[i];
    temp += add;
    arr[temp] = arr[temp] + 1 || 1;
  }
  let index = 0;
  for(let i=min; i<=max; i++) {
    let temp = arr[i+add];
    while(temp > 0) {
      nums[index++] = i;
      temp--;
    }
  }
}

堆排序

根據(jù)數(shù)組建立一個堆(類似完全二叉樹)奴饮,每個結點的值都大于左右結點(最大堆,通常用于升序)择浊,或小于左右結點(最小堆戴卜,通常用于降序)。對于升序排序琢岩,先構建最大堆后叉瘩,交換堆頂元素(表示最大值)和堆底元素,每一次交換都能得到未有序序列的最大值粘捎。重新調(diào)整最大堆薇缅,再交換堆頂元素和堆底元素,重復 n-1 次后就能得到一個升序的數(shù)組攒磨。

最好:O(n * logn)泳桦,logn是調(diào)整最大堆所花的時間。
最壞:O(n * logn)
平均:O(n * logn)

參考學習鏈接:
常見排序算法 - 堆排序 (Heap Sort)
圖解排序算法(三)之堆排序

function heapSort(nums) {
  // 調(diào)整最大堆娩缰,使index的值大于左右節(jié)點
  function adjustHeap(nums, index, size) {
    // 交換后可能會破壞堆結構灸撰,需要循環(huán)使得每一個父節(jié)點都大于左右結點
    while(true) {
      let max = index;
      let left = index * 2 + 1;   // 左節(jié)點
      let right = index * 2 + 2;  // 右節(jié)點
      if(left < size && nums[max] < nums[left])  max = left;
      if(right < size && nums[max] < nums[right])  max = right;
      // 如果左右結點大于當前的結點則交換,并再循環(huán)一遍判斷交換后的左右結點位置是否破壞了堆結構(比左右結點小了)
      if(index !== max) {
        [nums[index], nums[max]] = [nums[max], nums[index]];
        index = max;
      }
      else {
        break;
      }
    }
  }
  // 建立最大堆
  function buildHeap(nums) {
    // 注意這里的頭節(jié)點是從0開始的拼坎,所以最后一個非葉子結點是 parseInt(nums.length/2)-1
    let start = parseInt(nums.length / 2) - 1;
    let size = nums.length;
    // 從最后一個非葉子結點開始調(diào)整浮毯,直至堆頂。
    for(let i=start; i>=0; i--) {
      adjustHeap(nums, i, size);
    }
  }

  buildHeap(nums);
  // 循環(huán)n-1次泰鸡,每次循環(huán)后交換堆頂元素和堆底元素并重新調(diào)整堆結構
  for(let i=nums.length-1; i>0; i--) {
    [nums[i], nums[0]] = [nums[0], nums[i]];
    adjustHeap(nums, 0, i);
  }
}

希爾排序

通過某個增量 gap债蓝,將整個序列分給若干組,從后往前進行組內(nèi)成員的比較和交換盛龄,隨后逐步縮小增量至 1饰迹。希爾排序類似于插入排序芳誓,只是一開始向前移動的步數(shù)從 1 變成了 gap。

最好:O(n * logn)啊鸭,步長不斷二分锹淌。
最壞:O(n * logn)
平均:O(n * logn)

參考學習鏈接:
圖解排序算法(二)之希爾排序

function shellSort(nums) {
  let len = nums.length;
  // 初始步數(shù)
  let gap = parseInt(len / 2);
  // 逐漸縮小步數(shù)
  while(gap) {
    // 從第gap個元素開始遍歷
    for(let i=gap; i<len; i++) {
      // 逐步其和前面其他的組成員進行比較和交換
      for(let j=i-gap; j>=0; j-=gap) {
        if(nums[j] > nums[j+gap]) {
          [nums[j], nums[j+gap]] = [nums[j+gap], nums[j]];
        }
        else {
          break;
        }
      }
    }
    gap = parseInt(gap / 2);
  }
}
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赠制,隨后出現(xiàn)的幾起案子赂摆,更是在濱河造成了極大的恐慌,老刑警劉巖钟些,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件库正,死亡現(xiàn)場離奇詭異,居然都是意外死亡厘唾,警方通過查閱死者的電腦和手機褥符,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抚垃,“玉大人喷楣,你說我怎么就攤上這事『资鳎” “怎么了铣焊?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長罕伯。 經(jīng)常有香客問我曲伊,道長,這世上最難降的妖魔是什么追他? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任坟募,我火速辦了婚禮啡邑,結果婚禮上濒募,老公的妹妹穿的比我還像新娘。我一直安慰自己颁督,他們只是感情好单雾,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布赚哗。 她就那樣靜靜地躺著,像睡著了一般硅堆。 火紅的嫁衣襯著肌膚如雪屿储。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天渐逃,我揣著相機與錄音够掠,去河邊找鬼。 笑死朴乖,一個胖子當著我的面吹牛祖屏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播买羞,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼袁勺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了畜普?” 一聲冷哼從身側響起期丰,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吃挑,沒想到半個月后钝荡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡舶衬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年埠通,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逛犹。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡端辱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出虽画,到底是詐尸還是另有隱情舞蔽,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布码撰,位于F島的核電站渗柿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏脖岛。R本人自食惡果不足惜朵栖,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柴梆。 院中可真熱鬧混槐,春花似錦、人聲如沸轩性。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽揣苏。三九已至悯嗓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卸察,已是汗流浹背脯厨。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坑质,地道東北人合武。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓临梗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親稼跳。 傳聞我的和親對象是個殘疾皇子盟庞,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

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