1. 程式人生 > >資料結構和演算法躬行記(4)——二分查詢

資料結構和演算法躬行記(4)——二分查詢

  二分查詢(Binary Search)是對一種針對有序資料集合的查詢演算法,依賴陣列,適合靜態資料。通過 n/2^k=1(k 是比較次數),可以求得 k=log2^n,因此時間複雜度為高效地 O(logn)。

  其思路很簡單,就是每次與區間的中間資料做比較,縮小查詢範圍,但是期間涉及到的細節很容易踩坑,例如比較時是否帶等號、mid值是否要加一等。例題:704. 二分查詢。

  LeetCode的69. x 的平方根,x=sqrt(y); y=x^2,由於y和x是單調遞增的,因此可用二分查詢,每次取中值,不斷逼近。另一種解題思路是牛頓迭代法。

  在《劍指offer》一書中曾提到,寫出高質量的程式碼需要了解3個方面:

  (1)規範性,書寫清晰、佈局清晰和命名合理。

  (2)完整性,完成基本功能、考慮邊界條件、做好錯誤處理。

  (3)魯棒性,採取防禦性程式設計、處理無效的輸入。

  下面是二分查詢最基本的程式碼實現,改編自《二分搜尋》,為了防止mid的溢位,才設計成了 low + Math.floor((high - low) / 2)。

function binarySearch(nums, target) {
  let low = 0, high = ...;
  while(...) {
    let mid = low + ((high - low) >> 1);
    if (nums[mid] == target) {
      ...
    } else if (nums[mid] < target) {
      low = ...
    } else if (nums[mid] > target) {
      high = ...
    }
  }
  return ...;
}

  其中佔位符“...”處就是容易出錯的細節部分:

  (1)迴圈退出條件。

  (2)low 和 high 的更新。

  (3)找到目標值時的處理。

  (4)函式的返回值。

一、無重複資料

  在下面的binarySearch()函式中,nums是一個無重複資料的陣列,需要注意:

  (1)while迴圈的判斷條件是小於等於,因為high的取值是末尾索引,相當於兩端閉區間 [low, high]。

  (2)當不匹配時,縮小查詢區間,分成兩塊閉區間:[mid+1, high] 或 [low, mid-1]。

function binarySearch(nums, target) {
  let low = 0,
    high = nums.length - 1;      //注意
  while(low <= high) {           //注意
    let mid = low + ((high - low) >> 1);
    if (nums[mid] == target) {
      return mid;               //注意
    } else if (nums[mid] < target) {
      low = mid + 1;            //注意
    } else if (nums[mid] > target) {
      high = mid - 1;           //注意
    }
  }
  return -1;                    //注意
}

  面試題11 旋轉陣列的最小數字。二分查詢的思路,用兩個指標指向陣列的第一個和最後一個元素,如果中間元素位於前面的遞增子陣列,那麼它應該大於或等於第一個指標指向的元素。

  面試題53 在排序陣列中查詢數字。用二分查詢鎖定指定值,然後在左右兩邊順序掃描,直至找出第一個和最後一個指定值。延伸題:0~n-1 中缺失的數字。

二、含重複資料

  當查詢的陣列中包含重複資料時,二分查詢需要做些處理。

  面試題3 不修改陣列找出重複數字。二分查詢思想,從中間數字m分成兩部分,1~m和m+1~n,如果1~m的數字超過m,那麼區間有重複數字。

1)查詢第一個等於給定值的元素

  在下面的binarySearchLow()函式中,需要注意:

  (1)while迴圈的判斷條件是小於,因為 low 和 high 相等會陷入死迴圈。

  (2)查詢到匹配的資料不是立即返回,而是縮小範圍,並且增加一次 high 的邊界值判斷。

  (3)在跳出迴圈後,需要最終判斷是否與目標值匹配。

function binarySearchLow(nums, target) {
  let low = 0,
    high = nums.length - 1;
  while(low < high) {                //注意
    let mid = low + ((high - low) >> 1);
    if (nums[mid] == target) {
      high = mid - 1;                //注意
      if (nums[high] != target)      //注意
        return mid;                  //注意
    } else if (nums[mid] < target) {
      low = mid + 1;
    } else if (nums[mid] > target) {
      high = mid - 1;
    }
  }
  return nums[low] == target ? low : -1;    //注意
}

  另一個類似的問題是查詢最後一個等於給定值的元素,修改匹配時的範圍,如下所示。

function binarySearchHigh(nums, target) {
  let low = 0,
    high = nums.length - 1;
  while(low < high) {
    let mid = low + ((high - low) >> 1);
    if (nums[mid] == target) {
      low = mid + 1;                //注意
      if (nums[low] != target)      //注意
        return mid;                 //注意
    } else if (nums[mid] < target) {
      low = mid + 1;
    } else if (nums[mid] > target) {
      high = mid - 1;
    }
  }
  return nums[low] == target ? low : -1;
}

2)查詢第一個大於等於給定值的元素

  在下面的binarySearchLow()函式中,需要注意:

  (1)修改匹配給定值的條件,變為大於等於。

  (2)額外匹配一次 high 的邊界值,成功時直接返回 mid。

  (3)修改跳出迴圈後的匹配條件。

function binarySearchLow(nums, target) {
  let low = 0,
    high = nums.length - 1;
  while(low < high) {
    let mid = low + ((high - low) >> 1);
    if (nums[mid] >= target) {       //注意
      high = mid - 1;                //注意
      if (nums[high] < target)       //注意
        return mid;                  //注意
    } else if (nums[mid] < target) {
      low = mid + 1;
    }
  }
  return nums[low] >= target ? low : -1;    //注意
}

  另一個類似的問題是查詢最後一個小於等於給定值的元素,修改匹配時的範圍,如下所示。

function binarySearchHigh(nums, target) {
  let low = 0,
    high = nums.length - 1;
  while(low < high) {
    let mid = low + ((high - low) >> 1);
    if (nums[mid] <= target) {        //注意
      low = mid + 1;                  //注意
      if (nums[low] > target)         //注意
        return mid;                   //注意
    } else if (nums[mid] > target) {
      high = mid - 1;
    }
  }
  return nums[low] <= target ? low : -1;    //注意
}

  本文的這些示例都沒有經過大量測試用例的嚴謹驗證,歡迎指正其中的潛在錯誤。

&n