搞懂基本排序演算法

weixin_34377065發表於2018-02-28

搞懂基本排序演算法

上篇文章寫了關於 Java 內部類的基本知識,感興趣的朋友可以去看一下:搞懂 JAVA 內部類;本文寫的內容是最近學習的演算法相關知識中的基本排序演算法,排序演算法也算是面試中的常客了,實際上也是演算法中最基本的知識。由於 Android 開發中用到的地方並不多,所以也很容易遺忘,但是為了進階高階工程師鞏固基本演算法和資料結構也是必修課程之一。

基本排序演算法按難易程度來說可以分為:氣泡排序,選擇排序,插入排序,歸併排序,選擇排序。本文也將從這五種排序演算法來講解各自的中心思想,和 Java 實現方式。

氣泡排序

氣泡排序恐怕是我們計算機專業課程上以第一個接觸到的排序演算法,也算是一種入門級的排序演算法。

氣泡排序雖然簡單但是對於 n 數量級很大的時候,其實是很低效率的。所以實際生產中很少使用這種排序演算法。下面我們看下這種演算法的具體實現思路:

氣泡排序演算法原理:

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
  3. 針對所有的元素重複以上的步驟,除了最後一個。
  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

一次比較過程如圖所示(圖片 Google 來的侵刪)

搞懂基本排序演算法

氣泡排序 Java 程式碼實現:

/**
 * @param arr 待排序陣列
 * @param n   陣列長度 arr.length 
 */
 private static void BubbleSort(int[] arr, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 1; j < n - i; j++) {
            if (arr[j - 1] > arr[j]) {
                //交換兩個元素
                int temp = arr[j];
                arr[j] = arr[j - 1];
                arr[j - 1] = temp;
            }
        }
    }
 }
複製程式碼

氣泡排序時間空間複雜度及演算法穩定性分析

對於長度為 n 的陣列,氣泡排序需要經過 n(n-1)/2 次比較,最壞的情況下,即陣列本身是倒序的情況下,需要經過 n(n-1)/2 次交換,所以其

氣泡排序的演算法時間平均複雜度為O(n²)。空間複雜度為 O(1)。

可以想象一下:如果兩個相鄰的元素相等是不會進行交換操作的,也就是兩個相等元素的先後順序是不會改變的。如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個元素相鄰起來,最終也不會交換它倆的位置,所以相同元素經過排序後順序並沒有改變。

所以氣泡排序是一種穩定排序演算法。所以氣泡排序是穩定排序。這也正是演算法穩定性的定義:

排序演算法的穩定性:通俗地講就是能保證排序前兩個相等的資料其在序列中的先後位置順序與排序後它們兩個先後位置順序相同。

氣泡排序總結

  1. 氣泡排序的演算法時間平均複雜度為O(n²)。
  2. 空間複雜度為 O(1)。
  3. 氣泡排序為穩定排序。

選擇排序

選擇排序是另一種簡單的排序演算法。選擇排序之所以叫選擇排序就是在一次遍歷過程中找到最小元素的角標位置,然後把它放到陣列的首端。我們排序過程都是在尋找剩餘陣列中的最小元素,所以就叫做選擇排序。

選擇排序的思想

選擇排序的思想也很簡單:

  1. 從待排序序列中,找到關鍵字最小的元素;起始假定第一個元素為最小
  2. 如果最小元素不是待排序序列的第一個元素,將其和第一個元素互換;
  3. 從餘下的 N - 1 個元素中,找出關鍵字最小的元素,重複1,2步,直到排序結束。

示意圖:

搞懂基本排序演算法

選擇排序 Java 程式碼實現:

public static void sort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            int minIndex = i;
            // for 迴圈 i 之後所有的數字 找到剩餘陣列中最小值得索引
            for (int j = i + 1; j < n; j++) {
                if (arr[j]< arr[minIndex]) {
                    minIndex = j;
                }
            }
            swap(arr, i, minIndex);
        }
    }

    /**
     * 角標的形式 交換元素
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
複製程式碼

選擇排序時間空間複雜度及演算法穩定性分析

上述 java 程式碼可以看出我們除了交換元素並未開闢額外的空間,所以額外的空間複雜度為O(1)。

對於時間複雜度而言,選擇排序序氣泡排序一樣都需要遍歷 n(n-1)/2 次,但是相對於氣泡排序來說每次遍歷只需要交換一次元素,這對於計算機執行來說有一定的優化。但是選擇排序也是名副其實的慢性子,即使是有序陣列,也需要進行 n(n-1)/2 次比較,所以其時間複雜度為O(n²)。

即便無論如何也要進行n(n-1)/2 次比較,選擇排序仍是不穩定的排序演算法,我們舉一個例子如:序列5 8 5 2 9, 我們知道第一趟選擇第1個元素5會與2進行交換,那麼原序列中兩個5的相對先後順序也就被破壞了。

選擇排序總結:

  1. 選擇排序的演算法時間平均複雜度為O(n²)。
  2. 選擇排序空間複雜度為 O(1)。
  3. 選擇排序為不穩定排序。

插入排序

對於插入排序,大部分資料都是使用撲克牌整理作為例子來引入的,我們打牌都是一張一張摸牌的,沒摸到一張牌就會跟手裡所有的牌比較來選擇合適的位置插入這張牌,這也就是直接插入排序的中心思想,我們先來看下動圖:

搞懂基本排序演算法

相信大家看完動圖以後大概知道了插入排序的實現思路了。那麼我們就來說下插入排序的思想。

插入排序的思想

  1. 從第一個元素開始,該元素可以認為已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置
  4. 重複步驟 3,直到找到已排序的元素小於或者等於新元素的位置
  5. 將新元素插入到該位置後
  6. 重複步驟 2~5

插入排序的 Java 實現:

下面先看下最基本的實現:

    public static void sort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            //內層迴圈比較 i 與前邊所有元素值,如果 j 索引所指的值小於 j- 1 則交換兩者的位置
            for(int j = i; j > 0 && arr[j-1] > arr[j]; j--){
                swap(arr,j-1,j);
            }
        }
    }
複製程式碼

在上述演算法實現中我們每次尋找 i 應該處在陣列中哪個為位置的時候,都是以交換當前元素與上一個元素為代價的,我們知道交換操作是要比賦值操作要費時的,因為每次交換都需要經過三次賦值操作,我們想一下我們玩撲克的時候沒有拿起一張牌一個個向前挪知道放到其該放的位置的吧,都是拿出這張牌,找到位置就插進去(突然邪惡),實際上我們是將這個位置以後的牌一次向後挪了一個位置,那麼用Java 程式碼是否能實現呢?答案肯定是可以的:

   public static void sort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            //拎出來當前未排序的這樣牌
            int e = arr[i];
            //尋找其該放的位置
            for(int j = i; j > 0 && arr[j-1] > arr[j]; j--){
                arr[j]= arr[j-1];
            }
            //迴圈結束後  arr[j] >= arr[j-1] 那麼 j 角標就是e 應該在的位置。
             arr[j] = e;
        }
    }
複製程式碼

插入排序的時間複雜度和空間複雜度分析

對於插入的時間複雜度和空間複雜度,通過程式碼就可以看出跟選擇和冒泡來說沒什麼區別同屬於 O(n²) 級別的時間複雜度演算法 ,只是遍歷方式有原來的 n n-1 n-2 ... 1,變成了 1 2 3 ... n 了。最終得到時間複雜度都是 n(n-1)/2。

對於穩定性來說,插入排序和冒泡一樣,並不會改變原有的元素之間的順序,如果遇見一個與插入元素相等的,那麼把待插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序仍是排好序後的順序,所以插入排序是穩定的。

對於插入排序這裡說一個非常重要的一點就是:由於這個演算法可以提前終止內層比較( arr[j-1] > arr[j])所以這個排序演算法很有用!因此對於一些 NlogN 級別的演算法,後邊的歸併和快速都屬於這個級別的,演算法來說對於 n 小於一定級別的時候(Array.sort 中使用的是47)都可以用插入演算法來優化,另外對於近乎有序的陣列來說這個提前終止的方式就顯得更加又有優勢了。

插入排序總結:

  1. 插入排序的演算法時間平均複雜度為O(n²)。
  2. 插入排序空間複雜度為 O(1)。
  3. 插入排序為穩定排序。
  4. 插入排序對於近乎有序的陣列來說效率更高,插入排序可用來優化高階排序演算法

歸併排序

接下來我們看一個 NlogN 級別的排序演算法,歸併演算法。 歸併演算法正如其名字一樣採用歸併的方法進行排序:

我們總是可以將一個陣列一分為二,然後二分為四直到,每一組只有兩個元素,這可以理解為個遞迴的過程,然後將兩個元素進行排序,之後再將兩個元素為一組進行排序。直到所有的元素都排序完成。同樣我們來看下邊這個動圖。

搞懂基本排序演算法

歸併演算法的思想

歸併演算法其實可以分為遞迴法和迭代法(自低向上歸併),兩種實現對於最小集合的歸併操作思想是一樣的區別在於如何劃分陣列,我們先介紹下演算法最基本的操作:

  1. 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
  2. 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
  3. 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
  4. 重複步驟3直到某一指標到達序列尾
  5. 將另一序列剩下的所有元素直接複製到合併序列尾

假設我們現在在對一個陣列的 arr[l...r] 部分進行歸併,按照上述歸併思想我們可將陣列分為兩部分 假設為 arr[l...mid] 和 arr[mid+1...r]兩部分,注意這兩部分可能長度並不相同,因為基數個數的陣列劃分的時候總是能得到一個 長度為1 和長度為2 的部分進行歸併.

那麼我們按照上述思路進行程式碼編寫:

歸併排序的 Java 實現:

   /**
     * arr[l,mid] 和  arr[mid+1,r] 兩部分進行歸併
     */
    private static void merge(int[] arr, int l, int mid, int r) {

        // 複製等待歸併陣列 用來進行比較操作,最將原來的 arr 每個角標賦值為正確的元素
        int[] aux = new int[r - l + 1];
        for (int i = l; i <= r; i++) {
            aux[i - l] = arr[i];
        }
        
        int i = l;
        int j = mid + 1;

        for (int k = l; k <= r; k++) {
            if (i > mid) {
                //說明左邊部分已經全都放進陣列了
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {
                //說明左邊部分已經全都放進陣列了
                arr[k] = aux[i - l];
                i++;
            } else if (aux[i - l] < aux[j - l]) {
                //當左半個陣列的元素值小於右邊陣列元素值得時候 賦值為左邊的元素值
                arr[k] = aux[i - l];
                i++;
            } else {
                //當左半個陣列的元素值大於等於右邊陣列元素值得時候 賦值為左邊的元素值 這樣也保證了排序的穩定性
                arr[k] = aux[j - l];
                j++;
            }
        }
    }

複製程式碼

相信大家配合剛才的動圖和上述演算法實現已經理解了歸併演算法了,如果感到迷糊的話可以試著拿個一個陣列在紙上演算一下歸併的過程,相信大家一定可以理解。上述只是實現了演算法核心部分,那麼我們應該怎麼對整個陣列來進行排序呢?上邊也提到了有兩種方法,一種是遞迴劃分法,一種是迭代遍歷法(自低向上)那麼我們先來開來看遞迴實現:

    /**
     * 
     * @param arr 待排序陣列
     * @param l  其實元素角標 0
     * @param r 最後一個元素角標 n -1 
     */
    private static void mergeSort(int[] arr, int l, int r) {
        if (l >= r) {
            return;
        }

        //開始歸併排序 向下取整
        int mid = (l + r) / 2;
        
        //遞迴劃分陣列
        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);

        //檢查是否上一步歸併完的陣列是否有序,如果有序則直接進行下一次歸併
        if (arr[mid] <= arr[mid + 1]) {
            return;
        }
        //將兩邊的元素歸併排序
        merge(arr, l, mid, r);
    }
複製程式碼

如果對遞迴過程不理解可以配合下邊這個圖來理解(圖片來自網上,侵刪):

搞懂基本排序演算法

當然我們merge先對左半部分進行的也就是先進行到Level3的左邊最底層 8 | 6 ,然後歸併完成後進行右邊遞迴到底 最終是 8 6 2 3 | 1 5 7 4 進行歸併。

對於迭代實現歸併其實和遞迴實現有所不同,迭代的時候我們是將陣列分為 一個一個的元素,然後每兩個歸併一次,第二次我們將陣列每兩個分一組,兩個兩個的歸併,知道分組大小等於待歸併陣列長度為止,即先區域性排序,逐步擴大到全域性排序

  /**
     * 自低向上的歸併排序
     *
     * @param n   為陣列長度
     * @param arr 陣列
     */
    private static void mergeSortBU(Integer[] arr, int n) {
        //外層遍歷從歸併區間長度為1 開始 每次遞增一倍的空間 1 2 4 8 sz 需要遍歷到陣列長度那麼大
        //sz = 1 : [0] [1]...
        //sz = 2 : [0,1] [2.3] ...
        //sz = 4 : [0..3] [4...7] ...
        for (int sz = 1; sz <= n; sz += sz) {

            //內層遍歷要比較 arr[i,i+sz-1] arr[i+sz,i+sz+sz-1] 兩個區間的大小 也就是每次對 sz - 1 大小的陣列空間進行歸併
            // 注意每次 i 遞增  兩個 sz 的長度 ,因為每次 merge 的時候已經歸併了兩個 sz 長度 部分的陣列
            for (int i = 0; i + sz < n; i += sz + sz) {
                merge(arr, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1));
            }
        }
    }
複製程式碼

比如我們看第一次是 sz = 1 個長度的歸併即 i = 0 i = 1 的元素歸併 下次歸併應該為 i= 2 i = 3 一次類推 所以內層迴圈 i 每次應該遞增 兩個 sz 那麼大 為了避免角標越界且保證歸併的右半部分存在 所以 i + sz < n ,又考慮到陣列長度為奇數的情況,所以右半邊的右邊為 Math.min(i + sz + sz - 1, n - 1);可以參考下邊的圖片:

搞懂基本排序演算法

歸併排序的時間複雜度和空間複雜度分析

其實對於歸併排序的時間複雜對有一個遞迴公式來推斷出時間複雜度,但簡單來講假設陣列長度為 N ,那麼我們就有 logN 次劃分割槽間,而最終會劃分為常數 級別的歸併,將所有層的歸併時間加起來得到了一個 NlogN,想要了解歸併排序時間複雜度講解的同學可以左轉 歸併排序及其時間複雜度分析,這裡不再過多講解。

對於空間複雜度,我們通過演算法實現可以看出我們歸併過程申請了 長度為 N 的臨時陣列,來進行歸併所以空間複雜度為 O(n);

又由於我們在排序過程中對於 aux[i - l] = aux[j - l] 並沒有進行位置交換直接取得靠前的元素先賦值,所以演算法是穩定的。

** 歸併排序總結:**

  1. 歸併排序的演算法時間平均複雜度為O(nlog(n))。
  2. 歸併排序空間複雜度為 O(n)。
  3. 歸併排序為穩定排序。
  4. 對於

快速排序

快速排序為應用最多的排序演算法,因為快速二字而聞名。快速排序和歸併排序一樣,採用的都是分治思想。分治法的基本思想是:將原問題分解為若干個規模更小但結構與原問題相似的子問題。遞迴地解這些子問題,然後將這些子問題的解組合為原問題的解。我們只需關注最小問題該如何求解,和如何去遞迴既可以得到正確的演算法實現。快速排序可以分為:單路快速排序,雙路快速排序,三路快速排序,他們區別在於選取幾個指標來對陣列進行遍歷下面我們依次來講解。

單路快速演算法的思想:

搞懂基本排序演算法

首先我們選取陣列中的一個數,將其放在合適的位置,這個位置左邊的數全部小於該數值,這個位置右邊的數全部大於該數值 。

  1. 假設陣列為 arr[l...r] 假設指定數值為陣列第一個元素 int v = arr[l],假設 j 標記為比 v 小的最後一個元素, 即 arr[j+1] > v。當前考察的元素為 i 則有arr[l + 1 ... j] < v , arr[j+1,i) >= v 如上圖所示。

  2. 假設正在考察的元素值為 e ,e >= v 的時候我們只需交將不動,直接 i++ 去考察下一個元素,

  3. e < v 由上述假設我們需要將 e 放在<v 的部分 ,此時我們只需將 arr[j]arr[i] 交換一下位置即可。

  4. 最後一個元素考察完成以後,我們再講 arr[l]arr[j]調換一下位置就可以了。

  5. 上述遍歷完成以後 arr[l + 1 ... j] < v , arr[j+1,i) >= v 就滿足了,接下來我們只需要遞迴的去考察 arr[l + 1 ... j] 和 arr[j+1,r] 即可。

單路快速排序的 Java 實現:

 private static void quickSort(int[] arr, int l, int r) {

        if (l >= r) {
            return;
        }
        // p 為 第一次 排序完成後 v 應該在的位置,即分治的劃分點
        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);
    }

    private static int partition(Integer[] arr, int l, int r) {

        // 為了提高效率,減少造成快速排序的遞迴樹不均勻的概率,
        // 對於一個陣列,每次隨機選擇的數為當前 partition 操作中最小最大元素的可能性為 1/n 
        int randomNum = (int) (Math.random() * (r - l + 1) + l);
        swap(arr, l, randomNum);

        int v = arr[l];
        int j = l;

        for (int i = l + 1; i <= r; i++) {
            if (arr[i] < v) {
                swap(arr, j + 1, i);
                j++;
            }
        }
        swap(arr, l, j);
        return j;
    }

    private static void swap( int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
複製程式碼

對於上述演算法中為什麼選取了當前排序陣列中隨機一個元素進行比較,假設我們在考察的陣列已經為已經排序好的陣列,那麼我們遞迴樹就會向右側延伸 N 的深度,這種情況使我們不想要看到的,如果我們每次 partition 都隨機從陣列中取一個數,那麼這個數是當前排序陣列中最小元素可能性為 1/n 那麼每次都取到最小的數的可能性就很低了。

雙路快速排序演算法思想:

  1. 跟單路一樣,雙路快速排序,同樣選擇陣列的第一個元素當做標誌位(經過隨機選擇後的)

  2. 雙路快速排序要求有兩個指標,指標 i j 分別指向 l+1 和 r 的位置然後兩者同時向陣列中間遍歷 在遍歷過程中要保證arr[l+1 ... i) <= v, arr(j....r] >= v 因此我們可以初始化 i = l+1 以保證左側區間初始為空,j = r 保證右側空間為空

  3. 遍歷過程中要 i <= r 且 arr[i] <= v 的時候 i ++ 就可以了 當 arr[i] > v 時表示遇到了 i 的值大於 v 數值 此刻能等待 j 角標的值,從右向左遍歷陣列 當 arr[i] < v 表示遇到了 j 的值小於 v 的元素,它不該在這個位置呆著,

  4. 得到了 i j 的角標後 先要判斷是否到了迴圈結束的時候了,即 i 是否已經 大於 j 了。

  5. 否則 應該講 i 位置的元素和 j 位置的元素交換位置,然後 i++ j-- 繼續迴圈

  6. 遍歷結束的條件是 i>j 此時 arr[j]為最後一個小於 v 的元素 arr[i] 為第一個大於 v 的元素 因此 j 這個位置 就應該是 v 所應該在陣列中的位置 因此遍歷結束後需要交換 arr[l] 與 arr[j]

搞懂基本排序演算法

雙路快速排序的 Java 實現:

 private static void quickSort(int[] arr, int l, int r) {

        if (l >= r) {
            return;
        }
        // 這裡 p 為 小於 v 的最後一個元素,=v 的第一個元素 
        int p = partition(arr, l, r);

        quickSort(arr, l, p - 1);
        quickSort(arr, p + 1, r);
    }


    private static int partition(int[] arr, int l, int r) {
        // 為了提高效率,減少造成快速排序的遞迴樹不均勻的概率,
        // 對於一個陣列,每次隨機選擇的數為當前 partition 操作中最小最大元素的可能性降低

        int randomNum = (int) (Math.random() * (r - l + 1) + l);
        swap(arr, l, randomNum);

        int v = arr[l];

        int i = l + 1;
        int j = r;

        while (true) {

            while (i <= r && arr[i] <= v) i++;
            while (j >= l + 1 && arr[j] >= v) j--;

            if (i > j) break;

            swap(arr, i, j);
            i++;
            j--;
        }
        //j 最後角標停留在 i > j 即為 比 v 小的最後一個一元素位置
        swap(arr, l, j);

        return j;
    }
複製程式碼

雙路快速排序為最經常使用的快速排序實現,java 中對基本資料型別的排序 Arrays.sort() Collections.sort() 內部原理就是通過這種快速排序實現.

三路快速排序

上述兩種演算法我們發現對於與標誌位相同的值得處理總是,做了多餘的交換處理,如果我們能夠將陣列分為> = <三部分的話效率可能會有所提高。 如下圖所示:

  1. 我們將陣列劃分為 arr[l+1...lt] <v arr[lt+1..i) =v arr[gt...r] > v三部分 其中 lt 指向 < v 的最後一個元素前一個元素,gt 指向>v的第一個元素的前一個元素,i 為當前考察元素

  2. 定義初始值得時候依舊可以保證這初始的時候這三部分都為空 int lt = l; int gt = r + 1; int i = l + 1;

  3. e > v 的時候我們需要將 arr[i] 與 arr[gt-1] 交換位置,並將 > v 的部分擴大一個元素 即 gt-- 但是此時 i 指標並不需要操作,因為換過過來的數還沒有被考察。

  4. e = v 的時候 i ++ 繼續考察下一個

  5. e < v 的時候我們需要將 arr[i] 與 arr[lt+1] 交換位置

  6. 當迴圈結束的時候 lt 位於小於 v 的最後一個元素位置所以最後我們需要將arr[l] 與 arr[lt] 交換一下位置。

  7. 最後再遞迴的對 arr[l...lt-1] 和 arr[gt...r] 進行排序就能得到正確結果了。

如下圖2所示

搞懂基本排序演算法

搞懂基本排序演算法

三路快速排序 Java 程式碼實現:


    private static void quickSort3(int[] num, int length) {
        quickSort(num, 0, length - 1);
    }

    private static void quickSort(int[] arr, int l, int r) {

        if (l >= r) {
            return;
        }

        // 為了提高效率,減少造成快速排序的遞迴樹不均勻的概率,
        // 對於一個陣列,每次隨機選擇的數為當前 partition 操作中最小最大元素的可能性 降低 1/n!

        int randomNum = (int) (Math.random() * (r - l + 1) + l);
        swap(arr, l, randomNum);

        int v = arr[l];
        // 三路快速排序即把陣列劃分為大於 小於 等於 三部分
        //arr[l+1...lt] <v  arr[lt+1..i) =v  arr[gt...r] > v 三部分
        // 定義初始值得時候依舊可以保證這初始的時候這三部分都為空
        int lt = l;
        int gt = r + 1;
        int i = l + 1;

        while (i < gt) {
            if (arr[i] < v) {
                swap(arr, i, lt + 1);
                i++;
                lt++;
            } else if (arr[i] == v) {
                i++;
            } else {
                swap(arr, i, gt - 1);
                gt--;
                //i++ 注意這裡 i 不需要加1 因為這次交換後 i 的值仍不等於 v 可能小於 v 也可能等於 v 所以交換完成後 i 的角標不變
            }
        }
        //迴圈結束的後 lt 所處的位置為 <v 的最後一個元素 i 肯定與 gt 重合
        //但是 最終v 要放的位置並不是 i 所指的位置 因為此時 i 為大於 v 的第一個元素 v
        //而 v 應該處的位置為 lt 位置 並不是 i-1 所處的位置(arr[i-1] = arr[l])
        swap(arr, l, lt);
        quickSort(arr,l,lt-1);
        quickSort(arr,gt,r);
    }
複製程式碼

快速排序時間複雜度空間複雜度

由於我們最常使用的是雙路快排因此我們以此來分析:我們為了方便分析我們假定元素不是隨機選取的而是取得陣列第一個元素,在選取的標準元素和 partition 得到位置交換的時候,很有可能把前面的元素的穩定性打亂,

比如序列為 5 3 3 4 3 8 9 10 11

現在基準元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂。所以快速排序是一個不穩定的排序演算法,不穩定發生在基準元素和a[partition]交換的時刻。

對於快速排序的時間度取決於其遞迴的深度,如果遞迴深度又決定於每次關鍵值得取值所以在最好的情況下每次都取到陣列中間值,那麼此時演算法時間複雜度最優為 O(nlogn)。當然最壞情況就是之前我們分析的有序陣列,那麼每次都需要進行 n 次比較則 時間複雜度為 O(n²),但是在平均情況 時間複雜度為 O(nlogn),同樣若想看詳細的推到這裡推薦一個連結 快速排序最好,最壞,平均複雜度分析

快速排序的空間複雜度主要取決於表示為選擇的時候的臨時空間,所以跟時間複雜度掛鉤,所以平均的空間複雜度也是 O(nlogn)。

總結

本文總結了常見的排序演算法的實現,通過研究這些演算法的思想,也有助於演算法題的解題思路。對於這幾種演算法都是需要我們熟練掌握的,但是 Android 工作平時不會接觸太多的資料處理,因此我們需要刻意的去經常複習,本文的圖片大部分來自於網上,如果有問題的話可以私信我刪掉。如果文章所說的內容有技術問題也歡迎聯絡我。

簡書地址 CSDN Github 地址

參考連結: 幾種常見排序演算法 常用排序演算法穩定性、時間複雜度分析(轉,有改動) 慕課網波波老師的資料結構課程

相關文章