【算法 排序】冒泡·插入·歸併·快速·堆排序算法總結

排序算法總結及實現源碼

筆記

排序算法
		分類
			基本排序	[快速無Bug]
				冒泡
				插入
			高級排序	[必考]
				歸併
				快速
				拓撲 Topological
			其餘重點 [須要研究]
				堆排序
				桶排序

		冒泡排序 [穩定]
			耗時: O(n^2)
			空間: S(1)
			核心思想: 從頭開始,相鄰兩兩比較交換,直到尾部 
						=> 這一輪中最大/小元素 冒泡到 Array 尾部 [橫着看 吐泡泡]
		選擇排序 [非穩定]
			耗時: O(n^2/2)
			空間: S(1)
			和冒泡相似,但不須要頻繁兩兩鄰近賦值操做,只須要更新賦值當前最大/小 => 少許賦值

		插入排序 [穩定]
			耗時: O(n) / O(n^2/2)
			空間: S(1)
			核心思想: 從第二個元素開始,向前比較,[後部數組] 右/後移
						在原後部第一元素位置 覆蓋 插入元素

		歸併排序 [穩定]
			耗時: O(nlogn) / O(nlogn)
			空間: S(2n)
			核心思想: [分治] 將複雜問題拆分紅若干的子問題,而後一一解決
						一直將子數組拆分紅更小的子數組(2路=>2個子數組),直到數組中只有1個元素[先局部有序]
						而後再合併各個局部有序子數組 
						=> mergeSort(){
							if (hi <= lo) return;			// 遞歸終止條件
							int mid = lo + (hi - lo) / 2;	// 切分項
							mergeSort(左半)
							mergeSort(右半)
							merge()							// 合併排序
						}

		快速排序 [非穩]
			耗時: O(nlogn) / O(n^2/2)
			空間: S(nlogn)
			核心思想: [分治] 將數組 根據 切分項V 分爲2塊,不斷交換左側大於V 及 右側小於V 的2個數組項i,j
						也就是粗排序,交換左右不合規元素 => 區域有序,而後再進行遞歸切分,實現更細區域有序
						最終,遞歸到底達成全局有序 [反着來的歸併排序]
						=> quickSort(){
							if (hi <= lo) return;	// 遞歸終止條件
							partition()				// 粗排序+切分項
							quickSort(左半)
							quickSort(右半)
						}

			與歸併區別: 
				同
					兩者很像 [最好狀況下將全部內容精確地分紅兩半。這使它有點像「歸併排序」]
					知足歸併排序——分而治之遞歸算法:Cn = 2Cn + n 公式 ==> N*lgN
				異
					1.Partition in-place 就地分區
						使用一個額外數組來更容易分區及實現Stable,但這不值得
						QS 更快 更節省空間,這是QS優點所在
					2.shuffleing 是必要的
						對於保證性能來講,失去會致使性能受損。切分項也能夠隨機
						隨機性是爲了平衡最壞狀況下的性能 => 最壞狀況下的機率保證
					3.排序與遞歸順序
						QS [先粗排序再遞歸切分]
						MS [先遞歸切分再合併排序]
						區域性有序: 區域內 無序,但 均大於 x / 均小於 x
						局部性有序: 局部內 有序, 全局無序

		堆排序 [非穩]
			耗時: O(nlogn) / O(2nlogn + 2n)
			空間: S(n)

			用途: 在現實嵌入式較少資源狀況下惟一使用的原地排序算法
					最重要的是它能在插入、刪除 混合操做動態場景下能保持 對數級別的計算時間

			數據結構: 優先隊列([二叉堆]實現(數組))
				二叉堆: [堆有序]的[徹底[二叉樹]]結構元素,在數組中按層級存儲
				堆有序: 每一個節點都大於等於它的兩個子節點時,稱爲堆有序
				二叉樹: 空子節點,或者鏈接指向左右子節點(也是一顆二叉樹)
				徹底二叉樹: 完美平衡 => 若是樹高 4,則一二三層均是完整左右子樹,第四層能夠不完美

				徹底二叉樹性質:
					1.徹底二叉樹可根據 節點數 計算 樹高 [ N nodes => lgN ]
					2.只用數組而不須要指針就能夠表示/實現

				二叉堆性質: 
					1.不使用數組索引0
					2.從任意節點向上,都能獲取一列 非遞減的元素
						從任意節點向下,都能獲取一列 非遞增的元素
					3.二叉堆中索引位置爲 k 的元素的父親爲 k/2 向下取整,其子節點爲 2k 及 2k+1
			核心思想: 
				構建 MaxPriorityQueue 二叉堆實現的優先隊列[其實數組就行],並重復刪除最大元素/堆頂元素 
					=> 即兩階段: 1.堆構造階段(堆有序); 2.下沉階段 (刪除堆頂/輸出最大元素)

			與快排歸併區別
				異
					1.全局無序,條狀有序(從下至上/從上至下一條線)
					2.經過不斷 輸出(delete/sink) 全局最大/小 至數組末尾 實現 排序

		拓撲排序 [頂點排序]
			前提: 必須有向圖; 圖裏沒有環; 沒有孤島 【統計前驅/入度; 廣度優先搜索遍歷;】
			耗時: O(n)
			空間: S(n)
			
			用途: 理清具備依賴關係的任務 [將圖論的頂點按照相連的性質進行排序]

			void sort(){
				Queue<Integer> q = new LinkedList();
				for(int v = 0; v < V; v++)
					if(indegree[v] == 0) q.add(v);
				while(!q.isEmpty()){
					int v = q.poll();
					print(v);

					for(int u = 0; u < adj[v].length; u++)
						if(--indegree[u] == 0) q.add(u);
				}
			}

		時間複雜度比較圖
						inplace?	stable?	worst	average	best	remarks
			selection	x					N^2/2	N^2/2	N^2/2	N次交換
			insertion	x			x		N^2/2	N^2/4	N 		適用於小量(15)元素
			shell		x					?		?		N 		tight code, subquadratic
			quick 		x					N^2/2	2NlogN 	NlogN 	probabilistic guarantee fastest
			3-way quick x					N^2/2	2NlogN 	N 		針對重複Key提高
			merge 					x		NlogN 	NlogN 	NlogN 	穩定NlogN(2種含義)
			heap 		x					2NlogN 	2NlogN 	NlogN 	代碼簡單 插刪混合對數級別

實現源碼

/** * @Project Algorithm * @Author INBreeze * @Date 2020/3/28 9:51 * @Description Sort */
@SuppressWarnings({"unused"})
public class Sort {
    /*******************************************測試函數*******************************************/
    public static void main(String[] args) {
        int[] a = {0, 5, 4, 6, 2, 1};           // 堆排序默認 不使用 a[0] 故此,在a[0]填充0元素
        //a = popSort(a);
        //a = insertSort1(a);
        //a = insertSort2(a);
        //mergeSortEntrance(a);
        //quickSortEntrance(a);
        //heapSort(a);
        for (int i : a) {
            System.out.println(i);
        }

        // a[0], a[1]
        /*int i = 0; System.out.println(a[i++]); System.out.println(a[++i]);*/
    }

    /*******************************************冒泡排序*******************************************/
    static int[] popSort(int[] arr) {
        // 總冒泡次數 是 n-1 [最後一個數不須要冒泡]
        // 內部比較次數 是 愈來愈少的 n-1,n-2,n-3,...,1·等差數組求和 n(a1+an)/2 [由於數組尾部有序]
        // 空間複雜度 S(1) [只須要 tmp 變量]
        // 時間複雜度 O( (n-1)(1+n-1)/2 = (n-1)n/2 = (n^2-n)/2 ≈ O(n^2) [n最夠大時]
        // 最好狀況: 直接有序 O(n)
        // 最壞狀況: 徹底倒序 O(n^2)
        // 理解: 只管最大泡泡冒出

        boolean hasChanged = true;
        for (int n = 1; n < arr.length && hasChanged; n++) {    // 總冒泡次數
            hasChanged = false;                                     // 優化1: 提早終止
            for (int i = 0; i < arr.length - n; i++) {          // 每次冒泡兩兩比較的次數
                int j = i + 1, tmp;
                if (arr[i] > arr[j]) {
                    tmp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = tmp;
                    hasChanged = true;
                }
            }
        }
        return arr;
    }

    /*******************************************插入排序*******************************************/
    static int[] insertSort1(int[] arr) {
        // 空間複雜度 S(1) [只須要 current 變量]
        // 時間複雜度
        // 最好狀況: 直接有序 O(n)
        // 最壞狀況: 徹底倒序 O(n^2)
        // 理解: 累積局部有序 後部數組右移 插入新值

        for (int i = 1; i < arr.length; i++) {                  // 新插入的值位置索引i
            int k = i;                                              // 優化1: 避免i左移後的重複比較 k用於保存當前i
            for (int j = i - 1; j >= 0; j--) {                  // 局部有序數組範圍索引j
                if (arr[j] > arr[k]) {
                    int tmp = arr[k];
                    arr[k] = arr[j];
                    arr[j] = tmp;
                    k = j;                                  // i向左冒泡位置索引已改變 使用k保存變化的i
                }
            }
        }
        return arr;
    }

    static int[] insertSort2(int[] arr) {
        for (int i = 1, j, current; i < arr.length; i++) {
            current = arr[i];

            for (j = i - 1; j >= 0 && arr[j] > current; j--) {      // 優化2: 不進行冒泡交換,直接將 cur < [後部數組] 右/後移
                arr[j + 1] = arr[j];
            }

            arr[j + 1] = current;                                   // 當尋找到 [前部數組] > cur 進行 arr[j+1] <-覆蓋- cur
        }
        return arr;
    }

    /*******************************************歸併排序*******************************************/
    // 對外入口函數·迪米特法則
    static void mergeSortEntrance(int[] a) {
        int[] aux = new int[a.length]; // 輔助數組
        mergeSort(a, aux, 0, a.length - 1);    // 實際核心函數sort
    }

    // 實際核心函數sort
    static void mergeSort(int[] a, int[] aux, int lo, int hi) {
        if (hi <= lo) return;                   // 遞歸終止條件
        int mid = lo + (hi - lo) / 2;
        mergeSort(a, aux, lo, mid);             // 左半遞歸切分
        mergeSort(a, aux, mid + 1, hi);     // 右半遞歸切分
        merge(a, aux, lo, mid, hi);             // 合併排序 [左半右半] [先遞歸切分再合併排序]
    }

    static void merge(int[] a, int[] aux, int lo, int mid, int hi) {
        // assert isSorted(a, lo, mid);
        // assert isSorted(a, mid + 1, hi);
        // copy
        if (hi + 1 - lo >= 0) System.arraycopy(a, lo, aux, lo, hi + 1 - lo);
        //merge
        int i = lo, j = mid + 1;
        for (int k = lo; k <= hi; k++) {
            if (i > mid)                    // 1.左半已所有移入,將右半剩餘移入 原地數組a[]
                a[k] = aux[j++];
            else if (j > hi)                // 2.右半已所有移入,將左半剩餘移入 原地數組a[]
                a[k] = aux[i++];
            else if (less(aux[j], aux[i]))  // 3.右半目標小,則將右半移入 原地數組a[]
                a[k] = aux[j++];
            else                            // 4.左半目標小,則將左半移入 原地數組a[]
                a[k] = aux[i++];
        }
    }

    static boolean less(int a, int b) {
        return a < b;
    }

    /*******************************************快速排序*******************************************/
    static void quickSortEntrance(int[] a) {
        //StdRandom.shuffle(a); // Shuffle·消除輸入依賴
        quick(a, 0, a.length - 1);
    }

    static void quick(int[] a, int lo, int hi) {
        if (hi <= lo) return;                                // 遞歸終止條件
        int j = partition(a, lo, hi);                   // Partition·尋找切分項 && 左右半區粗排序 [先粗排序再遞歸]
        quick(a, lo, j - 1);                            // Sort·左半(<切分項)遞歸
        quick(a, j + 1, hi);                            // Sort·右半(切分項<)遞歸
    }

    static int partition(int[] a, int lo, int hi) {
        int i = lo, j = hi + 1;                         // a[lo]是不須要比較做爲切分項,但a[hi]須要比較故此先+1
        int v = a[lo];                                  // 通常假定a[lo]數組首項爲 切分項v · 也能夠進行隨機選擇
        while (true) {
            while (less(a[++i], v)) if (i == hi) break;    // 找到不小於 v 在左側(相對於切分項v)
            while (less(v, a[--j])) if (j == lo) break;    // 找到不大於 v 在右側(相對於切分項v)
            if (i >= j) break;                // 若是 i j 遊標相遇,則不須要再查找
            exch(a, i, j);                    // 交換,左側不小於v 與 右側不大於v 的兩個不合規數組項i,j
        }
        // j 最終會越過 i 指向 小於 v 的數組項
        exch(a, lo, j);                        // 最後交換,切分項v 至 正確位置j [a[j]以後所有都是>=v的]
        return j;
    }

    static void exch(int[] a, int left, int right) {
        int tmp = a[left];
        a[left] = a[right];
        a[right] = tmp;
    }

    /*******************************************堆排序*******************************************/
    static void heapSort(int[] a) {     // 注意: 堆排序不使用
        // 階段1 堆構建
        int N = a.length-1;
        for (int k = N / 2; k >= 1; k--) {
            sink(a, k, N);
        }
        // 階段2 下沉
        while (N > 1) {
            exch(a, 1, N);        // 交換 堆頂最大元素 與 最後元素 [這樣堆排序就不須要額外數組空間]
            sink(a, 1, --N);        // 第三個參數爲數組長度,由於是同一數組內原地排序,須要減小數組長度
        }
    }

    static void sink(int[] a, int k, int N) {
        while (2 * k <= N) {
            int j = 2 * k;                                  // j = 左子
            if (j < N && less(a[j], a[j + 1])) j++;         // 若是k位置有子節點 而且 左子<右子 則 j = 右子
            if (!less(a[k], a[j])) break;                   // 若是 k > 最大子節點 則不變
            exch(a, k, j);                                  // 交換 k 與其 最大子節點
            k = j;                                          // 繼續下沉 k 對象
        }
    }
}