齊姐漫畫:排序算法(三)之「快排」

算法

首先選一個基準 pivot,而後過一遍數組,java

  • 把小於 pivot 的都挪到 pivot 的左邊,
  • 把大於 pivot 的都挪到 pivot 的右邊。

這樣一來,這個 pivot 的位置就肯定了,也就是排好了 1 個元素。算法

而後對 pivot 左邊 👈 的數排序,
對 pivot 右邊 👉 的數排序,
就完成了。數組

那怎麼排左邊和右邊?dom

答:一樣的方法。ui

因此快排也是用的分治法的思想。spa

「分」

選擇一個 pivot,就把問題分紅了指針

  • pivot 左邊
  • pivot 右邊

這兩個問題。code

「治」

就是最開始描述的方法,直到每一個區間內沒有元素或者只剩一個元素就能夠返回了。blog

「合」

放在一塊兒天然就是。排序

可是如何選擇這個 pivot?

取中間的?

取第一個?

取最後一個?

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

好比選最後一個,就是 3.

而後咱們就須要把除了 3 以外的數分紅「比 3 大」和「比 3 小」的兩部分,這個過程叫作 partition(劃分)

這裏咱們仍然使用「擋板法」的思想,不用真的弄兩個數組來存這兩部分,而是用兩個擋板,把區間劃分好了。

咱們用「兩個指針」(就是擋板)把數組分紅「三個區間」,那麼

  • 左邊的區間用來放小於 pivot 的元素;
  • 右邊的區間用來放大於 pivot 的元素;
  • 中間是未排序區間。

那麼初始化時,咱們要保證「未排序區間」可以包含除了 3 以外的全部元素,因此

  • 未排序區間 = [i, j]

這樣左邊和右邊的區間就成了:

  • [0, i):放比 3 小的數;
  • (j, array.length -2]:放比 3 大的數

注意 ⚠️ i, j 是不包含在左右區間裏的呢。

那咱們的目的是 check 未排序區間裏的每個數,而後把它歸到正確的區間裏,以此來縮小未排序區間,直到沒有未排序的元素。

從左到右來 check:

Step1.

5 > 3, 因此 5 要放在右區間裏,因此 5 和 j 指向的 0 交換一下:

這樣 5 就排好了,指針 j --,這樣咱們的未排序區間就少了一個數;

Step2.

0 < 3,因此就應該在左邊的區間,直接 i++;

Step3.

2 < 3,同理,i++;

Step4.

1 < 3,同理,i++;

因此當兩個指針錯位的時候,咱們結束循環。

可是還差了一步,3 並不在正確的位置上呀。因此還要把它插入到兩個區間中間,也就是和指針 i 交換一下。

齊姐聲明:這裏並不鼓勵你們把 pivot 放最左邊。

基本全部的書上都是放右邊,既然放左右都是同樣的,咱們就按照你們默認的、達成共識的來,不必去「標新立異」。

就好比圍棋的四個星位,可是講究棋道的就是先落本身這邊的星位,而不是伸着胳膊去夠對手那邊的。

那當咱們把 pivot 換回到正確的位置上來以後,整個 partition 就結束了。

以後就用遞歸的寫法,對左右兩邊排序就行了。

最後還有兩個問題想和你們討論一下:

  1. 回到咱們最初選擇 pivot的問題,每次都取最後一個,這樣作好很差?

答:並很差。

由於咱們是想把數組分割的更均勻均勻的時間複雜度更低;可是若是這是一個有序的數組,那麼老是取最後一個是最不均勻的取法。

因此應該隨機取 pivot,這樣就避免了由於數組自己的特色老是取到最值的狀況。

  1. pivot 放在哪

隨機選取以後,咱們仍是要把這個 pivot 放到整個數組的最右邊,這樣咱們的未排序區間纔是連續的,不然每次走到 pivot 這裏還要想着跳過它,心好累哦。

class Solution {
  public void quickSort(int[] array) {
    if (array == null || array.length <= 1) {
      return;
    }
    quickSort(array, 0, array.length - 1);
  }
  private void quickSort(int[] array, int left, int right) {
    // base case
    if (left >= right) {
      return;
    }

    // partition
    Random random = new Random(); // java.util 中的隨機數生成器
    int pivotIndex = left + random.nextInt(right - left + 1);
    swap(array, pivotIndex, right);

    int i = left;
    int j = right-1;
    while (i <= j) {
      if (array[i] <= array[right]) {
        i++;
      } else {
        swap(array, i, j);
        j--;
      }
    }
    swap(array, i, right);

    //「分」
    quickSort(array, left, i-1);
    quickSort(array, i+1, right);
  }
  private void swap(int[] array, int x, int y) {
    int tmp = array[x];
    array[x] = array[y];
    array[y] = tmp;
  }
}

這裏的時空複雜度和分的是否均勻有很大關係,因此咱們分狀況來講:

1. 均分

時間複雜度

若是每次都能差很少均勻分,那麼

  • 每次循環的耗時主要就在這個 while 循環裏,也就是 O(right - left);
  • 均分的話那就是 logn 層;
  • 因此總的時間是 O(nlogn).

空間複雜度

  • 遞歸樹的高度是 logn,
  • 每層的空間複雜度是 O(1),
  • 因此總共的空間複雜度是 O(logn).

2. 最不均勻

若是每次都能取到最大/最小值,那麼遞歸樹就變成了這個樣子:

時間複雜度

如上圖所示:O(n^2)

空間複雜度

這棵遞歸樹的高度就變成了 O(n).

3. 總結

實際呢,大多數狀況都會接近於均勻的狀況,因此均勻的狀況是一個 average case.

爲何看起來最好的狀況其實是一個平均的狀況呢?

由於即便若是沒有取到最中間的那個點,好比分紅了 10% 和 90% 兩邊的數,那其實每層的時間仍是 O(n),只不過層數變成了以 9 爲底的 log,那總的時間仍是 O(nlogn).

因此快排的平均時間複雜度是 O(nlogn)。

穩定性

那你應該能看出來了,在 swap 的時候,已經破壞了元素之間的相對順序,因此快排並不具備穩定性。

這也回答了咱們開頭提出的問題,就是

  • 爲何對於 primitive type 使用快排

    • 由於它速度最快;
  • 爲何對於 object 使用歸併

    • 由於它具備穩定性且快。

以上就是快排的全部內容了,也是很常考的內容哦!那下一篇文章我會講幾道從快排引伸出來的題目,猜猜是什麼?😉

相關文章
相關標籤/搜索