二分查找 详细介绍以及算法衍生

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

在这里,我将介绍基础的二分查找以及分析每一步到底起到什么关键作用,如果少了或者多了一步会怎么样?以及为什么一定要有这一步?

二分查找基础版

需求:在有序数组 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) / 2;
        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 指向的元素会漏过比较,因为当查找的数字刚刚好在i==j上,此时while循环已经跳出,那岂不是没找到!!!

思考:(int m = (i + j) / 2;中的/2有没有问题?

回答:有问题:

 在这里我们要知道的就是:

那么这种问题如何解决?

答:我们利用我们的移位运算符>>>也能达到%2的效果,甚至比%2的效果还更好:

public static int binarySearchBasic(int[] a, int target){
        int i=0,j=a.length-1;       //设置指针和初值
        while(i<=j){
            int m=(i+j)>>>1;          //i~j范围内有东西
            if(target<a[m]){        //目标在左边
                j=m-1;
            }else if(a[m]<target){  //目标在右边
                i=m+1;
            }else {                 //找到了
                return m;
            }
        }
        return -1;//找不到
    }

注意:我们利用移位运算符>>>是向下取整,并且是集体往右移动,左边补零。

总结

  • 第一:JAVA默认二进制都是无符号的。
  • 第二:我们利用移位运算符>>>是向下取整,并且是集体往右移动,左边补零。

二分查找改变版

另一种写法:

public static int binarySearch(int[] a, int target) {
    int i = 0, j = a.length;        //不同点1
    while (i < j) {                //不同点2
        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 指向的还会再次比较,找不到时,会死循环

有个问题需要思考:

当while循环L次那么我们的if判断要指向多少次?因为这衡量着我们算法的好坏。

 那么我们应该如何使得循环内的平均比较次数减少呢?接下来就是解决方法:

二分查找平衡版

public static int binarySearchBalance(int[] a, int target) {
    int i = 0, j = a.length;
    while (1 < j - i) { //当判断到循环中还剩下应该需要比较的值时就结束
        int m = (i + j) >>> 1;
        //这样我们就只需要比较一次,要么执行if要么执行else
        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. 循环内的平均比较次数减少了

二分查找 Java 版

这个方法是JAVA自带二分查找的方法:

private static int binarySearch0(long[] a, int fromIndex, int toIndex,
                                     long key) {
    int low = fromIndex;
    int high = toIndex - 1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        long 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.
}
  • 例如 [1,3,5,6] 要插入 2 那么就是找到一个位置,这个位置左侧元素都比它小

    • 等循环结束,若没找到,low 左侧元素肯定都比 target 小,因此 low 即插入点

  • 插入点取负是为了与找到情况区分

  • -1 是为了把索引 0 位置的插入点与找到的情况进行区分并且+0与-0都是0

二分查找衍生算法

Leftmost 与 Rightmost

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

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

  • 对于数组 [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 改为

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;//返回<=target的最靠右索引
}
  • 大于等于中间值,都要向右找

二分查找Leftmost 与 Rightmost的应用

范围查询

  • 查询 x \lt 4,0 .. leftmost(4) - 1

  • 查询 x \leq 4,0 .. rightmost(4)

  • 查询 4 \lt x,rightmost(4) + 1 .. ...无穷

  • 查询 4 \leq x, leftmost(4) .. 无穷

  • 查询 4 \leq x \leq 7,leftmost(4) .. rightmost(7)

  • 查询 4 \lt x \lt 7,rightmost(4)+1 .. leftmost(7)-1

求排名:leftmost(target) + 1

  • target 可以不存在,如:leftmost(5)+1 = 6

  • target 也可以存在,如:leftmost(4)+1 = 3

求前任(predecessor):leftmost(target) - 1

  • leftmost(3) - 1 = 1,前任 a_1 = 2

  • leftmost(4) - 1 = 1,前任 a_1 = 2

求后任(successor):rightmost(target)+1

  • rightmost(5) + 1 = 5,后任 a_5 = 7

  • rightmost(4) + 1 = 5,后任 a_5 = 7

求最近邻居

  • 前任和后任距离更近者

力扣二分查找练习题

704. 二分查找 - 力扣(LeetCode)

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

35. 搜索插入位置 - 力扣(LeetCode)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值