搞懂基本排序算法

搞懂基本排序算法

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

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

冒泡排序

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

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

冒泡排序算法原理:

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

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

image

冒泡排序 Java 代碼實現:

/**
 * @param arr 待排序數組
 * @param n   數組長度
 */
 private static void BubbleSort(int[] arr, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 1; j < n - i - 1; 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(n2)。空間復雜度為 O(1)。

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

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

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

冒泡排序總結

  1. 冒泡排序的算法時間平均復雜度為O(n2)。
  2. 空間復雜度為 O(1)。
  3. 冒泡排序為穩定排序。

選擇排序

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

選擇排序的思想

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

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

示意圖:


image

選擇排序 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(n2)。

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

選擇排序總結:

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

插入排序

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

[圖片上傳失敗...(image-9dcd7b-1519835757103)]

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

插入排序的思想

  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(n2) 級別的時間復雜度算法 ,只是遍歷方式有原來的 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(n2)。
  2. 插入排序空間復雜度為 O(1)。
  3. 插入排序為穩定排序。
  4. 插入排序對于近乎有序的數組來說效率更高,插入排序可用來優化高級排序算法

歸并排序

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

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

image

歸并算法的思想

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

  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);
    }

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

image

當然我們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);可以參考下邊的圖片:

image

歸并排序的時間復雜度和空間復雜度分析

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

對于空間復雜度,我們通過算法實現可以看出我們歸并過程申請了 長度為 N 的臨時數組,來進行歸并所以空間復雜度為 O(n);

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

** 歸并排序總結:**

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

快速排序

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

單路快速算法的思想:

image

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

  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]

image

雙路快速排序的 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; 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] 交換一下位置。如下圖2所示

image
image

三路快速排序 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;
        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);
    }

我們可以看到三路快速排序沒有了遞歸的過程通過一次循環既可以完成排序。

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

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

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

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

對于快速排序的時間度取決于其遞歸的深度,如果遞歸深度又決定于每次關鍵值得取值所以在最好的情況下每次都取到數組中間值,那么此時算法時間復雜度最優為 O(nlogn)。當然最壞情況就是之前我們分析的有序數組,那么每次都需要進行 n 次比較則 時間復雜度為 O(n2),但是在平均情況 時間復雜度為 O(nlogn),同樣若想看詳細的推到這里推薦一個鏈接 快速排序最好,最壞,平均復雜度分析

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

總結

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

簡書地址
CSDN
Github 地址

參考鏈接:
幾種常見排序算法
常用排序算法穩定性、時間復雜度分析(轉,有改動)
慕課網波波老師的數據結構課程

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容

  • 排序算法說明 (1)排序的定義:對一序列對象根據某個關鍵字進行排序; 輸入:n個數:a1,a2,a3,…,an 輸...
    code武閱讀 669評論 0 0
  • 總結一下常見的排序算法。 排序分內排序和外排序。內排序:指在排序期間數據對象全部存放在內存的排序。外排序:指在排序...
    jiangliang閱讀 1,361評論 0 1
  • Ba la la la ~ 讀者朋友們,你們好啊,又到了冷鋒時間,話不多說,發車! 1.冒泡排序(Bub...
    王飽飽閱讀 1,808評論 0 7
  • 文/大輪子 最近,一部來自印度的電影《摔跤吧!爸爸》在中國卷粉無數。一位不走尋常路線的父親,因為無意中發現了女兒的...
    大輪子閱讀 1,204評論 6 10