C++ / a working model

68 / 80   ·   C++11   ·   约 10 分钟

算法:sort、边界查找与 erase-remove

先记住这句话

标准算法操作范围而非容器所有权。排序需要合法的严格弱序,二分边界查找依赖分区条件,remove 只改变逻辑末尾而不缩小容器;理解这些前置条件比背函数名更能避免错误。

本篇内容
  1. 排序首先要求比较关系正确
  2. 边界查找返回位置,不保证找到相等值
  3. remove 与 erase 各做一半工作
  4. 运行示例
  5. 动手练习

排序首先要求比较关系正确

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>。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录