前端也能學演算法:JS版常見排序演算法-冒泡,插入,快排,歸併

蔣鵬飛發表於2020-02-07

排序是很常見也很經典的問題,下面講幾種排序演算法:

氣泡排序

氣泡排序是最好理解的一種演算法,以升序排序為例,即最小的在前面,對陣列進行一次遍歷,如果相鄰的兩個數前面的比後面的大,則交換他們的位置,第一次遍歷會將最大的數字排到最後去,第二次遍歷會將第二大的數字排到倒數第二的位置。。。以此類推,遍歷n-1遍整個陣列就有序了。詳細解說參考www.runoob.com/w3cnote/bub…:

bubbleSort

下面我們自己來實現一遍程式碼:

const array = [1, 3, 2, 6, 4, 5, 9, 8, 7];

const sort = (arr) => {
  let result = [...arr];
  let temp;
  for(let i = 0; i < result.length - 1; i++){
    for(let j = 0; j < result.length - 1 -i; j++){
      if(result[j] > result[j + 1]){
        temp = result[j];
        result[j] = result[j + 1];
        result[j + 1] = temp;
      }
    }
  }
  
  return result;
}

const newArr = sort(array);
console.log(newArr); // [1, 2, 3, 4, 5, 6, 7, 8, 9]複製程式碼

插入排序

會打撲克的同學應該很熟悉這個排序法,每次摸牌的時候都去手裡面已經排好序的牌裡面比較下,找到它的位置,插入進去。這個查詢可以使用二分查詢,所以更快。具體分析看這裡:www.runoob.com/w3cnote/ins…

insertionSort

const array = [1, 3, 2, 6, 4, 5, 9, 8, 7];

const sort = (arr) => {
  let result = [...arr];
  let temp;
  for(let i = 0; i < result.length; i++){
    let j = i;
    while(result[j - 1] > result[j] && j>=0){
      temp = result[j - 1];
      result[j - 1] = result[j];
      result[j] = temp;
      j--;
    }
  }

  return result;
}

const newArr = sort(array);
console.log(newArr); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼
// 二分查詢版
const array = [1, 3, 2, 6, 4, 5, 9, 8, 7];

const sort = (arr) => {
  let result = [...arr];
  let i = 0;
  let length = result.length;
  for(i; i < length; i++) {
    let left = 0;
    let right = i - 1;
    let current = result[i];

    // 找目標位置, 最終的left就是目標位置
    while(left <= right) {
      let middle = parseInt((left + right) / 2);
      if(current < result[middle]){
        right = middle - 1
      } else {
        left = middle + 1;
      }
    }

    // 將目標位置後面的元素全部後移一個,位置讓出來
    for(let j = i - 1; j >= left; j--){
      result[j+1] = result[j];
    }

    // 最後將當前值插入到正確位置
    result[left] = current;
  }

  return result;
}

const newArr = sort(array);
console.log(newArr); // [1, 2, 3, 4, 5, 6, 7, 8, 9]複製程式碼

快速排序

快速排序是一個效率很高而且面試中經常出現的排序,他的平均時間複雜度是O(nlogn),最差時間複雜度是O(n^2)。他的核心思想是選定一個基準值x,將比x小的值放到左邊,比x大的值放到右邊。假設我們有如下陣列:

const a = [3, 6, 2, 1, 4, 5, 9, 8, 7];
複製程式碼

我們每次都取陣列的第一個值為x,然後將比他小的放到左邊,大的放到右邊。這裡我們的第一個值3,經過這麼一次運算後,我們期望的目標是得到類似這樣一個陣列:

const a = [2, 1, 3, 6, 4, 5, 9, 8, 7];
複製程式碼

注意這個陣列,3左邊的都比3小,3右邊的都比3大,左右兩邊裡面的順序可能是不對的,但是3本身的位置是對的。怎麼來實現這個呢?我們用x把3暫存下來,然後使用兩個指標i,j分別指向陣列最開始和最後面。初始狀態x = 3, i = 0, j = 8。

012345678
3
62145987

我們暫存了a[0],就相當於把a[0]挖出來了,需要找一個數填進去。我們從後往前找,找一個比3小的數,我們發現a[3]是1(j=3),比3小,將它填到a[0]的坑裡。注意,這時候i=0, j=3。

012345678
1
62
1
45987

a[3]被填到了a[0]的位置,相當於a[3]又被挖出來了,又需要找一個數來填充。這次我們從前往後找,找一個比x大的數,我們發現a[1]是6(i=1),比x大,將它填到a[3]的位置。注意,這時候i=1, j=3。

012345678
1
6
2
6
45987

這時候a[1]被填到了a[3]的位置,相當於a[1]又被挖出來了,又可以繼續填充數字,我們繼續從j的位置往前找一個小的。我們發現a[2]比3小,我們將a[2]填充到a[1]的位置。注意,這時候i=1, j=2;

012345678
1
2
2
645987

a[2]填充到了a[1],a[2]又空了,我們繼續從前往後找一個大的數字填充進去,i開始自增,但是他自增一個之後就不小於j了。這說明我們整個陣列已經遍歷完了,迴圈結束。注意,i自增一次後不小於j,觸發迴圈結束條件,此時i = 2。 而這時候的i就是我們最開始快取的x應該在的位置,我們將x放入a[i]。

012345678
12
3
645987

至此,一次遍歷就找到了基準值應該在的位置,並且調整了陣列,讓基準值左邊的數都比他小,右邊的都比他大。我們來實現下這個方法。

const partition = (arr) => {
  let x = arr[0];
  let length = arr.length;
  let i = 0;
  let j = length - 1;

  while(i < j) {
    // 先從後往前找小的, 沒找到繼續找
    while(i < j && arr[j] > x) {
      j--;
    }
    // 找到了,將值填入坑裡, a[j]又變成了坑
    if(i < j) {
      a[i] = a[j];
    }

    // 然後從前往後找大的,沒找到繼續找
    while(i < j && arr[i] < x) {
      i++;
    }
    // 找到了,將值填入之前的坑裡
    if(i < j) {
      a[j] = a[i];
    }
  }

  // 將基準值填入坑
  a[i] = x;

  return arr;
}

const a = [2, 1, 3, 6, 4, 5, 9, 8, 7];

// 測試下
let result = partition(a);
console.log(result);   // [1, 2, 3, 6, 4, 5, 9, 8, 7]
複製程式碼

在前面思路的基礎上繼續遞迴的對基準值左右兩邊呼叫這個調整方法,就能將陣列的每個數字都放到正確的位置上,這就是快速排序,這種思想叫分治法。前面調整陣列的方法我們需要進行微調,讓他接受開始位置和結束位置並返回基準值的位置。

const partition = (arr, left, right) => {
  let x = arr[left];
  let i = left;
  let j = right;

  while(i < j) {
    // 先從後往前找小的, 沒找到繼續找
    while(i < j && arr[j] > x) {
      j--;
    }
    // 找到了,將值填入坑裡, a[j]又變成了坑
    if(i < j) {
      a[i] = a[j];
    }

    // 然後從前往後找大的,沒找到繼續找
    while(i < j && arr[i] < x) {
      i++;
    }
    // 找到了,將值填入之前的坑裡
    if(i < j) {
      a[j] = a[i];
    }
  }

  // 將基準值填入坑
  a[i] = x;

  return i;
}

const quickSort = (arr, left, right) => {
  const length = arr.length;
  const start = left || 0;
  const end = right !== undefined ? right : length - 1;

  if(start < end) {
    const index = partition(arr, start, end);
    quickSort(arr, start, index - 1); // 調整基準值左邊
    quickSort(arr, index + 1, end); // 調整基準值右邊
  }

  return arr;
}

const a = [2, 1, 3, 6, 4, 5, 9, 8, 7];

// 測試下
let result = quickSort(a);
console.log(result);   // [1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼

歸併排序

歸併排序比快速排序好理解,時間複雜度也是O(nlogn),採用的思想也是分治法。假設我們已經有兩個有序陣列。

const a = [1 ,2, 6, 8];
const b = [3, 4, 9];
複製程式碼

我們現在寫一個方法來得到a跟b組合後的有序陣列,這個方法很簡單,用兩個指標i,j分別指向兩個陣列,然後開始遍歷,比較a[i]和a[j]的大小,將小的那個放入新的有序陣列。當任意一個陣列遍歷完,迴圈結束,將剩下的值全部放入新的有序陣列:

const merge = (arr1, arr2) => {
  const length1 = arr1.length;
  const length2 = arr2.length;
  const newArr = [];
  let i = 0;
  let j = 0;

  while(i < length1 && j < length2) {
    if(arr1[i] <= arr2[j]) {
      newArr.push(arr1[i]);
      i++;
    } else {
      newArr.push(arr2[j]);
      j++;
    }
  }

  // arr2還剩一些
  if(i === length1 && j < length2) {
    while(j < length2) {
      newArr.push(arr2[j]);
      j++;
    }
  }

  // arr1還剩一些
  if(j === length2 && i < length1) {
    while(i < length1) {
      newArr.push(arr1[i]);
      i++;
    }
  }

  return newArr;
}

// 測試一下
const a = [1 ,2, 6, 8];
const b = [3, 4, 9];

const result = merge(a, b);
console.log(result);   //  [1, 2, 3, 4, 6, 8, 9]
複製程式碼

然後我們遞迴的將待排序陣列分成左右兩個陣列,一直分到陣列只含有一個元素為止,因為陣列只含有一個元素,我們就可以認為他是有序的。

const mergeSort = (arr) => {
  const length = arr.length;
  if(length <= 1) {
    return arr;
  }
  const middle = Math.floor(length / 2);
  const left = arr.slice(0, middle);
  const right = arr.slice(middle);

  return merge(mergeSort(left), mergeSort(right));
}

// 測試一下
const a = [2, 1, 3, 6, 4, 5, 9, 8, 7];

// 測試下
let result = mergeSort(a);
console.log(result);   // [1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼


原創不易,每篇文章都耗費了作者大量的時間和心血,如果本文對你有幫助,請點贊支援作者,也讓更多人看到本文~~

更多文章請看我的掘金文章彙總



相關文章