二分查找算法&最靠左索引&最靠右索引详解与优化:图文全解+代码详注+思路分析(一)

简介: 二分查找算法&最靠左索引&最靠右索引详解与优化:图文全解+代码详注+思路分析

1.二分查找算法初识

1.1 简介

二分查找算法,也称折半查找算法,是一种在有序数组中查找某一特定元素的搜索算法。

1.2 实现思路

  1. 初始状态下,将整个序列作为搜索区域;
  2. 找到搜索区域内的中间元素,和目标元素进行比对;
  • 如果相等,则搜索成功;
  • 如果中间元素大于目标元素,表明目标元素位于中间元素的左侧,将左侧区域作为新的搜素区域;
  • 反之,若中间元素小于目标元素,表明目标元素位于中间元素的右侧,将右侧区域作为新的搜素区域;
  1. 重复执行第二步,直至找到目标元素。如果搜索区域无法再缩小,且区域内不包含任何元素,则表明整个序列中没有目标元素,查找失败。

2.二分查找基础版-查找范围左闭右闭

2.1 需求分析

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

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

2.2 算法描述

image.png

2.3 代码实现

/**
     * 二分查找基础版
     *
     * @param array       待查找的升序数组
     * @param targetValue 待查找的目标值
     * @return 找到则返回目标值的索引,找不到返回-1
     */
    public static int binarySearchBasic(int[] array, int targetValue) {
        // 设置指针 left 指向数组的开始索引位置
        int left = 0;
        // 设置指针 right 指向数组的最后索引位置
        int right = array.length - 1;
        // 定一个 mid,表示 left 和 right 的中间索引位置
        int mid;
        /*
            对 [left,right] 范围内的元素进行查找,left <= right 成立说明在 [left,right] 范围内还有元素可以可以查找
            while循环退出的两种方式:
                - 进入了 else,说明找到了,返回中间索引
                - 不满足 left <= right,说明范围不断缩小后依旧没有找到,退出循环
         */
        while (left <= right) {
            // 查找中间索引,如果 (left+right)/2 为小数则会自动向下取整
            mid = (left + right) / 2;
            if (targetValue < array[mid]) {
                // 如果 目标值 小于 中间索引值,说明 目标值索引 应该在 中间索引 的左边
                // 我们可以通过调整 right 的值缩小查找范围
                right = mid - 1;
            } else if (array[mid] < targetValue) {
                // 如果 目标值 大于 中间索引值,说明 目标值索引 应该在 中间索引 的右边
                // 我们可以通过调整 left 的值缩小查找范围
                left = mid + 1;
            } else {
                // 否则说明 目标值 等于 中间索引值,也就说明我们找到了
                return mid;
            }
        }
        // 走到这里说明没有进入过上面 while 中的else,while循环退出时 left > right
        // 说明没有找到
        return -1;
    }

3.基础版的几个问题

3.1 循环条件不加 left == right 行不行

不行,因为这意味着当 left 和 right 相等时,会漏掉 left 和 right 共同指向的元素会漏过与目标值的比较。

3.2 left + right 超过int最大值问题

我们先来举个模拟例子:

public static void main(String[] args) {
        // 模拟 二分查找中的 left
        int left = 100;
        // 模拟 二分查找中的 right
        int right = Integer.MAX_VALUE - 1;
        // 此时 left+right 的值超过了 int范围 的最大值,导致 left + right 的结果为负数
        // 然后对负数进行除以2操作,结果依旧为负数
        int mid = (left + right) / 2;
        // 输出结果为 -1073741775
        System.out.println(mid);
    }

那如何解决这个问题呢?我们可以使用 位运算 来代替 /2 的操作。

  • 算数右移 >> :低位溢出,符号位不变,并用符号位补溢出的高位。
  • 逻辑右击(无符号右移)>>>:低位溢出,高位补0。

由于最高位符号位为0表示该数为正数,因此相比于 >> 做到了能将一个 负数 无符号右移后变成 正数。

/**
     * 二分查找增强版
     *
     * @param array       待查找的升序数组
     * @param targetValue 待查找的目标值
     * @return 找到则返回目标值的索引,找不到返回-1
     */
    public static int binarySearchPlus(int[] array, int targetValue) {
        int left = 0;
        int right = array.length - 1;
        int mid;
        while (left <= right) {
            /*
                考虑到 left+right 的值可能会超过 int可表示 的最大值,我们不再对他们的和直接除以2
                我们知道 除以2 的操作可以用 位运算 >>1 来代替
                但还不够,由于 (left+right) 值溢出表示负数,>>1 只是做 除以2 操作,最高位符号位不变,依旧为1表示负数,负数除以2依旧是负数
                这时候我们可以修改为 无符号右移 >>>1 ,低位溢出,高位补0,那么最高位符号位为0就表示正数了
             */
            mid = (left + right) >>> 1;
            if (targetValue < array[mid]) {
                right = mid - 1;
            } else if (array[mid] < targetValue) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

4.二分查找改变版-查找范围左闭右开

right 只是作为一个边界,指向的一定不是查找目标。

/**
     * 二分查找改变版
     *
     * @param array       待查找的升序数组
     * @param targetValue 待查找的目标值
     * @return 找到则返回目标值的索引,找不到返回-1
     */
    public static int binarySearchPlus(int[] array, int targetValue) {
        // 设置指针 left 指向数组的开始索引位置
        int left = 0;
        // 设置指针 right 指向查找范围的后一个索引位置
        // 在这里 right 只是作为一个边界,指向的一定不是查找目标。
        int right = array.length;
        int mid;
        // 在 [left,right) 左闭右开的区间范围内进行目标值查找
        // 因为 right 只是一个边界不作为查找索引,因此不能将 left <= right 作为条件
        while (left < right) {
            mid = (left + right) >>> 1;
            if (targetValue < array[mid]) {
                // 目标值 小于 中间索引值,则应该将右范围缩小
                // 需要将查找范围变为 [left,mid)
                right = mid;
            } else if (array[mid] < targetValue) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

5.衡量算法的好坏-时间复杂度

5.1 两种算法代码语句执行次数

🍀 二分查找

public static int binarySearchBasic(int[] a, int target) {
        // 设置指针和初值
        int i = 0;
        int j = a.length - 1;
        // L 次  元素在最左边 L 次,  元素在最右边 2*L 次
        while (i <= j) {
            // 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;
    }
    /*
        1 [2,3,4,5] 5  右侧没找到更差
        int i = 0, j = a.length - 1;    2
        return -1;                      1
        元素个数                循环次数
        4-7                    3        floor(log_2(4)) = 2+1
        8-15                   4        floor(log_2(8)) = 3+1
        16-31                  5        floor(log_2(16)) = 4+1
        32-63                  6        floor(log_2(32)) = 5+1
        ...                    ...
        循环次数L  = floor(log_2(n)) + 1
        i <= j                   L+1
        int m = (i + j) >>> 1;   L
        target < a[m]            L
        a[m] < target            L
        i = m + 1;               L
        代码执行次数公式:(floor(log_2(n)) + 1) * 5 + 4
        当n=4时,(3) * 5 + 4 = 19*t
        当n=1024时,(10 + 1) * 5 + 4 = 59*t
     */

🍀 线性查找

/**
     * <h3>线性查找</h3>
     *
     * @param a      待查找的升序数组 (可以不是有序数组)
     * @param target 待查找的目标值
     * @return <p>找到则返回索引</p>
     * <p>找不到返回 -1</p>
     */
    public static int linearSearch(int[] a, int target) {
        for (int i = 0; i < a.length; i++) {
            if (a[i] == target) {
                return i;
            }
        }
        return -1;
    }
    // 1. 最差的执行情况
    // 2. 假设每行语句执行时间一样 t
    /*
        数据元素个数 n
        int i = 0;          1
        i < a.length;       n+1
        i++                 n
        a[i] == target      n
        return -1;          1
        算法运行语句总次数:3*n + 3
        当n=4时,3*4 + 3 = 15*t
        当n=1024时,3*1024 + 3 = 3075*t
     */

5.2 代码执行次数图形化比较

采用 Desmos | 图形计算器 工具对两种算法的代码执行次数进行比较。

  • x 轴表示数组的数据量大小。
  • y 轴表示代码语句执行次数。

二分查找算法执行次数公式:3x+3

线性查找算法执行次数公式:image.png

当数组数据量比较小,即 x 值比较小的时候,执行次数比较为:

当数组数据量比较大,即 x 值比较大的时候,执行次数比较为:

5.3 大O表示法图形化比较

二分查找算法:O(log(n))

线性查找算法:O(n)

6.二分查找性能

6.1 时间复杂度

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

6.2 空间复杂度

  • 需要常数个指针 left,right, mid,因此额外占用的空间是O(1)

7.二分查找平衡版

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

思想:

  1. 左闭右开的区间,i 向的可能是目标,而 j 指向的不是目标
  2. 不奢望循环内通过 m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 i)
  • j − i > 1 的含义是,在范围内待比较的元素个数 > 1
  1. 改变 i 边界时,它指向的可能是目标,因此不能 m + 1
  2. 循环内的平均比较次数减少了
  3. 时间复杂度 Θ(log(n)) (最坏最好情况)


相关文章
|
14天前
|
算法
基于GA遗传算法的PID控制器参数优化matlab建模与仿真
本项目基于遗传算法(GA)优化PID控制器参数,通过空间状态方程构建控制对象,自定义GA的选择、交叉、变异过程,以提高PID控制性能。与使用通用GA工具箱相比,此方法更灵活、针对性强。MATLAB2022A环境下测试,展示了GA优化前后PID控制效果的显著差异。核心代码实现了遗传算法的迭代优化过程,最终通过适应度函数评估并选择了最优PID参数,显著提升了系统响应速度和稳定性。
|
12天前
|
算法
基于WOA鲸鱼优化的购售电收益与风险评估算法matlab仿真
本研究提出了一种基于鲸鱼优化算法(WOA)的购售电收益与风险评估算法。通过将售电公司购售电收益风险计算公式作为WOA的目标函数,经过迭代优化计算出最优购电策略。实验结果表明,在迭代次数超过10次后,风险价值收益优化值达到1715.1万元的最大值。WOA还确定了中长期市场、现货市场及可再生能源等不同市场的最优购电量,验证了算法的有效性。核心程序使用MATLAB2022a实现,通过多次迭代优化,实现了售电公司收益最大化和风险最小化的目标。
|
15天前
|
算法
通过matlab分别对比PSO,反向学习PSO,多策略改进反向学习PSO三种优化算法
本项目使用MATLAB2022A版本,对比分析了PSO、反向学习PSO及多策略改进反向学习PSO三种优化算法的性能,主要通过优化收敛曲线进行直观展示。核心代码实现了标准PSO算法流程,加入反向学习机制及多种改进策略,以提升算法跳出局部最优的能力,增强全局搜索效率。
|
12天前
|
算法
通过matlab对比遗传算法优化前后染色体的变化情况
该程序使用MATLAB2022A实现遗传算法优化染色体的过程,通过迭代选择、交叉和变异操作,提高染色体适应度,优化解的质量,同时保持种群多样性,避免局部最优。代码展示了算法的核心流程,包括适应度计算、选择、交叉、变异等步骤,并通过图表直观展示了优化前后染色体的变化情况。
|
15天前
|
算法
基于大爆炸优化算法的PID控制器参数寻优matlab仿真
本研究基于大爆炸优化算法对PID控制器参数进行寻优,并通过Matlab仿真对比优化前后PID控制效果。使用MATLAB2022a实现核心程序,展示了算法迭代过程及最优PID参数的求解。大爆炸优化算法通过模拟宇宙大爆炸和大收缩过程,在搜索空间中迭代寻找全局最优解,特别适用于PID参数优化,提升控制系统性能。
|
17天前
|
存储 算法 程序员
C 语言递归算法:以简洁代码驾驭复杂逻辑
C语言递归算法简介:通过简洁的代码实现复杂的逻辑处理,递归函数自我调用解决分层问题,高效而优雅。适用于树形结构遍历、数学计算等领域。
|
15天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于贝叶斯优化CNN-GRU网络的数据分类识别算法matlab仿真
本项目展示了使用MATLAB2022a实现的贝叶斯优化、CNN和GRU算法优化效果。优化前后对比显著,完整代码附带中文注释及操作视频。贝叶斯优化适用于黑盒函数,CNN用于时间序列特征提取,GRU改进了RNN的长序列处理能力。
|
18天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
47 1
|
14天前
|
算法 决策智能
基于遗传优化算法的TSP问题求解matlab仿真
本项目使用遗传算法解决旅行商问题(TSP),目标是在四个城市间找到最短路径。算法通过编码、选择、交叉、变异等步骤,在MATLAB2022A上实现路径优化,最终输出最优路径及距离。
|
12天前
|
算法
基于WOA算法的SVDD参数寻优matlab仿真
该程序利用鲸鱼优化算法(WOA)对支持向量数据描述(SVDD)模型的参数进行优化,以提高数据分类的准确性。通过MATLAB2022A实现,展示了不同信噪比(SNR)下模型的分类误差。WOA通过模拟鲸鱼捕食行为,动态调整SVDD参数,如惩罚因子C和核函数参数γ,以寻找最优参数组合,增强模型的鲁棒性和泛化能力。