數據結構和演算法躬行記(4)——二分查找
- 2020 年 9 月 15 日
- 筆記
- 數據結構和演算法躬行記
二分查找(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; //注意 }
本文的這些示例都沒有經過大量測試用例的嚴謹驗證,歡迎指正其中的潛在錯誤。