小白的算法初識課堂(part4)--快速排序

學習筆記
學習書目:《算法圖解》- Aditya Bhargava



分而治之


在這裏,我想通過2個例子介紹一種著名的遞歸式問題解決方法–分而治之(D&C)


  • 分蛋糕

假如,我要分一塊1680mm*640mm的長方形巨形蛋糕,我要將這塊蛋糕均勻的分成同等大小的正方形,且分出的正方形要儘量大。

在這裏插入圖片描述

現在,我們將使用D&C的思路解決問題,D&C過程包括兩個步驟。

(1) 找出基線條件(函數不再調用自己的條件),這種條件必須儘可能簡單.

(2) 不斷將問題分解(或者說縮小規模),直到符合基線條件.

首先,我們要找出基線條件。一般來說,我們將基線條件定爲較爲簡單的情況,那麼現在最容易處理的情況是,長方形一條邊的長度是另一條邊的整數倍。比如下面的1000mm*500mm的長方形,長就是高的兩倍:

在這裏插入圖片描述

現在需要找出遞歸條件,這正是D&C的用武之地。根據D&C的定義,每次遞歸調用都必須縮小問題的規模。如何縮小前述問題的規模呢?

首先,我們要找出這塊大蛋糕可容納的最大方塊。

在這裏插入圖片描述

我們可以先從這塊蛋糕中畫出兩個640mm*640mm的正方形蛋糕,餘下了一塊小長方形蛋糕。再對餘下的那一小塊640mm*400mm的蛋糕採取相同的策略。

可以看到,最初我們是要劃分1680mm*640mm的大蛋糕,但現在我們要劃分640mm*400mm的小蛋糕。可以證明,適用於這小塊地的最大方塊,也是適用於整塊地的最大方塊。換而言之,我們將均勻劃分1680mm*640mm大蛋糕的問題,轉換爲了劃分640mm*400mm小蛋糕的問題。

之後,我們再採取相同的算法,將640mm*400mm的蛋糕劃分爲400mm*400mm的正方形和400mm*240mm的長方形…以此類推,最後我們將得到160mm*80mm的長方形蛋糕。餘下的這個蛋糕滿足基線條件,因爲160是80的整數倍。將這個蛋糕分爲兩個正方形後,將不會留下任何蛋糕。因此,對於原來的1680mm*640mm大蛋糕,適用的最大正方形爲80mm*80mm


  • 計算總和

給定一個數字數組:

arr = [1, 3, 5]

現在,我需要將這些數字相加,並返回結果。

我們可以使用循環完成這個任務:

arr = [2, 4, 6]

def my_sum(arr):
    total = 0
    for i in arr:
        total += i
    return total

print(my_sum(arr))

輸出:

12

可是,如果我想用遞歸來完成這個任務,該怎麼辦呢?

按照D&C的思路,我們首先得找到基線條件。

我們思考一下,加總中最簡單的情況:如果數組不包含任何元素,或者只有一個元素,那麼加總的結果就是0或者那一個元素本身。

[]  #不包含任何元素,總和爲0
[5] #只有1個元素,總和爲5

因此這就是基線條件。


那麼我們該怎麼縮小規模呢?我們可以採取下面這種方式:

my_sum([2, 4, 6])
#等價於
2 + my_sum([4, 6])

即,如果列表不爲空,就計算列表中除第一個元素以外的其他元素的總和,再將其與第一個元素相加,返回最終結果。

按照D&C思路,那麼我們的函數運行過程應該像下面這樣:

在這裏插入圖片描述


快速排序


快速排序是一種常用的排序算法,比選擇排序要快的多。快速排序也應用了D&C思想。

對於排序來說,最簡單的數組,就是不需要排序的數組,也就是爲空或者只包含一個元素的數組:

[]  #不包含任何元素,總和爲0
[5] #只有1個元素,總和爲5

因此,快速排序的基線條件爲數組爲空或只包含一個元素:

def quicksort(arr):
    if len(arr) < 2:
        return arr

現在,我們來看看長度大於1的數組。比如包含2個元素的數組:

[1, 5]

對於包含2個元素的數組進行排序也很容易,我們可以檢查第1個元素是否比第2個元素小,如果不比第2個元素小,就交換它們的位置:

def quicksort(arr):
    if len(arr) < 2:
        return arr
    elif len(arr) == 2:
        if arr[0] > arr[1]:
            arr.reverse()
            return arr

包含3個元素的數組呢:

[33, 15, 10]

首先,我們從數組中選擇一個元素,這個元素被稱爲基準值(pivot):

#我選擇33作爲基準值
[33]

接下來,找出比基準值小的元素以及比基準值大的元素。 這被稱爲分區:

#小於基準值的元素
[15, 10]
#基準值
[33]
#大於基準值的元素
[]

這裏只是進行了分區,得到的兩個子數組依然是無序的。


但是如果兩邊的子數組正好是有序的,那麼我們就可以像下面這樣進行合併得到有序數組:

In [62]: [10, 15] + [33] + []
Out[62]: [10, 15, 33]

如果兩邊的子數組無序,該如何對兩邊的子數組進行排序呢?

可以看到兩邊的子數組,一個是包含2個元素的數組,一個是空數組,因此,只要按照我們剛纔介紹的策略,分別對其進行排序,最後,再合併結果即可:

In [61]: quicksort([15, 10]) + [33] + []
Out[61]: [10, 15, 33]

值得一提的是,不管我們將哪個元素作爲基準值,這種方法都管用。

現在,我們複習一遍包含3個元素的數組應該怎樣排序:

(1) 選擇基準值;

(2) 將數組分成兩個子數組:小於基準值的元素和大於基準值的元素;

(3) 對這兩個子數組進行快速排序。


學完了包含3個元素的數組排序,我想對包含4個元素的數組進行快速排序,可咋整呢?

包含4個元素的數組:

[33, 10, 15, 7]

假設,我們還是將33作爲基準值,則左邊有包含3個元素的數組,右邊爲空數組。而我們已經知道了怎樣對3個元素的數組進行快速排序,就可以對左邊的數組遞歸地調用快速排序:

#第一步,設置基準值爲33
In [59]: [10, 15, 7] + [33] + []
Out[59]: [10, 15, 7, 33]
#第二步,設置基準值爲10
In [60]: [7] + [10] + [15] + [33] + []
Out[60]: [7, 10, 15, 33]

如果我們能對包含4個元素的數組進行快速排序,那麼我們就可以對包含5元素的數組進行排序,以此類推,我們就可以對包含k個元素的數組進行排序了,可以說我們可以對任何長度的數組進行排序。這就像我們爬樓梯,我們可以從1樓爬到2樓,就可以從2樓爬到3樓,就可以從3樓爬到4樓,最終我可以爬到頂樓。


下面是快速排序的全部代碼:

def quicksort2(arr):
    if len(arr) < 2:
        return arr
    else:
        pivot = arr[0]
        less = [i for i in arr[1:] if i <= pivot]
        greater = [i for i in arr[1:] if i > pivot]
        
        return quicksort2(less) + [pivot] + greater

print(quicksort2([33, 10, 5, 2, 3]))

輸出:

[2, 3, 5, 10, 33]

大O表示法


我們之前學習過選擇排序,其運行速度爲O(n2)O(n^2)。除了選擇排序以外,還有一種名爲合併排序的排序算法,其運行時間爲O(nlog2n)O(nlog_2 n),比選擇排序快得多!而快速排序的情況則比較棘手,快速排序的速度取決於選擇的基準值,在最糟糕的情況下其運行時間爲O(n2)O(n^2),與選擇排序一樣慢,但是在平均情況下,快速排序的運行時間爲O(nlog2n)O(nlog_2 n)

這時我們可能會問,看起來合併排序更快呀!爲啥我們不用合併排序呢?


比較合併排序和快速排序


假設我們有下面這兩個函數:

import time

def print_items1(my_list):
    for item in my_list:
        print(item)


def print_items2(my_list):
    for item in my_list:
        time.sleep(1)
        print(item)

print_items1([2, 4, 6, 8, 10])
print_items2([2, 4, 6, 8, 10])

可以看到,雖然這兩個函數的運行時間都是O(n)O(n),但print_items2函數每打印一個元素就停頓一秒,所以看起來print_items1要更快一些。因此,雖然使用大O表示法表示時,這兩個函數的速度相同,但實際上print_items1的速度更快。

在大O表示法O(n)O(n)中,nn實際上指的是cnc*ncc 是算法所需的固定時間量,被稱爲常量。例如,print_items1所需的時間可能是10毫秒 * n,而print_items2所需的時間爲1秒 * n。

在大O表示法中,通常不考慮這個常量,因爲如果算法的大O運行時間不同,這個常量就無關緊要(詳情參考簡單查找和二分查找)。但是有時候,常量的影響可能很大,對快速排序和合並排序來說就是如此。快速排序的常量比合並排序小,因此如果它們的運行時間都爲O(n log n),快速排序的速度將更快。實際上,快速排序的確更快,因爲相比於最糟糕的情況,它遇到平均情況的可能性要大得多。

備註 :最佳情況就是平均情況,在快速排序中只要你每次都隨機地選擇一個元素作爲基準值,快速排序的平均運行時間就將爲O(nlog2n)O(n log_2 n)。快速排序是最快的排序算法之一,也是D&C典範。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章