首页 > 其他分享 >二分查找细节分析

二分查找细节分析

时间:2023-12-17 18:05:22浏览次数:24  
标签:二分 return target nums int mid 细节 查找 length

二分查找细节分析

本篇仅分析二分查找的细节问题,在阅读前请确保已经对“二分查找”概念与步骤有初步了解。

二分查找的三个常用搜索区间

搜索区间 终止条件 左右指针初始赋值 左右指针赋值 循环条件
左闭右闭[l,r] 相错终止 l=0 r=nums.length-1 l = mid+1 r = mid-1 l<=r
左闭右开[l,r) 相交终止 l=0 r=nums.length l = mid+1 r = mid l<r
左开右开(l,r) 相邻终止 l=-1 r=nums.length l = mid r = mid l+1<r

还有左开右闭等搜索区间,在此不再赘述。

LeetCode704.二分查找为例:

左闭右闭

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

左闭右开

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

左开右开

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

二分查找变体

查找左边界

左边界是指在有序数组中,被搜索数的第一次出现下标

例如,在数组[5,7,7,8,8,10]中,7的左边界是1,8的左边界是3。

而根据搜索区间,我们可以得到上述三种写法的查找左边界。

下面以左闭右闭为例:

private int getLeft(int[] nums, int target) {
    int l = 0;
    int r = nums.length - 1;
    // int leftBoard = -2;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] < target) {
            l = mid + 1;
        } else {
            r = mid - 1;
            // leftBoard = r;
        }
    }
    // 检查出界情况
    if (l >= nums.length || nums[l] != target) {
        return -1;
    }
    return l;
    // return leftBoard;
}

对于左右边界,在这里有个核心问题:为什么当nums[mid] >= target时,r = mid - 1

从搜索区间来看,每次搜索时,我们都在搜索闭区间[l, r]。如果nums[mid]在>=target时,mid不一定是左边界,此时要迁移右指针,把mid排除出搜索区间。

根据相错终止的思路,最终l会停留在r的右侧,即l = r + 1,如果nums[mid] == target时已经是左边界,那么右指针将不会再挪动,在循环终止时根据l的位置,即可断言:l是左边界。

相应的,可以推理如果写判断条件nums[mid] == target时,l = mid + 1,此时l必定会越过左边界。所以我们要根据右指针返回结果:

private int getLeft(int[] nums, int target) {
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        // 检查出界情况
        if (r + 1 == nums.length || nums[r + 1] != target) {
            return -1;
        }
        return r + 1;
    }

但此种方法过于繁琐,所以使用l返回即可。

左闭右开写法:

private int getLeft(int[] nums, int target) {
        int l = 0;
        int r = nums.length;
        while (l < r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] < target) {
                l = mid + 1;
            } else {
                r = mid;
            }
        }
        // 检查出界情况
        if (l >= nums.length || nums[l] != target) {
            return -1;
        }
        return l;
    }

查找右边界

相似地,我们也可以反向推得到右边界写法:

左闭右闭写法:

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

左开右闭写法:

private int getRight(int[] nums, int target) {
        int l = 0;
        int r = nums.length;
        while (l < r) {
            int mid = l + (r - l) / 2;
            if (nums[mid] <= target) {
                l = mid + 1;
            } else {
                r = mid;
            }
        }
        //  注意:因为这里是左闭右"开" 所以r指针指向的并不是实际的位置,r-1才是。
        if (r == 0 || nums[r - 1] != target) {
            return -1;
        }
        return r - 1;
    }

查找插入位置

LeetCode.35搜索插入位置为例:

搜索插入位置实际上和查找左边界非常类似,如果能搜索到刚好等于target的mid,自然最好,搜索到的位置就是插入位置。

左闭右闭写法:

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

左闭右开写法:

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

中指针整型溢出问题

中指针溢出问题指的是在静态类型语言下,mid = (l + r) / 2可能会造成int溢出,本质上是l + r大小溢出。

在JDK下,该bug存在了9年之久。

一般有两种解决方案:

  • 改写为先减再加的方式:mid = l + (r - l) / 2。可以避免l + r溢出。
  • 右移替代除法,效率会高一点点:mid = l + ((r - l) >> 1)

特别的,在JDK中是这么写的:mid = (l + r) >>> 1

>>>是无符号右移运算符,与>>的区别是不考虑符号位,总是往左侧补0。l + r溢出的时候最高符号位从0变成了1,而用>>>又变成了0,所以可以解决溢出问题。

但是用这种写法要保证l + r >= 0,如果l + r为负数,高位补0就会得到错误的正数。

在一般情况下,l与r代表下标,相加恒为正数,但部分题目的搜索的不是下标,而是数值。l与r可能代表的是数值,相加可能为0。

例如:LeetCode.462最小操作次数使数组元素相等II

练习题

二分查找

标签:二分,return,target,nums,int,mid,细节,查找,length
From: https://blog.51cto.com/ErickRen/8862411

相关文章

  • [LeetCode] LeetCode373. 查找和最小的K对数字
    题目描述思路:大顶堆+翻转注意:该题有问题,代码可以通过测试用例。方法一:classSolution{publicList<List<Integer>>kSmallestPairs(int[]nums1,int[]nums2,intk){PriorityQueue<Node>heap=newPriorityQueue<>((e1,e2)->e2.sum-e1.sum);......
  • linux查找文件
    linux查找文件常用的有find和whereis两种方式.find适用于复杂的查询,指定目录和文件名,通常可以找到你想要的文件.不要指定从根目录开始找,与其这样不如先推测一下这个文件可能在什么地方.whereis通常用来定位二进制文件,帮助文件,源码文件,默认情况下是在包管理......
  • 二分图最大匹配和二分图最大权完美匹配
    二分图最大匹配和二分图最大权完美匹配二分图最大匹配题目描述对于一个二分图,求其最大匹配的边数(任意一个点只能匹配另一个点)算法解析使用匈牙利算法。遍历每一个左部点,若发现它所连到的右部点中有未被匹配的点就选择连接;若右部点已被匹配,就询问匹配该右部点的点能否更换至另......
  • 旋转数组 二分查找变种
    题目搜索旋转排序数组整数数组nums按升序排列,数组中的值互不相同。在传递给函数之前,nums在预先未知的某个下标k(0<=k<nums.length)上进行了旋转,使数组变为[nums[k],nums[k+1],...,nums[n-1],nums[0],nums[1],...,nums[k-1]](下标从0开始计数)。例如,[0,1,2,......
  • 洛谷P1824 进击的奶牛 题解 二分答案
    题目链接:https://www.luogu.com.cn/problem/P1824题目大意:本题相当于在\(n\)个数中选\(c\)个数,使得这\(c\)个数中相差最小的两个数之差尽可能地大。解题思路:我们首先可以给\(a_1\sima_n\)从小到大排一下序(这里有点贪心的思想,你会发现很多涉及贪心的问题在排序之后解......
  • python二分类模型精度低怎么办
    在二分类模型中,如果模型的精度较低,可能需要采取一些措施来改进模型性能。本文将介绍一些常见的方法和技巧,帮助提高二分类模型的精度。1.数据预处理确保对数据进行适当的预处理是提高模型精度的重要步骤。常见的数据预处理方法包括:-数据清洗:处理缺失值、异常值等。-特征选择:选择对目......
  • 线性探测法的查找函数 整型关键字的散列映射
    一、 实验目的掌握哈希表二、  实验内容实验题目线性探测法的查找函数整型关键字的散列映射  三、   设计文档 1.   2.   四、   源程序  1. PositionFind(HashTableH,ElementTypeKey){    intflag=0;    Positionp......
  • 线性探测法的查找函数
    #include<stdio.h>#defineMAXTABLESIZE100000/*允许开辟的最大散列表长度*/typedefintElementType;/*关键词类型用整型*/typedefintIndex;/*散列地址类型*/typedefIndexPosition;/*数据所在位置与散列地址是同一类型*//*散列单元状态类......
  • 查找列表(表格的列名)是否包含某些列名字符串
    lis=["非关键词","关键词1","/s关键词2/s","重复的关键词1"]keywords=["关键词1","关键词2","关键词3"]result={}foriinkeywords:find=Falseforj,kinenumerate(lis):ifnotfind:......
  • 特殊字符:安全攻防中容易遗漏的细节
    本文分享自华为云社区《【安全攻防】深入浅出实战系列专题-特殊字符校验》,作者:MDKing。特殊字符校验的背景SQL注入、XSS等常见的安全攻击场景会涉及到一些特殊字符的利用。尤其是界面输入框、API接口可支持输入字符串的情况,如果对来历不明的用户输入如果不加防范,很容易产生......