從一道筆試題談演算法優化(上)

2021-05-08 10:17:24 字數 3094 閱讀 9213

引子

每年十一月各大it公司都不約而同、爭后恐後地到各大高校進行全國巡迴招聘。與此同時,網上也開始出現大量筆試面試題;網上流傳的題目往往都很精巧,既能讓考查基礎知識,又在平淡中隱含了廣闊的天地供優秀學生馳騁。

這兩天在網上淘到一道筆試題目(注1),雖然真假未知,但的確是道好題,題目如下:

從10億個浮點數中找出最大的1萬個。

這是一道似易實難的題目,一般同學最容易中的陷阱就是沒有重視這個「億」字。因為有10億個單精度浮點數元素的陣列在32位平台上已經達到3.7gb之巨,在常見計算機平台(如win32)上宣告乙個這樣的陣列將導致堆疊溢位。正確的解決方法是分治法,比如每次處理100萬個數,然後再綜合起來。不過這不是本文要討論的主旨,所以本文把上題的10億改為1億,把浮點數改為整數,這樣可以直接地完成這個問題,有利於清晰地討論相關演算法的優化(注2)。

上面的**使用了乙個布林變數

bexchanged

標記是否發生過交換,這是乙個前文沒有談到的優化手段——用以標記元素交換的狀態,可以大大減少查詢

resarr

中最小元素的次數。也對

solution_3

進行測試一下,結果用時

2.0秒左右(不使用

bexchanged

則高達32

分鐘),遠小於

solution_2

的用時。

深思熟慮

在進入下一步優化之前,分析一下solution_3的成功之處。第一、solution_3的演算法只遍歷大陣列一次,即它是乙個o(n)的演算法,而solution_1是o(n*m)的演算法,solution_2是o(nlogn)的演算法,可見它在本質上有著天然的優越性;第

二、在solution_3中引入了bexchanged這一標誌變數,從測試資料可見引入bexchanged減少了約99.99%的時間,這是乙個非常大的成功。

上面這段話絕非僅僅說明了solution_3的優點,更重要的是把solution_3的主要矛盾擺上了桌面——為什麼乙個o(n)的演算法效率會跟o(n*m)的演算法差不多(不使用bexchanged)?為什麼使用了bexchanged能夠減少99.99%的時間?帶著這兩個問題再次審視solution_3的**,發現bexchanged的引入實際上減少了如下**段的執行次數:

上面的**段即是查詢resarr中最小元素的演算法,分析它可知這是乙個o(n)的演算法,到此時就水落石出了!原來雖然solution_3是乙個o(n)的演算法,但因為內部使用的查詢最小元素的演算法也是o(n)的演算法,所以就退化為o(n*m)的演算法了。難怪不使用bexchanged使用的時間跟solution_1差不多;這也從反面證明了solution_3被上面的這一**段導致效能退化。使用了bexchanged之後因為減少了很多查詢最小元素的**段執行,所以能夠節省99.99%的時間!

至此可知元凶就是查詢最小元素的**段,但查詢最小元素是必不可少的操作,在這個兩難的情況下該怎麼去優化呢?答案就是保持結果陣列(即resarr)有序,那樣的話最小的元素總是最後乙個,從而省去查詢最小元素的時間,解決上面的問題。但這也引入了乙個新的問題:保持陣列有序的插入演算法的時間複雜度是o(n)的,雖然在這個問題裡插入的數次比例較小,但因為基數太大(1億),這一開銷仍然會令本方案得不償失。

難道就沒有辦法了嗎?記得小學解應用題時老師教導過我們如果解題沒有思路,那就多讀幾遍題目。再次審題,注意到題目並沒有要求找到的最大的1萬個數要有序(注4),這意味著可以通過如下演算法來解決:

1) 將bigarr的前1萬個元素複製到resarr並用quicksort使resarr有序,並定義變數minelemidx儲存最小元素的索引,並定義變數zonebeginidx儲存可能發生交換的區域的最小索引;

2) 遍歷bigarr其它的元素,如果某一元素比resarr最小元素小,則將resarr中minelemidx指向的元素替換,如果zonebeginidx == minelemidx則擴充套件zonebeginidx;

3) 重新在zonebeginidx至res_arr_size元素段中尋找最小元素,並用minelemidx儲存其它索引;

4) 重複2)直至遍歷完所有bigarr的元素。

依上演算法,寫**如下:

經過測試,同樣情況下

solution_4

用時約1.8

秒,較solution_3

效率略高,總算不負一番努力。

待續……

從上面的**可以看出跟

selectsort

演算法的核心**是一樣的。因為

selectsort

是乙個o(n^2)

的演算法(

solution_1

的時間複雜度為

o(n*m)

,因為solution_1

沒有將整個大陣列全部排序),而我們又知道排序演算法可以優化到

o(nlogn)

,那們是否可以從這方面入手使用更快的排序演算法如

mergesor

、quicksort

呢?但這些演算法都不具備從大至小選擇最大的

n個數的功能,因此只有將

1億個數按從大到小用

quicksort

排序,然後提取最前面的

1萬個。

因為stl

裡的sort

演算法使用的是

quicksort

,在這裡直接拿來用了,是因為不想寫乙個寫乙個眾人皆知的

quicksort

**來佔篇幅(而且

stl的

sort

高度優化、速度快)。 對

solution_2

進行測試,執行時間是

32秒,約為

solution_1

的1.5%

的時間,已經取得了幾何數量級的進展。

拿到這道題,馬上就會想到的方法是建立乙個陣列把

1億個數裝起來,然後用

for迴圈遍歷這個陣列,找出最大的

1萬個數來。原因很簡單,因為如果要找出最大的那個數,就是這樣解決的;而找最大的

1萬個數,只是重複

1萬遍而已。

從一道筆試題談演算法優化(上)

因為受到經濟危機的影響,我在 bokee.com 的部落格可能隨時出現無法訪問的情況 因此將2005年到2006年間在 bokee.com 撰寫的部落格文章全部遷移到 csdn 部落格中來,本文正是其中一篇遷移的文章。每年十一月各大it公司都不約而同 爭后恐後地到各大高校進行全國巡迴招聘。與此同時,...

從一道筆試題談演算法優化(上)

因為受到經濟危機的影響,我在 bokee.com 的部落格可能隨時出現無法訪問的情況 因此將2005年到2006年間在 bokee.com 撰寫的部落格文章全部遷移到 csdn 部落格中來,本文正是其中一篇遷移的文章。每年十一月各大it公司都不約而同 爭后恐後地到各大高校進行全國巡迴招聘。與此同時,...

一道筆試題

看到一道筆試題,跟自己想的有點出入,就跑了下,看了看原因。我稍微改了下 include int main int argc,char argv 輸出結果 c 5 d 245 press any key to continue vc6.0 debug下的彙編 5 unsigned char a 0xa...