【數據結構與算法之美】二分查找(上):如何用最省內存的方式實現快速查找功能?

一、什麼是二分查找?

二分查找針對的是一個有序的數據集合,每次通過跟區間中間的元素對比,將待查找的區間縮小爲之前的一半,直到找到要查找的元素,或者區間縮小爲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 bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;

  while (low <= high) {
    int mid = (low + high) / 2;
    if (a[mid] == value) {
      return mid;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }

  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 bsearch(int[] a, int n, int val) {
  return bsearchInternally(a, 0, n - 1, val);
}

private int bsearchInternally(int[] a, int low, int high, int value) {
  if (low > high) return -1;

  int mid =  low + ((high - low) >> 1);
  if (a[mid] == value) {
    return mid;
  } else if (a[mid] < value) {
    return bsearchInternally(a, mid+1, high, value);
  } else {
    return bsearchInternally(a, low, mid-1, value);
  }
}

四、使用條件(應用場景的侷限性)

1.二分查找依賴的是順序表結構,即數組。
2.二分查找針對的是有序數據,因此只能用在插入、刪除操作不頻繁,一次排序多次查找的場景中。
3.數據量太小不適合二分查找,與直接遍歷相比效率提升不明顯。但有一個例外,就是數據之間的比較操作非常費時,比如數組中存儲的都是長度超過300的字符串,那這是還是儘量減少比較操作使用二分查找吧。
4.數據量太大也不是適合用二分查找,因爲數組需要連續的空間,若數據量太大,往往找不到存儲如此大規模數據的連續內存空間。

五、課後思考

1.如何在1000萬個整數中快速查找某個整數?

①1000萬個整數佔用存儲空間爲40MB,佔用空間不大,所以可以全部加載到內存中進行處理;
②用一個1000萬個元素的數組存儲,然後使用快排進行升序排序,時間複雜度爲O(nlogn)
③在有序數組中使用二分查找算法進行查找,時間複雜度爲O(logn)

2.如何編程實現“求一個數的平方根”?要求精確到小數點後6位?

/**
 *   使用二分查找實現平方根函數,要求精確到小數點後6位
 */
 public float sqrt_search(float n){
     float mid = 0.0f;
     if(n < -1e-6){
         // 小於0,拋異常
         throw new IllegalArgumentException();
     }else if(Math.abs(n) >= -1e-6 && Math.abs(n) <= 1e-6){
         return mid;
     }else{
        // 逐次逼近,默認平方根的不會超過n的一半
         float high = n / 2.0f;
         float low = 0.0f;
         while(Math.abs(high - low) > 1e-6){
             // 首先找到中間值
             mid = low + (high - low) / 2;
             float tmp = mid * mid;
             // 比較並更新 high和low
             if((tmp - n) > 1e-6){
                 high = mid;
             }else if((tmp -n) < -1e-6){
                 low = mid;
             }else{
                 return mid;
             }
         }
     }
     return mid;
 }

 

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