前言
最近自己在做二分查找类题目,二分查找是非常基础的算法,但其并不简单,有很多细节需要掌握,因此自己结合资料进行了一些整理,在看完这些后,建议做几道给出的习题这样效果会更好。
二分查找介绍
二分查找是基于有序序列的查找算法。二分查找的高效之处在于,每一步都可以去除当前区间的一半元素,因此其时间复杂度时O(logn)
,这是十分优秀的。
基本的二分查找
应用场景
“查找序列中是否存在某条件的元素”
代码
首先给出最基本的二分查找问题及解决代码
eg:查找某元素在数组中的位置,如果找到,则返回下标,未找到,返回-1
//二分区间为[left,right],传入的初值为[0,n-1]
int binarySearch(vector<int> nums, int left, int right, int x)
{
int mid;//mid为中点
while (left<=right)//如果left>right就没办法形成闭区间
{
mid = left + (right - left) / 2;//取中点-采用此种形式防止left+right超出int范围
if (nums[mid] == x) return mid;// 找到x,返回下标
else if (nums[mid]>x)//中间的数大于x
{
right = mid - 1;//往左子区间[left,mid-1]查找
}else {//中间的数小于x
left = mid + 1;//往右子区间[mid+1,right]查找
}
}
return -1;//查找失败,返回-1
}
二分查找的变形
应用场景
寻找有序序列中第一个满足某条件的元素的位置。
举例1:求序列中的第一个大于等于x的元素的位置
//二分区间为[left,right],传入的初值为[0,n],函数返回第一个大于等于x的元素的位置
int lower_bound(vector<int> nums, int left, int right, int x)
{
int mid;//mid为中点
while (left < right)//对于[left,right来说],left==right说明找到了唯一位置
{
mid = left + (right - left) / 2;//取中点
if (nums[mid]>=x)//说明第一个大于等于x的元素的位置一定在mid处或mid的左侧
{
right = mid;//往左子区间[left,mid]查找
}
else//说明第一个大于等于x的元素的位置一定在mid+1处处或mid+1的右侧
{
left = mid + 1;//往右子区间[mid+1,right]查找
}
}
return left;//返回夹出来的位置
}
注意点:
- 循环条件为
left<right
而非之前的left<=right
,这是由问题本身决定的。在上一个问题中,需要当元素不存在时返回-1,这样当left>right
时[left,right]
就不再是闭区间,可以此作为元素不存在的判定原则,因此left<=right
满足时循环应当一致执行;但是如果想要返回第一个大于等于x
的元素的位置,就不需要判断x
本身是否存在,因为就算它不存在,返回的也是“假设它存在,它应该在的位置”,于是当left==right
时,[left,right]
刚好能夹出唯一的位置,就是需要的结果,因此只需要当left<right
时让循环一直执行即可。 - 由于当
left==right
时while
循环停止,因此最后的返回值既可以是left
,也可以是right
。 - 二分的初始区间应当能覆盖到所有可能返回的结果。首先,二分下界是0是显然的,但是二分上界是
n-1
还是n
呢?考虑到要查询元素有可能比序列中的所有元素都要大,此时应当返回n
(即假设它存在,它应该在的位置),因此二分上界是n
,故二分的初始区间为[left,right]=[0,n]
举例2:求序列中第一个大于x的元素的位置
//二分区间为[left,right],传入的初值为[0,n],函数返回第一个大于x的元素的位置
int upper_bound(vector<int> nums, int left, int right, int x)
{
int mid;//中点
while (left<right)//对于[left,right来说],left==right说明找到了唯一位置
{
mid = left + (right - left) / 2;//取中点
if (nums[mid]>x)//说明第一个大于x的元素的位置一定在mid处或mid的左侧
{
right = mid;//往左子区间[left,mid]查找
}
else//说明第一个大于等于x的元素的位置一定在mid+1处或mid+1的右侧
{
left = mid + 1;//往右子区间[mid+1,right]查找
}
}
return left;//返回夹出来的位置
}
通过思考会发现,lower_bound
函数和upper_bound
函数都在解决这样一个问题:寻找有序序列中第一个满足某条件的元素的位置。这是一个非常重要的且经典的问题,平时能碰到的大部分二分法问题都可以归结于这个问题。
例如对lower_bound
函数来说,它寻找的就是第一个满足条件值“大于等于x
”的元素的位置;
而对upper_bound
函数来说,它寻找的是第一个满足条件“值大于x
”的元素的位置。
显然,所谓的**“某条件”在序列中一定是从左到右先不满足,然后满足的(否则把该条件取反即可)**。
变形问题的代码模板
//解决“寻找有序序列中第一个满足某条件的元素的位置”问题的固定模板
//二分区间为[left,right],传入的初值为[0,n],函数返回第一个大于x的元素的位置
int solve(vector<int> nums, int left, int right, int x)
{
int mid;//中点
while (left < right)//对于[left,right来说],left==right说明找到了唯一位置
{
mid = left + (right - left) / 2;//取中点
if (条件成立)//位置一定在mid处或mid的左侧
{
right = mid;//往左子区间[left,mid]查找
}
else//条件不成立,位置一定在mid+1处或mid+1的右侧
{
left = mid + 1;//往右子区间[mid+1,right]查找
}
}
return left;//返回夹出来的位置
}
另外,如果想要寻找最后一个满足“条件C”的元素的位置,则可以先求第一个满足“条件!C”的元素的位置,然后将该位置减1即可(在最长回文子串的二分解法用到了这一点)。
最后,如何判断lower_bound函数和upper_bound函数的查询是否成功,只需对上界进行处理即可。例如下面的处理代码:
if (left == nums.length) return -1;// 数组中未找到
return nums[left] == target ? left : -1;//如果该位置元素值和目标值相等,则找到,否则,未找到
LeetCode练习题
- "寻找有序序列中第一个满足某条件的元素的位置"的方法的应用
- "如果想要寻找最后一个满足“条件C”的元素的位置,则可以先求第一个满足“条件!C”的元素的位置,然后将该位置减1即可"该方法的应用
- 思路可以参考我的blog