10. 二分查找(上) 如何用最省內存的方式實現快速查找功能

一、什麼是二分查找?
二分查找針對的是一個有序的數據集合,每次通過跟區間中間的元素對比,將待查找的區間縮小爲之前的一半,直到找到要查找的元素,或者區間縮小爲0。
二、時間複雜度分析?
1.時間複雜度
假設數據大小是n,每次查找後數據都會縮小爲原來的一半,最壞的情況下,直到查找區間被縮小爲空,才停止。所以,每次查找的數據大小是:n,n/2,n/4,…,n/(2^k),…,這是一個等比數列。當n/(2^k)=1時,k的值就是總共縮小的次數,也是查找的總次數。而每次縮小操作只涉及兩個數據的大小比較,所以,經過k次區間縮小操作,時間複雜度就是O(k)。通過n/(2^k)=1,可求得k=log2n,所以時間複雜度是O(logn)。
2.認識O(logn)
①這是一種極其高效的時間複雜度,有時甚至比O(1)的算法還要高效。爲什麼?
②因爲logn是一個非常“恐怖“的數量級,即便n非常大,對應的logn也很小。比如n等於2的32次方,也就是42億,而logn才32。
③由此可見,O(logn)有時就是比O(1000),O(10000)快很多。
三、如何實現二分查找?
1.循環實現
代碼實現:

public int binarySearch1(int[] a, int val){
    int start = 0;
    int end = a.length - 1;
    while(start <= end){
        int mid = start + (end - start) / 2;
        if(a[mid] > val) end = mid - 1;
        else if(a[mid] < val) start = mid + 1;
        else return mid;
    }
    return -1;
}


注意事項:
循環退出條件是:start<=end,而不是start<end。
mid的取值,使用mid=start + (end - start) / 2,而不用mid=(start + end)/2,因爲如果start和end比較大的話,求和可能會發生int類型的值超出最大範圍。爲了把性能優化到極致,可以將除以2轉換成位運算,即start + ((end - start) >> 1),因爲相比除法運算來說,計算機處理位運算要快得多。
start和end的更新:start = mid - 1,end = mid + 1,若直接寫成start = mid,end=mid,就可能會發生死循環。
2.遞歸實現

public int binarySearch(int[] a, int val){
    return bSear(a, val, 0, a.length-1);
}
private int bSear(int[] a, int val, int start, int end) {
    if(start > end) return -1;
    int mid = start + (end - start) / 2;
    if(a[mid] == val) return mid;
    else if(a[mid] > val) end = mid - 1;
    else start = mid + 1;
    return bSear(a, val, start, end);
}

四、使用條件(應用場景的侷限性)
1.二分查找依賴的是順序表結構,即數組。
2.二分查找針對的是有序數據,因此只能用在插入、刪除操作不頻繁,一次排序多次查找的場景中。
3.數據量太小不適合二分查找,與直接遍歷相比效率提升不明顯。但有一個例外,就是數據之間的比較操作非常費時,比如數組中存儲的都是長度超過300的字符串,那這是還是儘量減少比較操作使用二分查找吧。
4.數據量太大也不是適合用二分查找,因爲數組需要連續的空間,若數據量太大,往往找不到存儲如此大規模數據的連續內存空間。
五、思考
1.如何在1000萬個整數中快速查找某個整數?
①1000萬個整數佔用存儲空間爲40MB,佔用空間不大,所以可以全部加載到內存中進行處理;
②用一個1000萬個元素的數組存儲,然後使用快排進行升序排序,時間複雜度爲O(nlogn)
③在有序數組中使用二分查找算法進行查找,時間複雜度爲O(logn)
2.如何編程實現“求一個數的平方根”?要求精確到小數點後6位?

public static double sqrt(double x, double precision) {
if (x < 0) {
return Double.NaN;
}
double low = 0;
double up = x;
if (x < 1 && x > 0) {
/** 小於1的時候*/
low = x;
up = 1;
}
double mid = low + (up - low)/2;
while(up - low > precision) {
if (mid * mid > x ) {//TODO mid可能會溢出
up = mid;
} else if (mid * mid < x) {
low = mid;
} else {
return mid;
}
mid = low + (up - low)/2;
}
return mid;
} 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章