珠璣之櫝 二分思想與分治法 排序思想

2021-08-27 13:10:04 字數 3304 閱讀 3030

目錄

排序思想

如果你對概念很敏感,會馬上意識到這兩者的細微不同:二分搜尋每次都要捨棄一半,從留下的一半中尋找目標;而分治法把乙個大問題分成兩個或多個小問題,遞迴地求這些小問題的解,最後再把它們小心謹慎的合併起來,並且要仔細考慮合併時產生的新的情況。這當然沒有錯,但你也馬上會從這裡意識到兩者的巨大聯絡。就拿選取陣列中第k個最小的數的演算法來說,有乙個版本便是從快速排序中修改而來:劃分後,捨棄掉不存在的區間,對剩餘部分迭代(後文將進行講解),而快速排序是分治法的典型代表。

正式地把這個問題敘述為:

(習題11.9、《程式設計珠璣(續)》第15章)在o(n)時間內從陣列x[0...n-1]中找出第k個最小的元素。可以改變原陣列中元素的位置。

下面這段**就是從快速排序中修改而來,同時考慮到了隨機選擇劃分元素的問題。

int partition(int *array, int p, int

r) }

swap_value(array+i+1,array+r);

return i+1;}

int random_select(int *array, int p, int r, int

i)

雖然《續》中作者用實驗和統計的方式說明了對於n元陣列,平均期望時間為o(n),但如果你不滿足於統計而想得到理論上的證明,請參考《演算法導論》9.3節。

擴充套件:(《續》習題15.2)如何從乙個3元陣列中選出第2小的?如果從1000000個中選出1000個最小元素、且輸入儲存在磁帶上呢?

分析:前者至多只需3次比較:1和2、1和2中最大的和3、1和2中最小的和3;後者是遍歷時用1000大小的最大堆儲存1000個當前最小的即可。其實前者是為了說明,如果問題只有幾步就可以解決,根本沒必要使用複雜的遞迴函式,直接解就是了;而後者是因為磁帶進行隨機i/o不方便而已,否則,直接用k=1001劃分,那麼k前面的1000個就是所求的元素。

擴充套件:

(《程式設計之美》2.5尋找最大的k個數)

分析:使用二分法找到了從大到小的第k個的數之後,那麼比它大的和它自己就是要找的最大的k個數了。當然這個問題還有其它解法,有興趣的讀者可以參考《程式設計之美》原書。

如果從「二分搜尋」中提煉出「二分法」,即這種捨去一半、留一半的方式,而又不用像分治法那樣考慮子問題解的合併,那麼我們的思路也應該更加廣闊一些:能夠二分的,不僅僅是陣列下標。如果這樣講很抽象,那麼考慮下面乙個例子:

(《程式設計珠璣》第二章問題a)給定乙個最多包含40億個隨機排列的32位整數的順序檔案,找出乙個不在檔案中的32位整數。

分析:32位整數一共有4294967296個,略大於40億。即使不重複出現,它們也不可能全部放入這40億個整數的陣列中,必然有一部分不出現。根據二分思想,我們把40億個數的集合分成兩個,其中必然有乙個至少缺少乙個數的集合,進行遞迴求解。劃分的依據是按數的位掃瞄,從第31位開始,分別統計這一位是0和1的數,把較小的那一部分用做下一次遞迴。掃瞄完第0位,必然得到乙個不含元素的空集,這個集合對應的就是缺失的元素。

為了演示這一過程,我編寫了相應的測試程式。由於包含大量的檔案i/o操作,看上去比較複雜,但是基本的思想框架是一樣的。為了簡化起見,只處理30000個帶符號的正數(這意味著我從每個數的第14位開始檢測,最多有37628個可能),執行前需要生成乙個含有30000個數的檔案output.txt。

#include #include 

int *****eck(int total,int n,int

last)

if(n==0

)

else

assert(input!=null && output0!=null&&output1!=null);

mask = 1

else

}fflush(output0);

fflush(output1);

fclose(output0);

fclose(output1);

fclose(input);

return num1}int search(int

n) printf(

"missing number:%d\n

",missing);

return0;

}int

main()

體驗過這個思想所展示的威力之後,也難怪《程式設計珠璣》的作者感嘆二分搜尋「無所不在」了。

另外值得一提的是,雖然分治法也用到了二分思想,但具體分法是五五開還是三七開,這可就不一定了。

擴充套件:(習題2.2)給定包含43億個32位整數,找出至少出現兩次的整數。

分析:如果每次都保留大於數目一半的集合,原先的方案並不能保證每次減少一半元素。為了每次盡可能多地拋棄元素,在檢查元素個數時,如果乙個集合的元素個數已經超過了這次遞迴中它所能容納不重複的元素個數m(起始時是232/2)而達到了m+1,那麼剩餘部分元素都沒有必要再檢查而直接拋棄,這m+1個元素的集合必然已經有重複元素,直接取這個集合即可。這就保證了每次元素個數減半。

延續上一節的主題。有時當我看到o(nlogn)時間複雜度的演算法,總會聯想到分治法和快速排序,這是因為快速排序是平均o(nlogn)的時間複雜度的。其實對於很多演算法,如果進行了排序特別是快速排序,能夠顯著地提高速度。甚至,排序部分是這個演算法的基石。其實,對於一組無序資料,元素之間的相互關係比較相當薄弱;而在排序後,或許能將一些有近似性質的元素篩選並放在一起,以便於下一步使用,這就是我所謂的排序思想。

問題1:(第2章問題c)

給定乙個英語字典,找出所有變位詞集合。所謂變位詞,比如"pots"、"stop"、"tops"互為變位詞。

分析:

檢測每對單詞是否為變位詞需要花費大量時間。為了將所有單詞標準化,可以先將所有單詞按字母表順序排序,比如pots變成opst,再把所有排序後的單詞再做一次排序。那麼,所有變位詞就一定是在相鄰的位置上了。為了儲存原先單詞的內容,可以使用索引來儲存原單詞的位置。

問題2:(習題2.8)給定乙個n元實數集合、乙個實數t和乙個整數k,如何快速確定是否存在乙個k元子集,其元素之和不超過t?

分析:

這裡只要求不超過t,那麼把這個集合按遞增排序,如果前k個數之和小於t,那麼必然存在這樣乙個k元子集。

往期回顧:

珠璣之櫝」系列簡介與索引

位向量/點陣圖的定義和應用

估算的應用與little定律

隨機數函式取樣與概率

****正確性:迴圈不變式、斷言、debug

珠璣之櫝 二分思想與分治法 排序思想

目錄 排序思想 如果你對概念很敏感,會馬上意識到這兩者的細微不同 二分搜尋每次都要捨棄一半,從留下的一半中尋找目標 而分治法把乙個大問題分成兩個或多個小問題,遞迴地求這些小問題的解,最後再把它們小心謹慎的合併起來,並且要仔細考慮合併時產生的新的情況。這當然沒有錯,但你也馬上會從這裡意識到兩者的巨大聯...

珠璣之櫝 二分思想與分治法 排序思想

目錄 排序思想 如果你對概念很敏感,會馬上意識到這兩者的細微不同 二分搜尋每次都要捨棄一半,從留下的一半中尋找目標 而分治法把乙個大問題分成兩個或多個小問題,遞迴地求這些小問題的解,最後再把它們小心謹慎的合併起來,並且要仔細考慮合併時產生的新的情況。這當然沒有錯,但你也馬上會從這裡意識到兩者的巨大聯...

二分思想和分治法

二分思想和分治法 如果你對概念很敏感,會馬上意識到這兩者的細微不同 二分搜尋每次都要捨棄一半,從留下的一半中尋找目標 而分治法把乙個大問題分成兩個或多個小問題,遞迴地求這些小問題的解,最後再把它們小心謹慎的合併起來,並且要仔細考慮合併時產生的新的情況。這當然沒有錯,但你也馬上會從這裡意識到兩者的巨大...