關於前端中常用的排序演算法-圖文講解

遊幕發表於2018-03-04

如果出現錯誤,請在評論中指出,我也好自己糾正自己的錯誤

關於前端中常用的排序演算法-圖文講解

author: thomaszhou

排序

先定義一些變數和方法

    function ArrayList() {
	  let arr = [];  
	  this.insert = function(item) { // 插入
	    arr.push(item);
		};

	  this.toString = function() { // 字串形式顯示數值
        return arr.join();
	  };
複製程式碼

很多排序演算法用到兩個值的交換功能,所以我抽象出一個函式:

this.swap = function(i, j, arr) {
  let temp = arr[i]; 
  arr[i] = arr[j];
  arr[j] = temp;
};

複製程式碼

直接插入排序

這個方法是排序中最簡答的方法

  • 思想:依次將待排序序列中的每一個值插入到一個已經排好序的序列中,直到全部記錄都排好序
  • 需要解決的問題:
    • 如何構造初始的有序佇列?
    • 如何查詢待插入記錄的插入位置?
  • 思路:如圖中第四趟排序結果,從第三趟的結果開始,我們要插入值6,然後我們只跟當前要插入的值(值為6)的前一個值比較(如果當前是arr[i],那前一個就是arr[i-1])來比較,( 因為看到的用[ ]包裹的是排好序的序列,最右邊是其中最大的值 ),我們將當前要排序的值存到變數current當中,如果current值比arr[i-1]小,那就繼續向前移動,繼續比較current和arr[i-2],依次類推,並且每次移動,都將值向後移(20移到之前6的位置,15移到之前20的位置,程式碼是arr[index + 1] = arr[index])。當找到current > arr[i-n]的時候,將current賦值給arr[i-n+1].
    image
// 設定arr = [12, 15, 9, 20, 6, 31, 24]
    this.InsertSort = function() {
      let len = arr.length,
		  current,
		  index;
      for (let i = 1; i < len; i++) {
        current = arr[i]; // current儲存的是當前要插入的值
        for ( index = i - 1; arr[index] > current; index--) {
        // index是有序序列的最後一個值,即current的前一個值
          arr[index + 1] = arr[index];
		}
		arr[index + 1] = current;
	  }
    };
複製程式碼
let arraylist = new ArrayList();
arraylist.InsertSort();
console.log(`InsertSort陣列是:${arraylist.toString()}`); // InsertSort陣列是:6,9,12,15,20,24,31
複製程式碼

簡單選擇排序

主要思想:每趟排序在當前待排序序列中選出最小值,和下標為i的值交換(i 初始為0,每躺排序結束後 i++)

從演算法邏輯上看, 選擇排序是一種簡單且直觀的排序演算法. 它也是兩層迴圈. 內層迴圈每執行一遍, 將選出本次待排序的元素中最小(或最大)的一個, 存放在陣列位置i. 外層迴圈就是設定迴圈結束條件(i < n)。

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

image

this.SlectSort = function() {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
	let index = i; 
    for (let j = i; j < len; j++) {
	  if (arr[j] < arr[index]) {  // 找最小值/或者最大值(< 是升序  > 是降序)
		index = j; 
	  }
	}
    //	exchange
    index !== i && this.swap(i,index,arr);
  }
};
複製程式碼

快速排序

快排被人們認為是最快的,所以呢,面試題中也會經常考。它將陣列拆分為兩個子陣列(也就是一次劃分操作), 其中一個子陣列的所有元素都比另一個子陣列的元素小, 然後對這兩個子陣列再重複進行上述操作(遞迴下去,不斷劃分), 直到陣列不可拆分, 排序完成.

如圖是快排的一次h劃分的過程;

image

虛擬碼表示一次劃分:

  • 將i和j分別指向待劃分割槽間的最左側和最右側記錄的位置
  • 重複下述過程,直到i = j
    • 右側掃描,直到下標 j 的值小於下標 i 的值
    • 將r[j]與r[i]交換,並執行i++
    • 左側掃描,直到下標 i 的值大於下標j 的值
    • 將r[i]與r[j]交換,並執行j--
  • 退出迴圈,說明i和j指向了軸值記錄的所在位置,返回該位置
//呼叫函式:arraylist.Partition(1, 7);但是陣列下標是0-6

    this.Partition = function(first, end) { // 如果提取出來會傳遞一個arr陣列,本例是直接呼叫全域性的arr陣列
	    let i = first - 1, // arr下標是(first-1, end-1)
		    j = end - 1;
	    while (i < j) {
	      while (i < j && arr[i] <= arr[j]) { j--; }
		  if (i < j) {
		    this.swap(i, j, arr); // 交換
    	    i++;
		  }
		  while(i < j && arr[i] <= arr[j]) { i++; }
          if (i < j) {
            this.swap(i, j, arr);
            j--;
          }
		}
		return i+1;
	};
複製程式碼

如圖,一次劃分後,我們可以得到[19, 13, 6]和[31, 49 ,28],然後再對這兩個子區間進行劃分,依次類推,此處需要遞迴

this.QuickSort = function(first, end) {// 如果提取出來會傳遞一個arr陣列,本例是直接呼叫全域性的arr陣列
	if (first < end) {
	  let pivot = this.Partition(first, end);
	  this.QuickSort(first,pivot - 1); // 前半部分
	  this.QuickSort(pivot + 1, end); // 後半部分
	}
};
複製程式碼

驗證

console.log(arraylist.Partition(1, 7));
console.log(`第一個劃分的結果${arraylist.toString()}`); // 第一個劃分的結果19,13,6,23,31,49,28
arraylist.QuickSort(1, 7);
console.log(`最終結果是:${arraylist.toString()}`); // 最終結果是:6,13,19,23,28,31,49
複製程式碼

歸併排序

首先將具有n個待排序的記錄序列變成n個長度為1的有序序列,然後再兩兩合併,邊合併邊排序。

歸併排序嚴格按照從左往右(或從右往左)的順序去合併子陣列, 它並不會改變相同元素之間的相對順序, 因此它也是一種穩定的排序演算法.

image

  • 合併過程(建議結合圖片和合並的程式碼理解!):我們看倒數第二行的右側,[1,7]和[4,5]進行合併,建立一個陣列result,先比較兩個陣列的首位置數值,1比4小,將1加入result,然後 7大於4,將4加入result,7大於5,將5加入result,然後原本[4,5]陣列長度為空,跳出迴圈
拆分過程:
function mergeSort(arr) {  //輸入一個長度為n的陣列,輸出長度為1的n個陣列
    var length = arr.length;
    if(length < 2) {
      return arr;
    }
    var m = (length >> 1),
        left = arr.slice(0, m),
        right = arr.slice(m); //拆分為兩個子陣列
    return merge(mergeSort(left), mergeSort(right));//子陣列繼續遞迴拆分,然後再合併
};
複製程式碼

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

function merge(left, right){ // 合併陣列

    let result = [];
    while (left.length && right.length) {
    // 注意:判斷的條件是小於或等於,如果只是小於,那麼排序將不穩定.
      let item = left[0] <= right[0] ? left.shift() : right.shift();
    }
    //      只要left.length和right.length其中一個不滿足,就會跳出while迴圈
    // 假設left為空,跳出迴圈,那麼right還剩一個值,反之同理
    return result.concat(left.length ? left : right); // 將最終的return返回
};
複製程式碼

由上, 長度為n的陣列, 最終會呼叫mergeSort函式2n-1次.

這個演算法會出現錯誤,棧溢位

Uncaught RangeError: Maximum call stack size exceeded
複製程式碼

相關文章