news 2026/7/11 8:30:06

【C++】 深入剖析 lower_bound 和 upper_bound 的二分查找奥秘(附实战代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++】 深入剖析 lower_bound 和 upper_bound 的二分查找奥秘(附实战代码)

1. 二分查找的前世今生

我第一次接触二分查找是在大学的数据结构课上,当时觉得这个算法简直太神奇了——只需要O(log n)的时间复杂度就能在有序数组中快速定位元素。后来在LeetCode刷题时,发现很多题目都需要用到它的变种,特别是STL中的lower_bound和upper_bound函数。

二分查找的核心思想很简单:每次都将搜索范围缩小一半。想象一下你在玩猜数字游戏,对方心里想了一个1-100的数字,你每次猜中间值,对方告诉你大了还是小了,这样最多7次就能猜中。这就是二分查找的精髓。

在C++中,标准库已经为我们封装好了高效的二分查找实现,其中lower_bound和upper_bound是最常用的两个函数。它们都定义在头文件中,底层实现都是基于二分查找,但在具体行为上有些微妙的区别。

2. lower_bound函数深度解析

2.1 基本用法与原理

lower_bound的函数原型有两种形式:

template <class ForwardIterator, class T> ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val); template <class ForwardIterator, class T, class Compare> ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);

这个函数的作用是在有序区间[first, last)中查找第一个不小于val的元素位置。如果所有元素都小于val,则返回last。

举个实际例子:

vector<int> v = {1, 2, 3, 3, 5, 6}; auto it = lower_bound(v.begin(), v.end(), 3); cout << distance(v.begin(), it); // 输出2,指向第一个3

2.2 自定义比较函数

lower_bound的强大之处在于支持自定义比较函数。这在处理复杂数据结构时特别有用。比如我们有一个学生结构体,想按照成绩查找:

struct Student { int id; int score; }; vector<Student> students = {{1, 80}, {2, 85}, {3, 90}}; auto comp = [](const Student& s, int val) { return s.score < val; }; auto it = lower_bound(students.begin(), students.end(), 85, comp);

这里我们定义了一个lambda表达式作为比较函数,告诉lower_bound如何比较Student对象和查找值。

2.3 底层实现剖析

理解lower_bound的底层实现对我们正确使用它很有帮助。下面是一个简化版的实现:

template <class ForwardIt, class T> ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& val) { ForwardIt it; typename std::iterator_traits<ForwardIt>::difference_type count, step; count = std::distance(first, last); while (count > 0) { it = first; step = count / 2; std::advance(it, step); if (*it < val) { first = ++it; count -= step + 1; } else { count = step; } } return first; }

这个实现展示了二分查找的精髓:通过不断缩小搜索范围来快速定位目标位置。

3. upper_bound函数全面掌握

3.1 与lower_bound的区别

upper_bound和lower_bound非常相似,但有一个关键区别:它返回的是第一个大于val的元素位置,而不是不小于。用数学符号表示就是:

  • lower_bound: 第一个 ≥ val 的元素
  • upper_bound: 第一个 > val 的元素

看个例子:

vector<int> v = {1, 2, 3, 3, 5, 6}; auto lb = lower_bound(v.begin(), v.end(), 3); // 指向第一个3 auto ub = upper_bound(v.begin(), v.end(), 3); // 指向5

3.2 实际应用场景

upper_bound特别适合处理存在重复元素的情况。比如统计某个值在有序数组中出现的次数:

int count = upper_bound(v.begin(), v.end(), val) - lower_bound(v.begin(), v.end(), val);

这个方法比线性扫描高效得多,特别是当数组很大时。

3.3 自定义比较示例

和lower_bound一样,upper_bound也支持自定义比较函数。比如我们想在一个降序排列的数组中查找:

vector<int> v = {6, 5, 3, 3, 2, 1}; auto it = upper_bound(v.begin(), v.end(), 3, greater<int>()); cout << *it; // 输出2

这里我们使用了标准库提供的greater()作为比较函数,告诉upper_bound数组是按降序排列的。

4. 实战应用与LeetCode题目解析

4.1 经典题目:查找元素范围

LeetCode第34题"在排序数组中查找元素的第一个和最后一个位置"完美展示了这两个函数的实际价值。题目要求在一个可能包含重复元素的有序数组中,找到给定值的开始和结束位置。

使用lower_bound和upper_bound可以优雅地解决:

vector<int> searchRange(vector<int>& nums, int target) { auto first = lower_bound(nums.begin(), nums.end(), target); if (first == nums.end() || *first != target) { return {-1, -1}; } auto last = upper_bound(nums.begin(), nums.end(), target); return {static_cast<int>(first - nums.begin()), static_cast<int>(last - nums.begin() - 1)}; }

4.2 性能优化技巧

虽然STL的实现已经很高效,但在某些特定场景下我们还可以进一步优化:

  1. 如果知道数据分布特征,可以使用插值查找代替纯二分
  2. 对于小型数据集,线性扫描可能更快(因为避免了函数调用开销)
  3. 在循环中使用时,可以缓存end()迭代器避免重复计算

4.3 常见陷阱与调试技巧

在使用这两个函数时,我踩过不少坑,这里分享几个常见问题:

  1. 忘记排序:这是最常见的错误,一定要确保区间是有序的
  2. 边界条件:特别注意当val小于最小值或大于最大值时的情况
  3. 自定义比较函数:确保比较逻辑与排序时使用的逻辑一致

调试时可以添加一些打印语句,比如:

cout << "Searching for " << val << " in range [" << *first << ", " << *(last-1) << "]\n";

5. 手写实现与STL源码对比

5.1 手写lower_bound

理解STL实现的最好方式就是自己实现一个简化版。下面是我的实现:

template <typename It, typename T> It my_lower_bound(It first, It last, const T& val) { It it; size_t count = distance(first, last); size_t step; while (count > 0) { it = first; step = count / 2; advance(it, step); if (*it < val) { first = ++it; count -= step + 1; } else { count = step; } } return first; }

这个实现和STL的思路基本一致,但省略了一些优化和泛型处理。

5.2 STL源码亮点分析

查看GCC的STL实现,有几个值得注意的优化点:

  1. 使用迭代器特性(iterator traits)来优化不同类型的迭代器
  2. 对于随机访问迭代器有特殊处理,可以直接计算位置
  3. 循环展开等编译器优化技巧

这些优化使得STL版本在各种场景下都能保持最佳性能。

6. 进阶应用与性能测试

6.1 在自定义容器中使用

这两个函数不仅适用于标准容器,也可以用于自定义容器,只要满足:

  1. 容器提供前向迭代器
  2. 元素可以比较(或有自定义比较函数)

比如在一个链表实现的有序集合中:

list<int> lst = {1, 3, 5, 7}; auto it = lower_bound(lst.begin(), lst.end(), 4); lst.insert(it, 4); // 在正确位置插入

6.2 性能对比测试

我做了个简单的性能测试,比较了三种查找方式:

  1. STL lower_bound
  2. 手写二分查找
  3. 线性查找

结果如下(100万元素数组,单位:微秒):

方法平均时间
STL15
手写18
线性1250

STL实现确实是最快的,这得益于编译器的各种优化。

7. 最佳实践与经验分享

经过多年使用,我总结了几个最佳实践:

  1. 对于简单类型,直接使用默认比较即可
  2. 复杂对象查找时,确保比较函数与排序时一致
  3. 在性能关键路径上,考虑数据局部性和缓存友好性
  4. 记得检查返回值是否有效(不等于end())

一个常见的模式是将查找和插入结合起来:

auto pos = lower_bound(container.begin(), container.end(), value); if (pos == container.end() || *pos != value) { container.insert(pos, value); }

这种模式在维护有序集合时非常有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/11 8:28:34

WPF运动控制框架实战:5分钟搞定激光切割机路径编辑(附源码下载)

WPF运动控制框架实战&#xff1a;激光切割路径编辑的高效解决方案 在工业自动化领域&#xff0c;激光切割机的操作效率直接影响生产线的整体产出。传统路径编辑软件往往需要复杂的培训过程&#xff0c;而基于WPF的运动控制框架则提供了直观的可视化操作界面&#xff0c;让技术人…

作者头像 李华
网站建设 2026/7/11 8:29:13

C#结合Spire.OCR实现高效文字识别与试用版水印处理

1. 为什么选择C#和Spire.OCR进行文字识别&#xff1f; 文字识别&#xff08;OCR&#xff09;技术在现代软件开发中越来越重要&#xff0c;无论是处理扫描文档、识别验证码&#xff0c;还是从图片中提取文字信息&#xff0c;都离不开这项技术。在众多OCR解决方案中&#xff0c;S…

作者头像 李华
网站建设 2026/6/14 10:19:53

ARMv7架构实战指南:从寄存器到Cache的嵌入式开发避坑手册

ARMv7架构实战指南&#xff1a;从寄存器到Cache的嵌入式开发避坑手册 1. ARMv7架构概览与开发挑战 在嵌入式系统开发领域&#xff0c;ARMv7架构至今仍是众多物联网设备和工业控制系统的核心选择。作为32位处理器架构的集大成者&#xff0c;它通过A/R/M三种配置文件的差异化设计…

作者头像 李华
网站建设 2026/7/7 18:54:19

无需上传数据!AgentCPM本地研报生成工具快速部署与场景应用

无需上传数据&#xff01;AgentCPM本地研报生成工具快速部署与场景应用 在金融分析、政策研究、学术课题等专业领域&#xff0c;撰写一份高质量的深度研究报告往往需要耗费数天甚至数周时间。研究者不仅要收集海量资料&#xff0c;还要进行复杂的逻辑梳理和观点提炼。更棘手的…

作者头像 李华
网站建设 2026/7/7 11:07:44

AI智能客服项目效率提升实战:从架构优化到生产环境部署

AI智能客服项目效率提升实战&#xff1a;从架构优化到生产环境部署 最近我们团队负责的AI智能客服项目遇到了一个典型的“成长的烦恼”——随着用户量激增&#xff0c;系统在高并发场景下开始出现明显的性能瓶颈。用户反馈客服响应变慢&#xff0c;对话经常中断&#xff0c;监控…

作者头像 李华
网站建设 2026/7/7 14:10:16

吃透 SAP Gateway Service Administration:从 OData V4 服务组、发布机制到排错实践的一体化理解

在很多项目里,开发团队对 OData V2 的管理方式已经非常熟悉:做完服务、激活 ICF、注册服务、联调前端,整套动作几乎成了肌肉记忆。可一旦进入 OData V4,尤其是在 SAP_GWFND、SAP S/4HANA、RAP 以及 ABAP Cloud 语境下,很多人会突然发现,过去那套经验并不能直接照搬。最容…

作者头像 李华