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

2021-08-23 11:38:15 字數 3636 閱讀 4945

因為受到經濟危機的影響,我在 bokee.com 的部落格可能隨時出現無法訪問的情況;因此將2023年到2023年間在 bokee.com 撰寫的部落格文章全部遷移到 csdn 部落格中來,本文正是其中一篇遷移的文章。

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

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

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

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

拿到這道題,馬上就會想到的方法是建立乙個陣列把1億個數裝起來,然後用for迴圈遍歷這個陣列,找出最大的1萬個數來。原因很簡單,因為如果要找出最大的那個數,就是這樣解決的;而找最大的1萬個數,只是重複1萬遍而已。

template< class t >

void solution_1( t bigarr, t resarr )

resarr[i] = bigarr[idx];

std::swap( bigarr[idx], bigarr[i] ); }

}設big_arr_size =1億,res_arr_size = 1萬,執行以上演算法已經超過40分鐘(注3),遠遠超過我們的可接受範圍。

從上面的**可以看出跟selectsort演算法的核心**是一樣的。因為selectsort是乙個o(n^2)的演算法(solution_1的時間複雜度為o(n*m),因為solution_1沒有將整個大陣列全部排序),而我們又知道排序演算法可以優化到o(nlogn),那們是否可以從這方面入手使用更快的排序演算法如mergesor、quicksort呢?但這些演算法都不具備從大至小選擇最大的n個數的功能,因此只有將1億個數按從大到小用quicksort排序,然後提取最前面的1萬個。

template< class t, class i >

void solution_2( t bigarr, t resarr )

因為stl裡的sort演算法使用的是quicksort,在這裡直接拿來用了,是因為不想寫乙個寫乙個眾人皆知的quicksort**來佔篇幅(而且stl的sort高度優化、速度快)。

對solution_2進行測試,執行時間是32秒,約為solution_1的1.5%的時間,已經取得了幾何數量級的進展。

壓抑住興奮回頭再仔細看看solution_2,你將發現乙個大問題,那就是在solution_2裡所有的元素都排序了!而事實上只需找出最大的1萬個即可,我們不是做了很多無用功嗎?應該怎麼樣來消除這些無用功?

如果你一時沒有頭緒,那就讓我慢慢引導你。首先,發掘乙個事實:如果這個大陣列本身已經按從大到小有序,那麼陣列的前1萬個元素就是結果;然後,可以假設這個大陣列已經從大到小有序,並將前1萬個元素放到結果陣列;再次,事實上這結果陣列裡放的未必是最大的一萬個,因此需要將前1萬個數字後續的元素跟結果陣列的最小的元素比較,如果所有後續的元素都比結果陣列的最小元素還小,那結果陣列就是想要的結果,如果某一後續的元素比結果陣列的最小元素大,那就用它替換結果陣列裡最小的數字;最後,遍歷完大陣列,得到的結果陣列就是想要的結果了。

template< class t >

void solution_3( t bigarr, t resarr )

} //這個後續元素比resarr中最小的元素大,則替換。

if( bigarr[i] > resarr[idx] )

else

bexchanged = false; }

}上面的**使用了乙個布林變數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的引入實際上減少了如下**段的執行次數:

for( idx = 0, j = 1; j < res_arr_size; ++j )

上面的**段即是查詢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的元素。

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

template< class t, class i >

void solution_4( t bigarr, t resarr )

minelemidx = idx; }

} }經過測試,同樣情況下solution_4用時約1.8秒,較solution_3效率略高,總算不負一番努力。

待續……

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

引子 每年十一月各大it公司都不約而同 爭后恐後地到各大高校進行全國巡迴招聘。與此同時,網上也開始出現大量筆試面試題 網上流傳的題目往往都很精巧,既能讓考查基礎知識,又在平淡中隱含了廣闊的天地供優秀學生馳騁。這兩天在網上淘到一道筆試題目 注1 雖然真假未知,但的確是道好題,題目如下 從10億個浮點數...

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

因為受到經濟危機的影響,我在 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...