java数据结构与算法之二分查找

二分查找算法也称折半查找,是一种非常高效的工作于有序数组的查找算法。后续的课程中还会学习更多的查找算法,但在此之前,不妨用它作为入门。

1) 基础版

需求:在有序数组 A 内,查找值 target

  • 如果找到返回索引

  • 如果找不到返回 -1

java 实现

public static int binarySearch(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 if (a[m] < target) {        // 在右边
            i = m + 1;
        } else {
            return m;
        }
    }
    return -1;
}

  • i,j 对应着搜索区间 [0,a.length-1](注意是闭合的区间),i<=j意味着搜索区间内还有未比较的元素,i,j指向的元素也可能是比较的目标

    • 思考:如果不加 i==j 行不行?

    • 回答:不行,因为这意味着 i,j 指向的元素会漏过比较

  • m 对应着中间位置,中间位置左边和右边的元素可能不相等(差一个),不会影响结果

  • 如果某次未找到,那么缩小后的区间内不包含 m

  • 为什么m = (i + j) >>> 1,而不是m=(i+j)/2呢?

  • mid = (left + right) >> 1 就相当于 mid = (left + right) / 2 但是运算的速度会变快很多。

  • 在使用除法运算时可以用位运算来代替.
  • left + ((right - left) / 2) 来代替 (left + right) / 2 这是因为当left 和 right 都很大的时候可以防止溢出情况的发生。

2) 增强版

public static int binarySearch(int[] a, int target) {
    int i = 0, j = a.length;
    while (i < j) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {            // 在左边
            j = m;
        } else if (a[m] < target) {        // 在右边
            i = m + 1;
        } else {
            return m;
        }
    }
    return -1;
}

  • i,j对应着搜索区间 [0,a.length)(注意是左闭右开的区间),i<j意味着搜索区间内还有未比较的元素,$j$ 指向的一定不是查找目标

    • 思考:为啥这次不加 i==j的条件了?

    • 回答:这回 j 指向的不是查找目标,如果还加 i==j条件,就意味着 j指向的还会再次比较,找不到时,会死循环

  • 如果某次要缩小右边界,那么 j=m,因为此时的 m 已经不是查找目标了

3) 平衡版

public static int binarySearchBalance(int[] a, int target) {
    int i = 0, j = a.length;
    while (1 < j - i) {
        int m = (i + j) >>> 1;
        if (target < a[m]) {
            j = m;
        } else {
            i = m;
        }
    }
    return (a[i] == target) ? i : -1;
}

思想:

  1. 左闭右开的区间,i 指向的可能是目标,而 j 指向的不是目标

  2. 不奢望循环内通过 m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 i)

    • j - i > 1 的含义是,在范围内待比较的元素个数 > 1

  3. 改变 i 边界时,它指向的可能是目标,因此不能 m+1

  4. 循环内的平均比较次数减少了

  5. 时间复杂度 \Theta(log(n))

4) Leftmost 与 Rightmost

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

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

public static int binarySearchLeftmost1(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 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;
}

应用

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

Leftmost 改为

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 返回值的另一层含义:$\lt target$ 的元素个数

  • 小于等于中间值,都要向左找

Rightmost也是类似上面的方法改进

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值