# 二分查找算法

# 目录

# 问题描述

从一个有序的数组中查找目标值,找到返回对应索引找不到则返回-1

# 二分查找基础版

/**
 * 二分查找基础版
 *
 * @param a      待查找的数组
 * @param target 待查找的目标值
 * @return 找的目标值返回索引则找不到返回-1
 */
public static int binarySearchBasic(int[] a, int target) {
        //1. 设置左指针和右指针的初值
        int l = 0, r = a.length - 1;
        while (l <= r) // 当左右指针中间范围有元素时执行
        {
            //获取中间值的索引, 使用逻辑右移(>>>), 而不是算术右移(>>)
            int m = (l + r) >>> 1;
            //比较
            if (target < a[m]) {
                r = m - 1; // 目标值在中间值左边
            } else if (a[m] < target) {
                l = m + 1; // 目标值在中间值右边
            } else {
                return m; //中间值等于目标值, 返回中间值索引
            }
        }
        return -1; //循环内没有找到目标值

    }

# 三个问题

# 为什么循环条件是 <= 而不是 < ?

因为 < 会漏掉一次判断, 当 左指针或者右指针指向的值是目标值时, 如果当 l == r 时不进行判断, 就会导致没有查找到这个元素

# 为什么不使用 /2 来获取左指针和右指针的 中间索引?

使用/2 会存在问题, 如果当 待搜索数组的大小约等于 int的最大值的一半时, 如果目标值在中间索引的右边,两个指针索引相加,这会产生一个负数,因为最高位变为了1,然而 int,long类型是有符号数字类型, 最高位如果为1 就是负数,当负数 /2 时. 得到的结果依然是一个负数, 导致索引报错等问题

解决方法 : 当负数 /2 时. 得到的结果依然是一个负数, 我们可以通过将数值进行 逻辑右移, 最高位补零,这样就可以使数值变为正常值

# 用自己的话描述算法

  1. 设置两个指针,分别指向数组的两端.
  2. 计算出两个指针索引的中间值
  3. 拿出中间值与目标值进行比较,假设是一个升序的数组
    • 如果大于中间值,说明目标值可能出现在中间值右边, 则左指针的指向就要跳到中间值的下一个值, 即左指针+1,而右指针保持不变.
    • 如果是小于中间值,那么说明目标值在中间值的左边,则右指针指向中间值的前一个值,即右指针-1.
    • 如果中间值等于目标值,相当于我们找到了目标值,此时返回 中间值对应的索引即可
  4. 重复 2,3 这两个步骤,直到 左指针大于了右指针,此时说明没有找到目标值,返回-1. 再次之前,最后一次比较机会是, 左指针等于右指针,此时他俩指向的就是中间值,如果中间值相等就是左指针或者右指针对应的索引,否则返回-1

# 二分查找的改动版本

一共有三处改动

  1. 将右指针的含义进行改变, 从有可能指向目标值以及作为右边界,变为了 只是右边界,没有可能是目标值, 也就是左闭右闭,变成左闭右开区间
  2. 为了配合这个右指针的改变, 循环条件变为 l < r, l是有可能指向目标值的,而右指针不可能指向目标值, 所以他俩不相等,如果可以相等会出现死循环的bug
  3. 当中间值大于了目标值时, 说明中间值不可能是目标值, 则 r 的指针从原来的 m-1 变为了 m, 因为m-1是有可能为目标值的

代码


 
 



 









public static int binarySearchAlternative(int[] a, int target) {
        int l = 0, r = a.length; //第一处改动
        while (l < r) //第三处改动
        {
            int m = (l + r) >>> 1;
            if (target < a[m]) {
                r = m;  //第三处改动
            } else if (a[m] < target) {
                l = m + 1;
            } else {
                return m;
            }
        }
        return -1;
    }

# java Arrays类提供的二分查找

在java 的java.util.Arrays 类中,提供了二分查找,其算法如下:

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

        while (low <= high) {
            int mid = (low + high) >>> 1;
            byte 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.
    }

三个参数分别是: 待查找数组,查找起始索引,查找目标 通过分析这个算法,发现除了最终的返回值与我们的二分查找算法的基础版不同,其他都相同.

  • 因此也会出现"不平衡",既向左查找时,比较次数比向右查找次数多, 因为每次循环需要多比较一次.

为什么返回值是 -(low + 1)?

通过阅读方法上的注释说明(这里没复制),可以得知当查找的目标值不在数组中时, 返回的是有序数组插入的位置,既按照这个位置插入依然有序. 这个插入位置就是变量low的值,为什么要加个负号呢? 因为算法的返回结果是非负数时,表示找到了目标值,则返回负数是为了进行区分, 那么为什么low要 -1呢(-low -1), 这是因为如果 low值为0时, -0和0的值是相等的(补码,0只有一种),如果不减1的话,就会以为0索引为待查找目标,所以减1错开1位

# 最左查找和最右查找

当数组中含有相同的元素时,我们的需求是返回,最左或者最右的那个目标值的索引

最左查找

 /**
     * 二分查找最左版本, 当数组中含有重复元素时, 要求返回最靠左边的,即第一个目标元素的索引
     *
     * @param a      搜索数组
     * @param target 待查找目标值
     * @return <p>
     * 找到返回索引,找不到返回-1
     */
    public static int binarySearchLeftMost(int[] a, int target) {
        int l = 0, r = a.length - 1;
        //候补,将找到的目标值赋值给候补,直到找到更加靠左的目标值将其更新
        int alternate = -1;
        while (l <= r) {
            int m = (l + r) >>> 1;
            //比较
            if (target < a[m]) {
                r = m - 1;
            } else if (a[m] < target) {
                l = m + 1;
            } else {
                //找到目标值,更新候补
                alternate = m;
                //最左查找,更新右边界
                r = m - 1;
            }
        }
        return alternate; //循环内没有找到目标值
    }

最右查找

/**
     * 二分查找最右版本, 当数组中含有重复元素时, 要求返回最靠右边边的,即最后一个目标元素的索引
     *
     * @param a      搜索数组
     * @param target 待查找目标值
     * @return <p>
     * 找到返回索引,找不到返回-1
     */
    public static int binarySearchRightMost(int[] a, int target) {
        int l = 0, r = a.length - 1;
        //候补,将找到的目标值赋值给候补,直到找到更加靠左的目标值将其更新
        int alternate = -1;
        while (l <= r) {
            int m = (l + r) >>> 1;
            //比较
            if (target < a[m]) {
                r = m - 1;
            } else if (a[m] < target) {
                l = m + 1;
            } else {
                //找到目标值,更新候补
                alternate = m;
                //最左查找,更新右边界
                l = m + 1;
            }
        }
        return alternate; //循环内没有找到目标值
    }

# 最左查找和最右查找变形算法

查找大于等于目标值的最左元素的索引

 /**
     * 最左二分查找变形.
     *
     * @param a      搜索数组
     * @param target 待查找目标值
     * @return <p>
     * 返回值的含义是: 最靠左的大于等于目标元素的元素的索引
     * <p>例子: int[] a = {1,2,4,4,4,6,7},
     * <p>查找4, 返回索引2
     * <p>查找3, 返回索引2
     */
    public static int binarySearchLeftMost2(int[] a, int target) {
        int l = 0, r = a.length - 1;
        while (l <= r) {
            int m = (l + r) >>> 1;
            if (target <= a[m]) {
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return l;
    }

查找小于等于目标值的最右元素的索引

/**
     * 最右二分查找变形.
     *
     * @param a      搜索数组
     * @param target 待查找目标值
     * @return <p>
     * 返回值的含义是: 最靠右的小于等于目标元素的元素的索引
     */
    public static int binarySearchRightMost2(int[] a, int target) {
        int l = 0, r = a.length - 1;
        while (l <= r) {
            int m = (l + r) >>> 1;
            if (target >= a[m]) {
                l = m + 1;
            } else {
                r = m - 1;
            }
        }
        return l - 1;
    }

# 最左和最右变形算法应用

计算最近邻居,前任和后任的索引

img

计算范围的元素

img

更新时间: 2024年5月26日星期日下午4点47分