讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議74:不推薦使用binarySearch對列表進行檢索 >

建議74:不推薦使用binarySearch對列表進行檢索

對一個列表進行檢索時,我們使用得最多的是indexOf方法,它簡單、好用,而且也不會出錯,雖然它只能檢索到第一個符合條件的值,但是我們可以生成子列表後再檢索,這樣也就可以查找出所有符合條件的值了。

Collections工具類也提供一個檢索方法:binarySearch,這個是幹什麼的?該方法也是對一個列表進行檢索的,可查找出指定值的索引值,但是在使用這個方法時就有一些注意事項了,我們看如下代碼:


public static void main(Stringargs){

List<String>cities=new ArrayList<String>();

cities.add(\"上海\");

cities.add(\"廣州\");

cities.add(\"廣州\");

cities.add(\"北京\");

cities.add(\"天津\");

//indexOf方法取得索引值

int index1=cities.indexOf(\"廣州\");

//binarySearch查找到索引值

int index2=Collections.binarySearch(cities,\"廣州\");

System.out.println(\"索引值(indexOf):\"+index1);

System.out.println(\"索引值(binarySearch):\"+index2);

}


先不考慮運行結果,直接看JDK上對binarySearch的描述:使用二分搜索法搜索指定列表,以獲得指定對象。其實現的功能與indexOf是相同的,只是使用的是二分法搜索列表,所以估計兩種方法返回結果是一樣的,看結果:


索引值(indexOf):1

索引值(binarySearch):2


結果不一樣,雖然說我們有兩個「廣州」這樣的元素,但是返回的結果都應該是1才對呀,為何binarySearch返回的結果是2呢?問題就出在二分法搜索上,二分法搜索就是「折半折半再折半」的搜索方法,簡單,而且效率高。看看JDK是如何實現的。


public static<T>int binarySearch(List<?extends Comparable<?super T>>list, T key){

if(list instanceof RandomAccess||list.size()<5000)

//隨機存取列表或者元素數量少於5000的順序存取列表

return Collections.indexedBinarySearch(list, key);

else

//元素數量大於5000的順序存取列表

return Collections.iteratorBinarySearch(list, key);

}


ArrayList實現了RandomAccess接口,是一個順序存取列表,使用了indexdBinarySearch方法,代碼如下:


private static<T>int indexedBinarySearch(List<?extends Comparable<?super T>>

list, T key){

//默認上界

int low=0;

//默認下界

int high=list.size()-1;

while(low<=high){

//中間索引,無符號右移1位

int mid=(low+high)>>>1;

//中間值

Comparable<?super T>midVal=list.get(mid);

//比較中間值

int cmp=midVal.compareTo(key);

//重置上界和下界

if(cmp<0)

low=mid+1;

else if(cmp>0)

high=mid-1;

else

//找到元素

return mid;

}

//沒有找到,返回負值

return-(low+1);

}


這也沒啥說的,就是二分法搜索的Java版實現。注意看加粗字體部分,首先是獲得中間索引值,我們的例子中也就是2,那索引值是2的元素值是多少呢?正好是「廣州」,於是返回索引值2,正確,沒問題。那我們再看看indexOf的實現,代碼如下:


public int indexOf(Object o){

if(o==null){

//null元素查找

for(int i=0;i<size;i++)

if(elementData[i]==null)

return i;

}else{

//非null元素查找

for(int i=0;i<size;i++)

//兩個元素是否相等,注意這裡是equals方法

if(o.equals(elementData[i]))

return i;

}

//沒找到,返回-1

return-1;

}


indexOf方法就是一個遍歷,找到第一個元素值相等則返回,沒什麼玄機。回到我們的程序上來看,for循環的第二遍即是我們要查找的「廣州」,於是就返回索引值1了,也正確,沒有任何問題。

兩者的算法都沒有問題,難道是我們用錯了?這還真是我們的錯,因為二分法查詢的一個首要前提是:數據集已經實現升序排列,否則二分法查找的值是不準確的。不排序怎麼確定是在小區(比中間值小的區域)中查找還是在大區(比中間值大的區域)中查找呢?二分法查找必須要先排序,這是二分法查找的首要條件。

問題清楚了,藐視解決方法也很簡單,使用Collections.sort排下序即可解決。但這樣真的可以解決嗎?想想看,元素數據是從Web或數據庫中傳遞進來的,原本是一個有規則的業務數據,我們為了查找一個元素對它進行了排序,也就是改變了元素在列表中的位置,那誰來保證業務規則此時的正確性呢?所以說,binarySearch方法在此處受限了。當然,拷貝一個數組,然後再排序,再使用binarySeach查找指定值,也可以解決該問題。

使用binarySearch首先要考慮排序問題,這是我們編碼人員經常容易忘記的,而且在測試期間還沒發現問題(因為測試時「製造」的數據都很有規律),等到投入生產系統後才發現查找的數據不準確,又是一個大Bug產生了,從這點來看,indexOf要比binarySearch簡單得多。

當然,使用binarySearch的二分法查找比indexOf的遍歷算法性能上高很多,特別是在大數據集而且目標值又接近尾部時,binarySearch方法與indexOf相比,性能上會提升幾十倍,因此在從性能的角度考慮時可以選擇binarySearch。

注意 從性能方面考慮,binarySearch是最好的選擇。