二分查找的定义
二分查找也叫折半查找,比如在一个有序的数组里面找到目标值,它是一种查询效率比较高的算法,时间复杂度
O
(
l
o
g
2
n
)
O({log}_2 n)
O(log2n)。
它的基本思想:将目标值与数组中间的元素进行比较,如果目标值小于中间元素,则在数组的左半部分继续查找,否则在右半部分查找,不断缩小搜索范围,直到找到目标值或确定目标值不存在为止。
lower_bound() 方法 用于在一个 有序 数组
n
u
m
s
nums
nums 中找到第一个 大于或等于 target 的元素的位置。
(1) 在
n
u
m
s
nums
nums 内,定义左侧和右侧指针 left 和 right,即 left=0, right=nums.size()-1
,表示待查找的范围 [left, right];
(2) 根据 left 和 right 找到中间指针 mid,将 nums[mid]
与 target 进行比较,根据比较结果更新 left 或 right 指针:① 若 nums[mid] < target
,相当于 [left, mid] 这个区间的元素都是小于 target,因此 target 可能在 [mid + 1, right] 这个范围内,更新 left = mid+1;② 若 nums[mid] >= target
,相当于 [mid, right] 这个区间的元素都是大于等于 target 的,因此第一次出现的 target 可能在 [left, mid - 1] 这个范围内,更新 right = mid-1;
(3) 更新结束后,比较 left 和 right:若是待查找的范围还能保持 [left, right],即 left <= right
,就继续重复步骤 (2);若是不能保持,比如 right < left
,说明在
n
u
m
s
nums
nums 中找到第一个与 target 相等的元素,或者找到了第一个大于 target 的元素,返回对应的下标,即 return left
。
lower_bound() 方法的总结:不断将区间 [left, right] 里的中间元素 nums[mid]
与 target 进行比较,根据比较结果不断缩减 [left, right] 区间,直到 left 和 right 不能构成一个区间,就返回对应元素的下标。
① lower_bound() 方法中 left 和 right 满足这个 循环不变量: n u m s [ l e f t − 1 ] < t a r g e t ; n u m s [ r i g h t + 1 ] > = t a r g e t nums[left - 1] < target; nums[right + 1] >= target nums[left−1]<target;nums[right+1]>=target,相当于 left 的左边恒小于 target,right 的右边恒大于等于 target,因此 left 最终会停在第一个大于或等于 target 的元素位置,而 right 就会停在的左侧。
② 闭区间的含义:因为 left 和 right 满足这个循环不变量,相当于 [0, left-1] 的元素均小于 target,而 [right+1, nums.size()-1] 的元素是大于等于 target,因此查找范围就落在 [left, right] 这个区间里。
lower_bound2() 方法 与 lower_bound() 方法的作用一致,不同点:
(1) 待查找的范围变为 [left, right),说明该区间中至少包含一个元素,即 left=0, right=nums.size()
,因此循环条件为 left < right
;
(2) 循环不变量变为
n
u
m
s
[
l
e
f
t
−
1
]
<
t
a
r
g
e
t
;
n
u
m
s
[
r
i
g
h
t
]
>
=
t
a
r
g
e
t
nums[left - 1] < target; nums[right] >= target
nums[left−1]<target;nums[right]>=target,相当于 left 的左边恒小于 target,right 及其右侧的元素恒大于等于 target;
(3) 根据 mid 指针更新 right 值,若是 nums[mid] >= target
,则 right 指针更新,即 right=mid
;
(4) 最后返回第一个大于或等于 target 的下标,可以返回 left 指针,也可以返回 right 指针;最后 left 和 right 会停在第一个大于或等于 target 的元素位置。
// 左闭右闭区间:待查找的区间 [left, right]
int lower_bound(vector<int> nums, int target) {
// 循环不变量:nums[left - 1] < target; nums[right + 1] >= target
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 防止溢出,right 可能是 INT_MAX,若是直接与 left 相加会有溢出的情况
if (nums[mid] < target)
left = mid + 1;
else
right = mid - 1;
// nums[mid] >= target
}
return left;
}
// 左闭右开区间:[left, right)
int lower_bound2(vector<int> nums, int target) {
// 循环不变量:nums[left - 1] < target; nums[right] >= target
// 当 left == right 时,说明查找区间已经缩小到没有元素可供检查
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid + 1;
else
right = mid;
// nums[mid] >= target
}
return left; // return right; 也是可以的
}
在排序数组中查找元素的第一个和最后一个位置
根据题目描述,我们要从非递减的数组给出 target 的开始位置和结束位置,且时间复杂度为
O
(
l
o
g
n
)
O(log n)
O(logn)。我们可以转变为,在数组中找到第一个 target 和 target+1 的开始位置,然后 target+1 元素对应的索引再减一就是 target 在数组的结束位置。
这个查找目标值的开始的位置和结束位置可以使用两次二分查找。其中结束位置 end
是基于开始位置 start
的,若 start
超过数组长度,或者 start=0
,但是 nums[start] != target
,相当于数组中不存在目标值,因此直接返回 {-1, -1}
,否则使用二分查找找到对应的 end
值。
// 左闭右闭区间:待查找的区间 [left, right]
int lower_bound(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 - 1;
}
return left;
}
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int start = lower_bound(nums, target);
if (start == nums.size() || nums[start] != target)
// target 大于要么小于 nums 的全部元素
return {-1, -1};
int end = lower_bound(nums, target + 1) - 1;
// 查找第一个大于或等于 target+1 的值,再 减一 就是目标值的结束位置
return {start, end};
}
};
第一个错误的版本
调用 bool isBadVersion(version)
接口来判断版本号 version 是否在单元测试中出错。
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);
class Solution {
public:
int firstBadVersion(int n) {
int left = 1, right = n;
while (left < right) { // 注意
int mid = left + (right - left) / 2;
if (!isBadVersion(mid))
left = mid+1;
else right = mid; // 注意
}
return left;
// 返回第一个错误版本; right == left ==> return right;
}
};
搜索插入位置
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int l = 0, r = nums.size()-1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
l = mid + 1;
else
r = mid - 1;
}
return l;
}
};
二分查找
class Solution {
public:
int search(vector<int>& nums, int target) {
int l = 0, r = nums.size()-1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
l = mid + 1;
else
r = mid - 1;
}
return -1;
}
};
寻找两个正序数组的中位数
// 方式一: 暴力解
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 暴力解
if (nums1.size() == 0) {
return count(nums2);
}
if (nums2.size() == 0) {
return count(nums1);
}
// 合并
nums1.insert(nums1.end(), nums2.begin(), nums2.end());
// 排序
sort(nums1.begin(), nums1.end());
return count(nums1);
}
double count(vector<int> nums) {
int size = nums.size();
if (size % 2 == 0)
return (nums[size/2 - 1] + nums[size/2]) / 2.0;
else
return nums[size/2];
}
};