当前位置: 首页 > news >正文

【算法】——会了二分查找,对O(logn)真的很敏感

前言

  在算法的世界里,二分查找(Binary Search)犹如一盏智慧的明灯,为我们在数据海洋中指明方向。这种基于分治思想的经典算法,能在O(log n)的时间复杂度内快速定位目标元素,将传统线性搜索的效率提升到了全新的高度。

  二分查找的精妙之处在于它每一次比较都能将搜索范围减半,这种指数级的效率提升使其成为处理有序数据的首选方案。从基础的数组查找到各种变形应用,二分查找展现了算法设计中"分而治之"思想的强大威力。无论是数据库索引、游戏开发中的碰撞检测,还是机器学习中的超参数调优,二分查找都扮演着不可或缺的角色。

  本文将深入剖析二分查找的核心原理,从标准实现到各种实用变体,通过清晰的代码示例和实际应用场景,带您领略这一经典算法的优雅与力量。让我们一起探索,如何用二分查找的智慧,在编程世界中实现更高效的搜索解决方案。

二分查找

二分查找是一种在​​有序集合​​中快速定位目标元素的分治算法,其核心是通过每次比较将搜索范围减半,实现指数级效率提升。

二分查找算法遵循三个关键原则:

  1. ​有序性​​:输入数据必须预先排序(升序或降序)
  2. ​边界收缩​​:通过比较中间元素动态调整搜索区间
  3. ​终止条件​​:当区间缩小到单个元素或空时结束

 这里的有序性,也可以理解为二段性

当你要分治的数组或者其他数据结构呈现两极分化,可以靠一个特征分成两段的时候,也可以使用二分查找

二分查找也有模板,并且它的模板很适用,直接往题目上套就行

二分查找因为使用目的不同,模板也有三种

模版一:标准二分查找

// 在有序数组中查找目标值,返回索引(不存在返回-1)
int binary_search(vector<int>& nums, int target) {
    int left = 0, right = nums.size() - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2; // 防止溢出
        if (nums[mid] > target) {
           //大于
           right = mid-1;
        } else if (nums[mid] < target) {
           //小于
           left = mid+1;
        } else {
           //等于
        }
    }
    return -1;
}

模版二:寻找左边界

// 找到第一个 = target 的元素位置(可用于重复元素查找)
int left_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size(); // 注意右边界
    int left = 0, right = nums.size()-1;
    while(left<right){
          int mid = left + (right-left)/2;
          if(nums[mid] < target) left = mid+1;
          else right = mid; 
    }
    return left; 

将数组分成左半区域和右半区域

  • 左半区域满足一个条件,例如均小于target
  • 右半区域满足一个条件,例如均大于或者等于target

寻找左边界,是寻找右半区域的左边界

  • 更新right时,不能越过右半区域,因此right = mid 
  • 更新left时,能越过左半区域,因此left=mid+1

 注意:寻找左边界时的mid更新策略,必须是mid = left+(right-left)/2,否则会死循环

 模版三:寻找右边界

// 找到最后一个 = target 的元素位置
int right_bound(vector<int>& nums, int target) {
    int left = 0, right = nums.size()-1;
    while(left<right){
          int mid = left + (right-left+1)/2;
          if(nums[mid] > target) right = mid-1;
          else left = mid; 
    }
    return right; 
}

将数组分成左半区域和右半区域

  • 左半区域满足一个条件,例如均小于或者等于target
  • 右半区域满足一个条件,例如均大于target

寻找右边界,是寻找左半区域的右边界

  • 更新right时,能越过右半区域,因此right = mid-1 
  • 更新left时,不能越过左半区域,因此left=mid

注意:寻找右边界时的mid更新策略,必须是mid = left+(right-left+1)/2,否则会死循环

很多时候,大家会把寻找左边界和寻找右边界的mid更新策略记混,不妨试试关注left或者right的更新策略中是否出现-1,如果出现-1,则mid中就需要+1,就是寻找右边界,否则就是寻找左边界,不+1

什么时候使用二分查找

二分查找(Binary Search)是一种高效的搜索算法,但并非所有问题都适用。以下是判断是否可以使用二分查找的关键条件:


​1. 数据必须有序(或部分有序)​

二分查找的核心是​​每次比较后能排除一半的搜索范围​​,因此数据必须满足​​单调性​​(升序或降序)。
​适用场景​​:

  • 完全有序数组(如 [1, 3, 5, 7, 9]
  • 部分有序数组(如旋转排序数组 [4, 5, 6, 1, 2, 3]
  • 其他具有单调性的问题(如数学函数、答案范围可二分的情况)

​不适用场景​​:

  • 完全无序的数组(需先排序,否则只能用线性搜索)

​2. 问题需要快速查找(优于 O(n))​

当暴力解法的时间复杂度为 ​​O(n)​​ 或更高时,二分查找能优化到 ​​O(log n)​​。
​典型问题​​:

  • ​精确查找​​:在有序数组中找目标值(如 std::lower_bound
  • ​边界查找​​:找第一个/最后一个满足条件的元素(如 >=x 的最小值)
  • ​最值问题​​:求满足条件的最大值/最小值(如「吃香蕉问题」)
  • ​数学问题​​:求平方根、对数等(利用单调性逼近答案)


​3. 问题具有「二分性」​

即问题的解可以通过​​中间值判断搜索方向​​,分为两类:

​(1) 显式二分(直接基于有序数据)​

  • 在有序数组中查找目标值(LeetCode 704)
  • 找插入位置(LeetCode 35)
  • 搜索旋转排序数组(LeetCode 33)

​(2) 隐式二分(答案范围可二分)​

  • ​求最值问题​​:
    • 最小化最大值(如「分割数组的最大值」LeetCode 410)
    • 最大化最小值(如「分配巧克力」LeetCode 1231)
  • ​数学逼近​​:
    • 求平方根(LeetCode 69)
    • 解方程(如 x^x = 1000 的解)

​关键特征​​:

  • 问题的解存在一个​​明确的边界​​(如「能否在 h 小时内吃完香蕉?」)
  • 可以通过​​中间值 mid​ 判断解在左半还是右半区间。

二分查找解决问题

例题:在排序数组中寻找元素的第一个位置和最后一个位置

 

题目要求,在数组中找到指定元素出现的第一个和最后一个位置

时间复杂度为O(logn)明着告诉你使用二分查找,这算是二分查找的一个标志

而因为要找到两个位置,则需要二分查找两次

  • 查找左边界一次
  • 查找右边界一次

直接套模版

代码:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size()-1;
        int begin = 0;
        int end = 0;
        if(right<0) return {-1,-1};
        //寻找左端点
        while(left<right){
            int mid = left + (right-left)/2;
            if(nums[mid] < target) left = mid+1;
            else right = mid;
        }
        if(nums[left] != target) return {-1,-1};
        begin = left;
        //寻找右端点
        right = nums.size()-1;
        while(left<right){
            int mid = left + (right-left+1)/2;
            if(nums[mid] > target) right = mid-1;
            else left = mid; 
        }
        end = right;
        return {begin,end};
        
    }
};

例题:山脉数组的峰顶元素

思路:

山脉数组满足这个条件:arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 

其中的arr[i]将数组分成两部分

  • 左半部分,前一个元素比后一个元素小
  • 右半部分,前一个元素比后一个元素大

这符合我们说的,使用二分查找时的二段性,通过前后大小关系将数组严格分成两个部分

山脉数组的定义​​要求峰顶元素是​​最后一个​​满足 arr[i] < arr[i+1] 的位置

因此,本题是寻找右边界,则套用寻找右边界的模版

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        if(arr.size()==0) return -1;
        int ret = 0;
        int left = 1;
        int right = arr.size()-2;
        //寻找右边界
        while(left<right){
            int mid = left + (right-left+1)/2;
            if(arr[mid]>arr[mid-1]) left = mid;
            else if(arr[mid]>arr[mid+1]) right = mid-1;
        }
        return left;
    }
};

 例题:寻找峰值

i < j

左右两边的最小值都是负无穷

随便两个元素,如果nums[i] > nums[j]

  • 代表[0,i]中,一定存在一个峰顶元素
  • 因为左边的最小值是负无穷,所以从0到峰顶元素是一直上升的,遇到峰顶元素开始下降

随便两个元素,如果nums[i]<nums[j]

  • 代表[j,n-1]中,一定存在一个峰顶元素
  • 因为最右边的最小值上服务器,所以从j位置到峰顶元素是一直上升的,遇到峰顶元素开始下降

因此,我们需要寻找的是开始下降的元素,即寻找右边区域的最左边

  • 当nums[mid] > nums[mid+1] ,right = mid;
  • 当nums[mid]  < nums[mid+1],left = mid + 1;

代码:

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int left =0;
        int right = nums.size()-1;
        while(left<right){
            int mid = left + (right-left)/2;
            if(nums[mid]<nums[mid+1]) left = mid+1;
            else if(nums[mid]>nums[mid+1]) right=mid;
        }
        return left;
    }
};

🌟 结语:二分查找——让搜索快如闪电的魔法 🔍✨

在这趟二分查找的探索之旅中,我们从最基础的模板出发,一路解锁了各种神奇的应用场景!无论是经典的有序数组查找🔢,还是充满挑战的山脉数组峰顶定位🏔️,二分查找都用它那O(log n)的超高效率惊艳了我们。

记住这三个黄金法则:
1️⃣ 数据要有序(或部分有序)📈
2️⃣ 问题要可二分(能通过中间值判断方向)🎯
3️⃣ 边界要明确(知道什么时候该收网)🎣

掌握了这些,你就能像算法界的福尔摩斯一样🔍,在各种数据迷宫中快速找到正确答案!下次遇到搜索问题,不妨先问问:这里能用二分查找吗?说不定就能收获意想不到的惊喜哦~

愿你在算法的世界里继续披荆斩棘,让二分查找成为你最锋利的宝剑⚔️!我们下个算法见~ 🚀

P.S. 记住:人生就像二分查找,有时候退一步(right=mid-1),反而能更快找到答案呢😉

相关文章:

  • LabVIEW 中 “Flatten To Json String” VI 应用及优势
  • 【C++取经之路】lambda和bind
  • LeetCode 3396 题解
  • 安装vllm
  • 【mllm】——x64模拟htp的后端无法编译debug
  • MySQL深分页问题
  • 【Code】《代码整洁之道》笔记-Chapter11-系统
  • Cuto壁纸 2.6.9 | 解锁所有高清精选壁纸,无广告干扰
  • 单细胞多组学及空间组学数据分析与应用
  • 《系统分析师-浏览试卷(一)总结》
  • 元生代品牌建设:平台实现工作流(comfyui)创建与技术文档说明
  • CVE-2025-32375 | Windows下复现 BentoML runner 服务器远程命令执行漏洞
  • JavaScript:基本语法
  • 电脑的usb端口电压会大于开发板需要的电压吗
  • 【从零开始学习JVM | 第二篇】HotSpot虚拟机对象探秘
  • ai-warp 开源的Platformatic Stackable 与 AI 服务交互
  • 快速idea本地和推送到远程仓库
  • .net 使用笔记
  • 【DDR 内存学习专栏 1. -- DDR 内存带宽与 CPU 速率】
  • 【Hadoop入门】Hadoop生态之Oozie简介
  • 商城网站建设都需要多少钱/网站流量查询站长之家
  • 免费公司网站建设/360排名优化工具
  • 哪些网站做的比较炫/免费的舆情网站app
  • 深圳做企业网站的公司/杭州推广系统
  • 手机网站模版免费下载/seo博客大全
  • 丹阳疫情最新情况/seo中国是什么