DAY 1 二分查找

DAY 01 二分查找

一、寻找一个数(基本的二分搜索)

int  erfen(vector<int>& nums, int target) {
    int left = 0; 
    int right = nums.size() - 1; // 注意左闭右闭区间

    while(left <= right) {
        int mid = left + (right - left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
    }
    return -1;
}

输入: nums = [-1,0,3,5,9,12], target = 9     
输出: 4       
解释: 9 出现在 nums 中并且下标为 4     

输入: nums = [-1,0,3,5,9,12], target = 2     
输出: -1        
解释: 2 不存在 nums 中因此返回 -1   

几点说明:

  1. 计算mid时需要防止溢出,代码中left + (right - left) / 2和(right + left) / 2的结果相同,这样写可以防止left和right太大,直接相加导致溢出的情况。

2. 为什么 while 循环的条件中是 <=,而不是 <?
其实 <=和 <都可以的,用哪一个取决于我们的初始条件,我们初始化时right= nums.size() - 1,是最后一个元素的索引,相当于两端都闭区间 [left,right]。

如果我们初始化right=num.size(),这个索引大小是越界的,相当于左闭右开区间[left,right)。
3.搜索的终止条件主要分两个:
第一,找到目标值的时候终止。

    if(nums[mid] == target)
    return mid

第二,没找到就while循环终止。(又分两种情况)

  • while(left <= right)

该循环终止条件是left==right+1,区间形式就为[right+1,right],这个时候区间为空,while循环终止,直接返回-1。

  • while(left < right)
    该循环终止条件是left==right,区间形式就为[right,right),这个时候区间为非空,比如[2,2],还有个数2,但此时while循环终止。那么索引2就会被漏掉,这里直接返回-1就是错误的。如果非要用 while(left < right) 也可以

while(left < right){
//…
}
return nums[left] == target ? left : -1;

4.缩小搜索区间
在上面的代码中的搜索区间是两端都闭的,即[left,right]。那么当发现索引mid不是要找的target时,就要去搜索区间[left,mid-1]或者[mid+1,right]。
因为mid已经搜索过,应该从搜索区间中除去。
5.算法缺点
算法存在局限性。比如一个有序数组 nums = [1,2,2,2,3],target 为 2,此算法返回的索引是 2,没错。但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。

二、寻找左侧边界的二分搜索

int left_bound(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size(); // 注意
    
    while (left < right)  // 注意
    { 
        int mid = left + (right - left) / 2;
        if (nums[mid] == target)
         {
            right = mid;
         } 
        else if (nums[mid] < target) 
        {
            left = mid + 1;
        } 
        else if (nums[mid] > target) 
        {
            right = mid; // 注意
        }
    }
    return left;
}

几点说明:

1、为什么 while 中是 < 而不是 <=?==
用相同的方法分析,因为 right = nums.size(); 而不是 nums.size() - 1。因此每次循环的「搜索区间」是 [left, right) 左闭右开。

while(left < right) 终止的条件是 left == right,此时搜索区间 [left, left) 为空,所以可以正确终止。

2、如果 nums 中不存在 target 这个值,计算出来的这个索引含义是什么

如果 target 不存在,搜索左侧边界的二分搜索返回的索引是大于 target 的最小索引。
举个例子,nums = [2,3,5,7], target = 4,left_bound 函数返回值是 2,因为元素 5 是大于 4 的最小元素。
现在让写这么一个函数

// 在一个有序数组中,找到「小于 target 的最大元素的索引」
// 比如说输入 nums = [1,2,2,2,3],target = 2,函数返回 0,因为 1 是小于 2 的最大元素。
// 再比如输入 nums = [1,2,3,5,6],target = 4,函数返回 2,因为 3 是小于 4 的最大元素。
int floor(int[] nums, int target);

那么这个函数就可以直接用 left_bound 函数来实现:

int floor(int[] nums, int target) {
    // 当 target 不存在,比如输入 [4,6,8,10], target = 7
    // left_bound 返回 2,减一就是 1,元素 6 就是小于 7 的最大元素
    // 当 target 存在,比如输入 [4,6,8,8,8,10], target = 8
    // left_bound 返回 2,减一就是 1,元素 6 就是小于 8 的最大元素
    return left_bound(nums, target) - 1;
}

如果没找到想让它返回 -1,怎么做?

while (left < right)
 {
    //...
 }
// 如果索引越界,说明数组中无目标元素,返回 -1
if (left < 0 || left >= nums.length)
 {
    return -1;
 }
// 提示:其实上面的 if 中 left < 0 这个判断可以省略,因为对于这个算法,left 不可能小于 0
// 判断一下 nums[left] 是不是 target
return nums[left] == target ? left : -1;

3、为什么 left = mid + 1,right = mid ?

我们的「搜索区间」是 [left, right) 左闭右开,所以当 nums[mid] 被检测之后,下一步应该去 mid 的左侧或者右侧区间搜索,即 [left, mid) 或 [mid + 1, right)。

4、为什么该算法能够搜索左侧边界?
关键在于对于 nums[mid] == target 这种情况的处理:

 if (nums[mid] == target)
        right = mid;

找到 target 时不要立即返回,而是缩小「搜索区间」的上界 right,在区间 [left, mid) 中继续搜索,即不断向左收缩,达到锁定左侧边界的目的。
5、为什么返回 left 而不是 right?

都是一样的,因为 while 终止的条件是 left == right

6、可不可以把 right 变成 nums.size() - 1,也就是使用两边都闭的「搜索区间」?这样就可以和第一种二分搜索在某种程度上统一起来了。

让搜索区间两端都闭,所以 right 应该初始化为 nums.size() - 1,while 的终止条件应该是 left == right + 1,也就是其中应该用 <=
完整代码:

int left_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;
    // 搜索区间为 [left, right]
    while (left <= right) 
    {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target)
         {
            // 搜索区间变为 [mid+1, right]
            left = mid + 1;
         }
         else if (nums[mid] > target) 
         {
            // 搜索区间变为 [left, mid-1]
            right = mid - 1;
         } 
        else if (nums[mid] == target)
         {
            // 收缩右侧边界
            right = mid - 1;
         }
    }
    // 判断 target 是否存在于 nums 中
    // 如果越界,target 肯定不存在,返回 -1
    if (left < 0 || left >= nums.size()) 
    {
        return -1;
    }
    // 判断一下 nums[left] 是不是 target
    return nums[left] == target ? left : -1;
}

三、寻找右侧边界的二分查找

类似寻找左侧边界的算法,这里也会提供两种写法,还是先写常见的左闭右开的写法,只有两处和搜索左侧边界不同:

int right_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size();

    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target)
         {
            left = mid + 1; // 注意
         } 
        else if (nums[mid] < target)
        {
            left = mid + 1;
        } 
        else if (nums[mid] > target)
         {
            right = mid;
         }
    }
    return left - 1; // 注意
}

几点说明:

1、为什么这个算法能够找到右侧边界?==

if (nums[mid] == target) {
    left = mid + 1;

当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的左边界 left,使得区间不断向右靠拢,达到锁定右侧边界的目的。

2、为什么最后返回 left - 1 而不像左侧边界的函数,返回 left?而且我觉得这里既然是搜索右侧边界,应该返回 right 才对。

首先,while 循环的终止条件是 left == right,所以 left 和 right 是一样的,你非要体现右侧的特点,返回 right - 1 好了。
为什么要减一呢,这是搜索右侧边界的一个特殊点,关键在锁定右边界时的这个条件判断:

// 增大 left,锁定右侧边界
if (nums[mid] == target) {
    left = mid + 1;
    // 这样想: mid = left - 1

在这里插入图片描述
因为我们对 left 的更新必须是 left = mid + 1,就是说 while 循环结束时,nums[left] 一定不等于 target 了,而 nums[left-1] 可能是 target。
为什么 left 的更新必须是 left = mid + 1,当然是为了把 nums[mid] 排除出搜索区间嘛。

3、如果 nums 中不存在 target 这个值,计算出来的这个索引含义是什么?如果我想让它返回 -1,怎么做?

如果 target 不存在,搜索右侧边界的二分搜索返回的索引是小于 target 的最大索引。
想一想刚才左边界返回的是什么,记住几个例子就可以。
比如 nums = [2,3,5,7], target = 4,right_bound 函数返回值是 1,因为元素 3 是小于 4 的最大元素。
如果想在 target 不存在时返回 -1,
只要在最后判断一下 nums[left-1] 是不是 target 就行,类似之前的左侧边界搜索,做一点额外的判断即可:

while (left < right) {
    // ...
}
// 判断 target 是否存在于 nums 中
// left - 1 索引越界的话 target 肯定不存在
if (left - 1 < 0 || left - 1 >= nums.size()) {
    return -1;
}
// 判断一下 nums[left - 1] 是不是 target
return nums[left - 1] == target ? (left - 1) : -1;

也可以把这个算法的「搜索区间」也统一成两端都闭的形式,其实只要改两个地方

int right_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 if (nums[mid] > target)
         {
            right = mid - 1;
         } 
        else if (nums[mid] == target)
         {
            // 这里改成收缩左侧边界即可
            left = mid + 1;
         }
    }
    // 最后改成返回 left - 1
    if (left - 1 < 0 || left - 1 >= nums.size()) {
        return -1;
    }
    return nums[left - 1] == target ? (left - 1) : -1;
    //或者return nums[right] == target ? right : -1;
    //因为right=left-1嘛
}

总结:

第一个,最基本的二分查找算法:

因为我们初始化 right = nums.size() - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1
因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回

第二个,寻找左侧边界的二分查找:

因为我们初始化 right = nums.size()
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界

第三个,寻找右侧边界的二分查找:

因为我们初始化 right = nums.size()
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界
又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一

并且这三种方法都可以统一的写成左右闭合区间形式,上面都有总结,这里不再重复。

  • 36
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值