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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

浅入浅出数据结构(20)——快速排序

發布時間:2023/11/29 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 浅入浅出数据结构(20)——快速排序 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

  正如上一篇博文所說,今天我們來討論一下所謂的“高級排序”——快速排序。首先聲明,快速排序是一個典型而又“簡單”的分治的遞歸算法。

  遞歸的威力我們在介紹插入排序時相比已經見識過了:只要我前面的隊伍是有序的,我就可以通過向前插隊來完成“我的排序”,至于前面的隊伍怎么有序……遞歸實現,我不管。

  遞歸就是如此“簡單”的想法:我不管需要的條件怎么來的,反正條件的實現交給“遞歸的小弟們”去做,只要有基準情形并且向著基準情形“遞”去,就可以保證“歸”回我一個需要的條件。

  不過插入排序雖然體現出了遞歸的想法,卻沒有解釋什么叫“分治”,其實分治就是分而治之的意思。如果要舉個例子的話,恐怕漢諾塔是最為合適的,畢竟大家學習C語言遞歸時應該都接觸過。

  漢諾塔的遞歸解法用大白話來說就是:作為老和尚,我希望自己只用做一件事,就是把最底下的盤子移到C柱去,至于上面的盤子怎么移到B柱,交給小和尚A去做,而我把最底下的盤子移到C柱后,B柱的盤子們怎么移到C柱來,交給小和尚B去做。

  

  上述漢諾塔的解法就是一種“分治”:把上層盤子移到B柱的任務“分給”A去“治”,而把B柱的盤子們移到C柱的任務又“分給”B去“治”,我只要把底下的盤子移到C柱就行了。需要注意的是,分治不需要什么基準情形,其本質就是將一個大的任務分成兩個或多個小的子任務,在子任務完成的情況下去更簡單地解決大任務。一般來說分治總是與遞歸同時出現,即“分治之后遞歸完成”。

    

?

  那么,快速排序又是怎樣的一個分治、遞歸呢?我們就用大白話來說說快速排序的想法:

  對于數組a,我隨便選一個元素a[x]作為樞紐,所有小于樞紐的元素都“站左邊去”,所有大于樞紐的元素都“站右邊去”(分治),小于樞紐的元素們給我“自行”排好隊,大于樞紐的元素們也給我“自行”排好隊(遞歸)。(若小于a[x]的元素共有i個,則它們放在a[0]到a[i-1],而后將a[x]放在a[i]處,大于a[x]的放于a[i+1]到a[size-1]處,i如何確定暫時不管)

  既然有遞歸,那么就必須有基準情形,那么快速排序的基準情形會是什么呢?顯然是當數組被“遞”得只有3個元素的時候,此時對該數組進行“分治”就會直接完成排序。

  我們先來試著給出快速排序的偽代碼,調用者調用方式為QuickSort(a,0,size-1):

void QuickSort(int *a, unsigned int left,unsigned int right) {//若a[left]到a[right]元素個數大于2,則繼續分治、遞歸if (left < right&&left != right - 1){/*偽代碼:隨機選一個a[x],要求x>=left&&x<=right,作為樞紐*//*偽代碼:將a[left]到a[right]中所有小于median的元素置于a[left]到a[i-1]處(i未知)*//*偽代碼:將a[left]到a[right]中所有大于median的元素置于a[i+1]到a[right]處*/
/*偽代碼:將a[x]放在a[i]處*/
QuickSort(a, left, i - 1);QuickSort(a, i + 1, right);}//若a[left]到a[right]元素個數恰為2,則直接排序else if (left == right - 1){if(a[left]>a[right])swap(&a[left], &a[right]);}//若left==right,則說明只有一個元素,已為“有序” }

  雖然快速排序的想法看似比較簡單,但其實現還是有“坑”與“捷徑”的,接下來我們就一步一步實現快速排序,看看都有什么“坑”與“捷徑”。

?

  回顧快速排序的想法和偽代碼,可以看出其第一步就是“隨便選一個a[x]”,這看似簡單的第一步其實是一個“坑”,因為雖說是隨便選,但“選一個a[x]”還是要寫出具體代碼來的,所以隨便選一個樞紐的代碼該如何寫,就有了三種做法:

  1.既然是隨便選,那就直接選a[left]好了。

  這個做法是最不可取的,原因非常簡單,假設數組已經接近有序,那么選取a[left]作為樞紐就很容易導致分治變得“無效”,因為a[left]很可能就是最小的元素。

  2.既然要隨便選,那就選a[rand()%(right-left+1)]好了

  這個做法可取,但是問題出在計算隨機數上,計算隨機數多少需要一點代價,而且計算隨機數對于排序這件事并沒有直接幫助。

  3.三數中值法

  很顯然,這就是我們的主角了。相比于方法1,本方法要更加安全,而相比于方法2,本方法要更加廉價并且可以幫助到排序本身。那么三數中值法究竟是怎樣的呢?其實就是:令center=(left+right)/2,然后選a[left]、a[center]、a[right]三者的中值作為樞紐。不過在選取中值的同時,我們也獲取了這三者的大小信息,因此可以順便將這三者“放好位置”,所以說相比于方法2,本方法對于排序要更有幫助一些。

?

  三數中值法我們一般用一個單獨的函數來實現(其返回值即樞紐的值):

int MedianOf3(int *a, unsigned int left, unsigned int right) {unsigned int center = (left + right) / 2;//前兩個if保證a[left]存儲著三數最小值//最后一個if保證a[center]為三數中值,a[right]為三數最大值//三個if不僅選出了樞紐,同時將另外兩元素的分治工作完成if (a[left] > a[center])swap(&a[left],&a[center]);if (a[left] > a[right])swap(&a[left],&a[right]);if (a[center] > a[right])swap(&a[center],&a[right]);//最后,將樞紐與a[right-1]交換,即“將樞紐放在a[right-1]處”swap(&a[center],&a[right-1]);return a[right-1]; }

  在三數中值法的代碼中,最后我們將樞紐放在了a[right-1]處,這是為什么呢?接下來的講解可以解釋這個做法的原因。

?

  實現了樞紐的選取后,接下來要實現的就是分治的“分”,在上述快速排序的想法中我們說過,我們將小于樞紐的元素們放在a[left]至a[i-1]處,樞紐放在a[i]處,大于樞紐的元素們放在a[i+1]至a[right]處。但是問題來了,怎么確定i的值呢?其實可以肯定的是,在開始分治(與樞紐的比較)前,i是絕對不可能知道的。只有所有元素都與樞紐比較完了才知道i到底是多少。

  不過,既然肯定了必須比完才知道,那我們就“比完再知道”唄。具體想法就是:

  1.先令樞紐與a[right-1]交換,即將樞紐暫且放在a[right-1]處(在三數中值代碼中已經完成此步驟)

  2.設變量l_pos從left+1開始遞增,直觀地說就是“讓l_pos從數組左側開始向右逐個掃描元素”(a[left]已經在三數中值時分治完畢,不需要再掃描)

  若l_pos掃描到a[l_pos]>樞紐,則l_pos暫停掃描

  3.設變量r_pos從right-2開始遞減,直觀地說就是“讓r_pos從數組右側開始向左逐個掃描元素”(a[right]已分治,a[right-1]為樞紐)

  若r_pos掃描到a[r_pos]<樞紐,則r_pos暫停掃描

  4.當l_pos與r_pos均停止時(若元素互異,必然存在此情況),若l_pos<r_pos(直觀地說就是它們“尚未碰頭”),則交換a[l_pos]與a[r_pos],然后l_pos繼續向右掃描,r_pos繼續向左掃描。若l_pos>r_pos,則它們“已經碰頭”,此時應有l_pos=r_pos+1,即l_pos就在r_pos右邊,于是我們徹底停止兩者的掃描,并確定了i的值為此時的l_pos。

?

  畫圖三張,以茲參考

  圖1,表示一種初始狀態:

  

?

  圖2,表示當a[l_pos]>樞紐,a[r_pos]<樞紐,且尚未結束時

  

?

  圖3,表示結束情況

  

?

?

  對應的分治部分代碼就是這樣:

//初始化l_pos與r_pos unsigned int l_pos = left + 1, r_pos = right - 2; //根據三數中值法得出樞紐同時完成兩個元素的分配 int median = MedianOf3(a, left, right);//l_pos與r_pos不斷向中間掃描 while (1) {while (a[l_pos] < median)l_pos++;while (a[r_pos] > median)r_pos++;//l_pos與r_pos均暫停掃描的兩種情況if (l_pos < r_pos)swap(&a[l_pos], &a[r_pos]);elsebreak; } //最后記得將樞紐交換至正確位置 swap(&a[l_pos], &a[right - 1]);

?

  解決了選取樞紐與分治,快速排序就算是完成了,從之前所給的快速排序偽代碼就可以看出這一點,偽代碼中沒有解決的就是這兩個地方:

void QuickSort(int *a, unsigned int left,unsigned int right) {if (left < right&&left != right - 1){//選樞紐:
//隨機選一個a[x],要求x>=left&&x<=right,作為樞紐median//分治:
//將a[left]到a[right]中所有小于median的元素置于a[left]到a[i-1]處//將a[left]到a[right]中所有大于median的元素置于a[i+1]到a[right]處//將a[x]放在a[i]處

//遞歸
QuickSort(a, left, i - 1);QuickSort(a, i + 1, right);}else if (left == right - 1){if(a[left]>a[right])swap(&a[left], &a[right]);} }

  將偽代碼中未完成部分填上,就有了如下快速排序:

void QuickSort(int *a, unsigned int left, unsigned int right) {//若a[left]到a[right]元素個數大于2,則繼續分治、遞歸if (left < right&&left != right - 1){/*——————選樞紐——————*/int median = MedianOf3(a, left, right);//根據三數中值法得出樞紐同時完成兩個元素的分配/*——————分治——————*/unsigned int l_pos = left + 1, r_pos = right - 2;//初始化l_pos與r_pos//l_pos與r_pos不斷向中間掃描while (1){while (a[l_pos] < median)l_pos++;while (a[r_pos] > median)r_pos++;//l_pos與r_pos均暫停掃描的兩種情況if (l_pos < r_pos)swap(&a[l_pos], &a[r_pos]);elsebreak;}//最后記得將樞紐交換至正確位置swap(&a[l_pos], &a[right - 1]);/*——————遞歸——————*/QuickSort(a, left, l_pos - 1);QuickSort(a, l_pos + 1, right);}//若a[left]到a[right]元素個數恰為2,則直接排序else if (left == right - 1){if (a[left]>a[right])swap(&a[left], &a[right]);}//若left==right,則說明只有一個元素,已為“有序” }

?

  但是請注意!上述代碼是有問題的,這是不容易察覺的第二個坑!坑在何處呢?讓我們揪出上述代碼的一部分:

while (1){while (a[l_pos] < median)l_pos++;while (a[r_pos] > median)r_pos++;//l_pos與r_pos均暫停掃描的兩種情況if (l_pos < r_pos)swap(&a[l_pos], &a[r_pos]);elsebreak;}

  如果數組的元素一定互異,那么這一部分代碼沒有問題,但是如果數組元素存在相同,那么這部分代碼就可能出現問題。

  假設l_pos暫停了掃描,原因是a[l_pos]==median,而且r_pos也暫停了掃描并且也是因為a[r_pos]==median,那么單純交換a[l_pos]與a[r_pos]只會使循環陷入死循環,因為兩個子循環的判斷條件將永遠為false,從而l_pos與r_pos一直不變。

  那么該如何解決這個問題呢?最直接的辦法就是增加新的判斷,判斷a[l_pos]與a[r_pos]是否都與median相等,如果是則不交換兩者,改為令l_pos++和r_pos--。

  但是實際上我們存在一個解決此問題的捷徑,這個捷徑的思路解說起來稍顯麻煩:既然問題是出在交換后l_pos與r_pos不會變化(遞增與遞減),那就在子循環處改為先變化再比較不就好了:

while (1){while (a[++l_pos] < median)/*Do nothing*/;while (a[--r_pos] > median)/*Do nothing*/;if (l_pos < r_pos)swap(&a[l_pos], &a[r_pos]);elsebreak;}

  同時,因為l_pos與r_pos都變成了“先變化再比較”,所以兩者的初始值也要改變為:

unsigned int l_pos = left, r_pos = right - 1;

?

?

  于是,完整的快速排序就實現好了,代碼如下:

void QSort(int *a, unsigned int left, unsigned int right) {if (left < right&&left != right - 1){unsigned int l_pos = left, r_pos = right - 1;int median = MedianOf3(a, left, right);int temp;while (1){while (a[++l_pos] < median);while (a[--r_pos] > median);if (l_pos < r_pos){temp = a[l_pos];a[l_pos] = a[r_pos];a[r_pos] = temp;}elsebreak;}temp = a[l_pos];a[l_pos] = a[right - 1];a[right - 1] = temp;QSort(a, left, l_pos - 1);QSort(a, l_pos + 1, right);}else if (left == right - 1 && a[left] > a[right]){int temp = a[left];a[left] = a[right];a[right] = temp;} }

  為了方便調用者,我們可以實現一個簡單的“接口”:

void QuickSort(int *a, unsigned int size) {return QSort(a, 0, size - 1); }

?

?

?

  對于快速排序,還要注意的一點是當數組的大小N很小時,快速排序是不如插入排序的,并且需要注意的是由于快速排序的遞歸,必然會出現“小數組”。因此實際實現快速排序時往往選擇對小數組執行一個插入排序。即:

void QSort(int *a, unsigned int left, unsigned int right) {if (left < right-N){//此部分代碼略 }else{InsertionSort(a,right-left+1);} }

?

  至此,快速排序實現完畢。

?

  接下來我們試著分析一下快速排序的時間復雜度。這一部分我們將分為兩個小部分:快速排序的最壞情況,快速排序的最好情況。

?

  首先,為了方便分析,我們假設快速排序的樞紐選擇是完全隨機的。對于大小為N的數組,快速排序耗時設為T(N),則T(1)=1。于是,T(N)=T(i)+T(N-i-1)+c*N,其中i為小于樞紐的元素個數,c為未知常數,c*N代表分治階段耗費的線性時間。

  那么,快速排序的最壞情況就是每一次選取的樞紐都是最小的元素,此時i=0,上述公式變為:

  T(N)=T(N-1)+c*N,遞推此公式可得

  T(N-1)=T(N-2)+c*(N-1)

  T(N-2)=T(N-3)+c*(N-2)

  ……

  T(2)=T(1)+c*2

  將上述公式左側與右側均全部相加,得:

  T(N)+T(N-1)+T(N-2)+……+T(2) = T(N-1)+T(N-2)+……T(2)+T(1)+c*(2+3+4+5+……+N)

  化簡可得:

  T(N)=T(1)+c*(2+3+4+……+N)=1+c*(2+3+4+……+N)=O(N2)

  也就是我們在上一篇博文提到的,快速排序最壞情況為O(N2)

  不難看出,選擇樞紐時不論是完全隨機還是三數中值,快速排序都不容易出現這樣的最壞情況。

?

  接下來我們看看快速排序的最好情況。快速排序的最好情況顯然就是每次樞紐都選擇了整個剩余數組的中間值,為了簡化推導,我們假定遞歸時將樞紐本身也帶進去,并且N為2的冪。從而

  T(N)=2*T(N/2)+c*N

  兩邊同時除以N,得:

  T(N)/N=T(N/2)/(N/2)+c,遞推此公式可得:

  T(N/2)/(N/2)=T(N/4)/(N/4)+c

  T(N/4)/(N/4)=T(N/8)/T(N/8)+c

  ……

  T(2)/2=T(1)/1+c

  將上述公式左側與右側均全部相加, 得:

  T(N)/N+T(N/2)/(N/2)+……+T(2)/2=T(N/2)/(N/2)+……T(1)/1+c*logN

  化簡,得:

  T(N)/N=T(1)/1+c*logN,即T(N)=N*c*logN=O(N*logN)。所以快速排序的最好情況就是O(N*logN)。

?

  快速排序的平均情況分析的公式繁雜,且化簡需要高深的數學,此處只給出基本思路:既然樞紐是隨機的,那么小于樞紐的元素個數i就也是隨機位于[0,N-1],那么i的平均值就應該是(0+1+2+……+(N-1))/N,同理大于樞紐的元素個數平均值為(0+1+2+……+(N-1))/N,基于這兩點,T(N)=2*(0+1+2+……+(N-1))/N+c*N,依據此公式進行遞推、相加、化簡,可以得出平均時間為O(N*logN)

?

  對于快速排序的分析并不是只有本文所提,比如在l_pos于r_pos掃描的過程中,我們為什么選擇在a[l_pos]或a[r_pos]等于median時停下,而不是繼續掃描呢?這是有原因的,簡述就是:防止極端情況下l_pos及r_pos越界,同時使分得的兩個子數組大小更加平衡。但是更多的分析本文就不做介紹了,時間有限╮(╯_╰)╭

?

?

  最后,對于大小為20萬的隨機整數數組,我們提過的“主流”排序算法的簡單比較結果如下(僅供參考):

?  

轉載于:https://www.cnblogs.com/mm93/p/7569529.html

總結

以上是生活随笔為你收集整理的浅入浅出数据结构(20)——快速排序的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 毛片毛片毛片毛片毛片毛片毛片毛片 | 激情五月在线 | japanese在线观看 | 国产免费一区二区三区最新6 | 免费人妻精品一区二区三区 | 国产午夜小视频 | 少妇精品无码一区二区免费视频 | 四虎看黄| 无码人妻精品一区二区蜜桃网站 | 三级特黄 | 四虎影城库 | 天天看天天做 | 一区成人 | 最近最新最好看的2019 | 日日干天天爽 | 欧美日韩爱爱 | 日韩av电影在线播放 | 红猫大本营在线观看的 | 伊人亚洲综合 | 在线播放一级片 | 亚洲成人黄色 | 日韩精品麻豆 | 亚洲一区在线电影 | 午夜精品影院 | 亚洲av成人无码网天堂 | 荷兰av| 美女久久久久久久 | 国产视频精品免费 | 爱插美女网 | 激情五月俺也去 | 一区免费在线观看 | 日韩视频福利 | 中文一区二区在线观看 | 伊伊总综合网 | 少女与动物高清版在线观看 | 日韩高清一级片 | 欧美性生活视频 | 欧美激情在线免费 | 性生活视频在线播放 | 久久免费看少妇 | 一区二区三区四区五区六区 | 91视频播放 | 久久精品黄aa片一区二区三区 | 手机看片日韩 | 日本乱码一区二区 | 国产91看片 | 国产激情视频一区二区 | 美女黄色大片 | 午夜免费观看 | 日韩精品一区二区三区 | 亚洲www久久久 | 人人爽人人做 | 亚洲每日在线 | 在线免费观看污 | 一本亚洲 | wwwxxxx国产| 黄色一级片. | 激情综合一区二区三区 | 污污视频网站在线 | 亚洲欧洲无码一区二区三区 | 成人精品三级 | 15p亚洲 | 国产免费不卡 | 日本精品视频一区二区 | 亚洲欧美另类在线视频 | 色综合中文字幕 | 亚洲制服另类 | 中文字幕av不卡 | 欧美日韩在线成人 | 日本熟妇一区二区三区 | 午夜免费av | 欧美日韩精品一区二区在线观看 | mm131美女大尺度私密照尤果 | 熟女少妇a性色生活片毛片 亚洲伊人成人网 | 亚洲色图 一区二区 | 欧美色一区二区三区在线观看 | 日韩不卡免费视频 | 亚色图 | 法国空姐在线观看视频 | 亚洲狠狠婷婷综合久久久久图片 | 国产亚洲综合精品 | 欧美生活一级片 | 东京热加勒比无码少妇 | 色悠久 | 亚洲精品鲁一鲁一区二区三区 | 天堂av影院 | 爱爱91| 亚洲精品丝袜 | 超碰av在线免费观看 | 精品久久久影院 | 欧美日韩字幕 | 欧美日韩无 | 久久特黄视频 | 中文字幕一区二区久久人妻网站 | 大胸喷奶水www视频妖精网站 | 在线观看免费av网站 | 狠狠干b| 日韩精品电影一区 | 欧美一级黄色大片 |