十問快速排序

算法是我們學習計算機的基礎之一,但時常在我們的日常工作中似乎並不是佔有那麼大的重要性,但不管怎麼樣,個人認爲,作爲一名優秀的程序員,沒事的時候看看算法,可以放鬆情緒,可以提高大腦的靈活性,更何況,很多公司的筆試中算法有很大的比重。其實,就算是一個簡單的算法,裏面還是有很多講究的,Jon Bentley在他的《Programming Perls》裏面說歷史上第一篇二分搜索的論文在1946年就發表了,但是第一個沒有錯誤的二分搜索程序卻直到1962年纔出現,各種究竟,值得深味。

回到正題,我們來說說快速排序吧,這裏我要講的是比較簡單一種快排:先上代碼!(c#) 

但是不知道大家有沒有深究過爲什麼是這樣的,或者說,如果不靠記憶而純粹的自己推導,能否不經調試一次寫出正確的代碼。裏面好幾個地方涉及到邊界值的問題,不知道你是否和我有過這樣的疑惑,爲什麼這裏是“<”而不是“<=”?爲了便於論述,我把問題的探討以問答的形式展開:(注意,下文中的”暫定算法“值的是上面的實現方式,而暫定結果,指的是下圖,我是在11行和12行之間加了一個小方法,現實當前arrInt的情況,然後再進行swap(),方括號中的爲行號)

 

 

問題1:14行中的,while (i <= j) 可以改成 while (i < j) 嗎?

我們看“暫定結果”的第四行,執行完這一句之後i = 6,j=6;按照暫定做法,會繼續執行07行的do語句,執行完09、10兩個while後,i = 6,j = 4;跳出14行while,進入選擇判斷,我們發現最終選擇的是quick(0, 4) 和quick(6,14)

而如果我們假定可以改爲while (i < j),則不會繼續執行07行do語句,跳出14行的while語句後,最終選擇的是quick(0, 6) 和 quick(6, 14)相比較而言,所以,該假設冗餘更多,但是還是會正確排序

 

 

問題2:15行中,if (left < j)可以改成 if (left <= j) 嗎?

看“暫定結果”第5行,執行完這一句之後i = 1,j = 1; 按照暫定的做法,會繼續執行07行的do語句,執行09、10兩個while後,i = 1, j = 0;進入15、16行的判斷,此時,按照暫定的辦法,left < i 返回false,所以不執行quick(left, j)。但是按照問題2中的修改,則需要執行一次quick(left, j)即quick(0, 0)這一步顯然是多餘的,也就是說,還是可以正確排序的,只不過冗餘更多

 

問題3:17行中,if (right > i) 可以改成 if (right >= i) 嗎?
同問題2。

 

問題4:09行中,i < right 改爲 i <= right  可否?
通過試驗,發現最後排序結果正確,使用swap方法的次數也相同,也就是“<”和“<=”沒什麼區別,我們來進一步討論
我們來看while (arrInt[i] < middle && i <= right) i++;這句話,尋找一個i,使得arrInt[i] >= middle或者,i = right + 1,也就是說,我們討論的重點是,i有沒有可能到達right + 1(實驗結果是問題4不影響排序,而一旦i = right + 1,如果right = arrInt.Count() - 1,那麼,i就涉及到溢出的問題了)。i從左向右查找,j從右向左查找,遇到arrInt[i] = middle 或者 arrInt[j] = middle肯定會結束09行或者10行的while語句
我們分三種情況:(如果有多個值爲middle,我們假設是第一個middle,第一個middle的索引是m)
      情況1:arrInt[i]、arrInt[j]同時遇到middle,那麼,此時,i = j = m(只管第一個middle),swap之後,i = m + 1,j = m - 1;此時執行14行代碼,結束,所以沒有遇到i = right + 1的問題。
      情況2:arrInt[i]先遇到middle,此時,i = m,j = m + k(k是一個未知的大於0的整數),swap後,i = m + 1, j = m + k -1;而arrInt[m + k] = middle,也就是說,當i繼續自增i = m + k時候,又會停下來,而此時,j <= m + k -1;這時候,由於i < j,跳出14行,再一次沒有遇到i = right + 1問題。
      情況3:arrInt[j]先遇到middle,此時,j = m, i = m - k(k是一個未知的大於0的整數),swap後,i = m - k + 1,j = m,同情況2,也不會遇到i = right + 1;所以說,09行代碼“i < right”改爲“i <= right”對排序沒有任何影響。

 


問題5: 10行,j > left可否 改爲 j >= left ?
同問題提4。

 

問題6:第11行,if (i <= j) 能否改爲 if (i < j) ?

通過測試,發現程序進入了死循環,講到這裏,我想到一個笑話:據說,以後的電腦運行速度會遠遠超過如今的電腦,跑完一個死循環只需要6秒鐘
下面我們在仔細分析一下,請看下圖,這是改爲if (i < j)之後出現的結果,(這還沒有結束),此時i = 6, j = 8 ,swap之後,i = j = 7;然後重新執行07行do語句,此時arrInt[7] 正好和middle的值相同,所以通過了09和10行之後i 和j都停在了7,而此時由於修改之後的11行i < j返回一個false,所以沒有執行swap,所以沒有執行i++和j--;然後再一次返回到07do語句,跑完一個循環,i 和j都卡在了7。。。所以說if (i <= j)不能改爲 if (i < j)

 

 

問題7:如果將14行的while (i <= j);改成了while (i < j);這個時候第11行,if (i <= j)是不是可以改爲 if (i < j)了呢?
我們將快速排序的代碼按照假設7來調整之後的下圖,

此時left = 0, right = 2, i = 0, j = 2, m = 1,swap()之後,i = j = 1,
通過 09、10行代碼後,i和j依然等於1,
由於我們修改後的11行(i < j)返回false,所以不進行swap(),
之後,通過14行代碼跳出循環,滿足15和17行判斷,
進入新的排序quickSort(0, 1) 和quickSort(1, 2),
代碼跑到這裏,我們已經發現一個問題了,這兩個排序都會操作索引爲1的數,這顯然是不對的,但這不是出錯的直接原因,
我們繼續來看quickSort(0, 1),left = i = 0, left = j = 1, m = arrInt[(0 + 1) / 2] = 0;(此時arrInt[0] = 0 , arrInt[1] = 1)
通過09、10行之後,i = 0, j = 0,
 請注意,此時的11行和14行的判斷都已經改成了(i < j)所以,程序會不進行swap() 並跳出大的do循環來到15和17行的判斷,
通過這兩個判斷之後,由於(i < right)返回true,程序會重新執行quickSort(0, 1)。
這就是死循環所在了,所以呢,
即便我們同時修改了11和14行的判斷,也還是無法正確的進行排序。

 

問題8:按照問題4,i會在到達邊界的時候通過arrInt[i] < middle來停止,那麼,是不是可以將09行改爲while (arrInt[i] < middle) i++?
記得以前有人說過,存在的即時合理的,其實做軟件開發也是,“正常情況”下,之前我的得到的代碼之所以在09、10行有i < right和j > left,肯定是有它的原因的,但是我用之前的例子測試之後,確確實實是對排序沒有影響。甚至可以妄下結論,可以將09行改爲while (arrInt[i] < middle) i++;同理將10行改爲while (arrInt[j] > middle) j--;雖然說用測試的例子沒有發現問題,但是我覺得我們也不妨搞個小程序驗證一下。我把我剛纔寫的代碼貼出來給大家看一下,沒有仔細推敲過,我暫時沒有測出有異常情況。

現在看來,09行中的i < right的存在的意義很有可能有以下兩種
一:給程序員一個雙保險,起到定心丸的作用
二:有人被忽悠了
三:可能確實有用,只是本人水平不高,沒能發現

 

 

問題9:遺漏了一個假設,還是09行的判斷中arrInt[i] < middle能否改爲 arrInt[i] <= middle?
如果改爲arrInt[i] <= middle,那麼09行的目的是尋找一個大於middle或者最後一個數字,10行的目的還是尋找一個大於等於middle或者第一個數字(10行不改動)
我們來看下圖中運算結果中第15行吧,現在只考慮arrInt[8 : 14] = {4, 4, 5, 4, 7, 5, 6}中間的計算結果我在這裏就不具體說明了,最後排序會跑到對arrInt[11 : 14] = {5, 7, 5,6}left = i = 11, right = j = 14, middle = arrInt[(11 + 14) / 2] = 7 ,通過修改後的09行和沒有修改的10行之後,i = j = 14,接着執行swap(),i = 15, j = 13, 跳出大循環,來到15和17行判斷,由於i < right 返回的是false ,所以不執行quickSort(i, right);我們只執行quickSort(left, j);即quickSort(11, 13);也就是說,arrInt[11 : 14] = {5, 7, 5,6}我們最後只進行quickSort(11, 13) 而把arrInt[14]排除,按照要求{5,7, 5 }應該是任何一個數都是小於或者等於{6}的,這樣的顯然相違背了。也就是說,i通過自增長從left->right,如果說arrInt[left : right]中沒有大於middle的數,這個時候就會出錯了。

另外,如果說09行arrInt[i] < middle改爲 arrInt[i] <= middle,那麼後面的判斷i < right就不能改爲i <= right了,因爲無法保證運行的時候在i = right + 1之前就跳出循環。

 

問題10:就一個快速排序犯得着這麼大動干戈嗎?

我覺得不用,如果的平常的工作中不需要總是和算法打交道的話,一般情況下,這個可以作爲業餘愛好。如果是你還是學生,我覺得不妨一試,好好的跑一邊快速排序,如果你上百度百科你會發現快速排序除了上述的這一種還有幾種,當然,萬變不離其宗,在學習的時候多問自己幾個爲什麼就當是腦筋急轉彎,如果你懶了,也可以拿着去問問老師,看看老師的講解。

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