二分查找详尽学习

二分查找

前言

跟着网课重新开始从头学习二分查找,发现原来二分查找也有这么多的变种与应用,写下了自己的学习笔记,如果有错误的地方,还请见谅。

二分查找基础版

二分查找(Binary Search)是一种在有序数组中查找特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是目标值,则搜索过程结束;如果目标值大于或小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且同样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

二分查找的基本步骤如下:

  1. 初始化左指针 left 指向数组的第一个元素,右指针 right 指向数组的最后一个元素。
  2. 计算中间索引 mid,通常使用 (left + right) / 2(整数除法)或 (left + right) >>> 1(无符号右移一位,等价于整数除法)。
  3. 如果 mid 对应的元素等于目标值 target,则搜索结束,返回 mid
  4. 如果 mid 对应的元素大于目标值 target,则在数组的左半部分(leftmid - 1)继续搜索,更新 right = mid - 1
  5. 如果 mid 对应的元素小于目标值 target,则在数组的右半部分(mid + 1right)继续搜索,更新 left = mid + 1
  6. 重复步骤 2-5,直到找到目标值或搜索范围为空(left > right)。
  7. 如果搜索范围为空,则目标值不存在于数组中,返回 -1 或其他表示未找到的值。
 public int search(int[] nums, int target) {
        int i = 0;
        int j = nums.length-1;
        while(i<=j){
            int m = (i+j)/2;
            if(target<nums[m]){
                j=m-1;
            }else if(nums[m]<target){
                i=m+1;
            }else{
                return m;
            }
        }
        return -1;
    }

无符号右移运算

​ 在Java语言中,会把二进制数字的首位看作是符号位,即当首位是0时,为正数,当首位为1时,为负数,这也是为什么当一个运算超出了Integer的最大值时,打印结果会是一个负数的原因,因此会采用无符号右移运算来避免这种情况,无符号右移运算一位简单来讲就是对这个数除以二,因为当一个二进制的偶数右移一个单位,所得结果就是这个数除以2,奇数就是对除以二以后结果进行取整。

​ 举个例子,如果我们有一个8位的二进制数 11001100(十进制中为204),将其无符号右移两位,操作如下:

​ 原始数值:11001100
​ 右移两位:00110011

​ 左边空出的两位用零填充,得到新的二进制数 00110011(十进制中为51)。

​ 无符号右移运算通常用于处理无符号整数(即只能为正数的整数),或者在需要保持位运算结果始终为正数的情况下使用。在一些编程语言中,比如Java,无符号右移是特别为处理32位整数设计的,因为在Java中,整数都是有符号的,但通过无符号右移,可以在某种程度上模拟无符号整数的行为。

​ 需要注意的是,并不是所有编程语言都支持无符号右移运算符。在一些不支持无符号右移的语言中,可能需要通过其他方式(如逻辑右移和算术右移的组合)来模拟无符号右移的效果。此外,在使用无符号右移时,也要考虑到不同数据类型和平台可能存在的差异,以确保代码的正确性和可移植性。

​ 无符号右移运算的实际应用:

public class test1 {
    public static void main(String[] args) {
        int target = 5;
        int[] arr = {1,2,3,4,5,6};
        //设置指针和初值
        int i =0;
        int j = arr.length-1;
        while(i<=j){
            int mid = (i+j)>>>1;
            if (arr[mid]==target){
                System.out.println(mid);
                return;
            }
            else if (arr[mid]>target){
                j=mid-1;
            }
            else if (arr[mid]<target){
                i=mid+1;
            }
        }
        System.out.println("-1");



    }
}

​ 在上述代码中,表示的是一个二分查找的模型,对于mid的值,如果这个数组特别大,就有可能出现数据溢出的情况,采取无符号右移一位,也能表示除以二的效果。

二分查找改动版

​ 在上面的二分查找代码中,循环条件必须为 i<=j,但是可以对代码进行一些改动,使得循环条件可以变为 i<j,代码如下。

public class test1 {
    public static void main(String[] args) {
        int target = 3;
        int[] arr = {1,2,3,4,5,6};
        int i =0;
        //第一处改动
        int j = arr.length;
        //第二处改动
        while(i<j){
            int mid = (i+j)>>>1;
            if (arr[mid]==target){
                System.out.println(mid);
                return;
            }
            else if (arr[mid]>target){
                //第三处改动
                j=mid;
            }
            else if (arr[mid]<target){
                i=mid+1;
            }
        }
        System.out.println("-1");
    }
}

​ 与上面代码相比较,明显可以看出,代码进行了三处改动,在基础版中,i和j不仅仅代表数组的边界,还有可能作为比较的值,但是在改动之后,j就不可能是比较的值了,所以当mid的值大于target的值时,j就应该取mid,而不是mid-1。

​ 那么为什么循环条件也要改成 i<j呢,因为根据上面我们可以知道,j只作为数组的边界,不再进入循环的比较中,如果不改为 i<j,那么在之后的代码中,会把j的值也带入到比较中,看起来似乎没有什么关系,好像无伤大雅,但是在某些情况下,会出现一些比较严重的错误。

​ 当使用二分查找时,查找一个数组中不存在的数,而循环条件写成了i<=j,那么就会使得循环一直进行下去,进入一个死循环。因为当我们查找一个不存在的值时,最终i和j的值会相等,但如果循环条件为i<=j,如果在mid的右边,那j仍然等于mid,没有改变,但又一直查找不到这个数,就会陷入死循环。

二分查找平衡板

​ 在上述的二分查找代码中,我们可以了解到,查找元素所在位置不同,那么所对应的比较次数也不同,那么在这里设计一个比较平衡的查找方法,代码如下

 public static int serch(int [] a,int target){
        int i = 0;
        int j = a.length;
//        j-i表示j和i之间的数据个数,当数据个数小于1时,由于j不可能是参
//        与比较的数据,那么就只剩一个i元素,此时跳出循环,把a[i]的值与target目标值进行比较。
        while(1<j-i){
            int m = (i+j)>>>1;
            if (target < a[m]){
                j=m;
            }
            else{
                //i的边界也要进行修改,因为中间值也要参与比较
                i=m;
            }
        }
        if (a[i] == target){
            return i;
        }
        else{
            return -1;
        }
    }

​ 可以看到,上述代码进行了一些改进,在while循环中只比较一次,知道i与j之间的元素仅剩一个,此时跳出循环,把a[i]的值与target的目标值进行比较,查找成功返回数组下标,查找失败则返回-1。循环内的平均比较次数减少了

​ 缺点:时间复杂度无论何种情况都是O(log(n)),不存在O(1)的情况了。

二分查找Java版

​ 学完了二分查找的几种形式,下面可以了解一下在Java中,二分查找的实现。查看Arrays下的binarySearch方法。

在这里插入图片描述

​ 可以看到这个binarySearch方法还调用了一个binarySearch0方法,并且参数也变多了。查看这个binarySearch0方法。

在这里插入图片描述

​ 不难看出,四个参数所代表的意义,fromIndex代表起始指针,toIndex代表截至指针,key代表要查找的元素。可以看出,这个方法底层是用的基础版的二分查找。

在这里插入图片描述

查看这个binarySearch的文档,翻译一下return值的含义

搜索键的索引(如果它包含在数组中);否则为 (-(插入点) - 1)。插入点定义为将键插入到数组中的点:第一个元素的索引大于键,如果数组中的所有元素都小于指定的键,则为 a.length。请注意,这保证了当且仅当找到键时,返回值将 >= 0。

​ 那么插入点是什么意思呢,举个例子,有一个数组a={2,5,8},我此时想要查找4这个数字,那么我调用这个方法之后,所得的结果是-2,为什么是-2呢,根据帮助文档可知,查找失败后,结果为-2=-(插入点) - 1,可以得到插入点为1,也就是如果把查找元素插入到这个数组里,那么插入的数组下标为1.

​ 对于基础版的二分查找,谁可以代表插入点呢,经过分析可知,在基础班的二分查找中,i可以代表插入点。因此,在Java底层代码中,low就代表了插入点。

​ 那为什么要加上-1呢,因为如果插入点是0,返回一个-0,0和-0是区分不出来的,那是查找到了0索引的数,还是插入点是0呢

这时候区分不出来,所以加上一个-1,就可进行区分了。

​ 如果要打印插入之后的数组,该如何进行操作,这时候可以用到System.arraycopy。

System.arraycopy

System.arraycopy 是 Java 中的一个原生方法,用于将一个源数组的部分或全部元素复制到另一个目标数组中。这个方法通常比手动复制数组元素更快,因为它是一个底层操作,可以直接在内存中进行数据传输。

方法签名如下:

java

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

参数说明:

src:源数组,即从中复制元素的数组。

srcPos:源数组中的起始位置,从这个位置开始复制元素。

dest:目标数组,即将元素复制到的数组。

destPos:目标数组中的起始位置,从这个位置开始放置复制的元素。

length:要复制的元素数量。

public class test {
    public static void main(String[] args) {
        int[] a = {2,5,8};
        int insert = 1;
        int[] b = new int[4];
        System.arraycopy(a,0,b,0,insert);
        b[insert]=4;
        System.arraycopy(a,insert,b,insert+1,a.length-insert);
        System.out.println(Arrays.toString(b));
    }
}

示例代码如上所示。

LeftRightmost

​ 在使用二分查找进行查找运算时,需要满足一个前提条件,也就是查找的数组必须是有序的,那如果在数组中有重复的元素,在使用二分查找时,只会返回一个数据元素,但是这个位置是不固定的,取决于那个元素被最先访问到,如果想要查找最左边的元素,那就需要对代码进行一些改动。

​ 改动也很简单,不需要什么大改动,只需要对返回数据那一处的代码进行修改

public static int Leftmost(int [] a,int target){
    int i = 0,j=a.length-1;
    //设定一个候选元素,初值为-1,这样当没有查到时,直接返回-1
    int candidates = -1;
    while(i<=j){
        int m = (i+j)>>>1;
        if(target<a[m]){
            j=m-1;
        }else if (a[m]<target){
            i=m+1;
        }else{
            //当找到元素时,把下标赋值给候选元素,因为要查找最左边元素,就缩小j的范围,看在这个值的左边还有没有符合条件的元素值
            candidates = m;
            j=m-1;
        }
    }
    return candidates;
}

​ 由上述代码可以看到,改动并不大,接下来写一个测试用例来测试代码是否可行。

@Test
public void testLeftmost(){
    int[] a = {1,2,4,4,4,5,6,7};
    assertEquals(0,Leftmost(a,1));
    assertEquals(1,Leftmost(a,2));
    assertEquals(2,Leftmost(a,4));
    assertEquals(5,Leftmost(a,5));
    assertEquals(6,Leftmost(a,6));
    assertEquals(7,Leftmost(a,7));
    assertEquals(-1,Leftmost(a,0));
    assertEquals(-1,Leftmost(a,3));
    assertEquals(-1,Leftmost(a,8));
}

​ 运行代码后可以看到代码时正确的,那有找最左边的元素,与之对应就有找最右边的元素,那此时只需要把找到符合条件元素时代码从j=m-1改为i=m-1,这样就可以找到最右边的元素。

​ 上述代码虽然能找到最左或最右边的数据元素位置,但如果没找到的话,直接返回-1,-1这个值没有什么实际意义,那对代码进行修改,使得代码在查找失败时,返回一个有意义的数据。

public static int Leftmost(int [] a,int target){
    int i = 0,j=a.length-1;
    while(i<=j){
        int m = (i+j)>>>1;
        if(target<=a[m]){
            j=m-1;
        }else if(a[m]<target){
            i=m+1;
        }
    }
    return i;
}

​ 进行上述修改后,在查找失败时,返回的i值就是大于target的最靠左的索引,同理,对最右查找也进行类似修改。

public static int Rightmost(int [] a,int target){
    int i = 0,j=a.length-1;
    while(i<=j){
        int m = (i+j)>>>1;
        if(target<a[m]){
            j=m-1;
        }else if(a[m]<=target){
            i=m+1;
        }
    }
    return i-1;
}

​ 此时代码进行修改后,在查找失败时,返回的值就是小于target的最靠右索引。

力扣真题演练

学完上面的各种二分查找,接下来做三道力扣题来实战一下。

第一题

在这里插入图片描述

这道题就是直接用简单的二分查找,三种版本都可以用。

//二分查找基础版
class Solution {
    public int search(int[] nums, int target) {
        int i = 0;
        int j = nums.length-1;
        while(i<=j){
            int m = (i+j)>>>1;
            if(target<nums[m]){
                j=m-1;
            }else if(nums[m]<target){
                i=m+1;
            }else{
                return m;
            }
        }
        return -1;
    }
}
//二分查找改动版
class Solution {
    public int search(int[] nums, int target) {
        int i = 0;
        int j = nums.length;
        while(i<j){
            int m = (i+j)>>>1;
            if(target<nums[m]){
                j=m;
            }else if(nums[m]<target){
                i=m+1;
            }else{
                return m;
            }
        }
        return -1;
    }
}
//二分查找平衡板
class Solution {
    public int search(int[] nums, int target) {
        int i = 0;
        int j = nums.length;
        while(j-i>1){
            int m = (i+j)>>>1;
            if(target<nums[m]){
                j=m;
            }else{
                i=m;
            }
        }
        if(target==nums[i]){
            return i;
        }else{
            return -1;
        }
    }
}

第二题

在这里插入图片描述

//直接使用基础版的二分查找,只不过把返回值切换为i,即插入点
class Solution {
    public int searchInsert(int[] nums, int target) {
        int i = 0;
        int j = nums.length-1;
        while(i<=j){
            int m = (i+j)>>>1;
            if(target<nums[m]){
                j=m-1;
            }else if(nums[m]<target){
                i=m+1;
            }else{
                return m;
            }
        }
        return i;
    }
}

第三题

在这里插入图片描述

class Solution {
    public int[] searchRange(int[] a, int target) {
        if (left(a,target)==-1){
            return new int[] {-1,-1};
        }else{
            return new int[]{left(a,target),right(a,target)};
        }
    }
    public int left(int[] a,int target){
        int i = 0;
        int j = a.length-1;
        int candidates = -1;
        while(i<=j){
            int m = (i+j)>>>1;
            if(target<a[m]){
                j=m-1;
            }else if(a[m]<target){
                i=m+1;    
            }
            else{
                 candidates=m;
                 j=m-1;
            }
        }
        return candidates;
    }
    public int right(int[] a,int target){
        int i = 0;
        int j = a.length-1;
        int candidates = -1;
        while(i<=j){
            int m = (i+j)>>>1;
            if(target<a[m]){
                j=m-1;
            }else if(a[m]<target){
                i=m+1;
            }else{
                candidates=m;
                i=m+1;
            }
        }
        return candidates;
    }
}
  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值