探讨jdk源码中的二分查找算法返回值巧妙之处

news2024/11/26 5:30:16

文章目录

  • 1.什么是二分查找算法
    • 1.1 简介
    • 1.2 实现思路
  • 2.二分查找的示例
  • 3.jdk 中的 Arrays.binarySearch()
  • 4.jdk 中核心二分查找方法解析
    • 4.1 为什么 low 是插入点
    • 4.2 为什么要进行取反:-(low + 1)
    • 4.3 为什么不直接返回 插入点 low 的相反数,还需要进行 +1 操作
    • 4.4 可以将 +1 改为 -1 吗
  • 5.未找到目标元素时根据返回值进行数组扩容

1.什么是二分查找算法

1.1 简介

二分查找算法,也称 折半查找 算法,是一种在 有序数组 中查找某一特定元素的搜索算法。

1.2 实现思路

  1. 初始状态下,将整个序列作为搜索区域。
  2. 找到搜索区域内的中间元素,和目标元素进行比对。
    • 如果相等,则搜索成功;
    • 如果中间元素大于目标元素,表明目标元素位于中间元素的左侧,将左侧区域作为新的搜素区域;
    • 反之,若中间元素小于目标元素,表明目标元素位于中间元素的右侧,将右侧区域作为新的搜素区域;
  3. 重复执行第二步,直至找到目标元素。如果搜索区域无法再缩小,且区域内不包含任何元素,则表明整个序列中没有目标元素,查找失败。

2.二分查找的示例

/**
 * 二分查找(升序数组版)
 *
 * @param array       待查找的升序数组
 * @param targetValue 待查找的目标值
 * @return 找到则返回目标值的索引,找不到返回-1
 */
public static int binarySearch(int[] array, int targetValue) {
    // 左边界
    int left = 0;
    // 右边界
    int right = array.length - 1;

    int mid;

    while (left <= right) {
        /*
            考虑到 left+right 的值可能会超过 int可表示 的最大值,我们不再对他们的和直接除以2
            我们知道 除以2 的操作可以用 位运算 >>1 来代替
            但还不够,由于 (left+right) 值溢出表示负数,>>1 只是做 除以2 操作,最高位符号位不变,依旧为1表示负数,负数除以2依旧是负数
            这时候我们可以修改为 无符号右移 >>>1 ,低位溢出,高位补0,那么最高位符号位为0就表示正数了
         */
        mid = (left + right) >>> 1;

        if (targetValue < array[mid]) {
            // 如果查找的目标值比中间索引值小,则缩小查找的右边界
            right = mid - 1;
        } else if (array[mid] < targetValue) {
            // 如果中间索引值小于查找的目标值,则缩小查找的左边界
            left = mid + 1;
        } else {
            // 如果找到了,就返回目标索引
            return mid;
        }
    }

    // 退出了 while 循环,说明如果没找到,则返回 -1 表示未找到
    return -1;
}

3.jdk 中的 Arrays.binarySearch()

public class Arrays {
    public static int binarySearch(int[] a, int key) {
        return binarySearch0(a, 0, a.length, key);
    }

    private static int binarySearch0(int[] a, int fromIndex, int toIndex,
                                     int key) {
        int low = fromIndex;
        int high = toIndex - 1;

        while (low <= high) {
            int mid = (low + high) >>> 1;
            int midVal = a[mid];

            if (midVal < key)
                low = mid + 1;
            else if (midVal > key)
                high = mid - 1;
            else
                return mid; // key found
        }
        return -(low + 1);  // key not found.
    }
}

有了刚才二分查找的示例,其实 jdk 中二分查找方法的实现也几乎差不多,只是返回值与我们的示例不同。当目标值在数组中找不到时:

  • 我们的示例会返回 -1 作为找不到元素的标识;
  • 而 jdk 中是将 目标值的待插入点的变式 作为找不到元素的标识。这意味着我们可以对这个返回值做更多的事情,例如当目标值不存在时根据待插入点进行数组扩容,将目标值加入到数组中。

关于 jdk 中找不到目标值情况的返回值,我们举个例子来帮助更好理解什么是 目标值的待插入点的变式

  1. 已知数组 [2, 5, 8],待查找的目标值是 4;

  2. 很明显,在数组中并不存在 4,那么 jdk 中的二分查找算法返回值是 -2,那么根据 返回值 = -(插入点 + 1) 可以推导出插入点应该是 1,也就是数组索引为 1 的位置;

  3. 即,如果需要将未找到的目标值 4 插入到数组中,应该放在索引为 1 的位置,即索引为 0 的元素 2 的后面。

    索引0123
    原数组258
    新数组2458

4.jdk 中核心二分查找方法解析

/**
 * 使用二分搜索算法在指定的整数数组中搜索指定的值。在进行此调用之前,必须对数组进行排序(按方法排序 sort(int[]) )。
 * 如果未排序,则结果未定义。如果数组包含多个具有指定值的元素,则无法保证会找到哪个元素。
 * 参数:
 * a – 要搜索的数组
 * key – 要搜索的值
 * 返回:搜索键的索引(如果它包含在数组中);否则返回 -(插入点 + 1)。
 * 插入点定义为将键插入数组的 点 :第一个元素的索引大于键,如果数组中的所有元素都小于指定的键,则为 a.length 。
 * 请注意,这保证了当且仅当找到键时返回值将为 >= 0。
 */
public static int binarySearch(int[] a, int key) {
    return binarySearch0(a, 0, a.length, key);
}

// Like public version, but without range checks.
private static int binarySearch0(int[] a, int fromIndex, int toIndex,
                                 int key) {
    // 左边界
    int low = fromIndex;
    // 右边界
    int high = toIndex - 1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        int midVal = a[mid];

        if (midVal < key)
            low = mid + 1;
        else if (midVal > key)
            high = mid - 1;
        else
            return mid; // key found
    }

    // 返回 -(插入点 + 1)
    return -(low + 1);  // key not found.
}

我们现在需要思考两个问题:

  • 为什么 low 是插入点
  • 为什么要进行取反:-(low + 1)
  • 为什么不直接返回 插入点 low 的相反数,还需要进行 +1 操作
  • 可以将 +1 改为 -1 吗

4.1 为什么 low 是插入点

以已知了找不到目标结果为前提,有这样几件事我们需要明白:

  1. 随着循环次数增加,low 与 high 的距离会越来越近。直到刚进入最后一轮循环时,一定是 low == high
  2. 最终未查找目标时,退出了 while 循环,会有 low > high,且 low = high + 1
// 最后一轮进入循环时,low == high
while (low <= high) {
    
    // 那么 mid == high == low
    int mid = (low + high) >>> 1;
    
    int midVal = a[mid];

    /*
    	要么进入 if,要么进入 else if
    		1.当中间值小于目标值时,意味着待插入的目标值应该要在中间值索引 mid 后面一个位置,
    		  即就是 low = mid + 1,所以 low 就是插入点
    		  
            2.当目标值小于中间值时,意味着待插入的目标值应该要在中间值索引 mid 前面一个位置,
              既然要排到 mid 的前面一个位置,不就意味着要将 mid 位置挤占,将 mid 及之后的元素向后移动一位吗?
              所以插入点也就是当前 mid 的位置,而 low 是等于 mid 的,所以等价于 low 就是插入点 
    */
    
    if (midVal < key)
        low = mid + 1;
    else if (midVal > key)
        high = mid - 1;
    else
        return mid; // key found
}	

4.2 为什么要进行取反:-(low + 1)

我们用正数来标识在数组中找到的目标值的索引。

因为 low + 1 一定是正数,。因此只能取反得到负数标识未找到目标值,再反推变式得到插入点。

4.3 为什么不直接返回 插入点 low 的相反数,还需要进行 +1 操作

📑 例如对于数组 [2, 5, 8],我们需要查找目标值为 -6 的索引,那么肯定是找不到的,循环结束时得到的 low 是 0,也就是目标值需要插在索引为 0 的位置。

如果返回的不是 -(low + 1) 而是 -low,即 -0。在 Java 中,0 == -0true,因此会被认为是索引为 0 的位置找到了目标值。

image-20231101223920639

4.4 可以将 +1 改为 -1 吗

不可以,low + 1 的结果一定是正数,但 low - 1 的结果能保证一定是正数吗?是不能的,比如当 待插入点 low 为 1 时,最终返回结果 -(low - 1) 的结果为 0。那么会被认为目标值找到了且索引为 0,这是不合理的。

5.未找到目标元素时根据返回值进行数组扩容

public static void main(String[] args) {
    // 二分查找目标值,不存在则插入
    /*
        原始数组:[2,5,8]
        查找目标值:4
        查询不到,返回的结果为 r = -待插入点索引-1
        在这里带插入点索引为 1,对应 r = -2
        那么我们分成这几步来进行拷贝:
            - 1.新建数组,大小为原数组的大小+1:         [0,0,0,0]
            - 2.将待插入点索引之前的数据放入新数组:     [2,0,0,0]
            - 3.将目标值放入到待插入点索引的位置:       [2,4,0,0]
            - 4.将原数组后面的数据都相继拷贝到新数组后面: [2,4,5,8]
     */

    // 定义原数组与目标值
    int[] oldArray = {2, 5, 8};
    int target = 4;

    // 搜索目标值4,没有找到,返回结果为 r =  -待插入点索引-1,这里的 r=-2
    int r = Arrays.binarySearch(oldArray, target);

    // r < 0 说明没有找到目标值,就插入
    if (r < 0) {
        // r = -(插入点 + 1) => 插入点 = -r - 1
        // 获取待插入索引
        int insertIndex = -r - 1;

        // 1.新建数组,大小为原数组的大小+1
        int[] newArray = new int[oldArray.length + 1];

        // 2.将待插入点索引之前的数据放入新数组
        // 新数组由 [0,0,0,0] --> [2,0,0,0]
        for (int i = 0; i <= insertIndex - 1; i++) {
            newArray[i] = oldArray[i];
        }

        // 3.将目标值放入到待插入点索引的位置
        // 新数组由 [2,0,0,0] --> [2,4,0,0]
        newArray[insertIndex] = target;

        // 4.将原数组后面的数据都相继拷贝到新数组后面
        // 新数组由 [2,4,0,0] --> [2,4,5,8]
        for (int i = insertIndex; i <= oldArray.length - 1; i++) {
            newArray[i + 1] = oldArray[i];
        }

        System.out.println(Arrays.toString(newArray)); // [2, 4, 5, 8]
    }

}

当然,第二步和第四步拷贝的方法其实类似,我们可以自己封装一个公共方法 myArraycopy 来进行调用:

public static void main(String[] args) {
    // 二分查找目标值,不存在则插入
    /*
        原始数组:[2,5,8]
        查找目标值:4
        查询不到,返回的结果为 r = -待插入点索引-1
        在这里带插入点索引为 1,对应 r = -2
        那么我们分成这几步来进行拷贝:
            - 1.新建数组,大小为原数组的大小+1:         [0,0,0,0]
            - 2.将待插入点索引之前的数据放入新数组:     [2,0,0,0]
            - 3.将目标值放入到待插入点索引的位置:       [2,4,0,0]
            - 4.将原数组后面的数据都相继拷贝到新数组后面: [2,4,5,8]
     */

    // 定义原数组与目标值
    int[] oldArray = {2, 5, 8};
    int target = 4;

    // 搜索目标值4,没有找到,返回结果为 r =  -待插入点索引-1,这里的 r=-2
    int r = Arrays.binarySearch(oldArray, target);

    // r < 0 说明没有找到目标值,就插入
    if (r < 0) {
        // r = -(插入点 + 1) => 插入点 = -r - 1
        // 获取待插入索引
        int insertIndex = -r - 1;

        // 1.新建数组,大小为原数组的大小+1
        int[] newArray = new int[oldArray.length + 1];

        // 2.将待插入点索引之前的数据放入新数组
        // 新数组由 [0,0,0,0] --> [2,0,0,0]
        myArraycopy(oldArray, 0, newArray, 0, insertIndex);

        // 3.将目标值放入到待插入点索引的位置
        // 新数组由 [2,0,0,0] --> [2,4,0,0]
        newArray[insertIndex] = target;

        // 4.将原数组后面的数据都相继拷贝到新数组后面
        // 新数组由 [2,4,0,0] --> [2,4,5,8]
        myArraycopy(oldArray, insertIndex, newArray, insertIndex + 1, oldArray.length - insertIndex);

        System.out.println(Arrays.toString(newArray));
    }

}

/**
 * 数组元素拷贝
 *
 * @param oldArray               旧数组
 * @param oldArrayStartCopyIndex 旧数组元素开始拷贝的位置
 * @param newArray               新数组
 * @param newArrayStartCopyIndex 新数组元素开始拷贝的位置
 * @param copyLength             拷贝的元素长度(个数)
 */
public static void myArraycopy(int[] oldArray,
                               int oldArrayStartCopyIndex,
                               int[] newArray,
                               int newArrayStartCopyIndex,
                               int copyLength) {
    for (int i = 0; i < copyLength; i++) {
        newArray[newArrayStartCopyIndex++] = oldArray[oldArrayStartCopyIndex++];
    }
}

但当然,其实 jdk 已经为我们提供了数组元素拷贝的方法 System.arraycopy,jdk 提供的这个方法的作用和我们自定义的 myArrayCopy 方法参数和效果是一样的,因此我们完全可以将拷贝方法修改为 System.arraycopy

public static void main(String[] args) {
    // 二分查找目标值,不存在则插入
    /*
        原始数组:[2,5,8]
        查找目标值:4
        查询不到,返回的结果为 r = -待插入点索引-1
        在这里带插入点索引为 1,对应 r = -2
        那么我们分成这几步来进行拷贝:
            - 1.新建数组,大小为原数组的大小+1:         [0,0,0,0]
            - 2.将待插入点索引之前的数据放入新数组:     [2,0,0,0]
            - 3.将目标值放入到待插入点索引的位置:       [2,4,0,0]
            - 4.将原数组后面的数据都相继拷贝到新数组后面: [2,4,5,8]
     */

    // 定义原数组与目标值
    int[] oldArray = {2, 5, 8};
    int target = 4;

    // 搜索目标值4,没有找到,返回结果为 r =  -待插入点索引-1,这里的 r=-2
    int r = Arrays.binarySearch(oldArray, target);

    // r < 0 说明没有找到目标值,就插入
    if (r < 0) {
        // r = -(插入点 + 1) => 插入点 = -r - 1
        // 获取待插入索引
        int insertIndex = -r - 1;

        // 1.新建数组,大小为原数组的大小+1
        int[] newArray = new int[oldArray.length + 1];

        // 2.将待插入点索引之前的数据放入新数组
        // 新数组由 [0,0,0,0] --> [2,0,0,0]
        System.arraycopy(oldArray, 0, newArray, 0, insertIndex);

        // 3.将目标值放入到待插入点索引的位置
        // 新数组由 [2,0,0,0] --> [2,4,0,0]
        newArray[insertIndex] = target;

        // 4.将原数组后面的数据都相继拷贝到新数组后面
        // 新数组由 [2,4,0,0] --> [2,4,5,8]
        System.arraycopy(oldArray, insertIndex, newArray, insertIndex + 1, oldArray.length - insertIndex);

        System.out.println(Arrays.toString(newArray));
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1161077.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL学习-获取排名,按行更新

获取排名 需求&#xff1a;获取分类平均值的名次&#xff1f; 比如10个班级的平均分&#xff0c;按照班级名称排序&#xff0c;后面跟着名次。 记录表&#xff1a;student &#xff1b; 字段&#xff1a;banji 班级&#xff1b;AvgS 平均分&#xff1b;pm 排名&#xff1b…

解决问题Conda:CondaValueError: Malformed version string ‘~’ : invalid character(s)

解决问题Conda&#xff1a;CondaValueError: Malformed version string ‘~’ : invalid character(s) 背景 今天使用Conda构建项目运行环境的时候报错&#xff1a;&#xff1a;CondaValueError: Malformed version string ‘~’ : invalid character(s) ##报错问题 在安装te…

Express框架开发接口之书城商店原型图

这是利用Axure画的&#xff0c;简单画一下原型图&#xff0c;根据他们的业务逻辑我们完成书城商店API开发 首页 分类 购物车 个人中心

探索C++中的不变之美:const与构造函数的深度剖析

W...Y的主页&#x1f60a; 代码仓库分享&#x1f495; &#x1f354;前言&#xff1a; 关于C的博客中&#xff0c;我们已经了解了六个默认函数中的四个&#xff0c;分别是构造函数、析构函数、拷贝构造函数以及函数的重载。但是这些函数都是有返回值与参数的。提到参数与返回…

Spring Security 6.1.x 系列(4)—— 基于过滤器链的源码分析

一、自动配置 在 Spring Security 6.1.x 系列&#xff08;1&#xff09;—— 初识Spring Security 中我们只引入spring-boot-starter-security 依赖&#xff0c;就可以实现登录认证&#xff0c;这些都得益于Spring Boot 的自动配置。 在spring-boot-autoconfigure模块中集成了…

MyBitis自动拼接了LIMIT

1.前言 最近系统在运营的过程中发现一个很奇怪的问题&#xff0c;莫名其妙的SQL语句会被拼接上一小段SQL&#xff0c;但是发现这被拼接的SQL并不是当前这个API所使用的SQL&#xff0c;因此导致select语句出错。 2.排查思路 2.1.第一步 首先我排查了打印日志里面的错误对应的…

Louis 谈 Restaking:去中心化信任的交流电时刻

人际信任是社会资本的主要形态。信任促成协作&#xff08;主要是经济交易&#xff09;&#xff0c;是人类文明的基石。 当全球已有数十亿人接入互联网&#xff0c;协作的物理限制已经消除&#xff0c;但传统的人际信任仍然局限于家族、长期积累的声誉和长期相处形成的私人关系…

【JAVA学习笔记】55 - 集合-Map接口、HashMap类、HashTable类、Properties类、TreeMap类(难点)

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter14/src/com/yinhai/map_ Map接口 一、Map接口的特点&#xff08;难点&#xff09; 难点在于对Node和Entry和EntrySet的关系 注意:这里讲的是JDK8的Map接口特点 Map java 1) Map与Collect…

【Mquant】2、量化平台的选择

文章目录 一、选择因素二、常见的量化平台三、为什么选择VeighNa&#xff1f;四、参考 一、选择因素 功能和工具集&#xff1a;量化平台应该提供丰富的功能和工具集&#xff0c;包括数据分析、策略回测、实时交易等。不同的平台可能有不同的特点和优势&#xff0c;可以根据自己…

【数据库】形式化关系查询语言(一):关系代数Relational Algebra:基本运算、附加关系代数、扩展的关系代数

目录 一、关系代数Relational Algebra 1. 基本运算 a. 选择运算&#xff08;Select Operation&#xff09; b. 投影运算&#xff08;Project Operation&#xff09; 组合 c. 并运算&#xff08;Union Operation&#xff09; d. 集合差运算&#xff08;Set Difference Op…

Vue3.0 reactive与ref :VCA模式

简介 Vue3 最大的一个变动应该就是推出了 CompositionAPI&#xff0c;可以说它受ReactHook 启发而来&#xff1b;它我们编写逻辑更灵活&#xff0c;便于提取公共逻辑&#xff0c;代码的复用率得到了提高&#xff0c;也不用再使用 mixin 担心命名冲突的问题。 ref 与 reactive…

pytorch学习第五篇:NN与CNN代码实例

这篇文章详细介绍了全链接神经网络实现方法,以及卷积的实现方法。最后我们发现,卷积的实现方法与全链接大同小异,因为 torch 为我们做了很多工作,我们来看看这两个有什么区别。 我们使用 torch 框架来实现两种神经网络,来对图形进行分类。 NN 首先我们引入依赖包 impor…

归并排序深度剖析

目录 一、什么是归并排序&#xff1f; 二、归并排序的实现 三、归并排序非递归 一、什么是归并排序&#xff1f; 归并排序是建立在归并操作上的一种有效&#xff0c;稳定 的排序算法&#xff0c;该算法是采用分治法&#xff08;Divide and Conquer&#xff09;的一个非常典型…

stm32 模拟spi

目录 简介 spi物理层 连接方式 框图 协议层&#xff1a; 数据处理 传输模式 模式0 起始和停止信号 发送和接收数据 模式1 模式2 模式3 总结 简介 spi物理层 SPI&#xff08; Serial Peripheral Interface&#xff0c; 串行外设接口&#xff09;是一种全双工同步…

Vlice DM蓝牙5.2双模热插拔PCB

键盘使用说明索引&#xff08;均为出厂默认值&#xff09; 软件支持&#xff08;驱动的详细使用帮助&#xff09;一些常见问题解答&#xff08;FAQ&#xff09;首次使用步骤蓝牙配对规则&#xff08;重要&#xff09;蓝牙和USB切换键盘默认层默认触发层0的FN键配置的功能默认功…

【51单片机】LED与独立按键(学习笔记)

一、点亮一个LED 1、LED介绍 LED&#xff1a;发光二极管 补&#xff1a;电阻读数 102 > 10 00 1k 473 > 47 000 2、Keil的使用 1、新建工程&#xff1a;Project > New Project Ctrl Shift N &#xff1a;新建文件夹 2、选型号&#xff1a;Atmel-AT89C52 3、xxx…

【Linux】Linux网络总结图详解

网络是进行分层管理的应用层HTTPHTPPS 传输层&#xff08;UDP、TCP&#xff09;UDPTCPTCP和UDP对比 网络层IP 数据链路层&#xff08;MAC&#xff09;/物理层&#xff08;以太网&#xff09;以太网通信&#xff08;负责网卡之间&#xff09; 网络是进行分层管理的 应用层 HTTP…

幂等性设计,及案例分析

一、redis锁处理幂等性失效 上面代码中&#xff0c;锁起不了作用&#xff1b; ——count方法&#xff0c;和insert方法在同一事务中&#xff0c;事务中包含锁&#xff0c;锁没有作用&#xff0c;锁的范围内&#xff0c;事务没提交&#xff0c;但释放锁后&#xff0c;事务提交前…

云安全与容器安全: 探讨在云环境和容器化应用中如何保护数据和工作负载的安全。

在当今数字化时代&#xff0c;云计算和容器化应用已经成为了企业业务的主要组成部分。这两项技术的普及&#xff0c;极大地提高了开发和部署的效率&#xff0c;但也带来了新的安全挑战。在本文中&#xff0c;我们将探讨云安全和容器安全的重要性&#xff0c;以及如何有效地保护…

WordPress外链页面安全跳转插件

老白博客我参照csdn和腾讯云的外链跳转页面&#xff0c;写了一个WordPress外链安全跳转插件&#xff1a;给网站所有第三方链接添加nofollow标签和重定向功能&#xff0c;提高网站安全性。插件包括两个样式&#xff0c;由于涉及到的css不太一样&#xff0c;所以分别写了两个版本…