二分法小总结

前言

最近在做算法题的时候,发现二分法有许多小细节,稍不注意就导致出错,特此写下这篇博客,记录一下!

二分法的应用场景

二分法的应用场景一般有搜索某个数寻找某个数的左右边界(当然,前提数组有序)。

下面直接贴上代码后再总结:

(1)搜索某个数:

right以num.length - 1 结尾

int Search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1; //细节点

    while(left <= right) {//细节点
        int mid = (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;
}

right以num.length 结尾

int Search(int[] nums, int target) {
    int left = 0;
    int right = nums.length; //细节点

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

总结:

  1. 第一种代码为什么 while 循环的条件中是 <=,而不是 < ?

答:因为初始化 right 的赋值是 nums.length-1,即最后一个元素的索引,而不是 nums.length。

这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。

我们这个算法中使用的是前者 [left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间,我们不妨称为「搜索区间」。

什么时候应该停止搜索呢?当然,找到了目标值的时候可以终止:

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

但如果没找到,就需要 while 循环终止,然后返回 -1。那 while 循环什么时候应该终止?搜索区间为空的时候应该终止,意味着你没得找了,就等于没找到嘛。

while(left <= right) 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见这时候搜索区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。

while(left < right) 的终止条件是 left == right,写成区间的形式就是 [left, right],或者带个具体的数字进去 [2, 2],这时候搜索区间非空,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。

当然,如果你非要用 while(left < right) 也可以,我们已经知道了出错的原因,就打个补丁好了:

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

通俗的讲:当你 right 的赋值是 nums.length-1时,要搜索的数刚好在两个指针中间的时候被搜索到,那程序没问题,但是当你要搜索的数恰好在两指针重叠时的位置,while(left < right)判断条件会将这个搜到的元素给遗漏,所以要加上 == 。

  1. 第二种代码为什么 while 循环的条件中是 <,而不是 <= ?

答:用相同的方法分析,因为 right = nums.length 而不是 nums.length - 1 。因此每次循环的「搜索区间」是 [left, right) 左闭右开。

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

通俗的讲:当 right 的赋值是 nums.length,while(left <= right)时,当搜索的数在数组里面存在时,不会出错,要是不存在,索引就会越界,情况分为以下两种:
(1)搜索时right一旦往左边走,则left和right的取值变成第一种代码的情况
(2)搜索时,left往右边走,因为 right = nums.length,所以left 等于 nums.length - 1的时候也可以取到nums[ nums.length-1]的值,但要是取不到,那left 变成 nums.length ,此时 while(left <= right)判断条件会让索引越界。

因此我们将 = 号去掉后就不会获取到越界的索引了,但这时又有一个问题,当搜索时right一旦往左边走,就变成我们上面所说的第一种代码情况。这时就又回到了第一种代码时while(left < right)判断条件的情况,即会遗漏指针重叠时的元素。怎么办?很简单,将right = mid - 1 改为 right = mid 即可
(或者这样解释:我们的「搜索区间」是 [left, right) 左闭右开,所以当 nums[mid] 被检测之后,下一步的搜索区间应该去掉 mid 然后分割成两个区间,即 [left, mid) 或 [mid + 1, right)。)。

(2)寻找某个数的左边界(即这个数出现第一次的索引)

下面直接贴上代码后再总结:

right以num.length 结尾

int left_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; //细节点
    
    while (left < right) { // 细节点
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            right = mid;//细节点
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid; // 细节点
        }
    }
    // target 比所有数都大(不存在)
    if (left == nums.length) return -1;
    // target比所有的数都小(不存在)
    return nums[left] == target ? left : -1;
}
小结:

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

right以num.length - 1 结尾

int left_bound(int[] nums, int target) {
    i = 0; j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] < target) i = m + 1;
            else j = m - 1;
        }
        //当target大于所有数或者(小于所有数以及在不存在)时
        if(i == nums.length || nums[i] != target) return -1;
        return i;
}

小结同上

(3)寻找某个数的右边界

下面直接贴上代码后再总结:

right以num.length 结尾

int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;
    
    while (left < right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; //细节点
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    if (left == 0) return -1;
    return nums[left-1] == target ? (left-1) : -1;
}
小结:

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

right以num.length - 1 结尾

int right_bound(int[] nums, int target) {
    int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] <= target) i = m + 1;
            else j = m - 1;
        }
        //当target小于所有数或者(大于所有数以及不存在)时
        if(j == -1 || nums[j] != target)  return -1;
        return j;
}

小结同上

总结

1、只要理解了当right初始化取值不同时,while条件的变化以及为什么这样缩小区间的操作的原因,那么搜索左右边界就是一件很简单地事情了,也就是搜到就缩小区间,对于一些特殊的例子,建议上手操作一下。

2、不论是right == nums.length - 1或者right == nums.length ,目标存在时,双指针都会重叠在目标上,前者,会在判断一次,所以必须取到恰当值(即取left还是right),后者取left和right都行。

最后,送上一道算法题给大家享用:

剑指 Offer 53 - I. 在排序数组中查找数字 I
统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0

限制:

0 <= 数组长度 <= 50000

来源:力扣(LeetCode)

class Solution {
    public int search(int[] nums, int target) {
        // 搜索右边界 right
        int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] <= target) i = m + 1;
            else j = m - 1;
        }
        int right;
        if(j == -1 || nums[j] != target)  right = -1;
        else right = j;
        //不存在直接返回
        if(right == -1) return 0;
        //搜索左边界 
        i = 0; j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] < target) i = m + 1;
            else j = m - 1;
        }
        int left;
        if(i == nums.length || nums[i] != target) left = -1;
        else left = i;

        if(left == -1 && right == -1){
            return 0;
        }
        return right - left + 1;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值