前端算法整理轉載

遞歸

default

遞歸算法 : 英語:recursion algorithm)在計算機科學中是指一種經過重複將問題分解爲同類的子問題而解決問題的方法。絕大多數編程語言支持函數的自調用,在這些語言中函數能夠經過調用自身來進行遞歸。node

遞歸的兩個要素:git

  • 調用自身
  • 能跳出循環

階乘
n! = n*(n-1)github

...2
1

6! = 6 * 5 * 4 * 3 * 2 *1算法

規律:n的階乘就是從n開始依次遞減值的積shell

function factorial(number){
  if(number==1) {
    return number;
  } else{
    return number*factorial(number-1);
  }
}複製代碼

斐波那契數列
斐波那契數列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... 求第n個數是多少編程

第一個數: 1 = 1
第二個數: 1 = 1
第三個數: 2 = 1 + 1
第四個數: 3 = 2 + 1
第五個數: 5 = 3 + 2
第六個數: 8 = 5 + 3
...數組

規律:後一項是前兩項的和瀏覽器

function fibonacci(number) {
  if (number <= 2) {
    return 1;
  }
  return fibonacci(number-1) + fibonacci(number - 2)
}複製代碼

排序算法


冒泡排序

冒泡

冒泡排序須要兩個嵌套的循環. 其中, 外層循環移動遊標; 內層循環遍歷遊標及以後(或以前)的元素, 經過兩兩交換的方式, 每次只確保該內循環結束位置排序正確, 而後內層循環週期結束, 交由外層循環日後(或前)移動遊標, 隨即開始下一輪內層循環, 以此類推, 直至循環結束.緩存

Tips
: 因爲冒泡排序只在相鄰元素大小不符合要求時才調換他們的位置, 它並不改變相同元素之間的相對順序, 所以它是穩定的排序算法.

因爲有兩層循環, 所以能夠有四種實現方式.bash

方案 外層循環 內層循環
1 正序 正序
2 正序 逆序
3 逆序 正序
4 逆序 逆序

四種不一樣循環方向, 實現方式略有差別.

以下是動圖效果(對應於方案1: 內/外層循環均是正序遍歷.

冒泡排序

以下是上圖的算法實現(對應方案一: 內/外層循環均是正序遍歷).

//先將交換元素部分抽象出來
function swap(i,j,array){
  var temp = array[j];
  array[j] = array[i];
  array[i] = temp;
}複製代碼複製代碼
function bubbleSort(array) {
  var length = array.length, isSwap;
  for (var i = 0; i < length; i++) {            //正序
    isSwap = false;
    for (var j = 0; j < length - 1 - i; j++) {     //正序
      array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array);
    }
    if(!isSwap)
      break;
  }
  return array;
}複製代碼複製代碼

以上, 排序的特色就是: 靠後的元素位置先肯定.

方案二: 外循環正序遍歷, 內循環逆序遍歷, 代碼以下:

function bubbleSort(array) {
  var length = array.length, isSwap;
  for (var i = 0; i < length; i++) {            //正序
    isSwap = false;
    for (var j = length - 1; j >= i+1; j--) {     //逆序
      array[j] < array[j-1] && (isSwap = true) && swap(j,j-1,array);
    }
    if(!isSwap)
      break;
  }
  return array;
}複製代碼複製代碼

以上, 靠前的元素位置先肯定.

方案三: 外循環逆序遍歷, 內循環正序遍歷, 代碼以下:

function bubbleSort(array) {
  var length = array.length, isSwap;
  for (var i = length - 1; i >= 0; i--) {     //逆序
    isSwap = false;
    for (var j = 0; j < i; j++) {            //正序
      array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array);
    }
    if(!isSwap)
      break;
  }
  return array;
}複製代碼複製代碼

以上, 因爲內循環是正序遍歷, 所以靠後的元素位置先肯定.

方案四: 外循環逆序遍歷, 內循環逆序遍歷, 代碼以下:

function bubbleSort(array) {
  var length = array.length, isSwap;
  for (var i = length - 1; i >= 0; i--) {                //逆序
    isSwap = false;
    for (var j = length - 1; j >= length - 1 - i; j--) { //逆序
      array[j] < array[j-1] && (isSwap = true) && swap(j,j-1,array);
    }
    if(!isSwap)
      break;
  }
  return array;
}複製代碼複製代碼

以上, 因爲內循環是逆序遍歷, 所以靠前的元素位置先肯定.

如下是其算法複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(n²) O(n) O(n²) O(1)

冒泡排序是最容易實現的排序, 最壞的狀況是每次都須要交換, 共需遍歷並交換將近n²/2次, 時間複雜度爲O(n²). 最佳的狀況是內循環遍歷一次後發現排序是對的, 所以退出循環, 時間複雜度爲O(n). 平均來說, 時間複雜度爲O(n²). 因爲冒泡排序中只有緩存的temp變量須要內存空間, 所以空間複雜度爲常量O(1).

雙向冒泡排序

雙向冒泡排序是冒泡排序的一個簡易升級版, 又稱雞尾酒排序. 冒泡排序是從低到高(或者從高到低)單向排序, 雙向冒泡排序顧名思義就是從兩個方向分別排序(一般, 先從低到高, 而後從高到低). 所以它比冒泡排序性能稍好一些.

以下是算法實現:

function bothwayBubbleSort(array){
  var tail = array.length-1, i, isSwap = false;
  for(i = 0; i < tail; tail--){
    for(var j = tail; j > i; j--){    //第一輪, 先將最小的數據冒泡到前面
      array[j-1] > array[j] && (isSwap = true) && swap(j,j-1,array);
    }
    i++;
    for(j = i; j < tail; j++){        //第二輪, 將最大的數據冒泡到後面
      array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array);
    }
  }
  return array;
}複製代碼複製代碼

選擇排序

從算法邏輯上看, 選擇排序是一種簡單且直觀的排序算法. 它也是兩層循環. 內層循環就像工人同樣, 它是真正作事情的, 內層循環每執行一遍, 將選出本次待排序的元素中最小(或最大)的一個, 存放在數組的起始位置. 而 外層循環則像老闆同樣, 它告訴內層循環你須要不停的工做, 直到工做完成(也就是所有的元素排序完成).

Tips
: 選擇排序每次交換的元素都有可能不是相鄰的, 所以它有可能打破原來值爲相同的元素之間的順序. 好比數組[2,2,1,3], 正向排序時, 第一個數字2將與數字1交換, 那麼兩個數字2之間的順序將和原來的順序不一致, 雖然它們的值相同, 但它們相對的順序卻發生了變化. 咱們將這種現象稱做 不穩定性 .

以下是動圖效果:

選擇排序

以下是上圖的算法實現:

function selectSort(array) {
  var length = array.length, min;
  for (var i = 0; i < length - 1; i++) {
    min = i;
    for (var j = i + 1; j < length; j++) {
      array[j] < array[min] && (min = j); //記住最小數的下標
    }
    min!=i && swap(i,min,array);
  }
  return array;
}複製代碼複製代碼

如下是其算法複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(n²) O(n²) O(n²) O(1)

選擇排序的簡單和直觀名副其實, 這也造就了它"出了名的慢性子", 不管是哪一種狀況, 哪怕原數組已排序完成, 它也將花費將近n²/2次遍從來確認一遍. 即使是這樣, 它的排序結果也仍是不穩定的. 惟一值得高興的是, 它並不耗費額外的內存空間.

插入排序

插入排序的設計初衷是往有序的數組中快速插入一個新的元素. 它的算法思想是: 把要排序的數組分爲了兩個部分, 一部分是數組的所有元素(除去待插入的元素), 另外一部分是待插入的元素; 先將第一部分排序完成, 而後再插入這個元素. 其中第一部分的排序也是經過再次拆分爲兩部分來進行的.

插入排序因爲操做不盡相同, 可分爲 直接插入排序 , 折半插入排序(又稱二分插入排序), 鏈表插入排序 , 希爾排序 .

直接插入排序

它的基本思想是: 將待排序的元素按照大小順序, 依次插入到一個已經排好序的數組之中, 直到全部的元素都插入進去.

以下是動圖效果:

直接插入排序

以下是上圖的算法實現:

function directInsertionSort(array) {
  var length = array.length, index, current;
  for (var i = 1; i < length; i++) {
    index = i - 1;         //待比較元素的下標
    current = array[i];     //當前元素
    while(index >= 0 && array[index] > current) { //前置條件之一:待比較元素比當前元素大
      array[index+1] = array[index];    //將待比較元素後移一位
      index--;                           //遊標前移一位
      //console.log(array);
    }
    if(index+1 != i){                   //避免同一個元素賦值給自身
      array[index+1] = current;            //將當前元素插入預留空位
      //console.log(array);
    }        
  }
  return array;
}複製代碼複製代碼

爲了更好的觀察到直接插入排序的實現過程, 咱們不妨將上述代碼中的註釋部分加入. 以數組 [5,4,3,2,1] 爲例, 以下即是原數組的演化過程.

可見, 數組的各個元素, 從後往前, 只要比前面的元素小, 都依次插入到了合理的位置.

Tips
: 因爲直接插入排序每次只移動一個元素的位置, 並不會改變值相同的元素之間的排序, 所以它是一種穩定排序.

折半插入排序

折半插入排序是直接插入排序的升級版. 鑑於插入排序第一部分爲已排好序的數組, 咱們沒必要按順序依次尋找插入點, 只需比較它們的中間值與待插入元素的大小便可.

Tips
: 同直接插入排序相似, 折半插入排序每次交換的是相鄰的且值爲不一樣的元素, 它並不會改變值相同的元素之間的順序. 所以它是穩定的.

算法基本思想是:

  1. 取0 ~ i-1的中間點( m = (i-1)>>1 ), array[i] 與 array[m] 進行比較, 若array[i] < array[m] , 則說明待插入的元素array[i] 應該處於數組的 0 ~ m 索引之間; 反之, 則說明它應該處於數組的 m ~ i-1 索引之間.
  2. 重複步驟1, 每次縮小一半的查找範圍, 直至找到插入的位置.
  3. 將數組中插入位置以後的元素所有後移一位.
  4. 在指定位置插入第 i 個元素.

注: x>>1 是位運算中的右移運算, 表示右移一位, 等同於x除以2再取整, 即 x>>1 == Math.floor(x/2) .

以下是算法實現:

function binaryInsertionSort(array){
  var current, i, j, low, high, m;
  for(i = 1; i < array.length; i++){
    low = 0;
    high = i - 1;
    current = array[i];

    while(low <= high){            //步驟1&2:折半查找
      m = (low + high)>>1;
      if(array[i] >= array[m]){//值相同時, 切換到高半區,保證穩定性
        low = m + 1;        //插入點在高半區
      }else{
        high = m - 1;        //插入點在低半區
      }
    }
    for(j = i; j > low; j--){     //步驟3:插入位置以後的元素所有後移一位
      array[j] = array[j-1];
    }
    array[low] = current;         //步驟4:插入該元素
  }
  return array;
}複製代碼複製代碼

爲了便於對比, 一樣以數組 [5,4,3,2,1] 舉例🌰. 原數組的演化過程以下(與上述同樣):

折半插入排序

雖然折半插入排序明顯減小了查詢的次數, 可是數組元素移動的次數卻沒有改變. 它們的時間複雜度都是O(n²).

希爾排序

希爾排序也稱縮小增量排序, 它是直接插入排序的另一個升級版, 實質就是分組插入排序. 希爾排序以其設計者希爾(Donald Shell)的名字命名, 並於1959年公佈.

算法的基本思想:

  1. 將數組拆分爲若干個子分組, 每一個分組由相距必定"增量"的元素組成. 比方說將[0,1,2,3,4,5,6,7,8,9,10]的數組拆分爲"增量"爲5的分組, 那麼子分組分別爲 [0,5], [1,6], [2,7], [3,8], [4,9] 和 [5,10].
  2. 而後對每一個子分組應用直接插入排序.
  3. 逐步減少"增量", 重複步驟1,2.
  4. 直至"增量"爲1, 這是最後一個排序, 此時的排序, 也就是對全數組進行直接插入排序.

以下是排序的示意圖:

希爾排序示意圖

可見, 希爾排序實際上就是不斷的進行直接插入排序, 分組是爲了先將局部元素有序化. 由於直接插入排序在元素基本有序的狀態下, 效率很是高. 而希爾排序呢, 經過先分組後排序的方式, 製造了直接插入排序高效運行的場景. 所以希爾排序效率更高.

咱們試着抽象出共同點, 便不難發現上述希爾排序的第四步就是一次直接插入排序, 而希爾排序本來就是從"增量"爲n開始, 直至"增量"爲1, 循環應用直接插入排序的一種封裝. 所以直接插入排序就能夠看作是步長爲1的希爾排序. 爲此咱們先來封裝下直接插入排序.

//形參增長步數gap(實際上就至關於gap替換了原來的數字1)
function directInsertionSort(array, gap) {
  gap = (gap == undefined) ? 1 : gap;       //默認從下標爲1的元素開始遍歷
  var length = array.length, index, current;
  for (var i = gap; i < length; i++) {
    index = i - gap;    //待比較元素的下標
    current = array[i];    //當前元素
    while(index >= 0 && array[index] > current) { //前置條件之一:待比較元素比當前元素大
      array[index + gap] = array[index];    //將待比較元素後移gap位
      index -= gap;                           //遊標前移gap位
    }
    if(index + gap != i){                   //避免同一個元素賦值給自身
      array[index + gap] = current;            //將當前元素插入預留空位
    }
  }
  return array;
}複製代碼複製代碼

那麼希爾排序的算法實現以下:

function shellSort(array){
  var length = array.length, gap = length>>1, current, i, j;
  while(gap > 0){
    directInsertionSort(array, gap); //按指定步長進行直接插入排序
    gap = gap>>1;
  }
  return array;
}複製代碼複製代碼

一樣以數組[5,4,3,2,1] 舉例🌰. 原數組的演化過程以下:

希爾排序

對比上述直接插入排序和折半插入排序, 數組元素的移動次數由14次減小爲7次. 經過拆分原數組爲粒度更小的子數組, 希爾排序進一步提升了排序的效率.

不只如此, 以上步長設置爲了 {N/2, (N/2)/2, ..., 1}. 該序列即希爾增量, 其它的增量序列 還有Hibbard:{1, 3, ..., 2^k-1}. 經過合理調節步長, 還能進一步提高排序效率. 實際上已知的最好步長序列是由Sedgewick提出的(1, 5, 19, 41, 109,…). 該序列中的項或者是9*4^i - 9*2^i + 1或者是4^i - 3*2^i + 1. 具體請戳 希爾排序-維基百科 .

Tips
: 咱們知道, 單次直接插入排序是穩定的, 它不會改變相同元素之間的相對順序, 但在屢次不一樣的插入排序過程當中, 相同的元素可能在各自的插入排序中移動, 可能致使相同元素相對順序發生變化. 所以, 希爾排序並不穩定.

歸併排序

歸併排序創建在歸併操做之上, 它採起分而治之的思想, 將數組拆分爲兩個子數組, 分別排序, 最後纔將兩個子數組合並; 拆分的兩個子數組, 再繼續遞歸拆分爲更小的子數組, 進而分別排序, 直到數組長度爲1, 直接返回該數組爲止.

Tips
: 歸併排序嚴格按照從左往右(或從右往左)的順序去合併子數組, 它並不會改變相同元素之間的相對順序, 所以它也是一種穩定的排序算法.

以下是動圖效果:

歸併排序

歸併排序可經過兩種方式實現:

  1. 自上而下的遞歸
  2. 自下而上的迭代

以下是算法實現(方式1:遞歸):

function mergeSort(array) {  //採用自上而下的遞歸方法
  var length = array.length;
  if(length < 2) {
    return array;
  }
  var m = (length >> 1),
      left = array.slice(0, m),
      right = array.slice(m); //拆分爲兩個子數組
  return merge(mergeSort(left), mergeSort(right));//子數組繼續遞歸拆分,而後再合併
}
function merge(left, right){ //合併兩個子數組
  var result = [];
  while (left.length && right.length) {
    var item = left[0] <= right[0] ? left.shift() : right.shift();//注意:判斷的條件是小於或等於,若是隻是小於,那麼排序將不穩定.
    result.push(item);
  }
  return result.concat(left.length ? left : right);
}複製代碼複製代碼

由上, 長度爲n的數組, 最終會調用mergeSort函數2n-1次. 經過自上而下的遞歸實現的歸併排序, 將存在堆棧溢出的風險. 親測各瀏覽器的堆棧溢出所需的遞歸調用次數大體爲:

  • Chrome v55: 15670
  • Firefox v50: 44488
  • Safari v9.1.2: 50755

如下是測試代碼:

function computeMaxCallStackSize() {
  try {
    return 1 + computeMaxCallStackSize();
  } catch (e) {
    // Call stack overflow
    return 1;
  }
}
var time = computeMaxCallStackSize();
console.log(time);複製代碼複製代碼

爲此, ES6規範中提出了尾調優化的思想: 若是一個函數的最後一步也是一個函數調用, 那麼該函數所須要的棧空間將被釋放, 它將直接進入到下次調用中, 最終調用棧裏只保留最後一次的調用記錄.

雖然ES6規範如此誘人, 然而目前並無瀏覽器支持尾調優化, 相信在不久的未來, 尾調優化就會獲得主流瀏覽器的支持.

如下是其算法複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n)

從效率上看, 歸併排序可算是排序算法中的"佼佼者". 假設數組長度爲n, 那麼拆分數組共需logn步, 又每步都是一個普通的合併子數組的過程, 時間複雜度爲O(n), 故其綜合時間複雜度爲O(nlogn). 另外一方面, 歸併排序屢次遞歸過程當中拆分的子數組須要保存在內存空間, 其空間複雜度爲O(n).

快速排序

快速排序借用了分治的思想, 而且基於冒泡排序作了改進. 它由C. A. R. Hoare在1962年提出. 它將數組拆分爲兩個子數組, 其中一個子數組的全部元素都比另外一個子數組的元素小, 而後對這兩個子數組再重複進行上述操做, 直到數組不可拆分, 排序完成.

以下是動圖效果:

快速排序

以下是算法實現:

function quickSort(array, left, right) {
  var partitionIndex,
      left = typeof left == 'number' ? left : 0,
      right = typeof right == 'number' ? right : array.length-1;
  if (left < right) {
    partitionIndex = partition(array, left, right);//切分的基準值
    quickSort(array, left, partitionIndex-1);
    quickSort(array, partitionIndex+1, right);
  }
  return array;
}
function partition(array, left ,right) {   //分區操做
  for (var i = left+1, j = left; i <= right; i++) {//j是較小值存儲位置的遊標
    array[i] < array[left] && swap(i, ++j, array);//以第一個元素爲基準
  }
  swap(left, j, array);            //將第一個元素移至中間
  return j;
}複製代碼複製代碼

如下是其算法複雜度:

平均時間複雜度 最好狀況 最壞狀況 空間複雜度
O(nlog₂n) O(nlog₂n) O(n²) O(nlog₂n)

快速排序排序效率很是高. 雖然它運行最糟糕時將達到O(n²)的時間複雜度, 但一般, 平均來看, 它的時間複雜爲O(nlogn), 比一樣爲O(nlogn)時間複雜度的歸併排序還要快. 快速排序彷佛更偏心亂序的數列, 越是亂序的數列, 它相比其餘排序而言, 相對效率更高. 以前在 捋一捋JS的數組 一文中就提到: Chrome的v8引擎爲了高效排序, 在排序數據超過了10條時, 便會採用快速排序. 對於10條及如下的數據採用的即是插入排序.

Tips
: 同選擇排序類似, 快速排序每次交換的元素都有可能不是相鄰的, 所以它有可能打破原來值爲相同的元素之間的順序. 所以, 快速排序並不穩定.

堆排序

1991年的計算機先驅獎得到者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德(Robert W.Floyd) 和威廉姆斯(J.Williams) 在1964年共同發明了著名的堆排序算法(Heap Sort).

堆排序是利用堆這種數據結構所設計的一種排序算法. 它是選擇排序的一種. 堆分爲大根堆和小根堆. 大根堆要求每一個子節點的值都不大於其父節點的值, 即array[childIndex] <= array[parentIndex], 最大的值必定在堆頂. 小根堆與之相反, 即每一個子節點的值都不小於其父節點的值, 最小的值必定在堆頂. 所以咱們可以使用大根堆進行升序排序, 使用小根堆進行降序排序.

並不是全部的序列都是堆, 對於序列k1, k2,…kn, 須要知足以下條件才行:

  • ki <= k(2i) 且 ki<=k(2i+1)(1≤i≤ n/2), 即爲小根堆, 將<=換成>=, 那麼則是大根堆. 咱們能夠將這裏的堆看做徹底二叉樹, k(i) 至關因而二叉樹的非葉子節點, k(2i) 則是左子節點, k(2i+1)是右子節點.

算法的基本思想(以大根堆爲例):

  1. 先將初始序列K[1..n]建成一個大根堆, 此堆爲初始的無序區.
  2. 再將關鍵字最大的記錄K1 (即堆頂)和無序區的最後一個記錄K[n]交換, 由此獲得新的無序區K[1..n-1]和有序區K[n], 且知足K[1..n-1].keys≤K[n].key
  3. 交換K1 和 K[n] 後, 堆頂可能違反堆性質, 所以需將K[1..n-1]調整爲堆. 而後重複步驟2, 直到無序區只有一個元素時中止.

以下是動圖效果:

桶排序示意圖

以下是算法實現:

function heapAdjust(array, i, length) {//堆調整
  var left = 2 * i + 1,
      right = 2 * i + 2,
      largest = i;
  if (left < length && array[largest] < array[left]) {
    largest = left;
  }
  if (right < length && array[largest] < array[right]) {
    largest = right;
  }
  if (largest != i) {
    swap(i, largest, array);
    heapAdjust(array, largest, length);
  }
}
function heapSort(array) {
  //創建大頂堆
  length = array.length;
  for (var i = length>>1; i >= 0; i--) {
    heapAdjust(array, i, length);
  }
  //調換第一個與最後一個元素,從新調整爲大頂堆
  for (var i = length - 1; i > 0; i--) {
    swap(0, i, array);
    heapAdjust(array, 0, --length);
  }
  return array;
}複製代碼複製代碼

以上, ①創建堆的過程, 從length/2 一直處理到0, 時間複雜度爲O(n);

②調整堆的過程是沿着堆的父子節點進行調整, 執行次數爲堆的深度, 時間複雜度爲O(lgn);

③堆排序的過程由n次第②步完成, 時間複雜度爲O(nlgn).

Tips
: 因爲堆排序中初始化堆的過程比較次數較多, 所以它不太適用於小序列. 同時因爲屢次任意下標相互交換位置, 相同元素之間本來相對的順序被破壞了, 所以, 它是不穩定的排序.

計數排序

計數排序幾乎是惟一一個不基於比較的排序算法, 該算法於1954年由 Harold H. Seward 提出. 使用它處理必定範圍內的整數排序時, 時間複雜度爲O(n+k), 其中k是整數的範圍, 它幾乎比任何基於比較的排序算法都要快( 只有當O(k)>O(n*log(n))的時候其效率反而不如基於比較的排序, 如歸併排序和堆排序).

使用計數排序須要知足以下條件:

  • 待排序的序列所有爲整數
  • 排序須要額外的存儲空間

算法的基本思想:

計數排序利用了一個特性, 對於數組的某個元素, 一旦知道了有多少個其它元素比它小(假設爲m個), 那麼就能夠肯定出該元素的正確位置(第m+1位)

  1. 獲取待排序數組A的最大值, 最小值.
  2. 將最大值與最小值的差值+1做爲長度新建計數數組B,並將相同元素的數量做爲值存入計數數組.
  3. 對計數數組B累加計數, 存儲不一樣值的初始下標.
  4. 從原數組A挨個取值, 賦值給一個新的數組C相應的下標, 最終返回數組C.

注意: 若是原數組A是包含若干個對象的數組,須要基於對象的某個屬性進行排序,那麼算法開始時,須要將原數組A處理爲一個只包含對象屬性值的簡單數組simpleA, 接下來便基於simpleA進行計數、累加計數, 其它同上.

以下是動圖效果:

計數排序

以下是算法實現:

function countSort(array, keyName){
  var length = array.length,
      output = new Array(length),
      max,
      min,
      simpleArray = keyName ? array.map(function(v){
        return v[keyName];
      }) : array; // 若是keyName是存在的,那麼就建立一個只有keyValue的簡單數組

  // 獲取最大最小值
  max = min = simpleArray[0];
  simpleArray.forEach(function(v){
    v > max && (max = v);
    v < min && (min = v);
  });
  // 獲取計數數組的長度
  var k = max - min + 1;
  // 新建並初始化計數數組
  var countArray = new Array(k);
  simpleArray.forEach(function(v){
    countArray[v - min]= (countArray[v - min] || 0) + 1;
  });
  // 累加計數,存儲不一樣值的初始下標
  countArray.reduce(function(prev, current, i, arr){
    arr[i] = prev;
    return prev + current;
  }, 0);
  // 從原數組挨個取值(因取的是原數組的相應值,只能經過遍歷原數組來實現)
  simpleArray.forEach(function(v, i){
    var j = countArray[v - min]++;
    output[j] = array[i];
  });
  return output;
}複製代碼複製代碼

以上實現不只支持了數值序列的排序,還支持根據對象的某個屬性值來排序。測試以下:

var a = [2, 1, 1, 3, 2, 1, 4, 2],
    b = [
      {id: 2, s:'a'}, 
      {id: 1, s: 'b'}, 
      {id: 1, s: 'c'}, 
      {id: 3, s: 'd'}, 
      {id: 2, s: 'e'}, 
      {id: 1, s: 'f'}, 
      {id: 4, s: 'g'}, 
      {id: 2, s: 'h'}
    ];
countSort(a); // [1, 1, 1, 2, 2, 2, 3, 4]
countSort(b, 'id'); // [{id:1,s:'b'},{id:1,s:'c'},{id:1,s:'f'},{id:2,s:'a'},{id:2,s:'e'},{id:2,s:'h'},{id:3,s:'d'},{id:4,s:'g'}]複製代碼複製代碼

Tips
: 計數排序不改變相同元素之間本來相對的順序, 所以它是穩定的排序算法.

桶排序

桶排序即所謂的箱排序, 它是將數組分配到有限數量的桶子裏. 每一個桶裏再各自排序(所以有可能使用別的排序算法或以遞歸方式繼續桶排序). 當每一個桶裏的元素個數趨於一致時, 桶排序只需花費O(n)的時間. 桶排序經過空間換時間的方式提升了效率, 所以它須要額外的存儲空間(即桶的空間).

算法的基本思想:

桶排序的核心就在於怎麼把元素平均分配到每一個桶裏, 合理的分配將大大提升排序的效率.

以下是算法實現:

function bucketSort(array, bucketSize) {
  if (array.length === 0) {
    return array;
  }

  var i = 1,
      min = array[0],
      max = min;
  while (i++ < array.length) {
    if (array[i] < min) {
      min = array[i];                //輸入數據的最小值
    } else if (array[i] > max) {
      max = array[i];                //輸入數據的最大值
    }
  }

  //桶的初始化
  bucketSize = bucketSize || 5; //設置桶的默認大小爲5
  var bucketCount = ~~((max - min) / bucketSize) + 1, //桶的個數
      buckets = new Array(bucketCount); //建立桶
  for (i = 0; i < buckets.length; i++) {
    buckets[i] = []; //初始化桶
  }

  //將數據分配到各個桶中,這裏直接按照數據值的分佈來分配,必定範圍內均勻分佈的數據效率最爲高效
  for (i = 0; i < array.length; i++) {
    buckets[~~((array[i] - min) / bucketSize)].push(array[i]);
  }

  array.length = 0;
  for (i = 0; i < buckets.length; i++) {
    quickSort(buckets[i]); //對每一個桶進行排序,這裏使用了快速排序
    for (var j = 0; j < buckets[i].length; j++) {
      array.push(buckets[i][j]); //將已排序的數據寫回數組中
    }
  }
  return array;
}複製代碼複製代碼

Tips
: 桶排序自己是穩定的排序, 所以它的穩定性與桶內排序的穩定性保持一致.

實際上, 桶也只是一個抽象的概念, 它的思想與歸併排序,快速排序等相似, 都是經過將大量數據分配到N個不一樣的容器中, 分別排序, 最後再合併數據. 這種方式大大減小了排序時總體的遍歷次數, 提升了算法效率.

基數排序

基數排序源於老式穿孔機, 排序器每次只能看到一個列. 它是基於元素值的每一個位上的字符來排序的. 對於數字而言就是分別基於個位, 十位, 百位 或千位等等數字來排序. (不明白沒關係, 我也不懂, 請接着往下讀)

按照優先從高位或低位來排序有兩種實現方案:

  • MSD: 由高位爲基底, 先按k1排序分組, 同一組中記錄, 關鍵碼k1相等, 再對各組按k2排序分紅子組, 以後, 對後面的關鍵碼繼續這樣的排序分組, 直到按最次位關鍵碼kd對各子組排序後. 再將各組鏈接起來, 便獲得一個有序序列. MSD方式適用於位數多的序列.
  • LSD: 由低位爲基底, 先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便獲得一個有序序列. LSD方式適用於位數少的序列.

以下是LSD的動圖效果:

基數排序
)

以下是算法實現:

function radixSort(array, max) {
    var buckets = [],
        unit = 10,
        base = 1;
    for (var i = 0; i < max; i++, base *= 10, unit *= 10) {
        for(var j = 0; j < array.length; j++) {
            var index = ~~((array[j] % unit) / base);//依次過濾出個位,十位等等數字
            if(buckets[index] == null) {
                buckets[index] = []; //初始化桶
            }
            buckets[index].push(array[j]);//往不一樣桶裏添加數據
        }
        var pos = 0,
            value;
        for(var j = 0, length = buckets.length; j < length; j++) {
            if(buckets[j] != null) {
                while ((value = buckets[j].shift()) != null) {
                      array[pos++] = value; //將不一樣桶裏數據挨個撈出來,爲下一輪高位排序作準備,因爲靠近桶底的元素排名靠前,所以從桶底先撈
                }
            }
        }
    }
    return array;
}複製代碼複製代碼

以上算法, 若是用來比較時間, 先按日排序, 再按月排序, 最後按年排序, 僅需排序三次.

基數排序更適合用於對時間, 字符串等這些總體權值未知的數據進行排序.

Tips
: 基數排序不改變相同元素之間的相對順序, 所以它是穩定的排序算法.

小結

各類排序性能對好比下:

排序類型 平均狀況 最好狀況 最壞狀況 輔助空間 穩定性
冒泡排序 O(n²) O(n) O(n²) O(1) 穩定
選擇排序 O(n²) O(n²) O(n²) O(1) 不穩定
直接插入排序 O(n²) O(n) O(n²) O(1) 穩定
折半插入排序 O(n²) O(n) O(n²) O(1) 穩定
希爾排序 O(n^1.3) O(nlogn) O(n²) O(1) 不穩定
歸併排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n) 穩定
快速排序 O(nlog₂n) O(nlog₂n) O(n²) O(nlog₂n) 不穩定
堆排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(1) 不穩定
計數排序 O(n+k) O(n+k) O(n+k) O(k) 穩定
桶排序 O(n+k) O(n+k) O(n²) O(n+k) (不)穩定
基數排序 O(d(n+k)) O(d(n+k)) O(d(n+kd)) O(n+kd) 穩定

注: 桶排序的穩定性取決於桶內排序的穩定性, 所以其穩定性不肯定. 基數排序中, k表明關鍵字的基數, d表明長度, n表明關鍵字的個數.

願以此文懷念下我那遠去的算法課程.

未完待續...


做者:路易斯
連接:https://juejin.im/post/58c9d5fb1b69e6006b686bce
來源:掘金
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。




檢索算法

二分查找算法:也稱折半查找(Binary Search),它是一種效率較高的查找方法。可是,折半查找要求線性表必須採用順序存儲結構,並且表中元素按關鍵字有序排列。

default

查找說明

1.將數組的第一個位置設置爲下邊界(0)
2.將數組最後一個元素所在的位置設置爲上邊界(數組的長度減1)。
3.若下邊界等於或小於上邊界,則作以下操做。

  • 將中點設置爲(上邊界加上下邊界)除以2
  • 若是中點的元素小於查詢的值,則將下邊界設置爲中點元素所在下標加1
  • 若是中點的元素大於查詢的值,則將上邊界設置爲中點元素所在下標減1
  • 不然中點元素即爲要查找的數據,能夠進行返回。

注意這裏的數組必須是有序

function binSearch(arr, data) {
  var upperBound = arr.length - 1;
  var lowerBound = 0;
  while (lowerBound <= upperBound) {
      var mid = Math.floor((upperBound + lowerBound) / 2);
      if (arr[mid] < data) {
          lowerBound = mid + 1;
      }
      else if(arr[mid] > data) {
          upperBound = mid - 1;
      } else {
          return mid;
      }
  }
  return -1;
}複製代碼

二叉樹算法

default

二叉樹
二叉樹是每一個節點最多有兩個子樹的樹結構。一般子樹被稱做「左子樹」(left subtree)和「右子樹」(right subtree)。二叉樹常被用於實現二叉查找樹和二叉堆。

二叉樹的特性

  • 每一個結點最多有兩顆子樹,結點的度最大爲2
  • 左子樹和右子樹是有順序的,次序不能顛倒
  • 即便某結點只有一個子樹,也要區分左右子樹。

二叉樹遍歷

這裏採用遞歸-先序遍歷一個樹形結構

var preOrder = function (node) {
  if (node) {
    preOrder(node.left);
    preOrder(node.right);
  }
}複製代碼
相關文章
相關標籤/搜索