齊姐漫畫:排序算法(一)

插入排序

借用《算法導論》裏的例子,就是咱們打牌的時候,每新拿一張牌都會把它按順序插入,這,其實就是插入排序。java

齊姐聲明:雖然咱們用打牌的例子,可是可不能學胡適先生啊。算法

對於數組來講怎麼作呢?api

有一個重要的思想,叫作擋板法,就是用擋板把數組分紅兩個區間:數組

  • 擋板左邊:已排序
  • 擋板右邊:未排序

那麼排序分三步走學習

  1. 最初擋板是在數組的最左邊,保證已排序區間裏一個數都沒有,或者也能夠包含一個數啦;
  2. 核心思想就是:

依次遍歷未排序區間裏的元素,在已排序區間裏找到正確的位置插入;優化

  1. 重複這個過程,直到未排序區間爲空。

舉個例子:{5, 2, 1, 0}

第一步,擋板最初在這裏:動畫

第二步,
把 2 插入已排序區間的正確位置,變成:spa

重複這個步驟,把 1 排好:.net

最後把 0 排好:3d

那代碼也很簡單:

public void insertionSort(int[] input) {
    if(input == null || input.length <= 1) {
        return;
    }
    for(int i = 1; i < input.length; i++) {
        int tmp = input[i];
        int j = i - 1;
        while(j >= 0 && input[j] > tmp) {
            input[j+1] = input[j];
            j --;
        }
        input[j+1] = tmp;
    }
}

咱們來分析一下這個算法的時空複雜度。

時間複雜度

關於時間複雜度大 O 有兩個要點

  • 是描述隨着自變量的增加,所需時間的增加率;
  • 是漸近線複雜度,就是說

    • 不看係數
    • 只看最高階項

那麼咱們關心的 worst case 的狀況就是:
若是數組是近乎倒序的,每次插入都要在數組的第一個位置插入,那麼已排序區間內的全部的元素都要日後移動一位,這一步平均是 O(n),那麼重複 n 次就是 O(n^2).

空間複雜度

重點是一個峯值的概念,並非累計使用的空間。

這裏是 O(1) 沒什麼好說的。

引入一個概念:sorted in place,也就是原地排序

原地排序就是指空間複雜度爲 O(1) 的算法,由於沒有佔用額外的空間,就是原地打轉嘛。

其實 in-place 的思想並非只在排序算法裏有,只不過排序算法是一個最廣爲人知的例子罷了。本質上就是一個節省使用空間的思想。

可是對於排序算法,只分析它的時空複雜度是不夠的,還有另一個重要指標:

穩定性

意思是元素之間的相對順序是否保持了不變。

好比說:{5, 2, 2, 1, 0}

這個數組排序完成後這裏面的兩個 2 的相對順序沒有變,那麼這個排序就是一個穩定排序。

那有同窗可能就想,順序變了又有什麼關係呢?

其實,在實際工做中咱們排序的對象不會只是一個數字,而是一個個的對象 (object),那麼先按照對象的一個性質來排序,再按照另外一個性質來排序,那就不但願原來的那個順序被改變了。好像有點抽象,咱們舉個例子。

好比在股票交易系統裏,有買賣雙方的報價,那是如何匹配的呢?

  • 先按照價格排序;
  • 在相等的價格中,按照出價的時間順序來排序。

那麼通常來講系統會維持一個按時間排序的價格序列,那麼此時只須要用一個具備穩定性的排序算法,再按照價格大小來排序就行了。由於穩定性的排序算法能夠保持大小相同的兩個對象仍維持着原來的時間順序。

那麼插入排序是不是穩定性的排序呢?

答案是確定的。由於在咱們插入新元素的時候是從後往前檢查,並非像打牌的時候隨便插一個位置不能保證相對順序。

你們能夠看下下面的動畫 就很是清楚了~

優化

插入排序實際上是有很大的優化空間的,你能夠搜一下「希爾排序」。

在剛開始學習的時候,深度當然重要,但由於廣度不夠,若是學的太深可能會很痛苦,一個知識點就無窮無盡的延展,這並非一個高效的學習方式。

時間有限時還要作好深度和廣度的平衡:

  • 在經常使用常考的知識點上多花時間精力,追求深度;
  • 在一些拓展性的知識點上點到爲止,先知道有這麼回事就行。

保持 open minded 的心態,後期就會有質的提升。

選擇排序

選擇排序也是利用了「擋板法」這個經典思想。

擋板左邊是已排序區間,右邊是未排序區間,那麼每次的「選擇」是去找右邊未排序區間的最小值,找到以後和擋板後面的第一個值換一下,而後再把擋板往右移動一位,保證排好序的這些元素在擋板的左邊。

好比以前的例子:{5, 2, 0, 1}

咱們用一個擋板來分隔數組是否排好序,
用指針 j 來尋找未排序區間的最小值;

第一輪 j 最初指向 5,而後遍歷整個未排序區間,最終指向 0,那麼 0 就和擋板後的第一個元素換一下,也就是和 5 交換一下位置,擋板向右移動一位,結束第一輪。

第二輪,j 從擋板後的2開始遍歷,最終指向1,而後1和擋板後的第一個元素 2 換一下,擋板向右移動一位,結束第二輪。

第三輪,j 從2開始遍歷,最終指向2,而後和2本身換一下,擋板向右移動一位,結束第三輪。

還剩一個元素,不用遍歷了,就結束了。

選擇排序與以前的插入排序對比來看,要注意兩點:

  1. 擋板必須從 0 開始,而不能從 1 開始。雖然在這兩種算法中,擋板的物理意義都是分隔已排序和未排序區間,可是它們的已排序區間裏放的元素的意義不一樣:
  • 選擇排序是隻能把當前的最小值放進來,而不能放其餘的;
  • 插入排序的第一個元素能夠爲任意值。

因此選擇排序的擋板左邊最開始不能有任何元素。

  1. 在外層循環時,
  • 選擇排序的最後一輪能夠省略,由於只剩下最大的那個元素了;
  • 插入排序的最後一輪不可省略,由於它的位置還沒定呢。

    class Solution {
      public void selectionSort(int[] input) {
        if(input == null || input.length <= 1) {
          return;
        } 
        for(int i = 0; i < input.length - 1; i++) {
          int minValueIndex = i;
          for(int j = i + 1; j < input.length; j++) {
            if(input[j] < input[minValueIndex]) {
              minValueIndex = j;
            }
          }
          swap(input, minValueIndex, i);
        }
      }
      private void swap(int[] input, int x, int y) {
        int tmp = input[x];
        input[x] = input[y];
        input[y] = tmp;
      }
    }

時間複雜度

最內層的 if 語句每執行一次是 O(1) ,那麼要執行多少次呢?

  • 當 i = 0 時,是 n-1 次;
  • 當 i = 1 時,是 n-2 次;
  • ...
  • 最後是 1 次;

因此加起來,總共是:
(n-1) + (n-2) + … + 1 = n*(n-1) / 2 = O(n^2)

是這樣算出來的,而不是一拍腦殼說兩層循環就是 O(n^2).

空間複雜度

這個很簡單,最多的狀況是 call swap() 的時候,而後 call stack 上每一層就用了幾個有限的變量,因此是 O(1)。

那天然也是原地排序算法了。

穩定性

這個答案是否認的,選擇排序並無穩定性。

由於交換的過程破壞了原有的相對順序,好比: {5, 5, 2, 1, 0} 這個例子,第一次交換是 0 和 第一個 5 交換,因而第一個 5 跑到了數組的最後一位,且再也無翻身之地,因此第一個 5 第二個 5 的相對順序就已經打亂了。

這個問題在石頭哥的那篇谷歌面經文章裏有被考到哦,若是尚未看過這篇面經文章的,在「碼農田小齊」公衆號裏回覆「谷歌」二字,就能夠看到了。

優化

選擇排序的其中一步是選出每一輪的最小值,那麼這一步若是使用 heapify() 來優化,就能夠從 O(n) 優化到 O(logn),這其實就變成了 heapSort.


相關文章
相關標籤/搜索