日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

快速排序及优化

發(fā)布時間:2025/3/21 编程问答 16 豆豆
生活随笔 收集整理的這篇文章主要介紹了 快速排序及优化 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.


轉(zhuǎn)自:http://blog.csdn.net/zuiaituantuan/article/details/5978009

看了編程珠璣Programming Perls第11章關(guān)于快速排序的討論,發(fā)現(xiàn)自己長年用庫函數(shù),已經(jīng)忘了快排怎么寫。于是整理下思路和資料,把至今所了解的快排的方方面面記錄與此。

?

綱要

  • 算法描述
  • 時間復(fù)雜度分析
  • 具體實(shí)現(xiàn)細(xì)節(jié)
  • 劃分
  • 選取樞紐元
  • 固定位置
  • 隨機(jī)選取
  • 三數(shù)取中
  • 分割
  • 單向掃描
  • 雙向掃描
  • Hoare的雙向掃描
  • 改進(jìn)的雙向掃描
  • 雙向掃描的其他問題
  • 分治
  • 尾遞歸
  • 參考文獻(xiàn)
  • 一、算法描述(Algorithm Description)

    快速排序由C.A.R.Hoare于1962年提出,算法相當(dāng)簡單精煉,基本策略是隨機(jī)分治。
    首先選取一個樞紐元(pivot),然后將數(shù)據(jù)劃分成左右兩部分,左邊的大于(或等于)樞紐元,右邊的小于(或等于樞紐元),最后遞歸處理左右兩部分。
    分治算法一般分成三個部分:分解、解決以及合并??炫攀蔷偷嘏判?#xff0c;所以就不需要合并了。只需要劃分(partition)和解決(遞歸)兩個步驟。因?yàn)閯澐值慕Y(jié)果決定遞歸的位置,所以Partition是整個算法的核心。

    對數(shù)組S排序的形式化的描述如下(REF[1]):

  • 如果S中的元素個數(shù)是0或1,則返回
  • 取S中任意一元素v,稱之為樞紐元
  • 將S-{v}(S中其余元素),劃分成兩個不相交的集合:S1={x∈S-{v}|x<=v} 和 S2={x∈S-{v}|x>=v}
  • 返回{quicksort(S1) , v , quicksort(S2)}
  • 二、時間復(fù)雜度分析(Time Complexity)

    快速排序最佳運(yùn)行時間O(nlogn),最壞運(yùn)行時間O(N^2),隨機(jī)化以后期望運(yùn)行時間O(nlogn),關(guān)于這些任何一本算法數(shù)據(jù)結(jié)構(gòu)書上都有證明,就不寫在這了,一下兩點(diǎn)很重要:

  • 選取樞紐元的不同, 決定了快排算法時間復(fù)雜度的數(shù)量級;
  • 劃分方法的劃分方法總是O(n), 所以其具體實(shí)現(xiàn)的不同只影響算法時間復(fù)雜度的系數(shù)。
  • 所以訴時間復(fù)雜度的分析都是圍繞樞紐元的位置展開討論的。

    三、具體實(shí)現(xiàn)細(xì)節(jié)(Details of Implementaion)

    1、劃分(Partirion)

    為了方便討論,將Partition從QuickSort函數(shù)里提出來,就像算法導(dǎo)論里一樣。實(shí)際實(shí)現(xiàn)時我更傾向于合并在一起,就一個函數(shù),減少了函數(shù)調(diào)用次數(shù)。


    劃分又分成兩個步驟:選取樞紐元

    按樞紐元將數(shù)組分成左右兩部

    a.選取樞紐元(Pivot Selection)

    固定位置

    同樣是為了方便,將選取樞紐元單獨(dú)提出來成一個函數(shù):select_pivot(T A[], int p, int q),該函數(shù)從A[p...q]中選取一個樞紐元并返回,且樞紐元放置在左端(A[p]的位置)。

    對于完全隨機(jī)的數(shù)據(jù),樞紐元的選取不是很重要,往往直接取左端的元素作為樞紐元。

    但是實(shí)際應(yīng)用中,數(shù)據(jù)往往是部分有序的,如果仍用兩端的元素最為樞紐元,則會產(chǎn)生很不好的劃分,使算法退化成O(n^2)。所以要采用一些手段避免這種情況,我知道的有“隨機(jī)選取法”和“三數(shù)取中法”。

    隨機(jī)選取

    顧名思義就是從A[p...q]中隨機(jī)選擇一個樞紐元,這個用庫函數(shù)可以很容易實(shí)現(xiàn)

    其中randInt(p, q)隨機(jī)返回[p, q]中的一個數(shù),C/C++里可由stdlib.h中的rand函數(shù)模擬。

    三數(shù)取中

    即取三個元素的中間數(shù)作為樞紐元,一般是取左端、右斷和中間三個數(shù),也可以隨機(jī)選取。(REF[1])

    b.按樞紐元將數(shù)組分成左右兩部分

    雖然說分割方法只影響算法時間復(fù)雜度的系數(shù),但是一個好系數(shù)也是比較重要的。這也就是為什么實(shí)際應(yīng)用中寧愿選擇可能退化成O(n^2)的快速排序,也不用穩(wěn)定的堆排序(堆排序交換次數(shù)太多,導(dǎo)致系數(shù)很大)。

    常見的分割方法有三種:

    單向掃描

    單向掃描代碼非常簡單,只有短短的幾行,思路也比較清晰。該算法由N.Lomuto提出,算法導(dǎo)論上也采用了這種算法。對于數(shù)組A[p...q], 該算法用一個循環(huán)掃描整個區(qū)間,并維護(hù)一個標(biāo)志m,使得循環(huán)不變量(loop invariant)A[p+1...m] < A[p] && A[m+1, i-1] >= x[l]始終成立。(REF[2],REF[3])

    順便廢話幾句,在看國外的書的時候,發(fā)現(xiàn)老外在分析和測試算法尤其是循環(huán)時,非常重視不變量(invariant)的使用。確立一個不變量,在循環(huán)開始之前和結(jié)束之后檢查這個不變量,是一個很好的保持算法正確性的手段。

    事實(shí)上第一種算法需要的交換次數(shù)比較多,而且如果采用選取左端元素作為樞紐元的方法,該算法在輸入數(shù)組中元素全部相同時退化成O(n^2)。第二種方法可以避免這個問題。

    雙向掃描

    雙向掃描用兩個標(biāo)志i、j,分別初始化成數(shù)組的兩端。主循環(huán)里嵌套兩個內(nèi)循環(huán):第一個內(nèi)循環(huán)i從左向右移過小于樞紐元的元素,遇到大元素時停止;第二個循環(huán)j從右向左移過大于樞紐元的元素,遇到小元素時停止。然后主循環(huán)檢查i、j是否相交并交換A[i]、A[j]。

    雙向掃描可以正常處理所有元素相同的情況,而且交換次數(shù)比單向掃描要少。

    Hoare的雙向掃描

    這種方法是Hoare在62年最初提出快速排序采用的方法,與前面的雙向掃描基本相同,但是更難理解,手算了幾組數(shù)據(jù)才搞明白:(REF[2])

    需要注意的是,返回值j并不是樞紐元的位置,但是仍然保證了A[p..j] <= A[j+1...q]。這種方法在效率上于雙向掃描差別甚微,只是代碼相對更為緊湊,并且用A[p]做哨兵元素減少了內(nèi)層循環(huán)的一個if測試。

    http://www.see2say.com/channel/music/player.aspx?v_album_id=9804

    改進(jìn)的雙向掃描

    樞紐元保存在一個臨時變量中,這樣左端的位置可視為空閑。j從右向左掃描,直到A[j]小于等于樞紐元,檢查i、j是否相交并將A[j]賦給空閑位 置A[i],這時A[j]變成空閑位置;i從左向右掃描,直到A[i]大于等于樞紐元,檢查i、j是否相交并將A[i]賦給空閑位置A[j],然后 A[i]變成空閑位置。重復(fù)上述過程,最后直到i、j相交跳出循環(huán)。最后把樞紐元放到空閑位置上。

    這種類似迭代的方法,每次只需一次賦值,減少了內(nèi)存讀寫次數(shù),而前面幾種的方法一次交換需要三次賦值操作。由于沒有哨兵元素,不得不在內(nèi)層循環(huán)里判 斷i、j是否相交,實(shí)際上反而增加了很多內(nèi)存讀取操作。但是由于循環(huán)計(jì)數(shù)器往往被放在寄存器了,而如果待排數(shù)組很大,訪問其元素會頻繁的cache miss,所以用計(jì)數(shù)器的訪問次數(shù)換取待排數(shù)組的訪存是值得的。

    關(guān)于雙向掃描的幾個問題

    1.內(nèi)層循環(huán)中的while測試是用“嚴(yán)格大于/小于”還是”大于等于/小于等于”。

    一般的想法是用大于等于/小于等于,忽略與樞紐元相同的元素,這樣可以減少不必要的交換,因?yàn)檫@些元素?zé)o論放在哪一邊都是一樣的。但是如果遇到所有 元素都一樣的情況,這種方法每次都會產(chǎn)生最壞的劃分,也就是一邊1個元素,令一邊n-1個元素,使得時間復(fù)雜度變成O(N^2)。而如果用嚴(yán)格大于/小 于,雖然兩邊指針每此只挪動1位,但是它們會在正中間相遇,產(chǎn)生一個最好的劃分,時間復(fù)雜度為log(2,n)。

    另一個因素是,如果將樞紐元放在數(shù)組兩端,用嚴(yán)格大于/小于就可以將樞紐元作為一個哨兵元素,從而減少內(nèi)層循環(huán)的一個測試。
    由以上兩點(diǎn),內(nèi)層循環(huán)中的while測試一般用“嚴(yán)格大于/小于”。

    2.對于小數(shù)組特殊處理

    按照上面的方法,遞歸會持續(xù)到分區(qū)只有一個元素。而事實(shí)上,當(dāng)分割到一定大小后,繼續(xù)分割的效率比插入排序要差。由統(tǒng)計(jì)方法得到的數(shù)值是50左右(REF[3]),也有采用20的(REF[1]), 這樣原先的QuickSort就可以寫成這樣。

    二、分治

    分治這里看起來沒什么可說的,就是一樞紐元為中心,左右遞歸,實(shí)際上也有一些技巧。

    1.尾遞歸(Tail recursion)

    快排算法和大多數(shù)分治排序算法一樣,都有兩次遞歸調(diào)用。但是快排與歸并排序不同,歸并的遞歸則在函數(shù)一開始, 快排的遞歸在函數(shù)尾部,這就使得快排代碼可以實(shí)施尾遞歸優(yōu)化。第一次遞歸以后,變量p就沒有用處了, 也就是說第二次遞歸可以用迭代控制結(jié)構(gòu)代替。雖然這種優(yōu)化一般是有編譯器實(shí)施,但是也可以人為的模擬:

    采用這種方法可以縮減堆棧深度,由原來的O(n)縮減為O(logn)。

    ?

    

    總結(jié)

    以上是生活随笔為你收集整理的快速排序及优化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。