【算法】二分查找

前言

最近自己在做二分查找类题目,二分查找是非常基础的算法,但其并不简单,有很多细节需要掌握,因此自己结合资料进行了一些整理,在看完这些后,建议做几道给出的习题这样效果会更好。

二分查找介绍

二分查找是基于有序序列的查找算法。二分查找的高效之处在于,每一步都可以去除当前区间的一半元素,因此其时间复杂度时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==rightwhile循环停止,因此最后的返回值既可以是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练习题

278. 第一个错误的版本

  • "寻找有序序列中第一个满足某条件的元素的位置"的方法的应用

69. x 的平方根

  • "如果想要寻找最后一个满足“条件C”的元素的位置,则可以先求第一个满足“条件!C”的元素的位置,然后将该位置减1即可"该方法的应用
  • 思路可以参考我的blog
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值