zoukankan      html  css  js  c++  java
  • 左右指针法:二分查找-寻找数

    左右指针法:二分查找-寻找数

    二分查找多用于数组和字符串之中。
    二分查找原理简单但是细节很容易出错。

    0.二分查找的框架

    int binarySearch(int[] nums, int target) {
        int left = 0, right = ...;
    
        while(...) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                ...
            } else if (nums[mid] < target) {
                left = ...
            } else if (nums[mid] > target) {
                right = ...
            }
        }
        return ...;
    }
    

    分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。
    计算 mid 时需要防止溢出,代码中left + (right - left) / 2就和(left + right) / 2的结果相同,但是有效防止了leftright太大直接相加导致溢出。

    1.寻找一个数(最基本的二分查找)

    搜索一个数,如果存在,返回其索引,否则返回 -1

    int binarySearch(int[] nums, int target) {
        int left = 0; 
        int right = nums.length - 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;
    }
    

    为什么 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 即可。

    为什么left = mid + 1right = mid - 1?我看有的代码是right = mid或者left = mid,没有这些加加减减,到底怎么回事,怎么判断?
    刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即[left, right]。那么当我们发现索引mid不是要找的target时,下一步应该去搜索哪里呢?当然是去搜索[left, mid-1]或者[mid+1, right]对不对?因为mid已经搜索过,应该从搜索区间中去除

    此算法有什么缺陷
    比如说给你有序数组nums = [1,2,2,2,3]target为 2,此算法返回的索引是 2,没错。但是如果我想得到target的左侧边界,即索引 1,或者我想得到target的右侧边界,即索引 3,这样的话此算法是无法处理的。这样的需求很常见,也许会说,找到一个 target,然后向左或向右线性搜索不行吗?可以,但是不好,因为这样难以保证二分查找对数级的复杂度了。

    2.寻找左侧边界的二分法完整代码如下:

    int left_bound(int[] nums, int target) {
        int left = 0, right = nums.length - 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;
            }
        }
        // 检查出界情况
        if (left >= nums.length || nums[left] != target)
            return -1;
        return left;
    }
    

    这里左侧边界的含义:
    比如对于有序数组nums = [2,3,5,7],target = 1,算法会返回 left=0,含义是:nums中小于 1 的元素有 0 个。
    再比如说nums = [2,3,5,7], target = 8,算法会返回 left=4,含义是:nums`中小于 8 的元素有 4 个。
    left即表示那个index值,也表示有多少比target小,这里可能会带来索引越界的问题。

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

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

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

    为什么要检查出界情况:
    由于 while 的退出条件是left == right + 1,所以当targetnums中所有元素都大时,会存在以下情况使得索引越界
    越界条件之中有nums[left] != target,是表示找不到这个数返回-1。

    3.寻找右侧边界的二分法

    int right_bound(int[] nums, int target) {
        int left = 0, right = nums.length - 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;
            }
        }
        // 这里改为检查 right 越界的情况,见下图
        if (right < 0 || nums[right] != target)
            return -1;
        return right;
    }
    

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

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

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

    target比所有元素都小时,right会被减到 -1,所以需要在最后防止越界

  • 相关阅读:
    Python学习笔记(三)
    自己出的一套前端笔试题
    Vue 数组封装和组件data定义为函数一些猜测
    前端Mvvm QC 上传了测试版
    为什么我们的web前端变的越来越复杂
    grootJsAPI文档
    grootjs 简明教程
    深入grootJs(进阶教程)
    也议 js闭包和ie内存泄露原理
    此utf8 非彼utf8 ——谈http协议里的编码问题
  • 原文地址:https://www.cnblogs.com/shiji-note/p/14394796.html
Copyright © 2011-2022 走看看