算法心得——排序

簡單排序

主要操作:比較兩個數據項、交換兩個數據項或複製其中一項,具體操作要看排序的類型。

1.冒泡排序

運行效率較低, 但是在概念上它是排序算法中最簡單的,最適合初學的一種排序,具體思路爲:

  • 對未排序的各元素從頭到尾依次比較相鄰的兩個元素大小關係
  • 如果左邊的隊員高, 則兩隊員交換位置
  • 向右移動一個位置, 比較下面兩個隊員
  • 當走到最右端時, 最高的隊員一定被放在了最右邊
  • 繼續從最左邊開始排序,把第二高的隊員排到倒數第二位置上
  • 依此類推,完成排序
ArrayList.prototype.bubbleSort = function () {
    // 1.獲取數組的長度
    var length = this.array.length

    // 2.反向循環, 因此次數越來越少
    for (var i = length - 1; i >= 0; i--) {
        // 3.根據i的次數, 比較循環到i位置
        for (var j = 0; j < i; j++) {
            // 4.如果j位置比j+1位置的數據大, 那麼就交換
            if (this.array[j] > this.array[j+1]) {
                // 交換
                this.swap(j, j+1)
            }
        }
    }
}

ArrayList.prototype.swap = function (m, n) {
    var temp = this.array[m]
    this.array[m] = this.array[n]
    this.array[n] = temp
}

冒泡排序的效率爲O(N²),比較次數爲N²,交換次數也爲N²

2.選擇排序

思路:

  • 選定第一個索引位置,然後和後面元素依次比較
  • 如果後面的隊員, 小於第一個索引位置的隊員, 則交換位置
  • 經過一輪的比較後, 可以確定第一個位置是最小的
  • 然後使用同樣的方法把剩下的元素逐個比較即可
  • 可以看出選擇排序,第一輪會選出最小值,第二輪會選出第二小的值,直到最後
ArrayList.prototype.selectionSort = function () {
    // 1.獲取數組的長度
    var length = this.array.length

    // 2.外層循環: 從0位置開始取出數據, 直到length-2位置
    for (var i = 0; i < length - 1; i++) {
        // 3.內層循環: 從i+1位置開始, 和後面的內容比較
        var min = i
        for (var j = min + 1; j < length; j++) {
            // 4.如果i位置的數據大於j位置的數據, 那麼記錄最小的位置
            if (this.array[min] > this.array[j]) {
                min = j
            }
        }
        // 5.交換min和i位置的數據
        this.swap(min, i)
    }
}

選擇排序雖然效率也是O(N²),但在一定程度上優化了冒泡排序的效率,比較次數跟選擇排序的效率一樣都是O(N²),交換次數是O(N)。

3.插入排序

插入排序是簡單排序中效率最好的一種,也是學習其他高級排序的基礎, 比如希爾排序/快速排序, 所以也非常重要。

思路:

  • 從第一個元素開始,該元素可以認爲已經被排序
  • 取出下一個元素,在已經排序的元素序列中從後向前掃描
  • 如果該元素(已排序)大於新元素,將該元素移到下一位置
  • 重複上一個步驟,直到找到已排序的元素小於或者等於新元素的位置
  • 將新元素插入到該位置後, 重複上面的步驟
ArrayList.prototype.insertionSort = function () {
    // 1.獲取數組的長度
    var length = this.array.length

    // 2.外層循環: 外層循環是從1位置開始, 依次遍歷到最後
    for (var i = 1; i < length; i++) {
        // 3.記錄選出的元素, 放在變量temp中
        var j = i
        var temp = this.array[i]

        // 4.內層循環: 內層循環不確定循環的次數, 最好使用while循環
        while (j > 0 && this.array[j-1] > temp) {
            this.array[j] = this.array[j-1]
            j--
        }

        // 5.將選出的j位置, 放入temp元素
        this.array[j] = temp
    }
}

插入排序對已經有序或基本有序的數據來說,效率高得多,它的效率還是O(N²),它的比較次數是選擇排序的一半,所以這個算法效率是高於選擇排序的。

高級排序

1. 希爾排序

希爾排序是插入排序的一種高效的改進版, 並且效率比插入排序要更快

插入排序的問題:當某個很小的數據比如1在最右端,要想把它放到正確的索引爲0的位置,必須把每個前面的數據都向右移動一位,即比較和交換的次數都是N。

希爾排序就是在此基礎上提出了一種不需要一個個移動所有中間數據項的算法,大致思路就是先將數據分成間隔比較大的分組進行排序,接着分成間隔小的分組進行排序,一步步縮小間隔,直到間隔爲1。分完間隔排序後每次都讓數據離自己正確位置更近了,間隔爲1的排序就是插入排序了,此時大家都離自己正確的位置很近了,就不需要再對比交換那麼多次數了,比如:

  • 有數組 81, 94, 11, 96, 12, 35, 17, 95, 28, 58, 41, 75, 15
  • 先讓間隔爲5, 進行排序. (35, 81), (94, 17), (11, 95), (96, 28), (12, 58), (35, 41), (17, 75), (95, 15)
  • 排序後的新序列, 一定可以讓數字離自己的正確位置更近一步
  • 再讓間隔位3, 進行排序. (35, 28, 75, 58, 95), (17, 12, 15, 81), (11, 41, 96, 94)
  • 排序後的新序列, 一定可以讓數字離自己的正確位置又近了一步
  • 最後, 讓間隔爲1, 也就是正確的插入排序. 這個時候數字都離自己的位置更近, 那麼需要複製的次數一定會減少很多
    在這裏插入圖片描述
    所以,希爾排序間隔的選取非常重要,大致的選擇思路爲:
  • 希爾排序的原稿中, 建議的初始間距是N / 2, 簡單的把每趟排序分成兩半
  • 希爾排序的效率很增量是有關係的,但是, 它的效率證明非常困難, 甚至某些增量的效率到目前依然沒有被證明出來
  • 但是經過統計, 希爾排序使用原始增量, 最壞的情況下時間複雜度爲O(N²), 通常情況下都要好於O(N²)
ArrayList.prototype.shellSort = function () {
    // 1.獲取數組的長度
    var length = this.array.length

    // 2.根據長度計算增量
    var gap = Math.floor(length / 2)

    // 3.增量不斷變量小, 大於0就繼續排序
    while (gap > 0) {
        // 4.實現插入排序
        for (var i = gap; i < length; i++) {
            // 4.1.保存臨時變量
            var j = i
            var temp = this.array[i]

            // 4.2.插入排序的內層循環
            while (j > gap - 1 && this.array[j - gap] > temp) {
                this.array[j] = this.array[j - gap]
                j -= gap
            }

            // 4.3.將選出的j位置設置爲temp
            this.array[j] = temp
        }
      
        // 5.重新計算新的間隔
        gap = Math.floor(gap / 2)
    }
}

總之, 我們使用希爾排序大多數情況下效率都高於簡單排序, 甚至在合適的增量和N的情況下, 還好好於快速排序

2.快速排序

快速排序幾乎可以說是目前所有排序算法中, 最快的一種排序算法。當然希爾排序確實在某些情況下可能好於快速排序.,但是大多數情況下, 快速排序還是比較好的選擇.
快速排序非常重要,是面試中提升水平的排序算法

快速排序是冒泡排序的升級版,冒泡排序要交換很多次才能將最大值放到正確的位置上,而快速排序可以在一次循環中(其實是遞歸調用)找出某個元素的正確位置, 並且該元素之後不需要任何移動。

快速排序最重要的思想是分而治之,比如:

  • 先選擇其中一個數字,將大於數字的放到右邊,小於數字的放到左邊
  • 這個數字就被放到了自己肯定正確的地方
  • 接下來再對左右兩堆數據進行遞歸
function quick_sort(arr) {
	let left = 0
	let right = arr.length-1
    if(left < right) {
		// 由x存儲本輪要排序的數,默認每次都排第一個,要提高快排速度,最好的方法就是從左、中、右的中位數排起
        let x = arr[left];
        while (left < right)
        {
        	// 從最右邊開始找第一個小於x的數
            while(arr[right] >= x) 
				right--;  
			// 找到了,判斷是否在x右邊
            if(left < right) 
            	// 把小於x的數放到x的位置上,同時左指針右移
				arr[left++] = arr[right];
			// 右邊的數空出來了位置,從左向右找第一個大於等於x的數
            while(left < right && arr[left] < x) 
				left++;  
            if(left < right) 
            	// 把大於x的數放到空出來的位置上,同時右指針左移
				s[right--] = s[left];
        }
        // 找到了x最終的位置
        arr[left] = x;
        quick_sort(arr, 0, left - 1); // 遞歸調用 
        quick_sort(arr, left + 1, arr.length);
    }
    return arr
}

快速排序的最壞效率就是每次選擇的樞紐都是最左邊或最右邊,此時效率等同於冒泡排序,但選擇中位數的樞紐選擇方法是不可能遇到最壞情況的,所以快速排序的平均效率是O(N * logN),其他某些算法的效率也可以達到O(N * logN), 但是快速排序是最好的。

3.歸併排序

歸併排序,是創建在歸併操作上的一種有效的排序算法。算法採用分治法,且各層分治遞歸可以同時進行。歸併排序思路簡單,速度僅次於快速排序,爲穩定排序算法,一般用於對總體無序,但是各子項相對有序的數列。

思路:

分解(Divide):將n個元素分成個含n/2個元素的子序列。
解決(Conquer):用合併排序法對兩個子序列遞歸的排序。
合併(Combine):合併兩個已排序的子序列已得到排序結果。

function mergeSort(arr) {
    const length = arr.length;
    if (length === 1) { //遞歸算法的停止條件,即爲判斷數組長度是否爲1
        return arr;
    }
    const mid = Math.floor(length / 2);
   
    const left = arr.slice(0,  mid);
    const right = arr.slice(mid, length);
  
    return merge(mergeSort(left), mergeSort(right)); //要將原始數組分割直至只有一個元素時,纔開始歸併
}

function merge(left, right) {
    const result = [];
    let il = 0;
    let ir = 0;

    //left, right本身肯定都是從小到大排好序的
    while( il < left.length && ir < right.length) {
        if (left[il] < right[ir]) {
            result.push(left[il]);
            il++;
        } else {
            result.push(right[ir]);
            ir++;
        }
        
    }

    //不可能同時存在left和right都有剩餘項的情況, 要麼left要麼right有剩餘項, 把剩餘項加進來即可
    while (il < left.length) { 
        result.push(left[il]);
        il++;
    }
    while(ir < right.length) {
        result.push(right[ir]);
        ir++;
    }
    return result;
}

4.堆排序

利用最大堆/最小堆的性質排序,基本思想是:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就爲最大值。然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了。

function heapSort(arr) {
  let heapSize = arr.length;
  buildHeap(arr);//構造一個所有節點都滿足arr[parent[i]] > arr[i]的堆結構數組,這樣就把值最大的那個節點換到了根節點
  while(heapSize > 1) { //*1

    //在當前樹中,交換位於根節點的最大值和最後一個節點的值,這樣就把最大值排在了最後一個節點,這樣就排好了最大值
    const temp = arr[0];
    arr[0]=arr[heapSize-1];
    arr[heapSize-1] = temp;

    heapSize--;//當前樹中最後一個節點已經排好了值,故後面就不用再考慮這個節點,故新的樹的大小減一

    if (heapSize>1) {
      heapify(arr, heapSize, 0);//上面的交換操作產生了新的根節點,新的根節點只是通過跟最後一個節點交換得到的值,故新的根節點不滿足條件arr[parent[i]]<arr[i],所以要對根節點再次進行h
    }
  }
}

/**
 * @description 構造一個所有節點都滿足arr[parent[i]] > arr[i]的堆結構數組
 * @param {Array} arr 待排序數組
 */
function buildHeap(arr) {
  const heapSize = arr.length;
  const firstHeapifyIndex = Math.floor(heapSize/2-1);//從樹的倒數第二層的最後一個有子節點的節點(對於滿二叉樹就是倒數第二層的最後一個節點)開始進行heapify處理。Math.floor(heapSize/2-1)就是這個最後一個有子節點的節點索引。
  for (let i=firstHeapifyIndex; i >= 0; i--) {//從0到firstHeapifyIndex都要進行heapify處理,才能把最大的那個節點換到根節點
    heapify(arr, heapSize, i);
  }
}

/**
 * @description 以數組arr的前heapSize個節點爲樹,對其中索引爲i的節點向子節點進行替換,直到滿足從i往下的子節點都有arr[parent[i]]>=arr[i]
 * @param {*} arr TYPE Array  待排序的數組
 * @param {*} heapSize TYPE Number 待排序的數組中要作爲當前樹處理的從前往後數的節點個數,即待排序數組中前heapSize個點是要作爲樹來處理
 * @param {*} i TYPE Number arr數組中、heapSize長度的樹中的當前要進行往子節點替換的節點的索引
 */
function heapify(arr, heapSize, i) {
  const leftIndex = i * 2 + 1;//索引i的節點的左子節點索引
  const rightIndex = i * 2 + 2;//索引i的節點的右子節點索引
  let biggestValueIndex = i;
  if (leftIndex < heapSize && arr[leftIndex] > arr[biggestValueIndex]) {
    //節點的最大index爲heapSize-1
    //注意:這兩次比較要跟arr[biggestValueIndex]比較,不能跟arr[i]比較,因爲biggestValueIndex是會在左右i之間更新的
    biggestValueIndex = leftIndex; //如果左子節點的值大於biggestValueIndex的值(此時就是根節點的值),那麼更新biggestValueIndex爲左子節點索引
  }
  if (rightIndex < heapSize && arr[rightIndex] > arr[biggestValueIndex]) {
    biggestValueIndex = rightIndex;//如果右子節點的值大於biggestValueIndex的值(此時可能是根節點的值,也可能是左子節點的值),那麼更新biggestValueIndex爲右子節點索引
  }
  if (biggestValueIndex !== i) { //如果biggestValueIndex是左子節點索引或右子節點索引,那麼交換根節點與biggestValueIndex節點的值
    const temp = arr[i];
    arr[i] = arr[biggestValueIndex];
    arr[biggestValueIndex] = temp;

    //交換後,被交換的那個子節點(左子節點或右子節點)往下可能就不再滿足[parent[i]]>=arr[i],所以要繼續對biggestValueIndex進行heaify處理,即將biggestValueIndex可能需要和子節點進行值交換,直到樹的這個分支到葉子節點都滿足arr[parent[i]]>=arr[i]
    heapify(arr, heapSize, biggestValueIndex);//要

  }
}

堆排序是一種選擇排序,整體主要由構建初始堆+交換堆頂元素和末尾元素並重建堆部分組成。其中構建初始堆經推導複雜度爲O(n),在交換並重建堆的過程中,需交換n-1次,而重建堆的過程中,根據完全二叉樹的性質,[log2(n-1),log2(n-2)…1]逐步遞減,近似爲nlogn。所以堆排序時間複雜度一般認爲就是O(nlogn)級。

5.計數排序

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章