面試題之演算法部分 深入快速排序

2021-07-04 19:38:31 字數 3495 閱讀 9439

本篇文章我將講述快速排序的基本思想,實現,和時間複雜度的深入分析。

基本思想:選取待排序列中的某個元素t,然後按照與該元素的大小關係重新整理序列中的元素,使得整理後的序列中排在t以前的元素均小於t,排在t以後的元素均大於等於t,我們將t稱為劃分元素。此時可以保證此時t的位置一定和最終有序序列t的位置相同,故我們可以選取t以前和以後的兩個子串行作為新的序列去做同樣的處理。不斷遞迴去處理直至每個元素都調整到它對應的位置上,最終使得整個序列有序。

由以上可知,快速排序是通過反覆的對原序列進行劃分來達到排序的目的,所以快排是一種基於劃分的排序方法。

下面給出兩個不同版本的實現:

//1.《演算法導論》上的實現

quicksort(a, p, r)

if p < r

q = partition(a, p, r)

quicksort(a, p, q-1)

quicksort(a, q+1, r)

partition(a, p, r)

x = a[r];

i = p-1

;for j = p to r-1

ifa[j] <= x

i = i + 1

exchange a[i] wich a[j]

exchange a[i+1] with a[r]

return i+1

劃分過程詳解:

partition總是選擇乙個x=a[r]作為主元,並圍繞它來劃分子陣列a[p…r],隨著程式的執行,陣列被劃分成4個(可能有空的)區域。for迴圈的每一輪迭代的開始,每乙個區域都滿足都滿足一定的性質:

1.若p <=k <= i, 則a[k] <= x。

2.若i+1 <= k <= j-1,則a[k] > x。

3.若 j <= k <= r-1,則a[k]可能為任意一種情況。

4.若 k=r,則a[k] = x。

劃分過程示例:

//2.另一種更常用的實現

void quicksort(int a, int lo, int hi)

s[i] = tmp;

quicksort(a, lo, i-1);

quicksort(a, i+1, hi);

}

第二個版本有它的侷限性:如果要求用單鏈表來實現快速排序時,由於沒有父指標,需要確定遞迴左區間的第二個引數(即處於i-1處的節點),需要o(n)的時間來查詢,降低了快速排序的時間複雜度。

下面是單鏈表來實現快排的**:

struct node

};node *partion(node *begin, node *end)

q = q->next;

}swap(p->key, begin->key);

return p;

}void quicksort(node *begin, node *end)

}

時間複雜度分析:

在最壞情況下,n個元素的陣列被切分為n-1個元素和0個元素的兩部分,partition因為要經歷n-1次迭代,所以執行代價為ө(n)。即:t(n) = t(n-1) + t(0) + ө(n) = t(n-1) + ө(n) (元素數為0時,quicksort直接返回,所以執行代價為ө(1),利用代換法,可以得到最壞情況下快速排序演算法的執行時間為ө(n^2)。

在最好情況下,每次partition都得到兩個元素數分別為floor(n/2)和ceiling(n/2)-1的子陣列,這種情況下:t(n) ≤ 2*t(n/2) + ө(n),所以最佳情況下快速排序演算法的執行時間為ө(n*lg(n))。

考慮平均情況,假設每次都以9:1的例劃分陣列,則得到:

t(n) ≤ t(9*n/10) + t(n/10) + ө(n)

它的遞迴樹如下:

樹的到最近的葉結點的路徑長度為log_10(n),在這層之前這棵樹每層都是滿的,所以執行時間為cn,而越往下直至最底層log_(10/9) (n),每層的代價都會小於cn。所以以9:1劃分情況下,總的執行時間

t(n) ≤ log_(10/9) (n) = o(lg(n))

事實上只要以常數比例劃分陣列的情況,哪怕是99:1,執行時間也仍然為o(lg(n)),只不過o記號中隱含的常量因子要大些。而一般情況下,平均下來的劃分情況不應該比9:1差,直觀上看來平均情況下快速演算法的執行時間為o(lg(n))。

下面來分析一下平均情況。快速排序主要在遞迴地呼叫partition過程。我們先看下partition呼叫的總次數,因為每次劃分時,都會選出乙個主元元素(作為基準、將陣列分隔成兩部分的那個元素),它將不會參與後續的quicksort和patition呼叫裡,所以patition最多只能執行n次。在 partiton過程裡,有一段迴圈**(第3至第8行,將各元素與主元元素比較,並根據需要將元素調換)。我們把這段迴圈**單獨提出來考慮,這樣在每次patitioin呼叫裡,除迴圈**外的其它**的執行時間為o(1),所以在整個排序過程中,除迴圈**外的其它**的總執行時間為o(n*1) = o(n)。

接下來分析整個排序過程中,上述迴圈**的總執行時間(注意:不是某次patition呼叫裡的迴圈**的執行時間)。可以看到在迴圈**裡,陣列中的各個元素之間進行比較。設總的比較次數為x,因為一次比較操作本身消耗常量時間,所以比較的總時間為o(x)。如此整個排序過程的執行時間為o(n+x)。

為了得到演算法總執行時間,我們需要確定總的比較次數x的值。為了便於分析,我們將陣列a中的元素重新命名為 z_1,z_2,z_3,…,z_n。其中z_i是陣列a中的第i小的元素。此外,我們還定義z_i_j = 為z_i和z_j之間(包含這兩個元素)的元素集合。

我們用指示器隨機變數x_i_j = i。這樣總的比較次數:

x = ∑ ∑ x_i_j

求期望得:

e[x] = e[∑ ∑ x_i_j] = ∑ ∑ e[x_i_j] = ∑ ∑ pr

注意兩個元素一旦被劃分到兩個不同的區域後,則不可能相互進行比較。它們能進行比較的條件只能為:z_i和z_j在同乙個區域,且z_i或z_j被選為主元元素,這樣:

pr = pr = pr + pr

= 1/(j-i+1) + 1/(j-i+1) = 2/(j-i+1) (因為兩事件互斥,所以概率可以直接相加)

得到x_i_j的概率後,就可以得到總的比較次數:

e[x] = ∑ ∑ pr = ∑ ∑ 2/(j-i+1)

設變數k = j - 1,則上式變為:

e[x] = ∑ ∑ 2/(k+1)

< ∑ ∑ 2/k

= ∑ o(lg(n)) (調合級數求和)

= o(n*lg(n))

所以在平均情況下快速排序的執行時間為o(n*lg(n))。

參考資料:

1.《演算法導論》

2.

面試題之演算法部分 LIS最長遞增子串行

動態規劃法 假設陣列a中元素為 設l j 為以aj結尾的子陣列序列的最長遞增子串行的長度。要用動態規劃來求解,則必須找到遞推關係式,即要找到當前狀態l j 與過去狀態l j 1 l j 2 l 0 之間的關係。假設我們已經求出了l j 1 l j 2 l 0 那麼如何得到l j 呢,我們這樣來做,要...

面試 演算法部分 氣泡排序

三 四 時間複雜度 第一次排序 6和3比較,6大於3,交換位置 3 6 8 2 9 1 第二次排序 6和8比較,6小於8,不交換位置 3 6 8 2 9 1 第三次排序 8和2比較,8大於2,交換位置 3 6 2 8 9 1 第四次排序 8和9比較,8小於9,不交換位置 3 6 2 8 9 1 第五...

面試題之 常用排序演算法

以下排序預設排序效果是從小到大,待排序序列 3,4,63,4,9,0,1,32,2 基本思想 依次交換相鄰兩個元素,使得大的資料往下沉 或小的資料往上附浮 第一步 比較相鄰的兩個元素,如果前者比後者大,則交換兩元素。否則,不交換。第二步 重複第一步直到最後兩個元素比較完成,此時,最大的元素已經在最後...