查找与计数算法:find, find_if, search, binary_search, lower_bound, upper_bound, equal_range

说实话,STL 里的查找算法,是我日常开发中用得最频繁的工具之一。不管是写业务逻辑还是做底层优化,总免不了要在容器里翻翻找找。今天咱们就把这些算法掰开揉碎了聊一聊。

线性查找:find 与 find_if

find 是最朴素的查找方式。它从容器头走到尾,找到第一个匹配的值就返回迭代器。找不到?返回 end()

std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
auto it = std::find(vec.begin(), vec.end(), 5);
if (it != vec.end()) {
    // 找到了,it 指向 5
}

我个人习惯用 find 来查简单类型。但如果你要按条件查,就得用 find_if 了。

// 查找第一个大于 5 的元素
auto it = std::find_if(vec.begin(), vec.end(),
    [](int x) { return x > 5; });

小技巧find_if 的 lambda 里可以捕获外部变量,灵活度很高。我在项目中经常用它来查「符合某个状态的对象」。

子序列查找:search

search 是用来查子序列的。比如你在一个长数组里找「1, 2, 3」这个模式第一次出现的位置。

std::vector<int> big = {0, 1, 2, 3, 4, 1, 2, 3, 5};
std::vector<int> pattern = {1, 2, 3};
auto it = std::search(big.begin(), big.end(),
                      pattern.begin(), pattern.end());
// it 指向 big 中第一个 1

嗯,这里要注意:search 用的是朴素匹配,不是 KMP。数据量大的时候性能会差一些。我曾经在一个日志分析工具里用 search 查模式,结果几百万条记录跑了半天。后来换了手写的 KMP,速度提升了十几倍。

避坑指南search 在 C++17 之后有 search 的并行版本,但默认还是串行的。如果你在超大容器上频繁调用,建议评估一下性能。

二分查找:binary_search

二分查找的前提是——数据必须有序。这个前提很多人会忽略。

std::vector<int> sorted = {1, 2, 3, 5, 8, 13, 21};
bool found = std::binary_search(sorted.begin(), sorted.end(), 8);
// found == true

为什么强调有序?因为 binary_search 内部用的是二分法,每次砍掉一半。如果数据无序,结果就是未定义的。我曾经接手过一个项目,前任工程师在无序的 vector 上用了 binary_search,查了半天查不到数据,最后发现是排序没做。

核心要点binary_search 只返回 true/false,不告诉你位置。想知道位置?用 lower_boundupper_bound

边界查找:lower_bound 与 upper_bound

这两个函数是二分查找的「进阶版」。它们返回的是迭代器,指向某个边界位置。

  • lower_bound:返回第一个 不小于 目标值的位置
  • upper_bound:返回第一个 大于 目标值的位置
std::vector<int> v = {1, 2, 2, 2, 3, 4, 5};
auto low = std::lower_bound(v.begin(), v.end(), 2);
auto up  = std::upper_bound(v.begin(), v.end(), 2);
// low 指向第一个 2(索引 1)
// up  指向 3(索引 4)

你想想看,这两个组合起来,就能拿到所有等于目标值的元素范围。这就是 equal_range 干的事。

范围查找:equal_range

equal_range 返回一个 pair,里面装着 lower_boundupper_bound 的结果。

auto range = std::equal_range(v.begin(), v.end(), 2);
// range.first  == lower_bound
// range.second == upper_bound
// 区间 [range.first, range.second) 里全是 2

我个人觉得 equal_range 在统计重复元素时特别好用。比如你要统计某个分数段的人数,用 equal_range 一次搞定。

经验之谈:如果你只需要判断是否存在,用 binary_search。如果你需要插入位置(比如维护有序容器),用 lower_bound。如果你需要统计重复个数,用 equal_range

知识体系总览

下面这张图把今天讲的算法按「是否有序」和「查找目标」做了分类。建议你保存下来,写代码时对照着看。

查找算法 无序容器(线性查找) 有序容器(二分查找) find find_if search binary_search lower_bound upper_bound equal_range 选择指南 • 只查是否存在 → binary_search • 查位置/插入点 → lower_bound / upper_bound • 查重复范围 → equal_range

性能对比一览

算法 时间复杂度 前提条件 返回值
find O(n) 迭代器
find_if O(n) 迭代器
search O(n*m) 迭代器
binary_search O(log n) 有序 bool
lower_bound O(log n) 有序 迭代器
upper_bound O(log n) 有序 迭代器
equal_range O(log n) 有序 pair<迭代器, 迭代器>

重要提醒binary_search 系列算法要求随机访问迭代器。对 listforward_list 用二分查找?编译器不会报错,但性能会退化到 O(n log n),而且结果可能不对。我见过有人把 list 传给 lower_bound,查了半天位置是错的,就是因为迭代器类型不匹配。

好了,今天的内容就到这里。这些算法看着简单,但用对地方能省不少事。下次写代码时,先想想你的数据是有序还是无序,再选合适的算法。别一上来就 find 走天下。

公众号:蓝海资料掘金营,微信 deep3321