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

二分查找:一种经典的 O(log n) 高效搜索算法

在算法学习中,二分查找是一个非常经典、非常重要的基础算法。它常用于在有序数组中快速查找某个目标值的位置。

相比于普通的线性查找,二分查找的效率非常高。线性查找需要从头到尾一个一个遍历,时间复杂度是 O(n);而二分查找每次都会排除一半的搜索区间,因此时间复杂度可以达到 O(log n)。

本文将结合一道经典题目:

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

来系统讲解二分查找的思想、代码实现以及边界处理方法。


一、什么是二分查找?

二分查找,也叫折半查找。

它的核心思想是:

在一个有序数组中,每次取中间位置的元素与目标值比较,根据比较结果缩小一半搜索范围。

假设数组是升序排列的:

nums = [5, 7, 7, 8, 8, 10] target = 8

我们想查找8的位置。

普通查找可能要从下标0开始一个一个看:

5 -> 7 -> 7 -> 8

而二分查找会先看中间元素,然后判断目标值在左半部分还是右半部分。

这种方法每次都能排除一半数据,因此非常高效。


二、二分查找为什么是 O(log n)?

假设数组长度为 n。

第一次查找后,范围变成:

n / 2

第二次查找后,范围变成:

n / 4

第三次查找后,范围变成:

n / 8

不断折半,直到搜索范围缩小为 1。

也就是说,我们需要找到一个 k,使得:

n / 2^k = 1

变形可得:

2^k = n

因此:

k = log₂n

所以二分查找的时间复杂度是:

O(log n)

举个例子,如果数组有 100 万个元素,线性查找最坏可能要查找 100 万次,而二分查找大约只需要:

log₂1000000 ≈ 20

也就是说,最多查找 20 次左右就能完成。

这就是二分查找高效的原因。


三、二分查找的基本模板

普通二分查找用于查找目标值是否存在,或者返回目标值的任意一个位置。

代码如下:

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

这里有一个细节:

int mid = left + (right - left) / 2;

不要直接写成:

int mid = (left + right) / 2;

因为当leftright都很大时,left + right可能会发生整数溢出。

所以更安全的写法是:

left + (right - left) / 2

四、题目分析:查找第一个和最后一个位置

题目要求:

给定一个升序排列的整数数组nums,和一个目标值target,找出目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值,返回:

[-1, -1]

示例:

输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]

因为8第一次出现的位置是下标3,最后一次出现的位置是下标4

再看一个例子:

输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]

因为数组中没有6


五、为什么不能只用普通二分查找?

普通二分查找只能找到目标值的某一个位置。

比如:

nums = [5, 7, 7, 8, 8, 10] target = 8

普通二分查找可能找到下标3,也可能找到下标4,但题目要求我们找到:

第一个 8 的位置:3 最后一个 8 的位置:4

所以我们需要对二分查找进行改造。

思路是:

  1. 用一次二分查找,寻找 target 的最左位置。

  2. 再用一次二分查找,寻找 target 的最右位置。


六、寻找左边界

寻找左边界时,如果nums[mid] == target,不能立即返回。

因为当前的mid虽然是 target,但它不一定是第一个 target。

例如:

nums = [5, 7, 7, 8, 8, 10]

当我们找到下标48时,左边下标3仍然也是8

所以我们应该记录当前答案,然后继续向左搜索。

核心代码:

if (nums[mid] == target) { result = mid; right = mid - 1; }

完整代码如下:

int findLeft(vector<int>& nums, int target) { int left = 0; int right = nums.size() - 1; int result = -1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) { result = mid; right = mid - 1; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return result; }

这段代码的关键是:

result = mid; right = mid - 1;

它表示:

虽然已经找到了一个 target,但是还要继续往左边找,看看有没有更靠前的位置。


七、寻找右边界

寻找右边界和寻找左边界类似。

区别在于,当nums[mid] == target时,我们要继续向右搜索。

核心代码:

if (nums[mid] == target) { result = mid; left = mid + 1; }

完整代码如下:

int findRight(vector<int>& nums, int target) { int left = 0; int right = nums.size() - 1; int result = -1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) { result = mid; left = mid + 1; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return result; }

这段代码的关键是:

result = mid; left = mid + 1;

它表示:

虽然已经找到了一个 target,但是还要继续往右边找,看看有没有更靠后的位置。


八、完整代码实现

#include <iostream> #include <vector> using namespace std; class Solution { public: vector<int> searchRange(vector<int>& nums, int target) { int left = findLeft(nums, target); int right = findRight(nums, target); return {left, right}; } private: int findLeft(vector<int>& nums, int target) { int left = 0; int right = nums.size() - 1; int result = -1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) { result = mid; right = mid - 1; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return result; } int findRight(vector<int>& nums, int target) { int left = 0; int right = nums.size() - 1; int result = -1; while (left <= right) { int mid = left + (right - left) / 2; if (nums[mid] == target) { result = mid; left = mid + 1; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return result; } };

九、代码执行过程分析

以数组:

nums = [5, 7, 7, 8, 8, 10] target = 8

为例。

寻找左边界时:

第一次:

left = 0 right = 5 mid = 2 nums[mid] = 7

因为7 < 8,所以目标值在右边:

left = mid + 1 = 3

第二次:

left = 3 right = 5 mid = 4 nums[mid] = 8

找到 target,记录:

result = 4

但还要继续向左找:

right = mid - 1 = 3

第三次:

left = 3 right = 3 mid = 3 nums[mid] = 8

再次找到 target,更新:

result = 3

继续向左找:

right = mid - 1 = 2

此时:

left = 3 right = 2

循环结束,左边界为:

3

同理,寻找右边界时最终会得到:

4

所以最终结果是:

[3, 4]

十、复杂度分析

这道题中,我们进行了两次二分查找。

第一次查找左边界,时间复杂度是:

O(log n)

第二次查找右边界,时间复杂度也是:

O(log n)

所以总时间复杂度为:

O(log n) + O(log n) = O(log n)

空间复杂度方面,只使用了几个变量:

left、right、mid、result

没有使用额外数组,因此空间复杂度为:

O(1)

最终复杂度:

时间复杂度:O(log n) 空间复杂度:O(1)

十一、二分查找的适用条件

二分查找虽然高效,但不是所有情况都能使用。

它通常需要满足以下条件:

1. 数据必须具有单调性

最常见的情况是数组已经排序。

例如:

[1, 3, 5, 7, 9]

这种数组可以使用二分查找。

但是如果数组是无序的:

[5, 1, 9, 3, 7]

普通二分查找就不能直接使用。

2. 可以通过中间值判断搜索方向

二分查找的关键是:

通过nums[mid]target的比较,判断下一步应该去左边还是右边。

如果不能判断方向,就无法排除一半数据。


十二、二分查找常见错误

1. 循环条件写错

常见写法有两种:

while (left <= right)

和:

while (left < right)

这两种写法都可以,但边界更新方式不同。

本文使用的是:

while (left <= right)

它表示搜索区间是闭区间:

[left, right]

也就是说,leftright指向的位置都可能是答案。

2. mid 计算可能溢出

不推荐写法:

int mid = (left + right) / 2;

推荐写法:

int mid = left + (right - left) / 2;

这样可以避免整数溢出。

3. 找到 target 后直接返回

在普通二分查找中,找到 target 可以直接返回。

但是在这道题中不行。

因为题目要求的是 target 的第一个和最后一个位置。

所以找到 target 后还要继续搜索:

寻找左边界:

right = mid - 1;

寻找右边界:

left = mid + 1;

十三、总结

二分查找是一种非常经典的高效搜索算法。

它的核心思想是:

利用有序性,每次排除一半搜索空间。

在普通查找中,时间复杂度是 O(n);而二分查找可以将复杂度优化到 O(log n)。

本文讲解的这道题并不是简单地判断目标值是否存在,而是要求查找目标值的左右边界。因此,我们需要对普通二分查找进行改造:

findLeft(nums, target)

用于查找第一个出现的位置;

findRight(nums, target)

用于查找最后一个出现的位置。

最终代码的时间复杂度为:

O(log n)

空间复杂度为:

O(1)

二分查找虽然代码不长,但边界处理非常重要。只要能够理解搜索区间、循环条件和左右边界更新方式,就可以解决大量与有序数组相关的问题。

对于算法学习来说,二分查找是必须掌握的基础内容,也是后续学习查找优化、答案二分、旋转数组搜索等问题的重要基础。

http://www.jsqmd.com/news/884826/

相关文章:

  • 如何一键获取B站视频字幕?BiliBiliCCSubtitle工具深度解析
  • 旺哥黄金回收(连锁品牌)|邵阳邵阳县黄金回收 2026 年 5 月行情解读、避坑攻略与常见疑问 - 润富黄金珠宝行
  • 石墨烯电吸收调制器:突破光互连带宽与能效瓶颈
  • Unity项目实战:用TriLib 2.x插件动态加载外部FBX/OBJ模型(含贴图自动读取)
  • 2026年保定GEO优化与短视频代运营:制造业精准获客完全指南 - 优质企业观察收录
  • Construct 3 零代码也能做游戏?手把手教你用事件表做个平台跳跃小游戏
  • 主城可上门回收!2026重庆爱马仕包包回收靠谱渠道,亲测有效 - 奢侈品回收测评
  • 黔南卫生类学校怎么选?2026年初高中毕业生升学完全指南 - 优质企业观察收录
  • 终极AMD Ryzen调试指南:为什么你需要SMUDebugTool这个免费神器?
  • 为什么你的Midjourney出图总是“糊”?3大隐性参数陷阱+5步锐化校准法(附V6.1实测数据)
  • 2026全国广告牌定制场景适配与工艺落地指南 - 深度智识库
  • Unity游戏开发实战:用XCharts插件5分钟搞定数据可视化UI(附完整C#脚本)
  • Lovable内部工具开发方法论(从需求黑洞到用户自发推广的完整闭环)
  • 经典音频功放模块现代化替代:基于IRFP240/9240的MEV5功放板设计与实践
  • 插班转学难?贵州这所 12 年一贯制优质名校插班名额开放,席位紧张速预约! - 深度智识库
  • 避坑指南:Unity动态加载模型时,TriLib插件材质丢失、缩放异常的5个常见问题解决
  • 2026年5月毕业生求职APP推荐!解决应届生求职难痛点 - 讲清楚了
  • 微服务通信链路崩塌预警,Claude异步消息设计:如何用Saga+补偿机制将P99延迟压至87ms以下
  • 3大技术突破:重新定义Switch游戏安装性能极限
  • 2026年保定GEO优化与短视频代运营深度横评:制造业工厂精准获客完全指南 - 优质企业观察收录
  • 融合图机器学习与时间序列分析的CAN总线入侵检测方法
  • Windows安卓应用安装器:3分钟快速上手跨平台应用体验
  • Unity项目实战:用TriLib插件动态加载FBX模型,5分钟搞定外部资源读取
  • 告别老版BindAction!UE5.1.1 EnhancedInput保姆级配置教程(从Action创建到C++回调)
  • 如何快速实现U盘文件自动备份:USBCopyer终极指南
  • 三步破解百度网盘限速:免费获取真实下载链接的终极指南
  • 别再踩坑了!PICO 4开发环境配置保姆级教程(Unity 2022 + PICO SDK)
  • Avidemux视频编辑器完整指南:如何在3分钟内完成专业级视频剪辑
  • 垚昌黄金回收:老旧黄金、断金、变形首饰都能收——2026年5月高位变现的正确打开方式 - 润富黄金珠宝行
  • AI采购决策迫在眉睫,Claude项目回本期究竟多久?——头部科技公司已验证的4.2个月临界阈值