二分查找详解(Java版)

 

目录

一.什么是二分查找?

二.二分查找的实现:

三.二分查找的进阶算法:

1.寻找左侧重复元素的索引值:

2.寻找元素有序排列的插入位置:

目的:

实现:

 举例:

细节:

3.左闭右开区间的二分查找处理:

理解:

细节:

总结:


一.什么是二分查找?

二分查找算法是一种高效的搜索算法,适用于 有序数组。它通过将查找范围不断减半,逐步缩小目标值的搜索空间,最终找到目标元素或确认目标不存在。

二分查找是解决有序数组查找问题的高效算法。

原理:

  • while 循环不断调整搜索范围,直到找到目标值或搜索范围为空。
  • 从数组的中间元素开始比较:
    • 如果中间元素是目标值,搜索结束。
    • 如果目标值小于中间元素,则搜索范围缩小到数组的左半部分。
    • 如果目标值大于中间元素,则搜索范围缩小到数组的右半部分。
  • 重复上述步骤,直到找到目标值或搜索范围缩小到零。

时间复杂度:

  • O(log n),因为每次都将搜索范围缩小了一半。

适用条件:

  • 数据必须是有序的。
  • 只适合静态数组,即在搜索过程中数组不会发生变化。

二.二分查找的实现:

按照上面的原理叙述,首先保证有序数组,不断利用循环算出左右两边界索引值的中间索引值,然后与目标值进行比较大小以此来不断压缩,最终找到目标值。

左闭右闭区间的细节:

  1. i <= j,因为是左闭右闭区间,需要保证边界索引值查找的数能查到的,有效的,所以需要在 i = j 的情况查找一次来判断目标值。
  2. int mid = (i + j) >>> 1,在计算两个边界索引值的中间索引值需要使用 Java 的 >>> 无符号右移运算符(高位补0,不考虑符号位),是为了避免在某些情况下当 ij 都很大时,加法超出 int 的表示范围导致溢出。
  3. if 语句判断尽量全写 < 以此来满足正常的数组遍历思维。
  4. 找不到目标值需要返回 -1 来确认。
//O(log n)
//向下取整
public class BinarySearchOne {
    //全写小于号以便于与数组查找思维一致
    public static int binarySearch(int[] arr, int key) {
        int i = 0, j = arr.length - 1;//左闭右闭
        while (i <= j) { //i与j重合也要继续查一次
            int mid = (i + j) >>> 1;  // int mid = (i + j) / 2  使用>>>1(无符号右移)是为了防止超出正整数表示范围导致结果为负
            if(arr[mid] < key){
                i = mid + 1;
            }else if(key < arr[mid]){
                j = mid - 1;
            }else{
                return mid;
            }
        }
        return -1;
    }
    //main
    public static void main(String[] args) {
        int[] a = {1,2,3,4,5,6,7,8,9,10};
        System.out.println(binarySearch(a, 10)); //输出:9
    }
}

三.二分查找的进阶算法:

对于二分查找,有很多的特例情况需要通过不同场景来分析加以使用。 

1.寻找左侧重复元素的索引值:

我们在上面最初的二分查找的基础上加以改进,设置第三变量 candidate 来记录最左侧重复索引值的位置,为了让找不到目标值的情况返回 -1 所以 candidate 的初始值设置为 -1,如果当 arr[mid] = target 的情况下出现,使用 candidate 来记录当前位置值,然后缩小 j 的边界位置,最后当 i > j 跳出循环返回 candicate 的值就可以找到最左侧重复元素索引值了。

public class BinarySearchLeftMost {
    //遇到重复元素找到最左侧的元素索引
    public static int binarySearchLeftMost(int[] arr,int target) {
        int i = 0, j = arr.length - 1;
        int candidate = -1;//如果找不到就会返回-1结果
        while(i <= j){
            int mid = (i + j) >>> 1;// int mid = (i + j) / 2  使用>>>1(无符号右移)是为了防止超出正整数表示范围导致结果为负
            if(arr[mid] < target){
                i = mid + 1;
            }else if(target < arr[mid]){
                j = mid - 1;
            }else{
                candidate = mid;//使用第三方变量记录当前找到的元素位置
                j = mid - 1;//继续查找是否还有最左侧的重复元素
            }
        }
        return candidate;
    }

    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6,7,7,7,7,8,9};
        System.out.println(binarySearchLeftMost(arr,7));//输出6
    }
}

2.寻找元素有序排列的插入位置:

上面我们讲述了在最左侧寻找重复元素的索引值,但是如果找不到目标值,结果会返回 -1,如果我们想要实现寻找到元素有序排列的插入位置,我们可以在上面的寻找左侧重复元素的索引值的基础上进行改动,让其找不到目标值返回当前可以插入的位置

目的:

有序数组 中查找一个元素 target,并返回它的 最左侧位置。如果数组中存在重复的 target,它会返回最左侧那个元素的索引。如果没有找到 target,则返回 target 应该插入的位置,这个位置可以被视为 target 在数组中的排名。

实现:

  • if (target <= arr[mid]):如果 target 小于等于中间元素 arr[mid],那么我们要缩小搜索范围到数组的左侧,因为我们想找到最左边的那个元素(即便 targetarr[mid] 相等,我们也要继续往左找)。所以更新右边界 j = mid - 1。(这个部分也就是上面的部分代码的合并)
  • else:如果 target 大于 arr[mid],说明目标元素只可能在数组的右侧,所以更新左边界 i = mid + 1

 举例:

假设数组是 arr = [1, 3, 5, 7]target = 4(找不到)

  • 第一次 mid = 1arr[mid] = 3target > arr[mid],所以 i = mid + 1 = 2
  • 第二次 mid = 2arr[mid] = 5target <= arr[mid],所以 j = mid - 1 = 1
  • 循环结束,返回 i = 2,表示 4 不存在数组中,但可以插入到索引 2 的位置。

细节:

注意使用左边界记录位置并返回,因为当 i = j 的情况出现, j 是在此基础上做减法来向前推进边界索引值,而 i 是不在改变原本位置。

public class BinarySearchLeftPlus {
    //遇到重复元素找到最左侧的元素索引,找不到的情况下可以获得当前target所在排名
    public static int binarySearchLeftMost(int[] arr,int target) {
        int i = 0, j = arr.length - 1;
        while(i <= j){
            int mid = (i + j) >>> 1;// int mid = (i + j) / 2  使用>>>1(无符号右移)是为了避免在某些情况下当 i 和 j 都很大时,加法超出 int 的表示范围导致溢出。
            if(target <= arr[mid]){
                j = mid - 1;
            }else{
                i = mid + 1;
            }
        }
        return i;
    }

    public static void main(String[] args) {
        int[] arr = {1,2,3,4,6,7,7,7,7,8,9};
        System.out.println(binarySearchLeftMost(arr,7));//输出5
        System.out.println(binarySearchLeftMost(arr,10));//输出11
        System.out.println(binarySearchLeftMost(arr,5));//输出4
    }
}

3.左闭右开区间的二分查找处理:

理解:

我们可以将这种区间理解成因为 j 是开区间的边界,以至于不可以将这个边界的索引值作为有效值去参与比较,所以我们如果要是出现 i = j 这种情况会陷入死循环,并且当 target < arr[mid] 这种情况出现的时候,不可以让 j = mid - 1,这样会漏掉 mid - 1 索引位置的元素。

细节:

  1. i < j,因为j是不参与比较,如果要是i=j这种情况发生会陷入死循环。
  2. j = mid,如果是 mid - 1 会导致因为右侧开区间计算 mid 后 mid - 1 会漏掉 mid - 1 这个索引元素,因为j指向的索引元素不参与比较
public class BinarySearchRightOpen {
    //左闭右开查找区间段内的元素
    public static int binarySearch(int[] arr, int target) {
        int i = 0, j = arr.length;//理解:j指向的不参与比较,因为j目前指向是null
        while (i < j) { //因为j是不参与比较,如果要是i=j这种情况发生会陷入死循环
            int mid = (i + j) >>> 1;  // int mid = (i + j) / 2  使用>>>1(无符号右移)是为了防止超出正整数表示范围导致结果为负
            if(target < arr[mid]) {
                j = mid;//如果是mid - 1会导致因为右侧开区间计算mid后mid - 1会漏掉mid - 1这个索引元素,因为j指向的索引元素不参与比较
            }else if(arr[mid] < target) {
                i = mid + 1;
            }else{
                return mid;
            }
        }
        return -1;
    }
    //main
    public static void main(String[] args) {
        int[] a = {1,2,3,4,5,6,7,8,9,10};
        System.out.println(binarySearch(a, 10)); //输出:9
    }
}

总结:

二分查找是一个优秀的查找算法,能够高效的对有序数组进行查找指定元素位置,以至于我们在这种对于有序数组快速定位元素位置可以使用二分查找算法。但是 Java 有自己封装好的二分查找算法:Arrays.binarySearch(arrayList,target)

这个内部封装好的方法除了会找到目标值位置,如果在有序数组内没有找到目标值,那么就会按照下面的公式进行返回值:- (插入点 + 1),所以我们可以通过这个返回时来反向算出目标值插入位置。

import java.util.Arrays;

public class BinarySearchSecond {

    public static void main(String[] args) {
        int target = 1;
        int[] arrayList = {1,2,3,4,5,6,7,8};
        int i = Arrays.binarySearch(arrayList,target);
        System.out.println(i);//如果找不到就会按照-(插入点+1)返回,也可以通过这个找到想要插入的不重复数字的位置
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

记得开心一点嘛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值