快速排序是比較排序的一種,其最壞執行時間 $\theta (n^2)$,期望執行時間為 $\theta(nlgn)$。並且能夠原址排序。實際中的很多排序都由此優化而來。
本文主要不對演算法的程式實現進行討論,主要關注其隨機化,及背後的數學證明。
在《演算法導論》上,快速排序用 python 表示大致如下:
def quicksort(arr,start,end):
pivot = end
i = partition(arr,pivot,start)
if (i > start + 1):
quicksort(arr,start,i - 1)
if (i < end - 1):
quicksort(arr,i + 1,end)
def partition(arr,pivot,start):
x = arr[pivot]
i = start - 1
for index in range(start,pivot):
if (arr[index] <= x):
i = i + 1
swap(arr,i,index)
swap(arr,i + 1,pivot)
return i + 1
def swap(arr,i,j):
tem = arr[i]
arr[i] = arr[j]
arr[j] = tem
需要注意的是關鍵的子程式 partition:即選定主元(pivot),對「部分」陣列進行重整,將比主元小的元素置於其前,比主元大的置於其後。
如上的快速排序並不能保證較好的最壞執行時間。設想當陣列倒序時,如上程式的執行時間就到 $n^2$ 這個數量級了。
為了能夠盡量地獲得期望的執行時間 $\theta(nlgn)$,應當在演算法中引入隨機性,減少最壞情況發生的概率。而引入隨機性的位置正是子程式 partition。可以看到在上述的快速排序中,partition 過程中是比較暴力地把 pivot 定為子陣列的最後乙個元素。
那麼只要我們加入類似這樣的步驟:
i = randomselect(arr,pivot,start)
swap(arr,i,pivot)
就能保證作為主元的元素是理論上隨機的了。
在保證其隨機性之後,就到了本文的重點:如何證明快排的期望執行時間為 $\theta(nlgn)$。
猜出這個值倒是並不難。畢竟接觸排序問題的時候分治思想是那麼普遍,式子裡不帶個對數都覺得不自在吧(逃
這一部分有抄書之嫌,有《演算法導論》在手的童鞋當然可以自己去看啦~
在整個快速排序的過程中,耗時的部分大致有二:
迴圈內 partition 中主元和其它元素的比較操作
迴圈外的常數級操作
存在變數的是第一部分,因為一次 partition 選擇的主元是由上一次決定的。而上一次 partition 後所在陣列的位置無法確定(主元的選擇本身就是隨機的嘛)。在最壞的情況下,主元依舊會落在陣列的頭部或尾部,如果一直都這麼「倒霉」,那麼每次迭代無非是對 $n - 1$ 個元素重複上次操作,並最終導向 $n^2$ 的數量級。
演算法導論的證明方法是整體性,而不侷限於單次的 partition 過程。書中的證明出發點是:
1.每次 partition 過程中,只有主元有和其它元素比較的機會,並且主元在此次過程後,就不會再和任何其它元素比較了(迭代時主元被排除在外了)。
2.一旦某次 partition 後兩個元素被主元分開成兩個部分,那麼也不會再發生比較了。
假設研究的物件是陣列 $a$ ,並且將 $a$ 中的每個元素定義為 $z_1,z_2,z_3 ... z_n$。這裡的 $z_i$ 表示為 a 中 第 $i$ 小的元素。
接著我們定義乙個集合
$$z_=\\lbrace z_i,z_,z_,...,z_j\rbrace $$
為 zi 和 zj 之間的元素集合。
需要格外注意的是,定義$z_$並不意味著將整個陣列 $a$ 按順序排列了。集合$z_$中位於$zi$和$zj$之間的元素可以分布於陣列的任何乙個位置。
此外,定義元素$i$和元素$j$發生比較的指示器隨機變數為:
$$x_ = i\lbrace z_i 和z_j發生比較 \rbrace$$
那麼總的比較次數 x 可表示為:
$$x = \sum_^ \sum_}^x_$$ $$
基於以上兩點,在整個迭代過程裡,$z_i$和 $z_j$ 發生比較的可能性只存在於,對於集合 $z_ij$ 中所有的元素,在某次 partition 操作中,$z_i$ 和 $z_j$ 中的任意乙個被「第乙個」選擇為主元,除此之外,這兩個元素都將被主元一分為二,不再有比較的機會。
所以對於 $z_i$ 和 $z_j$ ,其發生(一次)比較的概率可表示為:
$$pr\lbrace z_i 為 z_ 中第乙個被選出的主元\rbrace + pr\lbrace z_j 為 z_ 中第乙個被選出的主元 \rbrace$$
比較容易得出,$z_i$ 和 $z_j$ 被選為主元的概率同是
$$\frac$$
那麼考慮期望 $e(x)$ 就可以表示為:
$$x = \sum_^ \sum_}^\frac$$
令 $k = j - i$,上式可以轉化為:
$$x = \sum_^ \sum_^\frac$$
而$$x = \sum_^ \sum_^\frac < x = \sum_^ \sum_^\frac$$
調和級數本身是發散的,但是其與$lgn$的差所構成的通項:
$$x_n=1+\frac+\frac+...+\frac-lnn$$
隨著$n$趨向於無窮大,是收斂的。(選擇$lnn$是為了之後證明方便)
我們要用到的定式是
若:$$\sum_^|x_n-x_|$$
收斂,那麼
$$\sum_^(x_n-x_)$$
也收斂,從而:
$$\sum_^(x_n-x_) + x_1$$
自然存在乙個極限,而上式展開之後即為
$$ \lbrace x_n\rbrace $$
回到這個式子:
$$x_n=1+\frac+\frac+...+\frac-lnn$$
利用上面的公式,我們求$|x_n-x_|$,得到:
$$|x_n-x_|=\frac-[lnn - ln(n-1)]$$
使用一下拉格朗日中值定理:對於某個數 $m$ 在 $n-1$ 和 $n$之間,存在:
$$\fracf'(x)=lnn - ln(n-1)$$
當然,這裡
$$f(x) = f'(x)$$
那麼,利用$(n-m)<1$,得到:
$$|x_n-x_|=\frac < \frac$$
那麼:$$\sum_^|x_n-x_|$$
是收斂的就是顯而易見的了。
這樣我們可以認為:
$$x_n=1+\frac+\frac+...+\frac = \theta (lnn)$$
從而$$x_n=1+\frac+\frac+...+\frac = o(lgn)$$
也是易得的。
根據以上推論,稍作調整,就能得出:
$$\sum_^ \sum_^\frac=o(nlgn)$$
隨機化快速排序的期望時間得證。
手寫nth element模板(隨機化版快排)
特殊資料下會被卡成n 2 系統的nth element並不會,因為穩定的sort大概用到了3種方法以保持其穩定性 特殊資料下會被卡成n 2 系統的nth element並不會,因為穩定的sort大概用到了3種方法以保持其穩定性 特殊資料下會被卡成n 2 系統的nth element並不會,因為穩定的...
快速排序和隨機化快排學習
0.基本思想 選定基準值 通常為當前陣列的第乙個元素 經過一系列比較和交換,將比基準值小的放在其左邊,將比基準值大的放在其右邊,這稱為是一次迴圈。include using namespace std void qsort int arr,int low,int high 從右向左找比key小的值 ...
隨機化快排找第k大的值215
在未排序的陣列中找到第 k 個最大的元素。請注意,你需要找的是陣列排序後的第 k 個最大的元素,而不是第 k 個不同的元素。示例 1 輸入 3,2,1,5,6,4 和 k 2 輸出 5 示例 2 輸入 3,2,3,1,2,4,5,5,6 和 k 4 輸出 4 說明 你可以假設 k 總是有效的,且 1...