二分查找(学习笔记分享)

文章介绍了二分查找算法的基本原理和不同版本的实现,包括基础版、改动版(减少比较次数)、平衡版(处理最右侧效率问题)、左最靠和右最靠版本,以及递归实现,详细讲解了每个版本的特点和优化点。
摘要由CSDN通过智能技术生成

概述

二分查找算法,也称为折半查找算法,是一种常见的查找算法。该算法的基本思想是:将有序数组从中间分成两个部分,如果要查找的元素比中间元素大,则在右边继续查找;如果要查找的元素比中间元素小,则在左边继续查找。重复这个过程,直到找到目标元素或者确定目标元素不存在。

再次强调:使用二分查找的前提是该数组必须有序

基础版

下面来实现以下二分查找的基础代码:

/**
     * <h3>二分查找基础版</h3>
     *
     * <ol>
     *     <li>i, j, m 指针都可能是查找目标</li>
     *     <li>因为 1. i > j 时表示区域内没有要找的了</li>
     *     <li>每次改变 i, j 边界时, m 已经比较过不是目标, 因此分别 m+1 m-1</li>
     *     <li>向左查找, 比较次数少, 向右查找, 比较次数多</li>
     * </ol>
     *
     * @param a      待查找的升序数组
     * @param target 待查找的目标值
     * @return <p>找到则返回索引</p>
     * <p>找不到返回 -1</p>
     */
public static int binarySearchBasic(int[] a, int target) {
    int i = 0, j = a.length - 1;    // 设置指针和初值
    while (i <= j) {                // i~j 范围内有元素
        int m = (i + j) >>> 1;  //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 意味着区间内有未比较的元素, 而不是 i<j ?
   i==j 意味着 i,j 它们指向的元素也会参与比较
   i<j 只意味着 m 指向的元素参与比较

在设置中间元素m时,我们尽量写成右移1为进行定义,原因一是移位操作本质是对二进制位进行直接操作,没有复杂的运算逻辑,因此对比普通乘除法效率更高。原因二是Java会把二进制数的最高位判定为符号位,因此在进行两个较大的数相加时可能会超过范围会变成负数,无符号右移相当于除2后向下取整,右移就把原数字移动到java能表示的最大正数范围内了,但是除法运算不会动符号位,就会出现问题

下面为具体的循环过程:

改动版

在二分查找的基础版上进行改动,让原来指向最后元素的j指针作为边界条件,不参与查找的过程,我们就能减少比较的次数,让效率略微提高

/**
     * <h3>二分查找改动版</h3>
     *
     * <ol>
     *     <li>i, m 指针可能是查找目标</li>
     *     <li>j 指针不可能是查找目标,它作为边界,不会参与比较</li>
     *     <li>因为 1. 2. i >= j 时表示区域内没有要找的了</li>
     *     <li>改变 i 边界时, m 已经比较过不是目标, 因此需要 i=m+1</li>
     *     <li>改变 j 边界时, m 已经比较过不是目标, 同时因为 2. 所以 j=m</li>
     * </ol>
     *
     * @param a      待查找的升序数组
     * @param target 待查找的目标值
     * @return <p>找到则返回索引</p>
     * <p>找不到返回 -1</p>
     */
    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;               // 第三处  j作为边界,不会参与比较,因此不是m-1
            } else if (a[m] < target) {
                i = m + 1;
            } else {
                return m;
            }
        }
        return -1;
    }

平衡版

显然,在改动版的基础上,我们会发现当待查找的目标在数组的最右侧时,每次对其查找都需要进行两次比较,而若在左侧,则仅需进入第一个if即可,这就导致了左右查找效率的不平衡,因此我们还要稍加改进

元素在最左边 L 次,  元素在最右边 2*L

/**
     * <h3>二分查找平衡版</h3>
     *
     * <ol>
     *     <li>不奢望循环内通过 m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 i)</li>
     *     <li>i 指针可能是查找目标</li>
     *     <li>j 指针不可能是查找目标</li>
     *     <li>因为 1. 2. 3. 当区域内还剩一个元素时, 表示为 j - i == 1</li>
     *     <li>改变 i 边界时, m 可能就是目标, 同时因为 2. 所以有 i=m</li>
     *     <li>改变 j 边界时, m 已经比较过不是目标, 同时因为 3. 所以有 j=m</li>
     *     <li>三分支改为二分支, 循环内比较次数减少</li>
     * </ol>
     *
     * @param a      待查找的升序数组
     * @param target 待查找的目标值
     * @return <p>找到则返回索引</p>
     * <p>找不到返回 -1</p>
     */
    public static int binarySearchBalance(int[] a, int target) {
        int i = 0, j = a.length;
        while (1 < j - i) {         // 范围内待查找的元素个数 > 1 时
            int m = (i + j) >>> 1;
            if (target < a[m]) {    // 目标在左边
                j = m;
            } else {                // 目标在 m 或右边
                i = m;
            }
        }
        return (target == a[i]) ? i : -1;
    }

优先级版

我们还会发现,实际情况可能会出现数组中有两个元素的值相等,那么如果我们待查找的是这个元素值,则该返回的应该是左边的还是右边的呢?

对左和右谁先再完善:

左先:

/** 
     * <h3>二分查找 Leftmost </h3>
     *
     * @param a      待查找的升序数组
     * @param target 待查找的目标值
     * @return <p>返回 &ge; target 的最靠左索引</p>
     */
    public static int binarySearchLeftmost2(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;
    }

右先:

/**
     * <h3>二分查找 Rightmost </h3>
     *
     * @param a      待查找的升序数组
     * @param target 待查找的目标值
     * @return <p>返回 &le; target 的最靠右索引</p>
     */
    public static int binarySearchRightmost2(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;
    }

递归版

此外,递归也是实现二分查找的一个好方法,二分查找还可以用单路递归进行实现

/**
 * 递归二分查找
 */
public class E03BinarySearch {

    public static int search(int[] a, int target) {
        return f(a, target, 0, a.length - 1);
    }

    /**
     * <h3>递归(子问题)函数:查找 [i .. j] 范围内的目标</h3>
     *
     * @param a      数组
     * @param target 待查找值
     * @param i      起始索引
     * @param j      结束索引
     * @return <p>找到返回索引</p><p>找不到返回 -1</p>
     */
    private static int f(int[] a, int target, int i, int j) {
        if (i > j) {
            return -1;
        }

        int m = (i + j) >>> 1;
        if (target < a[m]) {
            return f(a, target, i, m - 1);
        } else if (a[m] < target) {
            return f(a, target, m + 1, j);
        } else {
            return m;
        }
    }
}
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值