二分查找 : 那個隱藏了 10 年的 Java Bug
因為它很好寫,卻很難寫對。可以想象問了這道題後,在5分鐘之內面試的同學會相當自信的將那一小段代碼交給我們,剩下的就是考驗面試官能否在更短的時間內看出這段代碼的bug了。
二分查找是什麽呢,這個不只程序員,其他很多非技術人員也會。比如我想一個1到100以內的數,你來猜,我告訴你每次猜的是大了還是小了,你會先猜50,然後25, 然後。。。用不了幾個問題就猜出來了。1到100範圍太小的話,我們放大點猜個人名,你問中國人外國人,古代人現代人,男的女的,用不了幾個問題也問出來了。在計算機裏,則是在一個有序數組裏面,不斷通過二分的方法縮小關鍵字的可能下標範圍。當然了,我們不一定在一個有序數組裏查找,也可以在一個很大的狀態空間裏,去查找一個單調函數的取值。這樣的做法,似乎編個程序很容易實現,但是,
D.Knuth大神說了:Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky 雖然二分查找的基本思想相對來說很直接,但具體實現起來有特別多的坑。
另一位大神,編程珠璣的作者Jon Bentley,他做了我們在文章開頭不敢做的事,他布置作業讓他的學生們寫二分查找,然後他一個個來看。結果呢,他發現90%是錯的。因此在他的編程珠璣這本書中,專門有一章講解了二分查找,雖然他的範例仍然是錯的,見下面的Java Bug。埋下這個bug的人,也正式Jon Bentley的學生。
還有好事者,更是找了許多教科書,發現20本教科書裏面,只有5本是寫對了的,於是他發了一篇文章到ACM。當然這是早在1988年的時候。
然而這些都不算啥,更能讓人感覺幸災樂禍的是,Java庫裏面的二分查找,有一個埋藏了10年之久的bug。這個bug呢,在 java.util.Arrays.binarySearch 裏面,雖然這個bug的修復也已經是10年前的事了。那麽我們來看下當年的錯誤代碼吧。
大家可能很難看出來,那畢竟這個bug藏了10年,不太容易發現。問題就在於
1
int mid = (low + high) / 2;
這裏。low + high 是會溢出的。只要這個數組我們開的足夠大,比如1100000000,就能重現這個問題,雖然這需要我們費點內存。因此正確的解法是:int mid = (low + high) >>> 1; 三個>,無符號位移的意思。正如修復bug的同學說的那樣:
1
"Can‘t even compute average of two ints" is pretty embarrassing.
這個bug的鏈接在這裏。
那麽我們究竟如何來把二分查找寫正確呢?我們二分查找中常見的錯誤除了上面的溢出之外,最多的是下面幾類:
差1錯誤。我們的左端點應該是當前可能區間的最小範圍,那麽右端點是最大範圍呢,還是最大範圍+1呢。我們取了中間值之後,在縮小區間時,有沒有保持左右端點的這個假設的一致性呢?
死循環。我們做的是整數運算,整除2了之後,對於奇數和偶數的行為還不一樣,很有可能有些情況下我們並沒有減小取值範圍,而形成死循環。
退出條件。到底什麽時候我們才覺得我們找不到呢?
我很喜歡二分查找這個案例。一個在理論上這麽簡單直接的算法,在計算機裏實現卻要考慮那麽多實際的情況,除了理論的細化比如差1錯誤和退出條件,還有計算機的實際問題如整除2,死循環,以及上面提到的溢出。正如我們以前同事每天掛在嘴邊的
You know the difference between in theory and in practice? In theory there’s no difference but in practice there are.
軟件工程師,就是把現實生活用理論進行建模,然後再用現實來實現這樣的理論。寫出好的代碼不容易,寫出既讓用戶滿意又好的代碼,那更不容易。也許有時候,成就感就來自於此吧。
歡迎工作一到五年的Java工程師朋友們加入Java架構師:697558955
群內提供免費的Java架構學習資料(裏面有高可用、高並發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
二分查找 : 那個隱藏了 10 年的 Java Bug