數據結構與算法之排序

排序

  • 冒泡排序(Bubble Sort)
  • 插入排序(Insertion Sort)
  • 歸併排序(Merge Sort)
  • 快速排序(Quick Sort)
  • 堆排序(Heap Sort)
  • 計數排序(Counting Sort)
  • 桶排序(Bucket Sort)
  • 拓撲排序(Topological Sort)

冒泡排序(Bubble Sort)

基本思想前端

給定一個數組,咱們把數組裏的元素統統倒入到水池中,這些元素將經過相互之間的比較,按照大小順序一個一個地像氣泡同樣浮出水面。java

實現算法

每一輪,從雜亂無章的數組頭部開始,每兩個元素比較大小並進行交換,直到這一輪當中最大或最小的元素被放置在數組的尾部,而後不斷地重複這個過程,直到全部元素都排好位置。其中,核心操做就是元素相互比較。後端

例題分析數組

給定數組 [2, 1, 7, 9, 5, 8],要求按照從左到右、從小到大的順序進行排序。數據結構

解題思路dom

從左到右依次冒泡,把較大的數往右邊挪動便可。函數

  1. 首先指針指向第一個數,比較第一個數和第二個數的大小,因爲 2 比 1 大,因此兩兩交換,[1, 2, 7, 9, 5, 8]。學習

  2. 接下來指針往前移動一步,比較 2 和 7,因爲 2 比 7 小,二者保持不動,[1, 2, 7, 9, 5, 8]。到目前爲止,7 是最大的那個數。優化

  3. 指針繼續往前移動,比較 7 和 9,因爲 7 比 9 小,二者保持不動,[1, 2, 7, 9, 5, 8]。如今,9 變成了最大的那個數。

  4. 再日後,比較 9 和 5,很明顯,9 比 5 大,交換它們的位置,[1, 2, 7, 5, 9, 8]。

  5. 最後,比較 9 和 8,9 比 8 大,交換它們的位置,[1, 2, 7, 5, 8, 9]。通過第一輪的兩兩比較,9 這個最大的數就像冒泡同樣冒到了數組的最後面。

  6. 接下來進行第二輪的比較,把指針從新指向第一個元素,重複上面的操做,最後,數組變成了:[1, 2, 5, 7, 8, 9]。

  7. 在進行新一輪的比較中,判斷一下在上一輪比較的過程當中有沒有發生兩兩交換,若是一次交換都沒有發生,就證實其實數組已經排好序了。

實現代碼

public static void bubbleSort(int[] nums) {
        // 定義一個布爾變量 hasChange,用來標記每輪遍歷中是否發生了交換
        boolean hasChange = true;
        for (int i = 0; i < nums.length - 1 && hasChange; i++) {
            // 每輪遍歷開始,將 hasChange 設置爲 false
            hasChange = false;
            // 進行兩兩比較,若是發現當前的數比下一個數還大,那麼就交換這兩個數,同時記錄一下有交換髮生
            for (int j = 0; j < nums.length - 1 - i; j++) {
                if (nums[j] > nums[j+1]) {
                    swap(nums, j, j+1);
                    hasChange = true;
                }
            }
        }
    }
	// 交換數組中的兩個數
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

算法分析

空間複雜度

假設數組的元素個數是 n,因爲在整個排序的過程當中,咱們是直接在給定的數組裏面進行元素的兩兩交換,因此空間複雜度是 O(1)。

時間複雜度

  1. 給定的數組按照順序已經排好

    在這種狀況下,咱們只須要進行 n−1 次的比較,兩兩交換次數爲 0,時間複雜度是 O(n)。這是最好的狀況。

  2. 給定的數組按照逆序排列

    在這種狀況下,咱們須要進行 n(n-1)/2 次比較,時間複雜度是 O(n2)。這是最壞的狀況。

  3. 給定的數組雜亂無章

    在這種狀況下,平均時間複雜度是 O(n2)。

因而可知,冒泡排序的時間複雜度是 O(n2)。它是一種穩定的排序算法。(穩定是指若是數組裏兩個相等的數,那麼排序先後這兩個相等的數的相對位置保持不變。)

插入排序(Insertion Sort)

基本思想

不斷地將還沒有排好序的數插入到已經排好序的部分。

特色

在冒泡排序中,通過每一輪的排序處理後,數組後端的數是排好序的;而對於插入排序來講,通過每一輪的排序處理後,數組前端的數都是排好序的。

例題分析

對數組 [2, 1, 7, 9, 5, 8] 進行插入排序。

解題思路

首先將數組分紅左右兩個部分,左邊是已經排好序的部分,右邊是尚未排好序的部分,剛開始,左邊已排好序的部分只有第一個元素 2。接下來,咱們對右邊的元素一個一個進行處理,將它們放到左邊。

  1. 先來看 1,因爲 1 比 2 小,須要將 1 插入到 2 的前面,作法很簡單,兩兩交換位置便可,[1, 2, 7, 9, 5, 8]。
  2. 而後,咱們要把 7 插入到左邊的部分,因爲 7 已經比 2 大了,代表它是目前最大的元素,保持位置不變,[1, 2, 7, 9, 5, 8]。
  3. 同理,9 也不須要作位置變更,[1, 2, 7, 9, 5, 8]。
  4. 接下來,如何把 5 插入到合適的位置。首先比較 5 和 9,因爲 5 比 9 小,兩兩交換,[1, 2, 7, 5, 9, 8],繼續,因爲 5 比 7 小,兩兩交換,[1, 2, 5, 7, 9, 8],最後,因爲 5 比 2 大,此輪結束。
  5. 最後一個數是 8,因爲 8 比 9 小,兩兩交換,[1, 2, 5, 7, 8, 9],再比較 7 和 8,發現 8 比 7 大,此輪結束。到此,插入排序完畢。

實現代碼

public static void insertionSort(int[] nums) {
        // 將數組的第一個元素看成已經排好序的,從第二個元素,即 i 從 1 開始遍歷數組
        for (int i = 1, j, current; i < nums.length; i++) {
            // 外圍循環開始,把當前 i 指向的值用 current 保存
            current = nums[i];
            // 指針 j 內循環,和 current 值比較,若 j 所指向的值比 current 值大,則該數右移一位
            for (j = i - 1; j >= 0 && nums[j] > current; j--) {
                nums[j + 1] = nums[j];
            }
            // 內循環結束,j+1 所指向的位置就是 current 值插入的位置
            nums[j + 1] = current;
        }
    }

算法分析

空間複雜度

假設數組的元素個數是 n,因爲在整個排序的過程當中,是直接在給定的數組裏面進行元素的兩兩交換,空間複雜度是 O(1)。

時間複雜度

  1. 給定的數組按照順序已經排好

    只須要進行 n-1 次的比較,兩兩交換次數爲 0,時間複雜度是 O(n)。這是最好的狀況。

  2. 給定的數組按照逆序排列

    在這種狀況下,咱們須要進行 n(n-1)/2 次比較,時間複雜度是 O(n2)。這是最壞的狀況。

  3. 給定的數組雜亂無章

    在這種狀況下,平均時間複雜度是 O(n2)。

因而可知,和冒泡排序同樣,插入排序的時間複雜度是 O(n2),而且它也是一種穩定的排序算法。

歸併排序(Merge Sort)

基本思想

核心是分治,就是把一個複雜的問題分紅兩個或多個相同或類似的子問題,而後把子問題分紅更小的子問題,直到子問題能夠簡單的直接求解,最原問題的解就是子問題解的合併。歸併排序將分治的思想體現得淋漓盡致。

實現

一開始先把數組從中間劃分紅兩個子數組,一直遞歸地把子數組劃分紅更小的子數組,直到子數組裏面只有一個元素,纔開始排序。

排序的方法就是按照大小順序合併兩個元素,接着依次按照遞歸的返回順序,不斷地合併排好序的子數組,直到最後把整個數組的順序排好。

例題分析

例題:利用歸併排序算法對數組 [2, 1, 7, 9, 5, 8] 進行排序。

解題思路

首先不斷地對數組進行切分,直到各個子數組裏只包含一個元素。

接下來遞歸地按照大小順序合併切分開的子數組,遞歸的順序和二叉樹裏的前序遍歷相似。

  1. 合併 [2] 和 [1] 爲 [1, 2]。
  2. 子數組 [1, 2] 和 [7] 合併。
  3. 右邊,合併 [9] 和 [5]。
  4. 而後合併 [5, 9] 和 [8]。
  5. 最後合併 [1, 2, 7] 和 [5, 8, 9] 成 [1, 2, 5, 8, 9],就能夠把整個數組排好序了。

合併數組 [1, 2, 7] 和 [5, 8, 9] 的操做步驟以下。

  1. 把數組 [1, 2, 7] 用 L 表示,[5, 8, 9] 用 R 表示。
  2. 合併的時候,開闢分配一個新數組 T 保存結果,數組大小應該是兩個子數組長度的總和
  3. 而後下標 i、j、k 分別指向每一個數組的起始點。
  4. 接下來,比較下標i和j所指向的元素 L[i] 和 R[j],按照大小順序放入到下標 k 指向的地方,1 小於 5。
  5. 移動 i 和 k,繼續比較 L[i] 和 R[j],2 比 5 小。
  6. i 和 k 繼續往前移動,5 比 7 小。
  7. 移動 j 和 k,繼續比較 L[i] 和 R[j],7 比 8 小。
  8. 這時候,左邊的數組已經處理完畢,直接將右邊數組剩餘的元素放到結果數組裏就好。

合併之因此能成功,先決條件必須是兩個子數組都已經分別排好序了。

實現代碼

public static void mergeSort(int[] arr, int lo, int hi) {
        // 判斷是否只剩下最後一個元素
        if (lo >= hi) {
            return;
        }
        // 從中間將數組分紅兩個部分
        int mid = lo + (hi - lo) / 2;
        // 分別遞歸地將左右兩半排好序
        mergeSort(arr, lo, mid);
        mergeSort(arr, mid + 1, hi);
        // 將排好序的左右兩半合併
        merge(arr, lo, mid, hi);
    }
    // 歸併
    public static void merge(int[] nums, int lo, int mid, int hi) {
        // 複製一份原來的數組
        int[] copy = nums.clone();
        // 定義一個 k 指針表示從什麼位置開始修改原來的數組,i 指針表示左半邊的起始位置,j 表示右半邊的起始位置
        int k = lo, i = lo, j = mid + 1;
        while(k <= hi) {
            if(i > mid) {
                nums[k++] = copy[j++];
            } else if(j > hi) {
                nums[k++] = copy[i++];
            } else if(copy[j] < copy[i]) {
                nums[k++] = copy[j++];
            } else {
                nums[k++] = copy[i++];
            }
        }
    }

其中,While 語句比較,一共可能會出現四種狀況。

  • 左半邊的數都處理完畢,只剩下右半邊的數,只須要將右半邊的數逐個拷貝過去。
  • 右半邊的數都處理完畢,只剩下左半邊的數,只須要將左半邊的數逐個拷貝過去就好。
  • 右邊的數小於左邊的數,將右邊的數拷貝到合適的位置,j 指針往前移動一位。
  • 左邊的數小於右邊的數,將左邊的數拷貝到合適的位置,i 指針往前移動一位。

算法分析

空間複雜度

因爲合併 n 個元素須要分配一個大小爲 n 的額外數組,合併完成以後,這個數組的空間就會被釋放,因此算法的空間複雜度就是 O(n)。歸併排序也是穩定的排序算法。

時間複雜度

歸併算法是一個不斷遞歸的過程。

舉例:數組的元素個數是 n,時間複雜度是 T(n) 的函數。

解法:把這個規模爲 n 的問題分紅兩個規模分別爲 n/2 的子問題,每一個子問題的時間複雜度就是 T(n/2),那麼兩個子問題的複雜度就是 2×T(n/2)。當兩個子問題都獲得瞭解決,即兩個子數組都排好了序,須要將它們合併,一共有 n 個元素,每次都要進行最多 n-1 次的比較,因此合併的複雜度是 O(n)。由此咱們獲得了遞歸複雜度公式:T(n) = 2×T(n/2) + O(n)。

對於公式求解,不斷地把一個規模爲 n 的問題分解成規模爲 n/2 的問題,一直分解到規模大小爲 1。若是 n 等於 2,只須要分一次;若是 n 等於 4,須要分 2 次。這裏的次數是按照規模大小的變化分類的。

以此類推,對於規模爲 n 的問題,一共要進行 log(n) 層的大小切分。在每一層裏,咱們都要進行合併,所涉及到的元素其實就是數組裏的全部元素,所以,每一層的合併複雜度都是 O(n),因此總體的複雜度就是 O(nlogn)。

快速排序(Quick Sort)

基本思想

快速排序也採用了分治的思想。

實現

把原始的數組篩選成較小和較大的兩個子數組,而後遞歸地排序兩個子數組。

舉例:把班裏的全部同窗按照高矮順序排成一排。

解法:老師先隨機地挑選了同窗 A,讓全部其餘同窗和 A 比高矮,比 A 矮的都站在 A 的左邊,比 A 高的都站在 A 的右邊。接下來,老師分別從左邊和右邊的同窗裏選擇了同窗 B 和 C,而後不斷地篩選和排列下去。

在分紅較小和較大的兩個子數組過程當中,如何選定一個基準值(也就是同窗 A、B、C 等)尤其關鍵。

例題分析

對數組 [2, 1, 7, 9, 5, 8] 進行排序。

解題思路

  1. 按照快速排序的思想,首先把數組篩選成較小和較大的兩個子數組。
  2. 隨機從數組裏選取一個數做爲基準值,好比 7,因而原始的數組就被分紅了兩個子數組。注意:快速排序是直接在原始數組裏進行各類交換操做,因此當子數組被分割出來的時候,原始數組裏的排列也被改變了。
  3. 接下來,在較小的子數組裏選 2 做爲基準值,在較大的子數組裏選 8 做爲基準值,繼續分割子數組。
  4. 繼續將元素個數大於 1 的子數組進行劃分,當全部子數組裏的元素個數都爲 1 的時候,原始數組也被排好序了。

實現代碼

public static void quickSort(int[] nums, int lo, int hi) {
        // 判斷是否只剩下一個元素,是則直接返回
        if (lo >= hi) {
            return;
        }
        // 利用partition函數找到一個隨機基準點
        int p = partition(nums, lo, hi);
        // 遞歸地對基準點左半邊和右半邊的數進行排序
        quickSort(nums, lo, p - 1);
        quickSort(nums, p + 1, hi);
    }
    // 得到基準值
    public static int partition(int[] nums, int lo, int hi) {
        // 隨機選擇一個數做爲基準值,nums[hi] 就是基準值
        swap(nums, randRange(lo, hi), hi);
        int i, j;
        // 從左到右用每一個數和基準值比較,若比基準值小,則放到指針 i 所指向的位置。循環完畢後,i 指針以前的數都比基準值小
        for (i = lo, j = lo; j < hi; j++) {
            if (nums[j] <= nums[hi]) {
                swap(nums, i++, j);
            }
        }
        // 末尾的基準值放置到指針 i 的位置,i 指針以後的數都比基準值大
        swap(nums, i, j);
        // 返回指針 i,做爲基準點的位置
        return i;
    }
    // 獲取隨機值
    public static int randRange(int lo, int hi) {
        return (int) (lo + Math.random() * (hi - lo + 1));
    }

    // 交換數組中的兩個數
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

算法分析

空間複雜度

和歸併排序不一樣,快速排序在每次遞歸的過程當中,只須要開闢 O(1) 的存儲空間來完成交換操做實現直接對數組的修改,又由於遞歸次數爲 logn,因此它的總體空間複雜度徹底取決於壓堆棧的次數,所以它的空間複雜度是 O(logn)。

時間複雜度

  1. 最優狀況:被選出來的基準值都是當前子數組的中間數。

    這樣的分割,能保證對於一個規模大小爲 n 的問題,能被均勻分解成兩個規模大小爲 n/2 的子問題(歸併排序也採用了相同的劃分方法),時間複雜度就是:T(n) = 2×T(n/2) + O(n)。

    把規模大小爲 n 的問題分解成 n/2 的兩個子問題時,和基準值進行了 n-1 次比較,複雜度就是 O(n)。很顯然,在最優狀況下,快速排序的複雜度也是 O(nlogn)。

  2. 最壞狀況:基準值選擇了子數組裏的最大或者最小值

    每次都把子數組分紅了兩個更小的子數組,其中一個的長度爲 1,另一個的長度只比原子數組少 1。劃分過程和冒泡排序的過程相似,算法複雜度爲 O(n2)。

tips:能夠經過隨機地選取基準值來避免出現最壞的狀況。

堆排序(Heap Sort)

基本思想

堆排序是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似徹底二叉樹的結構,並同時知足堆的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點。

實現

  • 將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆爲初始的無序區;
  • 將堆頂元素R[1]與最後一個元素R[n]交換,此時獲得新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且知足R[1,2…n-1]<=R[n];
  • 因爲交換後新的堆頂R[1]可能違反堆的性質,所以須要對當前無序區(R1,R2,……Rn-1)調整爲新堆,而後再次將R[1]與無序區最後一個元素交換,獲得新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成。

實現代碼

public static void heapSort(int[] arr) {
        len = arr.length;
        if (len < 1) {
            return;
        }
        //1.構建一個大根堆
        buildMaxHeap(arr);
        //2.循環將堆首位(最大值)與末位交換,而後再從新調整最大堆
        while(len > 0) {
            swap(arr, 0, len-1);
            len--;
            adjustHeap(arr, 0);
        }
    }
    // 創建大根堆
    public static void buildMaxHeap(int[] arr) {
        // 從最後一個非葉子節點開始向上構造大根堆
        for (int i = (len / 2 -1); i >= 0; i--) {
            adjustHeap(arr, i);
        }
    }
    // 調整使之成爲大根堆
    public static void adjustHeap(int[] arr, int i ) {
        int maxIndex = i;
        // 若是有左子樹且左子樹大於父節點,則將最大指針指向左子樹
        if (i * 2 < len && arr[i * 2] > arr[maxIndex]) {
            maxIndex = i * 2;
        }
        // 若是有右子樹且右子樹大於父節點,則將最大指針指向右子樹
        if (i * 2 + 1 < len && arr[i * 2 + 1] > arr[maxIndex]) {
            maxIndex = i * 2 + 1;
        }
        // 若是父節點不是最大值,則將父節點與最大值交換並遞歸調整與父節點交換的位置
        if (maxIndex != i) {
            swap(arr, maxIndex, i);
            adjustHeap(arr, maxIndex);
        }
    }
    // 交換數組中的兩個數
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

算法分析

時間複雜度

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

計數排序(Counting Sort)

基本思想

計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。做爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有肯定範圍的整數。

計數排序(Counting sort)是一種穩定的排序算法。計數排序使用一個額外的數組C,其中第i個元素是待排序數組A中值等於i的元素的個數。而後根據數組C來將A中的元素排到正確的位置。它只能對整數進行排序。

實現

  • 找出待排序的數組中最大和最小的元素;
  • 統計數組中每一個值爲i的元素出現的次數,存入數組C的第i項;
  • 對全部的計數累加(從C中的第一個元素開始,每一項和前一項相加);
  • 反向填充目標數組:將每一個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。

動圖演示

實現代碼

public static void countingSort(int[] arr) {
        if (arr.length == 0) {
            return;
        }
        int bias, min = arr[0], max = arr[0];
        // 1.確認數組中的最大值最小值
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
            if (arr[i] < min) {
                min = arr[i];
            }
        }
        bias = 0 - min; // bias記錄新數組的下標偏移量
        int[] bucket = new int[max - min + 1];
        // 2.統計並存入新數組
        for (int i = 0; i < arr.length; i++) {
            bucket[arr[i] + bias]++;
        }
        int index = 0, i = 0;
        // 3.反向填充目標數組
        while(index < arr.length) {
            if (bucket[i] != 0) {
                arr[index] = i - bias;
                bucket[i]--;
                index++;
            } else {
                i++;
            }
        }
    }

算法分析

當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。因爲用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,須要大量時間和內存。

最佳狀況:T(n) = O(n+k) 最差狀況:T(n) = O(n+k) 平均狀況:T(n) = O(n+k)

桶排序(Bucket Sort)

思想

桶排序是計數排序的升級版。當數列取值範圍過大,或者不是整數時不能適用計數排序,這時可使用桶排序來解決問題。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的肯定。

桶排序 (Bucket sort)的工做的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每一個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)

實現

每個桶(bucket)表明一個區間範圍,裏面能夠承載一個或多個元素。桶排序的第一步,就是建立這些桶,肯定每個桶的區間範圍:

具體創建多少個桶,如何肯定桶的區間範圍,有不少不一樣的方式。咱們這裏建立的桶數量等於原始數列的元素數量,除了最後一個桶只包含數列最大值,前面各個桶的區間按照比例肯定。

區間跨度 = (最大值-最小值)/ (桶的數量 - 1)

第二步,遍歷原始數列,把元素對號入座放入各個桶中:

第三步,每一個桶內部的元素分別排序(顯然,只有第一個桶須要排序):

第四步,遍歷全部的桶,輸出全部元素:

0.5,0.84,2.18,3.25,4.5

到此爲止,排序結束。

實現代碼

public static void bucketSort(double[] array){
        //1.獲得數列的最大值和最小值,並算出差值d
        double max = array[0];
        double min = array[0];
        for(int i=1; i<array.length; i++) {
            if(array[i] > max) {
                max = array[i];
            }
            if(array[i] < min) {
                min = array[i];
            }
        }
        double d = max - min;
        //2.初始化桶
        int bucketNum = array.length;
        ArrayList<LinkedList<Double>> bucketList = new ArrayList<LinkedList<Double>>(bucketNum);
        for(int i = 0; i < bucketNum; i++){
            bucketList.add(new LinkedList<Double>());
        }
        //3.遍歷原始數組,將每一個元素放入桶中
        for(int i = 0; i < array.length; i++){
            int num = (int)((array[i] - min) * (bucketNum-1) / d);
            bucketList.get(num).add(array[i]);
        }
        //4.對每一個通內部進行排序
        for(int i = 0; i < bucketList.size(); i++){
            //JDK底層採用了歸併排序或歸併的優化版本
            Collections.sort(bucketList.get(i));
        }
        //5.輸出所有元素
        int index = 0;
        for(LinkedList<Double> list : bucketList){
            for(double element : list){
                array[index] = element;
                index++;
            }
        }
    }

算法分析

時間複雜度:O(N + C)

對於待排序序列大小爲 N,共分爲 M 個桶,主要步驟有:

  • N 次循環,將每一個元素裝入對應的桶中
  • M 次循環,對每一個桶中的數據進行排序(平均每一個桶有 N/M 個元素)

通常使用較爲快速的排序算法,時間複雜度爲 O ( N l o g N ),實際的桶排序過程是以鏈表形式插入的。

整個桶排序的時間複雜度爲:

O ( N ) + O ( M ∗ ( N / M ∗ l o g ( N / M ) ) ) = O ( N ∗ ( l o g ( N / M ) + 1 ) )

當 N = M 時,複雜度爲 O ( N )

空間複雜度:O(N+M)

拓撲排序(Topological Sort)

基本思想

和前面介紹的幾種排序不一樣,拓撲排序應用的場合再也不是一個簡單的數組,而是研究圖論裏面頂點和頂點連線之間的性質。拓撲排序就是要將這些頂點按照相連的性質進行排序。

要能實現拓撲排序,得有幾個前提:

  1. 圖必須是有向圖
  2. 圖裏面沒有環

拓撲排序通常用來理清具備依賴關係的任務。

舉例:假設有三門課程 A、B、C,若是想要學習課程 C 就必須先把課程 B 學完,要學習課程 B還得先學習課程 A,因此得出課程的學習順序應該是 A -> B -> C。

實現

  1. 將問題用一個有向無環圖(DAG, Directed Acyclic Graph)進行抽象表達,定義出哪些是圖的頂點,頂點之間如何互相關聯。
  2. 能夠利用廣度優先搜索或深度優先搜索來進行拓撲排序。

例題分析

有一個學生想要修完 5 門課程的學分,這 5 門課程分別用 一、二、三、四、5 來表示,如今已知學習這些課程有以下的要求:

  • 課程 2 和 4 依賴於課程 1

  • 課程 3 依賴於課程 2 和 4

  • 課程 4 依賴於課程 1 和 2

  • 課程 5 依賴於課程 3 和 4

那麼這個學生應該按照怎樣的順序來學習這 5 門課程呢?

解題思路

能夠把 5 門課程當作是一個圖裏的 5 個頂點,用有向線段按照它們的相互關係連起來,因而得出下面的有向圖。

首先能夠看到,這個有向圖裏沒有環,不管從哪一個頂點出發,都不會再回到那個頂點。而且,這個圖裏並無孤島的出現,所以,咱們能夠對它進行拓撲排序。

方法就是,一開始的時候,對每一個頂點統計它們各自的前驅(也就是入度):1(0),2(1),3(2),4(1),5(2)。

  1. 選擇其中一個沒有前驅(也就是入度爲 0)的頂點,在這道題裏面,頂點 1 就是咱們要找的那個點,將它做爲結果輸出。同時刪除掉該頂點和全部以它做爲起始點的有向邊,更新頂點的入度表。
  2. 接下來,頂點 2 就是下一個沒有前驅的頂點,輸出頂點 2,並將以它做爲起點的有向邊刪除,同時更新入度表。
  3. 再來,頂點 4 成爲了沒有前驅的頂點,輸出頂點 4,刪除掉它和頂點 3 和 5 的有向邊。
  4. 而後,頂點 3 沒有了前驅,輸出它,並刪除它與 5 的有向邊。
  5. 最後,頂點 5 沒有前驅,輸出它,因而得出最後的結果爲:1,2,4,3,5。

通常來講,一個有向無環圖能夠有一個或多個拓撲排序的序列。

實現代碼

運用廣度優先搜索的方法對這個圖的結構進行遍歷。在構建這個圖的過程當中,用一個連接矩陣 adj 來表示這個圖的結構,用一個 indegree 的數組統計每一個頂點的入度,重點看如何實現拓撲排序。

void topologicalSort() {
    Queue<Integer> q = new LinkedList(); // 定義一個隊列 q

    // 將全部入度爲 0 的頂點加入到隊列 q
    for (int v = 0; v < V; v++) {
        if (indegree[v] == 0) q.add(v);
    }

    // 循環,直到隊列爲空
    while (!q.isEmpty()) {
        int v = q.poll();
        // 每次循環中,從隊列中取出頂點,即爲按照入度數目排序中最小的那個頂點
        print(v);
        
        // 將跟這個頂點相連的其餘頂點的入度減 1,若是發現那個頂點的入度變成了 0,將其加入到隊列的末尾
        for (int u = 0; u < adj[v].length; u++) {
            if (--indegree[u] == 0) {
                q.add(u);
            }
        }
    }
}

算法分析

時間複雜度

統計頂點的入度須要 O(n) 的時間,接下來每一個頂點被遍歷一次,一樣須要 O(n) 的時間,因此拓撲排序的時間複雜度是 O(n)。

相關文章
相關標籤/搜索