数据结构与算法-02_二分查找

前言

二分查找算法也称折半查找,是一种非常高效的工作于有序数组的查找算法。

1.二分查找基础版

需求:在有序数组 A A A 内,查找值 t a r g e t target target

  • 如果找到返回索引
  • 如果找不到返回 − 1 -1 1

代码实现:

public static int binarySearchBasic(int[] a, int target) {
        int i = 0, j = a.length - 1;

        while (i <= j) {
            int m = (i + j) / 2;
            if (target < a[m]) {
                //左边
                j = m - 1;
            } else if (a[m] < target) {
                //右边
                i = m + 1;
            } else {
                return m;
            }
        }
        return -1;
    }

问题1:为什么是 i < = j i <= j i<=j 意味着区间内有未比较的元素, 而不是 i < j i < j i<j ?

i == j 意味着 i,j 它们指向的元素也会参与比较
i < j 只意味着 m 指向的元素参与比较

问题2: ( i + j ) / 2 (i + j) / 2 (i+j)/2 有没有问题?

有可能超出正整数所表示的范围

2.二分查找改动版

public static int binarySearchAlternative(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 i,j i,j 对应着搜索区间 [ 0 , a . l e n g t h ) [0,a.length) [0,a.length)(注意是左闭右开的区间), i < j i<j i<j 意味着搜索区间内还有未比较的元素, j j j 指向的一定不是查找目标
  • 如果某次要缩小右边界,那么 j = m j=m j=m,因为此时的 m m m 已经不是查找目标了

问题:为啥这次不加 i = = j i==j i==j 的条件了?

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

3.衡量算法好坏

时间复杂度 比较查找算法与二分查找算法

查找算法

public static int search(int[] a, int k) {
    for (int i = 0;i < a.length; i++) {
        if (a[i] == k) {
            return i;
        }
    }
    return -1;
}

考虑最坏情况下(没找到)例如 [1,2,3,4] 查找 5

  • int i = 0 只执行一次
  • i < a.length 受数组元素个数 n n n 的影响,比较 n + 1 n+1 n+1
  • i++ 受数组元素个数 n n n 的影响,自增 n n n
  • a[i] == k 受元素个数 n n n 的影响,比较 n n n
  • return -1,执行一次

粗略认为每行代码执行时间是 t t t,假设 n = 4 n=4 n=4 那么

  • 总执行时间是 ( 1 + 4 + 1 + 4 + 4 + 1 ) ∗ t = 15 t (1+4+1+4+4+1)*t = 15t (1+4+1+4+4+1)t=15t
  • 可以推导出更一般地公式为, T = ( 3 ∗ n + 3 ) t T = (3*n+3)t T=(3n+3)t

二分查找算法(二分查找基础版),还是 [1,2,3,4] 查找 5

  • int i = 0, j = a.length - 1 各执行 1 次

  • i <= j 比较 f l o o r ( log ⁡ 2 ( n ) + 1 ) floor(\log_{2}(n)+1) floor(log2(n)+1) 再加 1 次

  • (i + j) >>> 1 计算 f l o o r ( log ⁡ 2 ( n ) + 1 ) floor(\log_{2}(n)+1) floor(log2(n)+1)

  • 接下来 if() else if() else 会执行 3 ∗ f l o o r ( log ⁡ 2 ( n ) + 1 ) 3* floor(\log_{2}(n)+1) 3floor(log2(n)+1) 次,分别为

    • if 比较
    • else if 比较
    • else if 比较成立后的赋值语句
  • return -1,执行一次

结果:

  • 总执行时间为 ( 2 + ( 1 + 3 ) + 3 + 3 ∗ 3 + 1 ) ∗ t = 19 t (2 + (1+3) + 3 + 3 * 3 +1)*t = 19t (2+(1+3)+3+33+1)t=19t
  • 更一般地公式为 ( 4 + 5 ∗ f l o o r ( log ⁡ 2 ( n ) + 1 ) ) ∗ t (4 + 5 * floor(\log_{2}(n)+1))*t (4+5floor(log2(n)+1))t

注意:
左侧未找到和右侧未找到结果不一样

两个算法比较,可以看到 n n n 在较小的时候,二者花费的次数差不多
但随着 n n n 越来越大,比如说 n = 1000 n=1000 n=1000 时,用二分查找算法也就是 54 t 54t 54t,而查找算法则需要 3003 t 3003t 3003t

4.二分查找平衡版

思想:

  1. 左闭右开的区间, i i i 指向的可能是目标,而 j j j 指向的不是目标
  2. 不奢望循环内通过 m m m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 i i i
    • j − i > 1 j - i > 1 ji>1 的含义是,在范围内待比较的元素个数 > 1
  3. 改变 i i i 边界时,它指向的可能是目标,因此不能 m + 1 m+1 m+1
  4. 循环内的平均比较次数减少了
  5. 时间复杂度 Θ ( l o g ( n ) ) \Theta(log(n)) Θ(log(n))
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;
    }

5. Leftmost 与 Rightmost

有时我们希望返回的是最左侧的重复元素,如果用 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,并不是最左侧的元素

public static int binarySearchLeftmost(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 binarySearchRightmost(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

  • leftmost 返回值的另一层含义: < t a r g e t \lt target <target 的元素个数
  • 小于等于中间值,都要向左找
 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;
    }

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;
    }

范围查询

  • 查询 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,$rightmost(4) + 1 … \infty $
  • 查询 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

求最近邻居

  • 前任和后任距离更近者

总结

二分查找性能

时间复杂度

  • 最坏情况: O ( log ⁡ n ) O(\log n) O(logn)
  • 最好情况:如果待查找元素恰好在数组中央,只需要循环一次 O ( 1 ) O(1) O(1)

空间复杂度

  • 需要常数个指针 i , j , m i,j,m i,j,m,因此额外占用的空间是 O ( 1 ) O(1) O(1)
  • 35
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值