二分查詢演算法詳解
阿新 • • 發佈:2021-07-21
一、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) { ... }
- 為什麼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 的右側邊界,```sh
即索引 3,這樣的話此演算法是無法處理的。
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:
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;
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]