C++ STL 二分查找算法族详解

C++ STL 二分查找算法族详解

本文面向面试和日常开发,先速查用法,再拆底层原理,最后上八股。


一、用法速查

1.1 前置条件:有序区间

STL 二分查找四件套全部要求输入区间已按 < 或传入的比较器排好序,否则行为未定义(可能 false negative,也可能死循环)。编译不报错,结果全不可靠。

1.2 四函数速查表

函数返回值含义
binary_searchboolval 是否存在
lower_boundForwardIt第一个 ≥ val 的位置
upper_boundForwardIt第一个 > val 的位置
equal_rangepair<It,It>[lower, upper) 区间

1.3 同一组数据上的差异

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{1, 3, 5, 5, 5, 7, 9};
    int val = 5;

    cout << boolalpha;
    cout << "binary_search: " << binary_search(v.begin(), v.end(), val) << "\n";
    // true

    auto lo = lower_bound(v.begin(), v.end(), val);
    cout << "lower_bound: v[" << (lo - v.begin()) << "] = " << *lo << "\n";
    // v[2] = 5

    auto hi = upper_bound(v.begin(), v.end(), val);
    cout << "upper_bound: v[" << (hi - v.begin()) << "] = " << *hi << "\n";
    // v[5] = 7

    auto [l, u] = equal_range(v.begin(), v.end(), val);
    cout << "equal_range: [" << (l - v.begin()) << ", " << (u - v.begin()) << ")\n";
    // [2, 5)

    cout << "count of 5: " << (u - l) << "\n";  // 3
}

执行二分

执行二分

v = [1,3,5,5,5,7,9]

lower_bound(5)
找到第一个≥5的位置

返回迭代器指向 v[2]=5

upper_bound(5)
找到第一个>5的位置

返回迭代器指向 v[5]=7

equal_range = [lower, upper)
即 [v[2], v[5]) = [2,5)

区间内有 5-2=3 个 5

1.4 自定义比较器 / 逆序场景

vector<int> v{9, 7, 5, 5, 5, 3, 1};  // 降序

// 降序区间必须用 greater<int>() 作为比较器
// lower_bound 返回第一个 ≤ val 的位置
auto lo = lower_bound(v.begin(), v.end(), 5, greater<int>());
// v[2] = 5

auto hi = upper_bound(v.begin(), v.end(), 5, greater<int>());
// v[5] = 3

1.5 C++20 ranges 版本

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v{1, 3, 5, 5, 5, 7, 9};

    cout << ranges::binary_search(v, 5) << "\n";    // true

    auto lo = ranges::lower_bound(v, 5);
    cout << "lower_bound: " << *lo << "\n";         // 5

    auto hi = ranges::upper_bound(v, 5);
    cout << "upper_bound: " << *hi << "\n";         // 7

    auto [l, u] = ranges::equal_range(v, 5);
    cout << "equal_range: [" << *l << ", " << *u << ")\n";  // 5, 7

    // 投影——按 second 查找
    vector<pair<int, int>> vp{{1, 10}, {2, 20}, {3, 30}};
    auto it = ranges::lower_bound(vp, 20, {}, &pair<int,int>::second);
    cout << "proj lower_bound: (" << it->first << "," << it->second << ")\n";
    // (2,20)
}

二、底层原理

2.1 核心二分框架

四个函数共用同一套二分逻辑,区别仅在于边界判断条件和返回的信息量。

*mid < val

*mid ≥ val

有序区间 [first, last)

取中点 mid
比较 *mid 与 val

目标在右半
first = mid + 1

目标在左半含 mid
收缩右边界

区间为空?

返回 first

2.2 lower_bound 标准实现(gcc libstdc++)

template<class ForwardIt, class T>
ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value) {
    ForwardIt it;
    typename iterator_traits<ForwardIt>::difference_type count, step;
    count = distance(first, last);

    while (count > 0) {
        it = first;
        step = count / 2;
        advance(it, step);
        if (*it < value) {
            first = ++it;
            count -= step + 1;
        } else {
            count = step;
        }
    }
    return first;
}

核心逻辑:

  • *mid < val → 目标在右半,排除 mid 及其左侧(first = ++mid
  • 否则 → 目标在左半(含 mid),收缩右边界
  • 循环结束,first 指向第一个 ≥ val 的位置

STL 使用 count/step 模式而非 left/right,天然避免 off-by-one——count 递减到 0 自然终止。

2.3 upper_bound 的差异

upper_bound 判断条件从 < 变成 <=(取反等价于 !(val < *mid)):

if (!(value < *it)) {   // 即 *it <= value
    first = ++it;
    count -= step + 1;
} else {
    count = step;
}
函数条件(找到第一个……的元素)
lower_bound*it < valfalse,即 *it ≥ val
upper_boundval < *ittrue,即 *it > val

2.4 equal_range = lower_bound + upper_bound

template<class ForwardIt, class T>
pair<ForwardIt, ForwardIt> equal_range(ForwardIt first, ForwardIt last, const T& value) {
    return {lower_bound(first, last, value),
            upper_bound(first, last, value)};
}

两次二分查找,对随机访问迭代器仍是 O(log n)。

2.5 lower_bound 在有序数组上的跳跃过程

v = [1,3,5,5,5,7,9]
查找 val = 5

first=0, last=7, mid=3
v[3]=5 ≥5 → last=3

first=0, last=3, mid=1
v[1]=3 <5 → first=2

first=2, last=3, mid=2
v[2]=5 ≥5 → last=2

first=2, last=2 → 退出
返回 v[2]=5

2.6 ForwardIterator 陷阱与链表二分

四个函数全部使用 ForwardIterator,不要求 RandomAccessIterator。这意味着 std::forward_list 也能调用——编译通过

否,但用的是 ForwardIterator
编译通过

对 forward_list 调 lower_bound

编译检查
是否为 RandomAccessIterator?

实际运行时

advance(it, step)
逐节点 O(n) 跳跃

每轮二分 O(log n) 轮次
每轮 O(n) advance

总复杂度 O(n log n)

正确做法:用 find 线性查找

O(n),但比 O(n log n) 快

2.7 STL 二分 vs 手写二分

问题表现STL 如何避免
(left+right)/2 溢出left+right > INT_MAX 时 UBcount/2 无加法溢出
死循环left = mid 而非 left = mid+1count -= step+1 保证递减
off-by-one区间开闭搞混count 到 0 自动终止
边界条件写反差一个 = 全错统一用 < 判断,按需取反

STL 的 count/step 模式把上述坑全封装了。


三、面试题

Q1:binary_search 为什么不返回位置?

“设计取舍。binary_search 的语义是判存在性,返回迭代器会迫使用户处理 end()。实际实现就是 auto it = lower_bound(...); return it != last && !(val < *it);——一层包装。单一职责:存在性给 bool,找位置给 lower_bound。”

Q2:lower_bound 和 upper_bound 的 off-by-one 陷阱?

“两组边界。第一,所有元素都小于 val 时,两者都返回 last——解引用 UB。第二,lower_bound 返回第一个 ≥ val,如果 val 不存在,它指向 val 本应在的位置(插入仍有序)。upper_bound 返回第一个 > val。所以 [lower, upper) 恰好是所有等于 val 的区间。”

Q3:equal_range 什么场景用?

“两种场景。第一,multiset/multimap 批量操作——auto [l, u] = mp.equal_range(5) 拿到区间,mp.erase(l, u) 一键删除。第二,统计计数——distance(l, u) 得个数,比手动调两次更安全。注意 equal_range 内部就是调 lower_bound + upper_bound,性能没加成,纯代码简洁。”

Q4:二分查找对 forward_list 的效率陷阱?

forward_list 是单向链表,迭代器是 ForwardIterator 而非 RandomAccessIterator。编译能过,但 advance 是 O(n),每轮 O(log n) 轮次,总 O(n log n)。对链表用 find O(n) 反而更快。标准没禁止这种组合,工程中绝不要做。”

Q5:为什么必须有序?

“二分查找核心假设是区间按比较器有序。如果无序,mid 和 val 的比较结果无法预测右侧元素大小关系。比如 [3,1,5],mid=1,发现 1 < val 就去右半找——左边可能藏着 val。这是算法前提,不是 STL 约束。”

Q6:手写二分的常见 bug?

“四个经典坑。第一,mid = (left + right) / 2 大数溢出,改成 left + (right - left) / 2。第二,死循环——while(left<right)left=mid 而非 left=mid+1right-left==1 时永不收敛。第三,区间边界不统一——[left,right)[left,right] 混用。第四,*mid < target 还是 *mid <= target 差一个 = 号决定是 lower_bound 还是 upper_bound。STL 的 count/step 实现把这些问题全封装了。”

Q7:lower_bound 在 map/set 上成员 vs 全局?

“map/set 有成员函数版本,std::lower_bound。全局版本在非 RandomAccessIterator 上只能 O(n) advance(虽然有特化),成员函数直接利用红黑树结构保证 O(log n)。对 map/set 永远用 mp.lower_bound(k)。”

Q8:C++20 ranges 二分的新东西?

“两点。第一,不用传 begin/end——ranges::lower_bound(v, 5) 直接传容器。第二,投影——ranges::lower_bound(vp, 20, {}, &pair<int,int>::second),比较时只看 second 成员,不需要写 Lambda。”

Q9:equal_range 为什么返回 pair 不是 tuple?

“pair 足够表达——两个迭代器分别表示开始和结束。C++11 结构化绑定后 auto [l, u] = equal_range(...) 读起来不输 tuple。标准委员会对 pair 的偏好在算法层面一致——minmax、partition_copy 等返回值都用 pair。”


一句话总结:STL 二分四件套共享一套 O(log n) 二分框架,区别仅在于边界判断条件和返回信息量;理解 lower_bound 返回"第一个 ≥ val"这一核心语义,分清数组和链表上的性能差异,知道手写二分的溢出和 off-by-one 陷阱,是掌握这一族算法的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ricky_Theseus

感谢大家,祝您生活愉快

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值