1. 从“大海捞针”到“精准定位”:为什么你需要lower_bound和upper_bound?
大家好,我是你们的老朋友,一个在C++和算法领域摸爬滚打了十多年的老码农。今天想和大家聊聊两个在竞赛和工程中出场率极高,但很多朋友又觉得有点“绕”的函数:std::lower_bound 和 std::upper_bound。
先来想象一个场景:你面前有一本厚厚的、按姓氏拼音排序的电话簿。现在让你找出所有姓“张”的人。你会怎么做?最笨的办法是从第一页开始,一页一页翻,直到找到第一个“张”,然后继续翻,直到“张”结束。这个过程,就是顺序查找,对应STL里的 find 或 find_if,时间复杂度是O(n)。电话簿薄还好,如果是一百万条数据呢?效率就太低了。
但因为我们知道电话簿是有序的,聪明人会直接翻到“Z”开头的部分,然后快速定位到“Zhang”附近开始查找。这个“快速定位”的过程,就是二分查找,时间复杂度是O(log n),效率有质的飞跃。而 lower_bound 和 upper_bound,就是C++ STL为我们封装好的、用于在有序序列中进行二分查找的“神兵利器”。
我见过太多新手朋友,遇到有序数据的查找问题,第一反应还是手写一个二分循环,调试半天边界条件,最后还可能出bug。其实,STL早就把轮子造好了,而且造得极其精良。lower_bound 和 upper_bound 不仅仅是“查找”,它们更是一种“定位”思想,能帮你解决“找第一个”、“找最后一个”、“统计个数”、“确定插入位置”等一系列衍生问题。掌握它们,你就能在有序数据的海洋里,实现从“大海捞针”到“精准定位”的跨越。
2. 核心概念拆解:lower_bound与upper_bound到底在找什么?
这是最核心也最容易混淆的部分。咱们不用死记硬背,我教你一个我用了很多年的“数轴记忆法”。
想象一根水平数轴,上面按顺序排列着许多点(我们的有序数据)。现在,我用手指定一个目标值 val,把它想象成一根垂直的“标尺线”插在数轴上。
std::lower_bound 的行为:它从数轴左侧(first)向右看,寻找第一个位于“标尺线” val 右侧或正好压在线上的点。用数学语言说,就是“第一个不小于 val 的元素”,即 第一个 >= val 的元素。
- 如果存在值等于
val的点,它返回第一个这样的点。 - 如果不存在等于
val的点,它返回第一个大于val的点。 - 如果所有点都在
val左边(都小于val),它返回数轴终点(last)。
std::upper_bound 的行为:它也从左侧向右看,但寻找的是第一个严格位于“标尺线” val 右侧的点。即 第一个 > val 的元素。
- 无论是否存在等于
val的点,它都跳过所有等于val的点,直接指向比它大的第一个点。 - 如果所有点都小于等于
val,它返回数轴终点(last)。
看个具体例子,数组 vec = {1, 2, 4, 4, 4, 6, 8},目标值 val = 4。
lower_bound(vec.begin(), vec.end(), 4)会指向第一个4(下标2)。upper_bound(vec.begin(), vec.end(), 4)会指向6(下标5)。
那如果 val = 5(不存在)呢?
lower_bound会指向6(第一个>=5的元素)。upper_bound也会指向6(第一个>5的元素)。
如果 val = 9(比所有元素都大)呢?
- 两者都会返回
vec.end()。
我刚开始也老记混,后来就记住一个口诀:lower_bound“可以相等”,upper_bound“必须大于”。理解了这个核心定位逻辑,所有应用都是在此基础上展开的。
3. 实战为王:五大高频应用场景与代码详解
懂了概念,咱们就来真刀真枪地干。下面这些场景,都是我实际项目和刷题中反复遇到的,掌握了它们,大部分有序查找问题都能迎刃而解。
3.1 场景一:精确判断元素是否存在
你可能会说,判断存在不是有 binary_search 吗?没错,但 binary_search 只返回 true/false。很多时候,我们找到元素后还想用它做点别的事(比如修改、删除),这时 lower_bound 的组合拳就更灵活。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> scores = {650, 680, 710, 710, 730, 780};
int target = 710;
// 使用 lower_bound 判断并定位
auto it = lower_bound(scores.begin(), scores.end(), target);
if (it != scores.end() && *it == target) {
cout << "找到分数 " << target << ",首次出现位置在索引: " << (it - scores.begin()) << endl;
// 后续可以直接操作 it,例如 *it = 715;
} else {
cout << "未找到分数 " << target << endl;
// 此时 it 指向第一个 >= target 的位置,可用于插入新成绩
// sc


82

被折叠的 条评论
为什么被折叠?



