对于该题,我们首先思考 暴力解法:
此时我们引入一个思考:
代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1; // 左右区间边界
while(left <= right)
{
int mid = left + (right - left + 1) / 2; // 更新 mid
if(nums[mid] < target) // 当前值<目标值 : 更新左边界
left = mid + 1;
else if(nums[mid] > target) // 当前值>目标值 : 更新右边界
right = mid - 1;
else // 找到目标值,返回下标
return mid;
}
return -1; // 未找到,返回-1
}
};
上面的代码很好理解:
当我们理解了二分思想,对于一道题,重点在于分析出该题具有二段性,随后写代码就不是难事了,但这里还是简单写出朴素模板:
while(left <= right)
{
int mid = left + (right - left + 1) / 2;
if(...) // 根据题意左右边界的更新也有所不同
left = mid + 1;
// left = mid;
else if(...)
right = mid - 1;
// right = mid;
else
return ...;
}
这里需要注意的是mid 的更新:
平时我们有mid = (left + right) / 2
写法来进行中间值的更新,这里不提倡这种写法。因为当right和left过大,这种写法会造成整形溢出。
而对于mid = left + (right - left + 1) / 2
与mid = left + (right - left) / 2
,是否+1,我们分为下面的情况
那么我们什么时候采用法①或采用法②?
(right - left + 1)
部分改为(right - left + 1)
。我们通过下面的一道题来理清二分查找的细节处理。
思路
细节问题
首先关于循环条件:使用left < right 而不是 <=。
关于求中点的操作:
我们直接选取一个极端情况:当区间只剩下两个元素时。
左右区间的更新
代码
vector<int> searchRange(vector<int>& nums, int target) {
// 处理边界情况
if(nums.size() == 0) return {-1, -1};
// 二分查找
int left = 0, right = nums.size() - 1;
// 1. 找左区间端点
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] < target) left = mid + 1;
else right = mid;
} // left与right相遇找到左区间端点
int begin = 0;
if(nums[left] != target) return {-1, -1};
else begin = left;
// 2. 找右区间端点
right = nums.size() - 1;
while(left < right)
{
int mid = left + (right - left + 1) / 2;
if(nums[mid] <= target) left = mid;
else right = mid - 1;
}
// int end = right;
return {begin, right};
}
使用条件
二分查找算法必须在有序的序列中才能使用。
二分查找的核心思想是通过比较中间位置的元素与目标元素的大小关系,确定目标元素可能存在的区域。如果数组是无序的,那么就无法保证中间位置的元素与目标元素的大小关系。
时间复杂度
二分查找的每一次迭代中,会将查找区域划分为两个子区域,并通过比较中间位置的元素与目标元素的大小关系,确定目标元素可能存在的区域。这样,每一次迭代都能将查找区域缩小一半。
假设要查找的数组长度为n,每次迭代后查找区域的长度会减少一半,直到找到目标元素或者确定目标元素不存在。因此,最坏情况下,二分查找的迭代次数为 k,满足 n / 2^k = 1。
通过求解上述方程可以得到 k = log2(n),即二分查找的时间复杂度为 O(log n)。
思路
代码
int mySqrt(int x) {
// 处理边界情况
if(x < 1) return 0;
long long left = 0, right = x;
// 二分法
while(left < right)
{
long long mid = left + (right - left + 1) / 2;
if(mid * mid <= x) left = mid;
else right = mid - 1; // 出现mid-1,上面求mid用"+1"
}
return (int)left;
}
思路
代码
int searchInsert(vector<int>& nums, int target) {
// 二分法
int left = 0, right = nums.size() - 1;
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] < target) left = mid + 1;
else right = mid;
}
if(nums[left] < target) return left+1; // 数组中无target,插入位置在数组外
else return left; // 数组中有t / 无t+插入位置在数组内
}
思路
int peakIndexInMountainArray(vector<int>& arr) {
int left = 0, right = arr.size() - 1;
// 峰值左侧区间 arr[i] > arr[i-1] 向右找 left = mid;
// 峰值右侧区间 arr[i] < arr[i-1] 向左找 right = mid - 1;
while(left < right)
{
int mid = left + (right - left + 1) /2;
if(arr[mid] > arr[mid-1]) left = mid;
else right = mid - 1;
}
return left; // 返回峰值索引
}
思路
代码
int findPeakElement(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
// 取下标i,如果有num[i] > nums[i+1] 这两个数是递减,则这两数左侧必定存在一个峰值
// 如果nums[i] < nums[i] 则右侧一定存在峰值
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] > nums[mid + 1]) right = mid;// 找左区间
else left = mid + 1;
}
return left;
}
思路
代码
int findMin(vector<int>& nums) {
int n = nums.size();
int left = 0, right = n - 1;
// 最小值的左侧区间,nums[i] < nums[n-1] 成立
// 右侧区间,nums[i] > nuns[n-1] 成立
// 二段性->二分法
while(left < right)
{
int mid = left + (right - left) / 2;
if(nums[mid] > nums[n-1]) left = mid + 1;
else right = mid;
}
return nums[left];
}
思路
代码
int takeAttendance(vector<int>& records) {
// 缺失的数左区间满足:值=下标
// 缺失的数右区间满足:值>下标
// 二段性->二分法
int n = records.size();
int left = 0, right = n - 1;
while(left < right)
{
int mid = left + (right - left) / 2;
if(records[mid] == mid) left = mid + 1;
else right = mid;
}
// 处理边界情况:当确实的数为n的时候
if(records[right] == right) return n;
return right;
}