資料結構和演算法躬行記(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