二分查找

4 阅读7分钟

来源:黑马基础数据结构-012-二分查找-平衡版_哔哩哔哩_bilibili

二分查找法

算法基础

二分查找(Binary Search)是一种在有序数组中快速查找目标值的算法。它的核心思想是"折半查找"——每次将搜索范围缩小一半,直到找到目标或确定不存在。

二分查找必须满足两个条件:

  1. ​数组必须有序​(升序或降序)
  2. ​数组必须是线性结构​(支持随机访问)

算法描述

  • 初始化:设定初始搜索范围为整个数组(升序排序)。左边界 left​为 0(第一个元素的索引),右边界 right​为 数组长度 - 1​(最后一个元素的索引)。
  • ​循环条件​:只要 left <= right​,就继续查找。
  • 计算中间点:取中间索引 mid​,计算公式为 mid = (left + right) / 2​。

从向量的角度理解为什么mid = (left + right) / 2​ :

从起点 left​指向终点 right​的方向向量是 (right - left)​,将这个方向向量除以2,得到 (right - left) / 2​,最后,将这个“半向量”的起点加到初始位置 left​上,就得到了中点的精确位置:mid = left + (right - left) / 2​ = (left + right) / 2​

  • ​比较与判断​:

    • 如果 arr[mid] == target​,查找成功,返回 mid​。
    • 如果 arr[mid] < target​,说明目标在右侧,调整左边界:left = mid + 1​。
    • 如果 arr[mid] > target​,说明目标在左侧,调整右边界:right = mid - 1​。
  • 终止:如果循环结束仍未找到目标值,返回 -1 表示查找失败

算法时间复杂度:O(logn): 最好情况就是找的值就在中间(O(1)) 最坏的情况就是两边(O(logn))

public class BinarySearch {

    /**
     *
     * @param a 升序的数组
     * @param target 要找的目标值
     * @return 目标值的在a中的索引。如果a中没有目标值,则返回-1
     */

    public static int binarySearch(int[] a, int target){
        // 1.设i = 0 ; j= a.length-1 ,表示两端的索引
        int i = 0 ;
        int j = a.length-1;
        // 2.若 i>j 则返回-1
        for ( ; i <= j ; ){
            // 3.求 m = (i+j)/2,中间的索引
            // 4.1 若a[m] = target ,返回m
            // 4.2 若 a[m] > target , j = m-1 ,返回第2步
            // 4.3 若 a[m] < target,  i = m+1 , 返回第2步
            int m = (i+j)/2;
            if (a[m] == target) return m;
            else if (a[m] > target) {
                j = m-1;
            }else {
                i = m +1;
            }

        }
        return -1;
    }
}

平衡版

已知上述代码中的二分查找要循环L次,如果target在最左边,那么就只进入了if和else if 里,比较执行了2L次,如果target在最右边,那么就会执行3L次,这个就是不平衡的。下面是一个平衡的算法,不管目标值是最左还是最右都是O(logn)

    public static int binarySearchBalance(int[] a, int target){
        int i = 0 ;
        int j = a.length-1;
        for ( ; j-i >1 ; ){
            int mid = (i+j)/2;
            if (target < a[mid]){
                j = mid - 1;
            }else {
                i = mid;
            }
        }
        if (a[i] == target){
            return i;
        }else if (a[j] == target){
            return j;
        }else return -1;
    }

相对于基础版

  1. 将target > a[mid] 和target = a[mid]的两种情况合并到了一个else里;
target > a[mid]: i = mid+1;  target = a[mid] return mid
->  i = mid 

这两种情况合并意味着什么?意味着

  • 要么 i 是目标元素,然后固定 i ,然后j一直左移(这个时候一直进入if判断里),直到i ,j 紧邻
  • 要么i + 1 是目标元素,也是固定i,然后j一直左移

Java源码版本

Java.utils.Arrays.binarySearch

数组是升序排序的,如果找到了目标值,返回数组中目标值所在的索引,如果没有找到目标值,则返回(插入点+1)的负数,插入点的意思是它本该在这个数组的索引(数组无重复要素)。

    /**
     * Searches the specified array of ints for the specified value using the
     * binary search algorithm.  The array must be sorted (as
     * by the {@link #sort(int[])} method) prior to making this call.  If it
     * is not sorted, the results are undefined.  If the array contains
     * multiple elements with the specified value, there is no guarantee which
     * one will be found.
     *
     * @param a the array to be searched
     * @param key the value to be searched for
     * @return index of the search key, if it is contained in the array;
     *         otherwise, <code>(-(<i>insertion point</i>) - 1)</code>.  The
     *         <i>insertion point</i> is defined as the point at which the
     *         key would be inserted into the array: the index of the first
     *         element greater than the key, or {@code a.length} if all
     *         elements in the array are less than the specified key.  Note
     *         that this guarantees that the return value will be &gt;= 0 if
     *         and only if the key is found.
     */
    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.
    }

插入点的应用:把缺失的元素插入到数组中,保持有序

public class InsertionPoint {
    public static void main(String[] args) {
        int[] src = {1, 2, 3, 4, 5};
        int target = 0;
        int targetPoint = Arrays.binarySearch(src, target);
        if (targetPoint <= -1){
            // 初始化目标数组
            int[] dest = new int[6];
            // 获取相对于原数组插入点的索引
            int insertionPoint = Math.abs(targetPoint + 1);
            // 将原数组中插入点之前的部分复制到目标数组中:将原数组从索引0开始复制insertionPoint个数的元素到目标数组的索引0
            System.arraycopy(src,0,dest,0,insertionPoint);
            // 目标元素的插入到目标数组的插入点
            dest[insertionPoint] = target;
            // 将原数组中插入点之后的部分复制到目标数组中:将原数组从索引insertionPoint开始复制src.length-insertionPoint个数的元素到目标数组的索引insertionPoint+1
            System.arraycopy(src,insertionPoint,dest,insertionPoint+1,src.length-insertionPoint);
            System.out.println(Arrays.toString(dest));

        }
    }
}

二分查找法寻找最左和最右侧的值

找最右侧的目标值:在基础版本上找到了目标值之后不直接返回,而是左边的i继续右移进行二分查找

    public static int binarySearchRightMost(int[] a, int target) {
        int i = 0;
        int j = a.length - 1;
        int result = -1;
        while (i <= j) {
            int mid = (i + j) / 2;
            if (target < a[mid]) {
                j = mid - 1;
            } else if (target > a[mid]) {
                i = mid + 1;
            } else {
                result = mid;
                i = mid +1;
            }
        }
        return result;
    }
    @Test
    public void testBinarySearchRightMost() {
        int[] a = {1, 2, 2, 2, 3, 4, 4};
        Assert.assertEquals(3, binarySearchRightMost(a, 2));
        Assert.assertEquals(-1, binarySearchRightMost(a, 5));

    }

找最左侧的目标值:在基础版本上找到了目标值之后不直接返回,而是右边的j继续左移进行二分查找

    public static int binarySearchLeftMost(int[] a, int target) {
        int i = 0;
        int j = a.length - 1;
        int result = -1;
        while (i <= j) {
            int mid = (i + j) / 2;
            if (target < a[mid]) {
                j = mid - 1;
            } else if (target > a[mid]) {
                i = mid + 1;
            } else {
                // 如果找到了目标值,记录下索引,然后继续找
                result = mid;
                j = mid - 1;
            }
        }
        return result;
    }
    @Test
    public void testBinarySearchLeftMost() {
        int[] a = {1, 2, 2, 2, 3, 3, 4};
        Assert.assertEquals(1, binarySearchLeftMost(a, 2));

        Assert.assertEquals(0, binarySearchLeftMost(a, 1));

        Assert.assertEquals(4, binarySearchLeftMost(a, 3));

    }

找到最后一个小于等于目标值元素的索引(≤target的最大的索引):rightMost

在基础版的基础上,找到目标值后不要直接返回,而是left指针继续右移,直到两个指针重合之后,这个时候的左指针之前所在的位置就是最终结果,如果返回了-1,则表示左指针就没移动过,也就是目标值一直在左指针的左边,所以目标值是在数组的左边界之外的

    public static int findLastLessOrEqual(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }

        int left = 0;
        int right = nums.length - 1;
        int result = -1; // 用于记录满足条件的索引

        while (left <= right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] < target) {
                result = mid; // 记录候选位置
                left = mid + 1; // 继续向右查找
            } else if (nums[mid] > target) {
                right = mid - 1; // 向左调整搜索区间
            }else {
                result = mid; // 记录候选位置
                left = mid + 1; // 继续向右查找
            }
        }
        // 最终检查result是否满足条件
        return (result != -1 && nums[result] <= target) ? result : -1;
    }

找到第一个大于等于目标值元素的索引(>=target的最左侧的索引):leftMost

在基础版本的基础上,找到目标值后不要直接返回,而是右指针一直左移,直到两个指针重合之后,这个时候的右指针之前所在的位置就是最终结果,如果返回了-1,则表示右指针就没移动过,所以目标元素在数组的右边界之外。

    public static int findLastLessOrEqual2(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }

        int left = 0;
        int right = nums.length - 1; // 闭区间:[left, right]
        int result = -1;

        while (left <= right) {
            int mid = left + (right - left) / 2; // 防止整数溢出
            if (nums[mid] < target) {
                left = mid + 1; // 目标在右半部分
            } else if (nums[mid] > target) {
                result = mid;
                right = mid - 1; // 目标在左半部分(包含等于情况)
            } else {
                result = mid ;
                right = mid -1;
            }
        }

        // 循环结束时,left 是第一个 >= target 的位置
        // 需检查 left 是否在有效范围内
        return result;
    }

应用

求排名

    @Test
    public void testFindRank(){
        int[] a = {1, 2, 4, 4, 4, 7, 7};
        int target = 4;
        System.out.println(findFirstMoreThanTargetIndex(a, target)+1);
        target = 5;
        System.out.println(findFirstMoreThanTargetIndex(a, target)+1);
    }

求范围