號外,號外 -幾乎全部的binary search和mergesort都有錯

號外,號外 -幾乎全部的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說,他從中學到的教訓是謙卑:哪怕一個簡單的程序都很難寫對,而整個社會卻運行在龐大而複雜的代碼上面。

最後的總結頗有意思:咱們程序員須要各類幫助,別無它法。仔細設計很好。測試很好。形式化方法很好(不過我仍是以爲有教授研究用形式化電子商務需求(好比用範疇論),純粹無事找事)。代碼評審很好,靜態分析很好。但他們並不能幫咱們完全消除代碼錯誤--他們將永遠存在。咱們半個世紀以來不遺餘力都不能消除一個程序錯誤。咱們必須當心翼翼,防護性地編程,而且保持警醒。

相關文章
相關標籤/搜索