二分查找是基于分治的一种搜索方法。他需要原始数据是有序的,将中间值与目标值比较,进而减少一半的搜索范围,所以一般时间复杂度O(logn)。
除了经典的二分查找,本文还将介绍几种变种问题,二分查找插入点、二分查找左右边界。
二分查找
题目链接:二分查找
代码(左闭右闭):
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int l = 0; //左右指针
int r = n - 1;
while (l <= r){ //需要取等,不取等会漏掉左右两侧的值
int mid = (r - l) / 2 + l;
if (nums[mid] < target){ //三种情况,缩小一般范围
l = mid + 1;
}else if (nums[mid] > target){
r = mid - 1;
}else{
return mid;
}
}
return -1;
}
};
代码(左闭右开):
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int l = 0;
int r = n; //区别一:右开,所以不减一 [0,n)
while (l < r){ //右区间取不到,所以不取等
int mid = (r - l) / 2 + l;
if (nums[mid] < target){
l = mid + 1; //左侧可以取到,所以可以+1
}else if (nums[mid] > target){
r = mid; //右侧取不到,所以不能—1
}else{
return mid;
}
}
return -1;
}
};
注: mid = (r - l) / 2 + l;因为(r+l)/2可能越界(如超int范围)。
二分查找插入点
区别:不同于找一个值,如果有重复数字,插入点是最左侧的数字的下标;如果没有目标值,插入点是第一个大于目标值的下标。因此肯定有插入点。
1.不存在重复的元素
int binarySearchInsertionSimple(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 初始化双闭区间 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) {
i = m + 1; // target 在区间 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在区间 [i, m-1] 中
} else {
return m; // 找到 target ,返回插入点 m
}
}
// 未找到 target ,返回插入点 i
return i;
}
注:循环完成后,𝑖 指向最左边的 target,𝑗 指向首个小于 target 的元素,因此索引 𝑖 就是插入点。
2. 有重复元素查找插入点
思路:如果按照无重复元素找插入点,不能确定是不是第一个。
向左顺序遍历的话时间复杂度最坏就是O(n),因此此方法不行。
我们可以改变中间值和目标值相等的代码,当 nums[m] == target 时,说明小于 target 的元素在区间 [𝑖, 𝑚 − 1] 中,因此采用 𝑗 = 𝑚 − 1 来缩小区间, 从而使指针 𝑗 向小于 target 的元素靠近。
int binarySearchInsertion(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // 初始化双闭区间 [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m
if (nums[m] < target) {
i = m + 1; // target 在区间 [m+1, j] 中
} else if (nums[m] > target) {
j = m - 1; // target 在区间 [i, m-1] 中
} else {
j = m - 1; // 区别:首个小于 target 的元素在区间 [i, m-1] 中
}
}
// 未找到 target ,返回插入点 i
return i;
}
二分查找左右边界
区别:该问题与插入点的区别在于,如果没有这个数,需要返回-1,存在这个数就等于寻找插入点。
- 查找左边界
使用查找插入点函数(返回值范围[0,n]),如果不等于目标值则说明没有找到,但是前提是不能越界,所以需要加上i==nums.size()进行判断。
int binarySearchLeftEdge(vector<int> &nums, int target) {
// 等价于查找 target 的插入点
int i = binarySearchInsertion(nums, target);
// 未找到 target ,返回 -1
if (i == nums.size() || nums[i] != target) {
return -1;
}
// 找到 target ,返回索引 i
return i;
}
2.查找右边界
查找target+1的左边界,返回值再-1。
int binarySearchRightEdge(vector<int> &nums, int target) {
// 转化为查找最左一个 target + 1
int i = binarySearchInsertion(nums, target + 1);
// j 指向最右一个 target , i 指向首个大于 target 的元素
int j = i - 1;
// 未找到 target ,返回 -1
if (j == -1 || nums[j] != target) { //因为该函数返回范围[0,n],-1后范围[-1,n-1]
return -1;
}
// 找到 target ,返回索引 j
return j;
}
总结
本文简单介绍了二分查找以及几种变形问题,关键就是如何缩小查询的范围。希望大家对二分法有更深的理解。