【二分查找】Java实现二分查找算法(基础版、改进版、平衡版、Java版、Leftmost、Ringhtmost)

Java实现二分查找算法

  本文主要演示通过Java实现二分查找算法的各种版本,包括:基础版、改进版、平衡版、Java版、Leftmost、Ringhtmost

1. 基础版

public static int binarySearchBasics(int[] arr, int target) {
        int i = 0;
        int j = arr.length - 1;
        //思考1:为什么是 i<=j
        while (i <= j) {
            int m = (i + j) / 2;
            //思考2:m = (i + j) / 2会不会出现问题?
            if(target < arr[m]){
                j = m - 1;
            } else if (target > arr[m]) {
                i = m + 1;
            } else {
            	//找到了,返回下标m
                return m;
            }
        }
        //没找到,返回-1
        return -1;
    }

思考:

1.1 循环条件为什么是 i <= j , 而不是 i < j ?

  i <= j 可以推到成 j - i >= 1,表示数组arr中还有一个元素没有跟目标值target进行比较,如果循环条件设置为 i < j,就不会比较这个元素,如果这个元素恰巧是要寻找的元素,则会出现错误。

1.2 m = (i + j) / 2会不会出现问题?该如何改进?

  会。
  m = (i + j) / 2 中,如果 i 和 j 的值非常大,i + j 的结果可能会超出数据类型的表示范围,从而导致溢出。例如,在 32 位整数中,如果 i 和 j 都接近 Integer.MAX_VALUE,那么 i + j 可能会超出 int 类型的范围,导致计算错误。

解决方法:

  为了避免溢出问题,通常会使用 m = (i + j) >>> 1 来计算中间值,其中 >>> 是无符号右移操作符。这个操作符有以下特点:

“>>>” 是无符号右移,即无符号地将二进制数向右移动,并且填充 0。
m = (i +j) >>> 1 相当于 (i + j) /2,但它避免了溢出问题。

2. 改进版

public static int binarySearchAlternative(int[] arr, int target){
        int i = 0;
        int j = arr.length;
        while (i < j) {
            int m = (i + j) >>> 1;    //等价于 int m = (i + j) / 2;
            if(target < arr[m]){
                j = m;
            } else if (arr[m] < target) {
                i = m + 1;
            } else {
                //找到了,返回下标m
                return m;
            }
        }
        //没找到,返回-1
        return -1;
    }

改进点:

1. 将 j = arr.length - 1改成 j = arr.length

好处: 下标 j 指向的元素 一定不是 查找目标,i == j的情况不可能是目标值,循环条件变成了i < j。如果某次要缩小右边界,那么 j = m,因为此时的 m m m 已经不是查找目标了

2. 将 m = (i + j) / 2 改成 m = (i + j) >>> 1

好处: 避免了因 i ,j 的值过大而引起的溢出问题

3.比较大小符号统一用<

好处: 与数组大小顺序保持一致(从小到大),提高代码可读性

3. 平衡版

解决: 基础版目标值在最左边,查找L次;在最右边,查找2*L次,左右不平衡

public static int binarySearchBalance(int[] arr, int target){
        int i = 0;
        int j = arr.length;
        while (1 < j - i){//要查找的范围 > 1 时
            int m = (i + j) >>> 1;

            if(target < arr[m]){
                j = m;
            } else {
                i = m;
            }
        }

        return (target == arr[i]) ? i : -1;
    }

4. Java版

  直接调用Arrays.binarySearch( )方法即可。底层用基础版二分查找实现。

    public static int binarySearchJava(int[] arr, int target){
        return Arrays.binarySearch(arr, target);
    }

5. Leftmost

  有时我们希望返回的是最左侧的重复元素,如果用 Basic 二分查找

  • 对于数组 [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 3, 4, 4, 5, 6, 7] [1,2,3,4,4,5,6,7],查找元素4,结果是索引3

  • 对于数组 [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 4, 4, 4, 5, 6, 7] [1,2,4,4,4,5,6,7],查找元素4,结果也是索引3,并不是最左侧的元素

  所以,二分查找Leftmost版本来了

public static int binarySearchLeftmost1(int[] arr, int target){
        //arr[] = 1,2,3,4,4,4,4,5,5,5,6,7,8
        int i = 0;
        int j = arr.length - 1;
        int candidate = -1;
        while (i <= j){
            int m = (i + j) >>> 1;
            if(target < arr[m]){
                j = m - 1;
            } else if (arr[m] < target) {
                i = m + 1;
            } else {
                candidate = m;
                j = m - 1;
            }
        }
        return candidate;
    }

  调用该方法,

  • 对于数组 [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 3, 4, 4, 5, 6, 7] [1,2,3,4,4,5,6,7],查找元素4,结果是索引3

  • 对于数组 [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 4, 4, 4, 5, 6, 7] [1,2,4,4,4,5,6,7],查找元素4,结果是索引2

  符合预期

6. Rightmost

同理,希望返回的是最右侧的重复元素,则有Rightmost版本

public static int binarySearchRightmost1(int[] a, int target) {
    int i = 0, j = a.length - 1;
    int candidate = -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 {
            candidate = m; // 记录候选位置
            i = m + 1;	   // 继续向右
        }
    }
    return candidate;
}

7. Leftmost(改进版)

对于 Leftmost 与 Rightmost,可以返回一个比 -1 更有用的值

public static int binarySearchLeftmost(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 {
            i = m + 1;
        }
    }
    return i; 
}

Leftmost 返回值的另一层含义: < t a r g e t \lt target <target 的元素个数

8. Rightmost(改进版)

同理,Rightmost版本

public static int binarySearchRightmost(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 {
            i = m + 1;
        }
    }
    return i - 1;
}

Rightmost 返回值的另一层含义:>   t a r g e t \ target  target 的元素个数

应用:

范围查询
  • 查询 x < 4 x \lt 4 x<4 0.. l e f t m o s t ( 4 ) − 1 0 .. leftmost(4) - 1 0..leftmost(4)1
  • 查询 x ≤ 4 x \leq 4 x4 0.. r i g h t m o s t ( 4 ) 0 .. rightmost(4) 0..rightmost(4)
  • 查询 4 < x 4 \lt x 4<x r i g h t m o s t ( 4 ) + 1.. ∞ rightmost(4) + 1 .. \infty rightmost(4)+1..∞
  • 查询 4 ≤ x 4 \leq x 4x l e f t m o s t ( 4 ) . . ∞ leftmost(4) .. \infty leftmost(4)..∞
  • 查询 4 ≤ x ≤ 7 4 \leq x \leq 7 4x7 l e f t m o s t ( 4 ) . . r i g h t m o s t ( 7 ) leftmost(4) .. rightmost(7) leftmost(4)..rightmost(7)
  • 查询 4 < x < 7 4 \lt x \lt 7 4<x<7 r i g h t m o s t ( 4 ) + 1.. l e f t m o s t ( 7 ) − 1 rightmost(4)+1 .. leftmost(7)-1 rightmost(4)+1..leftmost(7)1
求排名 l e f t m o s t ( t a r g e t ) + 1 leftmost(target) + 1 leftmost(target)+1
  • t a r g e t target target 可以不存在,如: l e f t m o s t ( 5 ) + 1 = 6 leftmost(5)+1 = 6 leftmost(5)+1=6
  • t a r g e t target target 也可以存在,如: l e f t m o s t ( 4 ) + 1 = 3 leftmost(4)+1 = 3 leftmost(4)+1=3
求前任(predecessor) l e f t m o s t ( t a r g e t ) − 1 leftmost(target) - 1 leftmost(target)1
  • l e f t m o s t ( 3 ) − 1 = 1 leftmost(3) - 1 = 1 leftmost(3)1=1,前任 a 1 = 2 a_1 = 2 a1=2
  • l e f t m o s t ( 4 ) − 1 = 1 leftmost(4) - 1 = 1 leftmost(4)1=1,前任 a 1 = 2 a_1 = 2 a1=2
求后任(successor) r i g h t m o s t ( t a r g e t ) + 1 rightmost(target)+1 rightmost(target)+1
  • r i g h t m o s t ( 5 ) + 1 = 5 rightmost(5) + 1 = 5 rightmost(5)+1=5,后任 a 5 = 7 a_5 = 7 a5=7
  • r i g h t m o s t ( 4 ) + 1 = 5 rightmost(4) + 1 = 5 rightmost(4)+1=5,后任 a 5 = 7 a_5 = 7 a5=7
求最近邻居
  • 前任和后任距离更近者

E03. 搜索开始结束位置-Leetcode 34

  给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

  如果数组中不存在目标值 target,返回 [-1, -1]。

  你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题

例如:

输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]

输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]

输入:nums = [], target = 0 输出:[-1,-1]

参考答案:

public static int left(int[] a, int target) {
    int i = 0, j = a.length - 1;
    int candidate = -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 {
            candidate = m;
            j = m - 1;
        }
    }
    return candidate;
}

public static int right(int[] a, int target) {
    int i = 0, j = a.length - 1;
    int candidate = -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 {
            candidate = m;
            i = m + 1;
        }
    }
    return candidate;
}

public static int[] searchRange(int[] nums, int target) {
    int x = left(nums, target);
    if(x == -1) {
        return new int[] {-1, -1};
    } else {
        return new int[] {x, right(nums, target)};
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值