68 / 80 · C++11 · 约 10 分钟
算法:sort、边界查找与 erase-remove
标准算法操作范围而非容器所有权。排序需要合法的严格弱序,二分边界查找依赖分区条件,remove 只改变逻辑末尾而不缩小容器;理解这些前置条件比背函数名更能避免错误。
排序首先要求比较关系正确
std::sort 需要随机访问迭代器和可移动、可交换的元素,比较次数为 O(n log n)。它不保证等价元素保持原先次序;需要这种稳定性时使用 stable_sort。标准约束结果与复杂度,并没有规定 sort 必须使用快速排序或某个具体混合算法。
比较器必须形成严格弱序:comp(x,x) 为 false,先后关系可传递,不分先后的等价关系也可传递。写成 a <= b 会违反自反条件;依赖随机数或变化的全局状态同样不行。浮点 NaN 会破坏普通小于关系在含 NaN 数据上的严格弱序要求,应先排除它,或明确定义包含 NaN 的一致排序策略。
边界查找返回位置,不保证找到相等值
升序数据上,lower_bound 返回第一个不小于目标的位置,upper_bound 返回第一个大于目标的位置;两者之间就是等价元素区间。若 lower_bound 返回 end,不能解引用;即使没返回 end,也需检查目标是否等价,不能把插入位置误认为匹配位置。
通用边界算法真正要求的是针对当前查询表达式已经分区,完整有序是最常用的充分条件。查询的比较方向必须与排序一致。其比较次数为对数,但非随机访问迭代器可能需要线性次数前进;对 set、map,应优先调用成员边界查询。
remove 与 erase 各做一半工作
remove_if 稳定地把保留元素压到范围前部,返回新的逻辑末尾;容器 size 不变,尾部对象仍存在但值处于有效而未指定状态。不能把尾部当成“被删除值的集合”,也不要依赖某次运行留下的具体数字。最后调用容器 erase 才真正销毁尾部元素并缩小大小。
示例先排序和查询重复区间,再删除所有偶数,分别验证逻辑长度与物理长度。整个筛选和尾删是线性工作,通常优于在 vector 中每遇到一个匹配就 erase。C++20 的 std::erase_if 可表达常见容器筛选;而 list 的成员 remove_if 能直接删除节点,应优先利用它。
容易答错的地方
- 把 <= 当排序比较器不是“允许相等”的正确方式;等价应由双向比较都为 false 来表达。
- remove_if 的返回位置不是被删元素,也不改变 size;必须配合 erase,且不要读取尾部来推断原始删除项。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <algorithm>
#include <cassert>
#include <iostream>
#include <vector>
int main() {
std::vector<int> values{5, 2, 3, 2, 4, 1};
std::sort(values.begin(), values.end());
assert((values == std::vector<int>{1, 2, 2, 3, 4, 5}));
auto first = std::lower_bound(values.begin(), values.end(), 2);
auto last = std::upper_bound(values.begin(), values.end(), 2);
assert(last - first == 2);
assert(std::lower_bound(values.begin(), values.end(), 9) == values.end());
auto new_end = std::remove_if(values.begin(), values.end(),
[](int x) { return x % 2 == 0; });
assert(values.size() == 6 && new_end - values.begin() == 3);
values.erase(new_end, values.end());
assert((values == std::vector<int>{1, 3, 5}));
std::cout << values[0] << ' ' << values[1] << ' ' << values[2] << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread stl-algorithms.cpp -o example && ./example预期结果
1 3 5
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
已按 std::greater<int> 降序排列为 {9,7,7,3},如何查找所有 7,并说明 lower_bound 与 upper_bound 的位置?
查看参考答案
两次查询都传入同一个 std::greater<int>{} 比较器,或直接调用 std::equal_range(v.begin(), v.end(), 7, std::greater<int>{})。lower_bound 返回下标 1,upper_bound 返回下标 3,区间长度为 2。降序时 lower_bound 是首个不排在目标前面的元素,不能机械套用自然数意义上的“不小于”。使用 greater 需包含 <functional>。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。