一位大佬曾經說過:二分查找思路很簡單,細節是魔鬼
做了一些題後感慨真的是這樣…
二分查找法:實質就是將一個有序的數據集不斷地對半分割,直至找到目標值;其中兩個最關鍵的也是在面對不同題目會稍有不同的就是怎麼退出循環,怎麼縮小查找空間。怎麼突破這兩點就是關鍵
在一個有序數據集裏查找一個一個目標數可以有一下兩種寫法
int Search(vector<int>& nums, int target) {
int left = 0;
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;
}
int Search(vector<int>& nums, int target) {
int left = 0;
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 nums[left] == target ? left : -1;
}
先來看第一個,它退出循環的條件是left <= right
,表示當left == right
成立的時候,還有一個元素,即 left(right)位置的元素還沒有看到,需要繼續查看這個元素的值,看看是不是我們想要的。第二個是退出循環的時候left == right
成立。此時left (right) 這個位置的值可能程序還沒有讀取到,因此“有可能”需要再對 left(right) 這個位置的值是否是目標元素的值做一次判斷。這是考慮循環退出條件的思路,接下來看具體怎麼縮小比較空間
對於思考怎麼縮小比較空間我們就想什麼時候不是解,當中間這個元素是大於target是那中間這個元素肯定也不是結果值,所以就left = mid + 1
;
上面代碼是針對一旦找到目標值就返回的情況,那如果是要找目標值的左側分界呢(有對個目標值),來屢一下分析思路;當數組裏的元素都大於目標值是應該返回0,當有序數組的元素都小於目標值的時候左邊界應該爲n(數組元素個數),可以看出返回值的可能區間是[0, nums.size()]
,所以要設right的起始值是nums,size(),但因爲不能訪問nums[n],所以搜索空間爲[left, right)
左閉右開,再接着想退出條件,就是[left, left)
,循環退出條件可寫成while(left < right)
,因爲找的是左邊界所以在得到nums[mid] == target
這種情況時就要繼續往左找
if (nums[mid] == target)
right = mid;
在區間[left, mid)
中繼續搜索,即不斷向左收縮,達到鎖定左側邊界的目的。所以完整代碼就如下
int left_bound(vector<int>& nums, int target) {
if (nums.size() == 0) return -1;
int left = 0;
int right = nums.size();
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else{
right = mid;
}
}
return left;
}
來總結下怎麼推敲具體的細節
- 確定搜索區間初始化時候的左右邊界,有時需要關注一下邊界值。在初始化時,有時把搜索區間設置大一點沒有關係,但是如果恰好把邊界值排除在外,再怎麼搜索都得不到結果
- 確定while裏的條件,如果right其實是size的話當left==right的時候就應該立馬退出,所以循環條件是while (left < right)
- 思考怎麼縮小比較空間就想什麼時候不是解
下面來列出幾道題根據上面的思路來練練腦
1.計算並返回 x 的平方根,其中 x 是非負整數
class Solution {
public:
int mySqrt(int x) {
if(x == 0) return 0;
long long start = 1;
long long end = x/2;
while(start < end){
long long mid = (end+start+1)>>1;
long long res = mid*mid;
if(res > x)
end = mid-1;
else{
start = mid;
}
}
return start;
}
};
這個情況就相當於查找右邊界
2.假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
注意數組中可能存在重複的元素
class Solution {
public:
int findMin(vector<int>& nums) {
int size = nums.size();
if(size == 0) return 0;
int left = 0;
int right = size-1;
while(left < right){
int mid = (left + right)/2;
if(nums[mid] == nums[right]) right--;
else if(nums[mid] > nums[right])
left = mid + 1;
else
right = mid;
}
return nums[right];
}
};
**
3.峯值元素是指其值大於左右相鄰值的元素。給定一個輸入數組 nums,其中 nums[i] ≠ nums[i+1],找到峯值元素並返回其索引。數組可能包含多個峯值,在這種情況下,返回任何一個峯值所在位置即可。
**
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] <= nums[mid + 1])
l = mid + 1;
else
r = mid;
}
return l;
}
};
4.給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans;
ans.push_back(left_bound(nums, target));
ans.push_back(right_bound(nums, target));
return ans;
}
int left_bound(vector<int> &nums, int target) {
if (nums.size() == 0) return -1;
int i = 0, j = nums.size(), mid = -1;
while (i < j) {
//左半區間[i, mid) 右半區間[mid+1, j)
mid = i + (j - i) / 2; //中間位置
if (nums[mid] == target) {
j = mid;
}
else if (nums[mid] > target) { //左半區間 [i, mid)
j = mid;
}
else if (nums[mid] < target) { //右半區間 [mid+1, j)
i = mid + 1;
}
}
if (i == nums.size()) return -1;
else return (nums[i] == target) ? i : -1;
}
int right_bound(vector<int> &nums, int target) {
if (nums.size() == 0) return -1;
int i = 0, j = nums.size(), mid = -1; //搜索區間是左閉右開的
while (i < j) {
mid = i + (j - i) / 2;
//左半區間 [i, mid) 右半區間 [mid+1, j)
if (nums[mid] == target) { //收縮下界
i = mid + 1;
}
else if (nums[mid] > target) { //左半區間
j = mid;
}
else if (nums[mid] < target) { //右半區間
i = mid + 1;
}
}
if (j == 0) return -1;
return nums[j - 1] == target ? (j - 1) : -1;
}
};