【二分查找】二分查找怎么写,边界如何确定,我应该是要左边还是要右边,我为何如此的蠢???

1 二分查找简介

二分查找算法的效率很高,而且它的思想也比较简单,但是由于内部的某些细节,导致二分查找不是很容易写对,有的时候也会陷入自我怀疑,我为何如此的蠢???
在这里插入图片描述

1.1 二分查找的核心思想

核心思想很简,单就一句: 每次尽可能多的排除掉无用元素

1.2 适用场景

二分查找只适合 有序集合 使用,

2 举个栗子 - 猜数字

0 - 100 的范围内猜一个目标值数字,每猜一次会反馈猜大了还是猜小了,使用最少的次数多少次才能猜对呢?

这里我们就能用上二分的思想,每次猜一半,最后的结果次数就能达到最小

每次都猜 有效范围 内的 中间值,这样就能以最少的次数猜对

public class Guess_Num {
    
    
    public static void main(String[] args) {
    
    
        // 产生随机数
        int num = new Random().nextInt(100);
        Scanner sc = new Scanner(System.in);
        // 循环判断
        while (true) {
    
    
            int guess = sc.nextInt();
            if (guess > num) System.out.println("猜的数" + guess + "大了"); // 猜大
            if (guess < num) System.out.println("猜的数" + guess + "小了"); // 猜小
            if (num == guess) {
    
     // 猜对
                System.out.println("恭喜你猜中了");
                break; // 结束循环
            }
        }
    }
}

代码不是重点,重点是我们在猜数字时的思考过程,每次我们都会排除一半

图示栗子

在这里插入图片描述

分析栗子

下图所示,我们每次都会排除当前数字的个数的一半,n/21、n/22、n/23 . . . n/2k

在这里插入图片描述

计算得知,26 < 100,27 > 100,所以理论来说最少7次能得到最后的结果,以此类推,1000 需要 9 次,10000 需要 13 次,数据量是 指数 增长,而我们的查找次数是 线性 增长,这足以体现二分查找的优越性,也就不难发现二分查找的时间复杂度为 O(lgn)

3 二分查找中的几个关键部分

1、 关键指针:leftrightmid

2、 循环退出条件的判断

4 二分查找怎么写

4.1 查找数组中的目标值

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

例子

输入:nums = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ], target = 3
输出:2

    int binarySearch(int[] nums, int target) {
    
    
        if (nums == null || nums.length == 0) return -1;

        int l = 0, r = nums.length - 1;
        while (l <= r) {
    
     // 1
            int mid = l + (r - l) / 2;
            if (nums[mid] == target) {
    
     // 2
                return mid;
            } else if (nums[mid] < target) {
    
    
                l = mid + 1; // 3
            } else {
    
    
                r = mid - 1; // 3
            }
        }

        return -1;
    }

这是最基础的二分查找了,这里的几个重要的部分

1 、r = nums.length - 1nums[l]nums[r] 都会被遍历到,l 和 r 都在 nums.length 之内移动,如果 l 超出边界,那肯定达成了 l > r,然后就退出了循环,所以从 nums.length - 1 开始即可

2、 while (l <= r): 为啥是 <= ,因为 在 l = r 时的值也需要判断,循环的退出条件时 l > r

3、 l = mid + 1r = mid - 1: 因为 mid 判断过,所以要向左或者向右移动一位

4.2 查找数组中的第一个目标值

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,搜索 nums 中的 target,如果目标值存在返回 target 第一次出现的下标,否则返回 -1

例子

输入:nums = [ 1, 2, 2, 2, 3, 4, 5, 6, 7, 8, 9 ], target = 2
输出:1

    public int binarySearch(int[] nums, int target) {
    
    
        int len = nums.length;
        int l = 0;
        int r = len - 1; // 1
        while (l <= r) {
    
     // 2
            int mid = l + ((r - l) / 2);
            if (nums[mid] == target) {
    
    
                if ((mid == 0) || (nums[mid - 1] != target)) return mid; // 4
                else r = mid - 1; // 5
            } else if (nums[mid] > target) {
    
    
                r = mid - 1; // 3
            } else if (nums[mid] < target) {
    
    
                l = mid + 1; // 3
            }
        }
        return -1;
    }

1、 int r = len - 1nums[l]nums[r] 都会被遍历到,l 和 r 都在 nums.length 之内移动,如果 l 超出边界,那肯定达成了 l > r,然后就退出了循环,所以从 nums.length - 1 开始即可

2、 while (l <= r) : 为啥是 <= ,因为 在 l = r 时的值也需要判断,循环的退出条件时 l > r

3、 r = mid - 1l = mid + 1 : 因为 mid 判断过,所以要向左或者向右移动一位

4、 if ((mid == 0) || (nums[mid - 1] != target)) : 这是 nums[mid] = target 的情况,如果此时 mid = 0,是第一个元素,条件就直接为true了,如果 mid != 0,那还要再判断 nums[mid] 的前一个元素 nums[mid - 1] 是不是等于 target,如果不是的话就满足了条件返回 mid

这里要注意的一点是,这里的两个判断条件关系是或的关系,第一条件不满足时,才会进行第二个条件的判断,所以不用担心 mid = 0nums[mid - 1] 会出现索引溢出的问题

5、 r = mid - 1 : 这是 nums[mid] = target 的情况,如果不满足条件,那么就要把右边界向前移动一位,因为这个时候 nums[mid - 1] = nums[mid]

5 关于二分查找的边界问题

这是二分查找的两种形式,对比一下这两者的代码,它们只有三处不一样,解释在下方

int binarySearch(int[] nums, int target) {
    
    
    if (nums == null || nums.length == 0) return -1;
    int l = 0, r = nums.length - 1;
    while (l <= r) {
    
    
        int mid = l + (r - l) / 2;
        if (nums[mid] == target) {
    
    
            return mid;
        } else if (nums[mid] < target) {
    
    
            l = mid + 1;
        } else {
    
    
            r = mid - 1;
        }
    }
    return -1;
}
int binarySearch2(int[] nums, int target) {
    
    
    if (nums == null || nums.length == 0) return -1;
    int l = 0, r = nums.length;
    while (l < r) {
    
    
        int mid = l + (r - l) / 2;
        if (nums[mid] == target) {
    
    
            return mid;
        } else if (nums[mid] < target) {
    
    
            l = mid + 1;
        } else {
    
    
            r = mid;
        }
    }
    return -1;
}

在这里插入图片描述
对于 r 的取值,取 nums.length - 1 还是 nums.length 似乎是个很难抉择的问题,但是事实上来说,这两个值都是可以的,只不过这和 跳出循环r 的移动 有着密切的关系,也常常是在这种地方陷入疯狂挠头的处境,也就是这种情况,感觉一定不是自己的问题,头发也日渐减少…


如果取值是 l = 0, r = nums.length - 1

1、 取值范围是什么?

我们的取值区间的两个端点都是闭区间,也就是 [ l, r ],左闭右闭,lr 的值都可以取得到

2、 l 能不能等于 r

这个时候如果出现 l = r 的情况是没有任何问题的,因为 [ r, r ] 是有意义的

3、 什么时候退出循环?

闭区间 [ l, r ],如果 l = r,这个时候是一个临界值,当 l > r 时,我们应该退出循环

4、 关于 r = mid - 1

第一次的查询范围是[ l, r],判断mid之后,应该变为[ l, mid -1 ]或者[ mid + 1, r ],r总会指向需要进行判断的元素。我们当前判断的是mid,mid判断完成下一步要找的就是下一个要判断的区间。


如果取值是 l = 0, r = nums.length

1、 取值范围是什么?

这和前面得那种情况不一样,r得取值是 nums.length ,这个值我们是取不到的,所以这个时候区间是左闭右开得,也就是 [ l, r )

2、 l 能不能等于 r

这个时候如果出现 l = r 的情况,[ r, r ) ,既包含 r ,又不包含 r ,这个时候是没有意义的,所以 l 不能等于 r

3、 什么时候退出循环?

左闭右开区间 [ l, r ),如果 l = r,这个时候是没有意义的,这个时候的临界值是 l = r - 1,所以,当 l = r 时,我们应该退出循环

4、 关于 r = mid

第一次的查询范围是 [ l, r),判断 mid 之后,应该变为 [ l, mid ) 或者 [ mid + 1, r ) ,关于为什么是 [ l, mid ) ,我们这个时候的 mid 位置的元素是不会访问的,r指向的是 最后一个元素的后一个位置,其实 [ l, mid ) 就相当于 [ l, mid - 1 ]

6 写在最后

文章可能略有粗糙,有些内容也许表达的不是很清楚,如果有争议的地方可以提出来一起讨论一下~

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq1515312832/article/details/105846202