淺談二分查詢框架+劍指 Offer 53 - I (C++)
二分查詢目標分類及其框架
以下討論的場景的前提都包括:給定一個正向排序的陣列vector<int> nums
和給定需要求取的目標值target
,來進行討論。
查詢數值的下標位置
因為二分查詢的目標是確定某個具體下標,所以每次取中值mid
後,只要其對應的數值與target
相等就可以進行函式的返回。如果在迴圈退出後,仍然沒有查詢到確定值,就可以判定未不存在合理解。
因此我們考慮使用閉口區間進行while迴圈的篩選,當mid值不等於target時,左邊界變動未left = mid + 1
right = mid - 1
。while(left <= right)
迴圈退出的條件即為left = right - 1
。
基本框架如下:
// 給定vector陣列 // 給定目標值 vector<int> nums{ ... }; int target; int left = 0; // right邊界初始化為末尾元素 int right = nums.size() - 1; while (left <= right) { int mid = left + (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;
查詢目標值的左邊界
而搜尋左邊界我們使用的迴圈終止條件是半開半閉區間。其大致框架如下:
// 給定vector陣列 // 給定目標值 vector<int> nums{ ... }; int target; int left = 0; // right邊界初始化為陣列長度 int right = nums.size(); while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] == target) right = mid; else if (nums[mid] < target) left = mid + 1; else if (nums[mid] > target) right = mid; }
與具體下標的程式碼框架有所不同,重點在於對於nums[mid] == target
的處理。
if (nums[mid] == target)
right = mid;
當找到目標值並沒有立刻退出迴圈返回下標,而是縮小搜尋區間的上界right
,在區間[left, mid)
中繼續搜尋,不斷向左收縮,達到鎖定左側邊界的目的。
並且同時右邊界也沒有向之前一樣修改為right = mid - 1
,而是:
if (nums[mid] > target)
right = mid;
造成右邊界變換方式改變的原因,主要是搜尋區間由閉區間轉變成左閉右開區間的緣故:
- 閉區間
[left, right]
討論完mid
後,下一步搜尋空間去掉mid
並分割成[left, mid-1]
和[mid+1, right]
- 左閉右開區間討論完
mid
後,剩下的區間應該按照迴圈條件的格式分割成分割成[left, mid)
和[mid+1, right)
。
另外,關於右邊界初始化的細節:
int left = 0, right = nums.size();
不同於求具體下標的題目,將右邊界初始為right = nums.size()-1
,即末位陣列元素。其實也是跟搜尋區間有關:
- 對於閉區間
[lef, right]
,如果取陣列長度為下標回越界出錯。 - 而對於
[left, right)
,右側開區間取不到,可以嘗試此型別的初始化。
迴圈退出的條件為left == right
,所以函式只會在退出迴圈後進行函式值的返回。我們需要對返回的left
或者right
進行討論和篩選,錯誤的情況直接返回-1。
對於上圖的情況,該演算法回返回1。可以理解成nums中小於2的元素有1個。
-
對於
target
小於有序陣列所有元素的情況,比如對於有序陣列nums = [2,3,5,7]
,target = 1
,演算法會返回 0,含義 是:nums 中⼩於 1 的元素有 0 個。 -
對於
target
大於有序陣列所有元素的情況,比如說nums = [2,3,5,7]
,target = 8
,演算法會返回 4,含義是: nums 中⼩於 8 的元素有 4 個。
因此對於循壞外的篩選程式碼如下:
if (left == nums.size()) return -1;
if (nums[left] != target) return -1;
return left;
通過上述討論,我們知道退出迴圈後left
可能有以下幾種情況:為0也可能為nums.size()
:
left == 0
,說明陣列中沒有比target小的元素。但同時我們也要篩選掉目標值target
就在下標為0的情況,所以我們需要對下標left
處的元素值進行篩選。if (nums[left] != target)
,那麼二分查詢就是失敗的,否則函式就返回下標值。left == nums.size()
,說明陣列中沒有比target大的元素。二分查詢失敗,如果不經過此步驟討論直接進行left
下標對應元素值的判斷,可能回出現數組越界的段錯誤,所以上述兩句篩選缺一不可,並且程式碼順序也不可更改。
查詢目標值的右邊界
與查詢左邊界類似,程式碼框架如下:
// 給定vector陣列
// 給定目標值
vector<int> nums{ ... };
int target;
int left = 0;
// right邊界初始化為陣列長度
int right = nums.size();
while (left < right) {
int mid = left + (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 (nums[mid] == target)
left = mid + 1;
當nums[mid] == target
時不要立即返回,而是增大搜索區間的下界left
,使得區間不斷向右收縮,達到鎖定右側邊界的目的。
同樣,對於迴圈終止時的left == right
,我們也要進行篩選,對於二分查詢失敗的情況返回-1。
對於上圖的情況,該演算法回返回3。可以理解成nums中小於等於target
2的元素有3個。
-
對於
target
小於有序陣列所有元素的情況,比如對於有序陣列nums = [2,3,5,7]
,target = 1
,演算法會返回 0,含義 是:nums 中⼩於等於target
1 的元素有 0 個。 -
對於
target
大於有序陣列所有元素的情況,比如說nums = [2,3,5,7]
,target = 8
,演算法會返回 4,含義是: nums 中小於等於target
8 的元素有 4 個。
if (left == 0) return -1;
if (nums[left - 1] != target) return -1;
return left - 1;
不要跟左邊界的區別弄混,雖然兩者都涉及到0下標和陣列長度下標的討論。由於我們對於下標nums的理解,查詢左邊界時,確定的下標時target的首位元素位置,而查詢右邊界時,確定的下標是末位target元素位置的後一位!!!。因此對於查詢右邊界的情況,如果返回的下標是0,可以肯定二分查詢失敗了。而取右邊界時,返回下標前的值是小於等於target的,所以需要討論前一位nums[left - 1]
的值是否等於target
。
例題:劍指 Offer 53 - I. 在排序陣列中查詢數字 I
題目描述
統計一個數字在排序陣列中出現的次數。
示例 1:
輸入: nums = [5,7,7,8,8,10], target = 8
輸出: 2
示例 2:
輸入: nums = [5,7,7,8,8,10], target = 6
輸出: 0
限制:
- 0 <= 陣列長度 <= 50000
題解
class Solution {
public:
int search(vector<int>& nums, int target) {
// 先尋找左邊界
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (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.size()) return 0;
// 篩選左右指標重合到0位置,但對映元素不為目標元素的情況
if (nums[left] != target) return 0;
// 開區間尋找左邊界是可取的目標值
// 開區間尋找有邊界是不可取的非目標值
int leftBorder = left;
right = nums.size();
while (left < right) {
int mid = left + (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;
}
int rightBorder = left;
return (rightBorder - leftBorder);
}
};