號外,號外 -幾乎全部的binary search和mergesort都有錯java
這是Joshua Bloch(Effective Java的做者)在google blog上發的帖子。在說這個帖子以前,不得不強力重複Joshua Bloch的推薦:若是你尚未讀過Programming Pearls (中文版叫《編程珠璣》)這本書,如今就去讀吧。若是你只讀了一遍,如今就去再讀一遍吧。程序員
仍是說回Joshua的文章。當初Programming Pearls的做者Jon Bentley到CMU作講座。他叫在場的計算機系博士生們寫出binary search的算法,而後當場分析了其中一份。固然,那份算法以及絕大部分人寫的算法都錯了。Jon Bentley在Programming Pearls裏也提到,雖然1946年就有人發表binary search,但直到1962第一個正確運行的算法才寫出來。這個小故事的關鍵教訓就是寫程序時要仔細考慮算法的不變量(invariant)。若是我記得沒錯,Programming Pearls第4章講解了怎麼證實binary search的正確性。固然,每本離散數學的教科書都會教咱們列出pre-condition, invariant, 和post-condition,證實循環開始前pre-condition成立,循環中invariant始終成立,而循環結束後post-condition被知足,而幾乎每本教科書(至少我看過的)都會用binary search做例子。因此有興趣的本身去看吧,俺就不羅嗦了。算法
JDK裏的binary search代碼是這樣實現的(Joshua Bloch本人寫的)編程
public static int binarySearch(int[] a, int key) { int low = 0; int high = a.length - 1; while (low <= high) { int mid = (low + high) / 2; int midVal = a[mid]; if (midVal < key) low = mid + 1; else if (midVal > key) high = mid - 1; else return mid; // key found } return -(low + 1); // key not found. }
錯誤就在第6行:數組
int mid = (low + high) / 2;
這行的問題是當low和high的和超過2^31-1, 也就是Java裏最大整數值時,整數溢出就發生了,而mid就變成負數了, 因而JVM就抓狂了,因而ArrayIndexOutOfBoundsException就發生了。post
當一個數組包含多過2^30元素時,這個錯誤就會被發現。那麼大的數組在80年代Programming Pearls初版寫就的時候不可思議,但在如今卻很常見。因此說,儘管1962年正確的binary search問世,現實倒是直到如今流行系統裏的binary search還有錯。測試
解決的辦法不難。把第6行改寫成google
int mid = low + ((high - low) / 2);
或者.net
int mid = (low + high) >>> 1;
C和C++裏沒有這個">>>",咱們能夠這樣作:設計
int mid = ((unsigned) (low + high)) >> 1。
那如今binary search就徹底正確了麼?咱們仍是不知道。咱們獲得的深入教訓是,僅僅證實一個程序正確是不夠的。咱們必須仔細測試。高德納在寫給Peter van Emde Boas的信裏說,「上面那段程序可能有錯。我只證實了它是正確的,但尚未測過」。人們每每用這段話來彰顯高德納的一絲不苟和學究氣,誰知道這句話背後是高德納深入的洞察力。人們常說「理論上講實踐和理論沒有差異。實踐上講,二者確有差異」,可爲旁證。
binary search的這個錯誤一樣會出如今其它「分而治之」的算法裏,好比說mergesort。若是你有相似的算法代碼,趕快修改吧。Joshua說,他從中學到的教訓是謙卑:哪怕一個簡單的程序都很難寫對,而整個社會卻運行在龐大而複雜的代碼上面。
最後的總結頗有意思:咱們程序員須要各類幫助,別無它法。仔細設計很好。測試很好。形式化方法很好(不過我仍是以爲有教授研究用形式化電子商務需求(好比用範疇論),純粹無事找事)。代碼評審很好,靜態分析很好。但他們並不能幫咱們完全消除代碼錯誤--他們將永遠存在。咱們半個世紀以來不遺餘力都不能消除一個程序錯誤。咱們必須當心翼翼,防護性地編程,而且保持警醒。