1. 程式人生 > 其它 >二分查詢演算法詳解

二分查詢演算法詳解

一、binarySearch框架
  • 陣列必須升序排列;
  • 不要出現else,而是把所有情況用elseif寫清楚,這樣可以清楚地展現所有細節;
  • 計算mid時需要技巧防止溢位:letmid=left+Math.floor((right-left)/2);
    function binarySearch(nums, target) {
        let left = 0, right = ...;
        while(...) {
            let mid = (right + left) / 2;
            if (nums[mid] == target) {
                ...
            } 
    else if (nums[mid] < target) { left = ... } else if (nums[mid] > target) { right = ... } } return ...; }
二、Note:搜尋一個數,如果存在,返回其索引,否則返回-1.
  • 為什麼while迴圈的條件中是<=,而不是<?
因為初始化 right 的賦值是 nums.length - 1,即最後一個元素的索引,而不是 nums.length;
  • while迴圈當找到目標值就停止搜尋,返回索引;但是當while迴圈結束都沒有找到的時候,就返回-1,那什麼時候while迴圈終止呢?
  • [left,right]終止條件是left==right+1,即[right+1,right],帶個具體的數字進去[3,2],可見這時候搜尋區間為空,因為沒有數字既大於等於3又小於等於2的吧。所以這時候while迴圈終止是正確的,直接返回-1即可;
  • [left,right)終止條件是left==right,即[left,right],帶個具體的數字進去[2,2],這時候搜尋區間非空,還有一個數2,但此時while迴圈終止了。也就是說這區間[2,2]被漏掉了,索引2沒有被搜尋,如果這時候直接返回-1就可能出現錯誤,所以打個補丁:最後一行return語句變為returnnums[left]===target?left:-1,且right的變化應該為right=mid,因為我們的「搜尋區間」是[left,right)左閉右開,所以當nums[mid]被檢測之後,下一步的搜尋區間應該去掉mid分割成兩個區間,即[left,mid)或[mid+1,right)
  • 為什麼left=mid+1,right=mid-1?我看有的程式碼是right=mid或者left=mid,沒有這些加加減減,到底怎麼回事,怎麼判斷?
這也是二分查詢的一個難點,本演算法的「搜尋區間」是兩端都閉的,即 [left, right]。那麼當我們發現索引 mid 對應的值不是要找的 target 時,如何確定下一步的搜尋區間呢?
  當然是去搜索 [left, mid - 1] 或者 [mid + 1, right] 對不對?因為 mid 已經搜尋過,應該從搜尋區間中去除
  • 此演算法有什麼缺陷?
比如說給你有序陣列 nums = [1,2,2,2,3],target = 2,此演算法返回的索引是 2,沒錯。但是如果我想得到 target 的左側邊界,即索引 1,或者我想得到 target 的右側邊界,
即索引 3,這樣的話此演算法是無法處理的。
```sh
function binarySearch(nums, target) {
  let left = 0;
  let right = nums.length - 1;
  while (left <= right) {
    let mid = left + Math.floor((right - left) / 2);
    if (nums[mid] === target) {
      return mid;
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid - 1;
    }
  }
  return -1;
}
console.log(binarySearch([1, 2, 3, 4, 5, 6, 7], 3));//2
``` 三、 Note:搜尋一個數,如果存在,返回其第一次出現時的索引,否則返回-1.
  • 比如對於有序陣列nums=[2,3,5,7],target=1,演算法會返回0,含義是:nums中小於1的元素有0個。如果target=8,演算法會返回4,含義是:nums中小於8的元素有4個。綜上可以看出,函式的返回值(即left變數的值)取值區間是閉區間[0,nums.length],所以我們簡單新增兩行程式碼就能在正確的時候return-1:
```sh
function binarySearchLeft(nums, target) {
  if (nums.length == 0) return -1;
  let left = 0;
  let right = nums.length;
  while (left < right) {
    let mid = left + Math.floor((right - left) / 2);
    if (nums[mid] === target) {
      right = mid;
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid;
    }
  }
  if (left == nums.length) return -1;
  return nums[left] === target ? left : -1;
}
console.log(binarySearchLeft([1, 2, 2, 2, 3], 2));//1
``` 四、 Note:搜尋一個數,如果存在,返回其最後一次出現時的索引,否則返回-1.
  • 當nums[mid]==target時,不要立即返回,而是增大「搜尋區間」的下界left,使得區間不斷向右收縮,達到鎖定右側邊界的目的;
  • 因為我們對left的更新必須是left=mid+1,就是說while迴圈結束時,nums[left]一定不等於target了,而nums[left-1]可能是target;
```sh
function binarySearchRight(nums, target) {
  if (nums.length == 0) return -1;
  let left = 0;
  let right = nums.length;
  while (left < right) {
    let mid = left + Math.floor((right - left) / 2);
    if (nums[mid] === target) {
      left = mid + 1;
    } else if (nums[mid] < target) {
      left = mid + 1;
    } else if (nums[mid] > target) {
      right = mid;
    }
  }
  if (left === 0) return -1;
  return nums[left - 1] === target ? left - 1 : -1;
}
console.log(binarySearchRight([1, 2, 2, 2, 3], 2)); //3
``` 五、總結
  • 一個基本的二分查詢
    因為我們初始化 right = nums.length - 1
    所以決定了我們的「搜尋區間」是 [left, right]
    所以決定了 while (left <= right)
    同時也決定了 left = mid+1 和 right = mid-1
    因為我們只需找到一個 target 的索引即可
    所以當 nums[mid] == target 時可以立即返回
  • 尋找左側邊界的二分查詢
    因為我們初始化 right = nums.length
    所以決定了我們的「搜尋區間」是 [left, right)
    所以決定了 while (left < right)
    同時也決定了 left = mid+1 和 right = mid
    
    
    因為我們需找到 target 的最左側索引
    所以當 nums[mid] == target 時不要立即返回
    而要收緊右側邊界以鎖定左側邊界
  • 尋找右側邊界的二分查詢
因為我們初始化 right = nums.length
所以決定了我們的「搜尋區間」是 [left, right)
所以決定了 while (left < right)
同時也決定了 left = mid+1 和 right = mid


因為我們需找到 target 的最右側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊左側邊界以鎖定右側邊界


又因為收緊左側邊界時必須 left = mid + 1
所以最後無論返回 left 還是 right,必須減一
六、給定一個按照升序排列的整數陣列nums,和一個目標值target。找出給定目標值在陣列中的開始位置和結束位置。如果陣列中不存在目標值target,返回[-1,-1]。
  • 方法一:使用上面寫好的api
    var searchRange1 = function (nums, target) {
      // let left = nums.indexOf(target);
      // let right = nums.lastIndexOf(target);
      // return [left, right];
      let left = binarySearchLeft(nums, target);
      let right = binarySearchRight(nums, target);
      return [left, right];
    };
  • 方法二、封裝一個統一的api
    var binarySearch = function (nums, target, flag) {
      if (nums.length == 0) return -1;
      let left = 0;
      let right = nums.length;
      while (left < right) {
        let mid = left + Math.floor((right - left) / 2);
        if (nums[mid] === target) {
          flag ? (right = mid) : (left = mid + 1);
        } else if (nums[mid] < target) {
          left = mid + 1;
        } else if (nums[mid] > target) {
          right = mid;
        }
      }
      if (flag && left === nums.length) return -1;
      if (!flag && left === 0) return -1;
      return flag
        ? nums[left] === target
          ? left
          : -1
        : nums[left - 1] === target
        ? left - 1
        : -1;
    };
    var searchRange2 = function (nums, target) {
      let len = nums.length;
      if (len === 0) {
        return [-1, -1];
      }
      let left = binarySearch(nums, target, true);
      if (left === -1) {
        return [-1, -1];
      }
      let right = binarySearch(nums, target, false);
      return [left, right];
    };
    // console.log(searchRange2([5, 7, 7, 8, 8, 10], 8)); //[3,4]
北梔女孩兒