二分搜索算法通解框架

news2024/12/23 16:28:28

文章介绍了二分搜索最常见的几个场景的使用:寻找一个数寻找左侧边界以及寻找右侧边界。阅读本文只需读者了解二分搜索的使用限制和基本原理即可。

我相信,友好的讨论交流会让彼此快速进步!文章难免有疏漏之处,十分欢迎大家在评论区中批评指正。

发现更多好文,请前往柿子先生的博客。


寻找一个数

搜索一个数,如果存在,返回其索引,否则返回 -1。

int binarySearch(int[] nums, int target) {
    // 搜索区间 [left, right]
    int left = 0, right = nums.length - 1;
    while (left <= right) { // !!!
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            return mid; // !!!
        } else if (nums[mid] < target) { // 搜索区间变为 [mid+1, right]
            left = mid + 1; // !!!
        } else { // 搜索区间变为 [left, mid-1]
            right = mid - 1; // !!!
        }
    }
    return -1; // !!!
}

计算 mid 时需要防止溢出,left + ((right - left) >> 1)(left + right) / 2 的结果相同,但是有效防止了 leftright 太大,直接相加导致溢出的情况。

  • 搜索区间: [left, right],因为 right 初始化时是 nums.length - 1,即最后一个元素的索引。

  • 停止搜索: nums[mid] == target,找到目标值即停止。如果没有找到,while 循环终止,并返回 -1。

  • 循环终止: 搜索区间为空的时候,循环终止。while (left <= right) 的循环终止条件是当 left 的值为 right + 1 时,写成区间形式就是 [right + 1, right],此时搜索区间为空。此时,while 循环终止是正确的,直接返回 -1 即可。

  • 区间移动: 在搜索区间为 [left, right] 时,若索引 mid 上的元素不是要找的 target 时,要去 [left, mid - 1][mid + 1, right] 区间上搜索,因为 mid 已经被搜索过了,应当从搜索区间中删除

  • 代码缺陷: 无法找到升序数组中存在多个目标值的左右边界索引情况。假设有升序数组 nums = [1, 2, 3, 3, 3, 3, 3],target 为 3,该算法返回的索引是 3。如果此时,我想得到 target 的左侧边界索引,即 2,或者想得到 target 的右侧边界,即 6,上述代码是无法处理的。

下面,我们来学习如何使用二分搜索找到目标值的左右边界。


寻找左侧边界的二分搜索

int searchLeftBound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            right = mid - 1; // 收缩右边界
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    // 越界检查,不存在检查
    if (left >= nums.length || nums[left] != target) {
        return -1;
    }
    return left;
}

1. 为什么能够搜索左侧边界?

关键点在于 nums[mid] == target 时的处理。

if (nums[mid] == target) {
    right = mid - 1; // 收缩右边界
}

当找到 target 时,不立即返回,而是缩小搜索区间的右边界 right,在区间 [left, mid - 1] 中继续搜索,也就是不断向左靠拢,达到锁定左侧边界的目的。

2. 为什么最终返回的是 left 而不是 right

while (left <= right) 的循环终止条件是 left == right + 1 ,因此左边界的索引值一定是在 left == right 时出现的。然而此时,循环无法停止,right 还要继续收缩,因此只能返回左边界的索引值 left

3. 为什么越界检查只检查左边界 left

我们最终返回的是左边界索引 left,因此只需校验左边界 left 最终是否合法即可,另一方面,由于 nums[mid] == target 时,right = mid - 1; 这样 right 在很多情况下都会越界(比如,左边界的索引为 0 时),校验其是否合法没有意义,还会导致返回错误。


寻找右侧边界的二分查找

int searchRightBound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            left = mid + 1; // 收缩左边界
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    if (right < 0 || nums[right] != target) {
        return -1;
    }
    return right;
}

1. 为什么能够搜索右侧边界?

关键点在于 nums[mid] == target 时的处理。

if (nums[mid] == target) {
    left = mid + 1; // 收缩左边界
}

当找到 target 时,不立即返回,而是增大搜索区间的左边界 left,在区间 [mid + 1, right] 中继续搜索,也就是不断向右靠拢,达到锁定右侧边界的目的。

2. 为什么最终返回的是 right 而不是 left

while (left <= right) 的循环终止条件是 left == right + 1 ,而右边界的索引值一定是在 left == right 时出现的。然而此时,循环无法停止,left 还要继续增大,因此只能返回右边界的索引值 right

3. 为什么越界检查只检查右边界 right

我们最终返回的是右边界索引 right,因此只需校验右边界 right 最终是否合法即可,另一方面,由于 nums[mid] == target 时,left = mid + 1; 这样 left 在很多情况下都会越界(比如,右边界的索引为 0 时),校验其是否合法没有意义,还会导致返回错误。


逻辑统一

首先,我们先来梳理一下,需要统一哪些内容?

1. 搜索区间:[left, right],即搜索区间左右皆闭合。

2. 循环条件:left <= right,因此,终止条件为 left == right + 1

3. 收缩区间: 寻找左边界,找到目标值后,收缩 right,即 right = mid - 1;寻找右边界,找到目标值后,扩大 left,即 left = mid + 1

4. 越界校验: 寻找左边界,跳出循环后,校验 left;寻找右边界,跳出循环后,校验 right

5. 返回值: 寻找左边界,返回 left;寻找右边界,返回 right

再次回顾之前的代码:

寻找一个数

int binarySearch(int[] nums, int target) {
    int left = 0, right = nums.length - 1; // !!!
    while (left <= right) { // !!!
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            return mid; // !!!
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1; // !!!
}

寻找左边界

int searchLeftBound(int[] nums, int target) {
    int left = 0, right = nums.length - 1; // !!!
    while (left <= right) { // !!!
        int mid = left + ((right - left) >> 1); 
        if (nums[mid] == target) {
            right = mid - 1; // !!!
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    if (left >= nums.length || nums[left] != target) { // !!!
        return -1;
    }
    return left; // !!!
}

寻找右边界

int searchRightBound(int[] nums, int target) {
    int left = 0, right = nums.length - 1; // !!!
    while (left <= right) { // !!!
        int mid = left + ((right - left) >> 1); 
        if (nums[mid] == target) { 
            left = mid + 1; // !!!
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    if (right < 0 || nums[right] != target) { // !!!
        return -1;
    }
    return right; // !!!
}

写在最后,二分搜索最有价值的思想在于,通过已知信息尽可能多地收缩(折半)搜索空间,从而提高穷举效率,快速找到目标。


实战一下

力扣-704-二分查找

题目描述:

力扣-704-二分查找

参考代码:

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target)
                return mid;
            else if (nums[mid] < target)
                left = mid + 1;
            else
                right = mid - 1;
        }
        return -1;
    }
}

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

题目描述:

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

参考代码:

class Solution {
    public int[] searchRange(int[] nums, int target) {
        return new int[] {searchLeftBound(nums, target), searchRightBound(nums, target)};
    }    
    int searchLeftBound(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target) {
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (left >= nums.length || nums[left] != target) {
            return -1;
        }
        return left;
    } 
    int searchRightBound(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target) {
                left = mid + 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (right < 0 || nums[right] != target) {
            return -1;
        }
        return right;
    }
}

剑指 Offer 53 - I. 在排序数组中查找数字 I

题目描述:

剑指 Offer 53 - I. 在排序数组中查找数字 I

参考代码:

class Solution {
    public int search(int[] nums, int target) {
        int L = leftBound(nums, target);
        int R = rightBound(nums, target);
        if ( L == -1 || R == -1) {
            return 0;
        } else {
            return R - L + 1;
        }
    }

    int leftBound(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target) {
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (left >= nums.length || nums[left] != target)
            return -1;
        return left;
    }

    int rightBound(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target) {
                left = mid + 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        if (right < 0 || nums[right] != target)
            return -1;
        return right;
    }
}

写在最后

文章主体内容在《labuladong 的算法小抄》的基础上进行了个性化增删,文章围绕 「通解框架」 这一核心,删除了许多个人认为不必要的内容,大幅缩减了文章篇幅。同时,在使用二分搜索寻找左右侧边界问题上提出并回答了几个比较关键的问题,最后在逻辑统一部分提炼了几个关键点,更加有助于通解框架的理解与记忆。


参考资料

  1. labuladong 的算法小抄
  2. 力扣网

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/483673.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

密码学【java】初探究加密方式之对称加密

文章目录 一 常见加密方式二 对称加密2.1 Cipher类简介2.2 Base算法2.3 补充&#xff1a;Byte&bit2.4 DES加密演示2.5 DES解密2.6 补充&#xff1a;对于IDEA控制台乱码的解决方法2.7 AES加密解密2.8 补充&#xff1a; toString()与new String ()用法区别2.9 加密模式2.9.1 …

MySQL学习笔记第六天

第06章多表查询 5. 7种SQL JOINS的实现 A是员工表&#xff0c;B是部门表。 5.7.1 代码实现 #8. UNION 和 UNION ALL的使用 # UNION&#xff1a;会执行去重操作 # UNION ALL:不会执行去重操作&#xff0c;效率优于前者&#xff0c;开发中优先使用 #结论&#xff1a;如果明确…

【Java入门合集】第二章Java语言基础(四——第二章结束)

【Java入门合集】第二章Java语言基础&#xff08;四——第二章结束&#xff09; 博主&#xff1a;命运之光 专栏&#xff1a;JAVA入门 学习目标 掌握变量、常量、表达式的概念&#xff0c;数据类型及变量的定义方法&#xff1b; 掌握常用运算符的使用&#xff1b; 掌握程序的顺…

【LeetCode股票买卖系列:188. 买卖股票的最佳时机 IV | 暴力递归=>记忆化搜索=>动态规划】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

Hibernate(一)——入门

在之前经常用到操作数据库的框架是Mybatis或者Mybatis-plus。 Hibernate在項目中用过&#xff0c;但没有深入的了解过&#xff0c;所以这次趁着假期把这个框架了解一下。 目录 概念Hibernate和Mybatis的区别Hibernate使用依赖引入Hibernate配置文件XML配置文件详解properties文…

2023 年 五一杯 B 题过程 + 代码(第一问)

文章目录 第一题问题分析PageRank 算法&#xff08;可跳过&#xff09;PageRank 算法修正权重系数 结果各城市链出与链入链出 权重链入 权重 PageRank 算法结果代码 第一题 问题分析 从收货量、发货量、快递数量增长/减少趋势、相关性等多角度考虑&#xff0c;建立数学模型&…

如何使用git更新别人的代码

文章目录 如何使用git更新别人的代码问题说明省流问题示例操作步骤总结总结 如何使用git更新别人的代码 问题说明 当自己git clone别人的代码之后&#xff0c;代码一直停留到本地电脑上&#xff0c;而你就跑了一次程序就搁置了。 后来有一天你想再次运行该代码&#xff0c;但…

可观测性:你的应用健康吗?

一、需求来源 首先来看一下&#xff0c;整个需求的来源&#xff1a;当把应用迁移到 Kubernetes 之后&#xff0c;要如何去保障应用的健康与稳定呢&#xff1f;其实很简单&#xff0c;可以从两个方面来进行增强&#xff1a; 首先是提高应用的可观测性&#xff1b;第二是提高应…

Matplotlib 安装介绍

文章目录 安装步骤 Matplotlib 不止是一个数学绘图库&#xff0c;它也是可视化和分析工具中最流行之一。我们可用其制作简单的图表&#xff0c;如折线图和散点图。 安装步骤 先进入&#xff1a;python官网 跳转到界面&#xff1a; 录入并搜索 下载之前&#xff0c;看一下自…

嵌入式linux学习笔记--虚拟局域网组网方案分享,基于自组zerotier -planet 网络的方案

0. 前言 五一假期期间重新考虑了目前的组网环境&#xff0c;准备对目前的组网进行一个重新的划分。 目前有的资源 ① 两台 服务器&#xff0c;阿里云-深圳&#xff08;5M上行&#xff09;和腾讯云 广州&#xff08;3M上行&#xff09; ② 带动态公网IP的家庭宽带 &#xff08;…

伽马校正的前世今生

关于伽马校正的前因后果&#xff0c;在网上有不同版本的说法&#xff0c;由于年代久远的因素&#xff0c;导致原本很简单的事情越说越复杂。今天我们的目标就是抓住伽马的头&#xff0c;而不是摸一下伽马的尾巴。 一&#xff0c;鱼龙混杂的论调 1&#xff0c;CRT 显示器的物理…

系统集成项目管理工程师下午真题 计算题 及考点 汇总(更新中。。。)

文章目录 2022下半年广东卷 2022下半年广东卷 1、质量保证、质量控制。质量管理方面存在的问题&#xff0c;并给出正确的做法。判断下列选项的正误。 2、下表是一个软件项目在编码阶段各活动的计划和实际完成情况&#xff08;工作量&#xff0c;单位&#xff1a;人天&#xf…

Linux环境下的redis

一&#xff1a;安装与启动 1.下载redis安装包 2.解压&#xff1a;tar –xvf 文件名.tar.gz 3.安装 进入redis目录&#xff08;cd redis-x.x.x/)后&#xff0c;执行make install 命令 4.启动 进入src目录&#xff0c;执行redis-server 此时该界面无法再使用&#xff0c;需要…

Eureka 服务注册源码探秘——图解、源码级解析

&#x1f34a; Java学习&#xff1a;社区快速通道 &#x1f34a; 深入浅出RocketMQ设计思想&#xff1a;深入浅出RocketMQ设计思想 &#x1f34a; 绝对不一样的职场干货&#xff1a;大厂最佳实践经验指南 &#x1f4c6; 最近更新&#xff1a;2023年5月2日 &#x1f34a; 点…

NPOI导出word文档中插入公式总结

1. XWPFOMath类 XWPFDocument doc new XWPFDocument(); //创建新行 XWPFParagraph p doc.CreateParagraph(); //创建空的公式 XWPFOMath math p.CreateOMath();通过XWPFParagraph的扩展方法创建 方法名备注CreateAcc();创建XWPFAcc类&#xff0c;实现字符在文字上面的类Cr…

【前端】2.HTML基础知识

文章目录 1. 基本概念1.1 HTML是什么1.2 HTML的作用1.3. 学习导引1.4 开发工具 2. HTML 基础语法2.1 demo2.1.1 HTML 详述2.1.2 HTML标签2.1.3 HTML网页结构2.1.4HTML版本 2.2 常用元素2.3 属性2.4 文本相关语法2.5 链接相关语法2.6 头部相关语法 3. 总结3.1 HTML 基础语法总结…

什么是VLAN?为什么要划分VLAN?

VLAN(Virtual Local Area Network)即虚拟局域网&#xff0c;是将一个物理的LAN在逻辑上划分成多个广播域的通信技术。每个VLAN是一个广播域&#xff0c;VLAN内的主机间可以直接通信&#xff0c;而VLAN间则不能直接互通。这样&#xff0c;广播报文就被限制在一个VLAN内。 一、为…

如何简单快速搭建自己的云对象存储服务(OSS)

简单来说&#xff0c;其实我们只需要有一台服务器&#xff0c;利用服务器的各种资源&#xff0c;搭配其它厂商开发的软件&#xff0c;就能很轻易拥有自己的云对象存储服务。不需要在阿里云上花钱买什么服务&#xff0c;甚至还能自己给别人提供服务&#xff0c;真的是太爽了。 云…

五一创作【Android构建篇】MakeFile语法

前言 对于一个看不懂Makefile构建文件规则的人来说&#xff0c;这个Makefile语法和shell语法是真不一样&#xff0c;但是又引用了部分shell语法&#xff0c;可以说是shell语法的子类&#xff0c;Makefile语法继承了它。 和shell语法不一样&#xff0c;这个更难一点&#xff0…

云原生架构的发展历史

目录 1 单机小型机时代2 垂直拆分3 集群化负载均衡架构4 服务化改造架构5 服务治理6 微服务时代7 服务网格新时期 &#xff08;service mesh&#xff09;7.1 背景7.2 SideCar7.3 Linkerd7.4 istio7.5 什么是服务网格7.6 什么是Service Mesh7.7 CNCF云原生组织发展和介绍7.8 国内…