CppCon 2015 学习:STL Algorithms in Action
算法(Algorithm) 是一种解决问题的有限步骤的程序,通常涉及重复操作。
具体来说:
- 是为了解决某个问题(比如求最大公约数)而设计的步骤序列。
- 这些步骤是有限的,最终能得出结果。
- 广义上,算法就是一套逐步执行的指令,用来解决问题或完成某项任务,尤其是计算机执行的任务。
简单说,算法就是“做事的方法”或“操作流程”。
STL算法就是:
- C++标准模板库(STL)里预先写好的通用算法库,用于解决各种常见问题。
- 这些算法随编译器自带,使用时无需额外安装。
- 它们作用于序列(如数组、vector等容器)。
- 语法声明式(declarative),不用写显式的循环代码,比如不用写for或while循环。
- 通过迭代器遍历序列中的元素,逐个对元素执行操作。
- 由专家设计,经过大量真实项目使用验证,稳定可靠,几乎没有bug。
Raw loop 就是指你直接写的传统循环结构,比如 for
、while
、或者 do...while
:
- 你得手动写循环控制、条件判断、迭代操作。
- 往往代码比较长,不够简洁。
- 容易在循环体里写出副作用,比如修改外部变量(比如示例里的
global_count
、found
、以及往out
里插入元素)。 - 代码可读性差,维护成本高。
你举的例子:
vector<int> out;
bool found = false;
for (const auto& i: v) { if (i >= 42) { out.emplace_back(i); ++global_count; if (i == 42) { found = true; } }
}
就是典型的raw loop写法:显式写循环、条件和副作用。
为什么用算法
- 效率高:STL算法通常比自己写的循环快,因为它们经过优化。
- 代码更简洁、更抽象:不需要写具体的循环细节,代码更易读。
- 副作用控制明确:算法内部控制副作用,不会随意影响外部状态。
- 防止副作用泄露:减少意外修改变量的风险。
- 方便推理和验证:更容易理解代码做了什么,也更容易判断结果是否正确。
- 更健壮:经过大量测试,减少不明显的bug。
- 更利于维护:整体代码更清晰,方便别人理解和改动。
STL算法的几大类,具体包括:
不修改序列内容的算法,比如查找、统计、判断等。
会修改序列内容的算法,比如复制、替换、移除元素等。
排序相关算法,比如排序、部分排序、检查是否已排序等。
适配C语言风格的算法,比如 qsort
的对应算法等。
数值相关算法,比如求和、累积、内积等。
关于 STL非修改序列操作算法(Non-modifying sequence operations):
- 它们不会修改输入序列,也就是说,输入的容器或数组的元素不会被改变。
- 这些算法通常不产生新的结果序列,它们主要是用来查询、检查或者统计原序列的信息。
- 算法本身不会对输入序列产生副作用(不会更改序列中的数据)。
- 如果算法中带有函数对象(function object,比如回调函数),那么这个函数对象可能会有副作用,比如修改自己内部的状态,或者在某些特殊情况下(例如
for_each
),可能会修改环境或者序列外部的变量。 std::find
:查找元素std::count
:统计元素出现次数std::all_of
、std::any_of
、std::none_of
:判断序列中元素是否满足某个条件std::for_each
:对每个元素执行操作(函数对象可能有副作用)std::equal
:比较两个序列是否相等std::adjacent_find
:查找相邻重复元素
这一部分是 C++ STL 中的非修改序列操作算法,列出了常用的函数:
- all_of:判断序列中是否所有元素都满足某个条件。
- any_of:判断序列中是否有任意一个元素满足某个条件。
- none_of:判断序列中是否没有元素满足某个条件。
- for_each:对序列中的每个元素执行指定操作(可能带副作用)。
- find / find_if / find_if_not:查找满足条件的元素。
- find_end:查找子序列在序列中最后一次出现的位置。
- find_first_of:查找序列中第一次出现指定集合中任意元素的位置。
更多“实战”算法: - adjacent_find:查找相邻重复元素。
- count / count_if:统计满足条件的元素个数。
- mismatch:找出两个序列首次不匹配的位置。
- equal:判断两个序列是否相等。
- is_permutation:判断两个序列是否为置换(排列)。
- search / search_n:查找子序列或者连续重复元素。
这些算法都不会修改输入序列,且通常不产生新的序列,而是用于查询或遍历。
C++ STL 中的修改序列操作算法(mutating sequence operations),重点是:
- 通常情况下,这些算法不会直接修改输入序列(除非输出序列和输入序列重叠,比如
transform
)。 - 这些算法会产生一个新的输出序列,有些情况下输出序列可能和输入序列重叠,导致原地修改(in-place)。
- 其他算法(比如
copy
)明确禁止输入和输出序列重叠。 - 算法会对输出序列产生显式的副作用(修改输出内容)。
- 传入的函数对象如果有,会影响自身或外部环境,但不应修改输入或输出序列。
总结就是:
这些算法主要是产生修改后的输出,不直接修改输入,或者只在允许重叠时原地修改。
这部分列举了 C++ STL 中的**修改序列操作算法(mutating sequence operations)**的具体例子:
复制和移动类
copy
,copy_n
,copy_if
,copy_backward
move
,move_backward
swap_ranges
,iter_swap
变换和替换类transform
replace
,replace_if
,replace_copy
,replace_copy_if
填充和生成类fill
,fill_n
generate
,generate_n
移除和去重类remove
,remove_if
,remove_copy
,remove_copy_if
unique
,unique_copy
反转和旋转类reverse
,reverse_copy
rotate
,rotate_copy
随机打乱shuffle
,random_shuffle
(C++17后废弃random_shuffle
)
分区相关partition
,is_partitioned
,stable_partition
,partition_copy
,partition_point
这些算法有的生成新序列,有的原地修改,都是为了处理容器里的数据结构,方便对序列进行各种修改和重排。
STL 中的排序及相关操作算法 的特点:
- 既有非修改序列的操作,也有修改序列的操作。
- 修改操作会原地修改序列(例如
sort
、make_heap
),或者将结果输出到另一个序列(例如merge
、partial_sort_copy
)。 - 默认的比较函数是
operator<
,如果你传入自定义比较函数,该函数不应修改序列或迭代器。
换句话说,这组算法用于对容器中的元素进行排序、合并、堆构造等操作,灵活且高效。
这部分是对 STL 排序和相关算法 的详细分类和说明。下面是逐项解释:
排序算法
sort
:快速排序,默认使用operator<
,时间复杂度 O(N log N),不稳定排序。stable_sort
:保持相同元素的相对顺序,适合需要稳定性的场景。partial_sort
:只对前 N 个元素排序,其余元素顺序未定义。partial_sort_copy
:从输入中选出前 N 小元素排序后,复制到输出区。
元素选取
nth_element
:对序列重新排列,使得第 N 个元素处于其排序后应在的位置,其前后分别是较小和较大的元素(但这两部分不一定是有序的)。
二分查找算法(前提:已排序)
lower_bound
:返回第一个不小于目标值的迭代器。upper_bound
:返回第一个大于目标值的迭代器。equal_range
:返回一个 pair,表示目标值的范围[lower, upper)
。binary_search
:判断是否存在目标值,返回true/false
。
合并
merge
:将两个已排序序列合并为一个新排序序列。inplace_merge
:原地合并两个连续的已排序子区间。
集合操作(在排序容器上操作)
includes
:判断一个排序序列是否包含另一个。set_union
:并集。set_intersection
:交集。set_difference
:差集(存在于第一个而不在第二个中的元素)。set_symmetric_difference
:对称差(仅存在于一个集合中的元素)。
堆操作
make_heap
:将一个序列构建成堆。push_heap
:将新元素加入堆中。pop_heap
:将堆顶元素移到末尾。sort_heap
:对堆排序(升序)。
最值查找
min
,max
:两个值之间取最小或最大。minmax
:返回两个值中较小和较大的一个。min_element
,max_element
,minmax_element
:在序列中找出最小、最大或最小最大值对。
字典序比较
lexicographical_compare
:比较两个序列的字典序。
排列
next_permutation
:下一个排列(按字典序)。prev_permutation
:上一个排列。
总结
这些算法:
- 是 STL 的精华部分。
- 性能高,封装好,安全性高。
- 在处理排序、集合操作、最值计算等方面,避免手写低效或错误的循环逻辑。
这部分讲的是 STL(标准模板库)中的 通用数值操作算法(general numeric operations),主要用于执行各种数值相关的计算任务。以下是详细解释:
总体特性
- 是一个专门处理 数值操作 的算法集合。
- 提供对 复杂数(complex numbers)、随机数生成(random numbers)、数组操作(如
valarray
) 和 泛型数值计算 的支持。 - 一部分功能也来源于 ISO C 标准库,例如
accumulate
这种和std::numeric
有关的内容。
常见的 STL 数值算法(来自 <numeric>
头文件)
算法名 | 功能说明 |
---|---|
std::accumulate | 求和或其他二元操作(可自定义),如:accumulate(v.begin(), v.end(), 0) |
std::inner_product | 计算两个序列的内积(点积) |
std::adjacent_difference | 相邻元素之间的差值(输出序列) |
std::partial_sum | 前缀和(部分和)计算 |
std::iota | 填充一个序列以递增整数,如:iota(v.begin(), v.end(), 0) 从0开始填充 |
示例
#include <numeric>
#include <vector>
#include <iostream>
int main() {std::vector<int> v = {1, 2, 3, 4};// 累加和int sum = std::accumulate(v.begin(), v.end(), 0); // 结果是 10// 内积int product = std::inner_product(v.begin(), v.end(), v.begin(), 0); // 1*1 + 2*2 + 3*3 + 4*4 = 30// 部分和std::vector<int> partial;//1 1+2 1+2+3 1+2+3+4std::partial_sum(v.begin(), v.end(), std::back_inserter(partial)); // {1, 3, 6, 10}std::cout << "sum: " << sum << ", product: " << product << '\n';
}
总结
STL 的数值算法部分:
- 适合用于泛型编程中的数学计算。
- 可以与容器(如
vector
,array
,valarray
)配合使用。 - 功能强大,使用简洁,避免手写循环逻辑。
- 是性能和可读性之间的良好平衡。
这一部分讲的是 C 标准库中的算法,比如 bsearch
和 qsort
,它们虽然属于 C 标准库(<cstdlib>
),但有时在 C++ 项目中也可能遇到,尤其是 遗留代码(legacy code) 中。
为什么还要提它们?
- 这些是 早期 C 语言风格 的通用算法。
- 在现代 C++ 中,它们已经 不推荐使用,因为:
- 类型安全性差(使用
void*
) - 不够灵活(无法使用泛型、lambda 等)
- 不符合现代 C++ 的设计哲学
- 类型安全性差(使用
- 但你仍然需要了解它们:
- 因为老项目中常用(维护老代码时会碰到)
- 作为知识补全有价值
常见的两个 C 风格算法
函数名 | 用途简介 |
---|---|
qsort | 快速排序(C风格),参数是 void* 类型的数组指针和比较函数 |
bsearch | 二分查找(C风格),在已排序数组中查找元素 |
示例(使用 qsort
)
#include <cstdlib>
#include <iostream>
int compare(const void* a, const void* b) {return (*(int*)a - *(int*)b);
}
int main() {int arr[] = { 5, 2, 9, 1, 3 };int n = sizeof(arr) / sizeof(arr[0]);qsort(arr, n, sizeof(int), compare);for (int i = 0; i < n; ++i)std::cout << arr[i] << " "; // 输出:1 2 3 5 9
}
替代方案(现代 C++)
使用 C++ 的 std::sort
更安全、强大:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {std::vector<int> v = { 5, 2, 9, 1, 3 };std::sort(v.begin(), v.end());for (int i : v)std::cout << i << " "; // 输出:1 2 3 5 9
}
总结
比较项 | C 风格 (qsort ) | C++ 风格 (std::sort ) |
---|---|---|
类型安全 | 使用 void* | 模板函数,类型安全 |
可读性 | 一般 | 更清晰简洁 |
性能 | 不一定最优 | 可以内联比较函数,编译器优化更强 |
推荐程度 | 仅限遗留支持 | 现代 C++ 推荐使用 |
这段内容是在深入对比 for_each
和 transform
——虽然它们都对序列中每个元素应用某种操作,但本质和用途有显著差异。
for_each
的特点
- 非修改性算法(non-modifying sequence operation)
- 不生成输出序列,只遍历执行操作
- 本身不造成副作用,但传入的函数对象(比如 lambda)可能修改元素或修改自身
- 返回的是函数对象(可带状态),常用于统计、打印、计数等
- 不要求输出范围,因为它默认不会生成新数据
总结:
适合做“观察”和“副作用”的操作,如输出、记录日志、累加计数。
int count = 0;
std::for_each(v.begin(), v.end(), [&](int x) {if (x > 5) ++count;
});
transform
的特点
- 修改性算法(mutating sequence operation)
- 显式地将一个输入序列转换为输出序列(输出范围必须提供)
- 可就地修改输入数据,或者写入另一个容器
- 算法本身就是副作用的产生者(它直接修改输出序列)
- 要求函数对象是纯函数(不能修改序列元素或迭代器本身)
- 返回的是结果序列中最后一个位置的迭代器
总结:
适合“数据转换”,例如将一组数字变为平方,或从一个类型映射成另一个。
std::transform(v.begin(), v.end(), out.begin(), [](int x) {return x * x;
});
对比表:
特性 | for_each | transform |
---|---|---|
算法类型 | 非修改(non-modifying) | 修改(mutating) |
是否需要输出范围 | 否 | 是 |
算法是否本身修改数据 | 否(操作留给函数对象) | 是(transform 自己修改结果序列) |
函数对象是否可以有副作用 | 可以(修改元素、记录状态等) | 通常不推荐有副作用 |
返回值 | 函数对象(可带状态) | 结果序列末尾的迭代器 |
常见用途 | 输出、计数、副作用操作 | 数据转换、新序列生成 |
结论
- 用
**for_each**
来做“观察型”的操作(打印、累加、标记),行为是副作用驱动。 - 用
**transform**
来做“转换型”的操作(映射、变换、就地替换),结果是数据驱动。
这个是一个使用 std::for_each
和函数对象(HashString
)的例子,目的是:
将 vector 中的所有字符串串联起来计算一个统一的 hash 值。
代码说明
struct HashString {void operator()(const string& s) {hash = accumulate(s.begin(), s.end(), hash, hash_char);}uint32_t hash = 0;
};
成员变量:
uint32_t hash = 0;
初始化为 0,是累积的哈希值。
成员函数:
operator()(const string& s)
这是一个仿函数(函数对象),用来对每个字符串s
执行哈希操作:
hash = accumulate(s.begin(), s.end(), hash, hash_char);
这里用 std::accumulate
:
- 对
s
的每个字符调用hash_char
函数进行累积 - 起始值是当前的
hash
,所以是“连锁”计算 —— 所有字符串字符合并后共同决定了一个最终哈希
假设的使用方法
std::vector<std::string> strings = {"hello", "world"};
HashString hasher;
std::for_each(strings.begin(), strings.end(), std::ref(hasher));
std::cout << "Final hash = " << hasher.hash << '\n';
你可以看到:
for_each
把每个字符串s
传给了hasher.operator()(s)
hasher.hash
是最终结果,表示所有字符串字符经过哈希计算后的聚合值
hash_char
是什么?
hash_char
应该是一个自定义函数,可能像这样:
uint32_t hash_char(uint32_t acc, char c) {return acc * 31 + static_cast<uint8_t>(c);
}
这是一个常见的字符串哈希算法(类似 Java 的字符串哈希公式)。
总结:
这段代码利用 for_each
和 std::accumulate
实现了对多个字符串的统一哈希计算:
- 使用
for_each
来遍历序列并累加状态(hash
) - 用函数对象封装状态(
hash
)并提供运算逻辑(operator()
) accumulate
再次体现了 STL 算法的力量:表达清晰、代码简洁
这是for_each
用于副作用+状态记录的典型案例!
#include <iostream>
#include <vector>
#include <string>
#include <numeric>
#include <cstdint>
#include <algorithm>
// 定义 hash_char:给一个字符追加到当前哈希值中
uint32_t hash_char(uint32_t acc, char c) {return acc * 31 + static_cast<uint8_t>(c); // 31 是常用的乘数因子
}
struct HashString {void operator()(const std::string& s) {hash = std::accumulate(s.begin(), s.end(), hash, hash_char);}uint32_t hash = 0;
};
int main() {std::vector<std::string> strings = {"hello", "world"};HashString hasher;std::for_each(strings.begin(), strings.end(), std::ref(hasher));std::cout << "Final hash = " << hasher.hash << '\n';
}
你这段代码是一个 使用 std::for_each
和自定义函数对象 HashString
来为一个字符串序列生成单个哈希值 的完整示例。我们来逐步理解:
1. 核心目标
为一个 vector<string>
中的所有字符 计算一个累积的 hash 值,即:
vector<string> v = { "one", "two", "three", "four", "five" };
对每个字符串中的每个字符执行特定的 hash 操作,最终得出一个统一的 uint32_t
hash 值。
2. for_each
+ HashString
struct HashString {void operator()(const string& s) {hash = accumulate(s.begin(), s.end(), hash, hash_char);}uint32_t hash = 0;
};
HashString
是一个函数对象(仿函数),用于给for_each
用。- 它的
operator()
接受string& s
,然后使用std::accumulate
和hash_char
处理这个字符串中每个字符。 - 它通过成员变量
hash
累积结果。
template<typename Cont>
uint32_t hash_all_strings(const Cont& v) {const auto hasher = for_each(v.begin(), v.end(), HashString());return hasher.hash;
}
for_each
会将容器v
中的每个字符串传给HashString::operator()
- 它返回的是
HashString
对象的一个副本,因此我们可以从中取出累积的hash
值
3. hash_char
和 rotl
uint32_t rotl(uint32_t value, unsigned int count) {const uint32_t mask = (CHAR_BIT * sizeof(value) - 1);count &= mask;return (value << count) | (value >> ((-count) & mask));
}
uint32_t hash_char(uint32_t hash, char c) {hash = rotl(hash, c); // 左旋hash ^= c; // 异或当前字符return hash;
}
rotl
实现了一个“循环左移”函数。用于增加哈希的“扩散性”。hash_char
是核心的字符哈希函数,每个字符都对当前 hash 值做旋转与异或处理,从而增强分布效果。
4. 示例输出函数
void test_for_each_hash() {vector<string> v{ "one", "two", "three", "four", "five" };uint32_t hash = hash_all_strings(v);cout << "Hash: " << hash << dec << endl;
}
这段测试代码调用整体哈希计算流程,并输出最终 hash。
总结:你要理解的是
for_each
应用HashString
这个函数对象HashString
内部用std::accumulate
调用hash_char
来处理每个字符hash_char
使用rotl
和^
运算符构建 hash- 所有字符串的所有字符会顺序处理,最终得到一个统一 hash 值
#include <iostream>
#include <vector>
#include <string>
#include <cstdint>
#include <algorithm>
#include <numeric> // for std::accumulate, std::for_each
#include <climits> // for CHAR_BIT
using namespace std;
// 左循环位移函数(循环左移)
uint32_t rotl(uint32_t value, unsigned int count) {const uint32_t mask = (CHAR_BIT * sizeof(value) - 1);count &= mask;return (value << count) | (value >> ((-count) & mask));
}
// 单个字符的 hash 计算函数
uint32_t hash_char(uint32_t hash, char c) {hash = rotl(hash, c); // 先旋转hash ^= c; // 然后异或return hash;
}
// 用于 for_each 的函数对象
struct HashString {void operator()(const string& s) {// 使用 std::accumulate 把每个字符都混入 hash 值中hash = accumulate(s.begin(), s.end(), hash, hash_char);}uint32_t hash = 0;
};
// 通用模板:对任意字符串容器做 hash
template <typename Cont>
uint32_t hash_all_strings(const Cont& v) {auto hasher = for_each(v.begin(), v.end(), HashString());return hasher.hash;
}
// 测试入口
void test_for_each_hash() {vector<string> v{"one", "two", "three", "four", "five"};uint32_t hash = hash_all_strings(v);cout << "Hash: " << hash << dec << endl;
}
int main() {test_for_each_hash();return 0;
}
一个使用 std::transform
的完整示例,下面是逐行的解释和理解:
#include <iostream>
#include <vector>
#include <string>
#include <cstdint>
#include <algorithm>
#include <numeric>
#include <climits>
using namespace std;
// 循环左移函数
uint32_t rotl(uint32_t value, unsigned int count) {const unsigned int mask = (CHAR_BIT * sizeof(value) - 1);count &= mask;return (value << count) | (value >> ((-count) & mask));
}
// 字符哈希函数
uint32_t hash_char(uint32_t hash, char c) {hash = rotl(hash, c); // 循环左移hash ^= c; // 异或return hash;
}
// 对单个字符串生成哈希值
uint32_t hash_string(const string& s) { return accumulate(s.begin(), s.end(), 0u, hash_char); }
// 对字符串容器中每个字符串生成哈希值,返回哈希值vector
template <typename Cont>
vector<uint32_t> hash_each_string(const Cont& v) {vector<uint32_t> res;transform(v.begin(), v.end(), back_inserter(res), hash_string);return res;
}
// 测试函数,打印所有字符串的哈希值
void test_transform_hash() {vector<string> v{"one", "two", "three", "four", "five"};auto res = hash_each_string(v);cout << "Hashes: ";for_each(res.begin(), res.end(), [](uint32_t rh) { cout << rh << " "; });cout << endl;
}
int main() {test_transform_hash();return 0;
}
总体作用
这个示例的功能是:
对一个字符串
vector
中的每个字符串,计算它的 hash 值(基于字符级操作),并把所有 hash 存入一个vector<uint32_t>
中,最后打印出来。
核心部分详解
1. 字符串哈希函数
uint32_t hash_char(uint32_t hash, char c) {hash = rotl(hash, c); // 左旋转hash ^= c; // 异或混入字符return hash;
}
uint32_t hash_string(const string& s) {return accumulate(s.begin(), s.end(), 0, hash_char);
}
hash_char
:每处理一个字符就对当前hash
值进行更新(左旋+异或)hash_string
:对字符串的每个字符累加哈希
2. transform
应用:对每个字符串哈希
template<typename Cont>
vector<uint32_t> hash_each_string(const Cont& v) {vector<uint32_t> res;transform(v.begin(), v.end(), back_inserter(res), hash_string);return res;
}
std::transform
:对v
的每个元素(string
),调用hash_string
back_inserter(res)
:结果被插入到res
向量末尾- 这个函数返回的是一个包含所有字符串 hash 的
vector<uint32_t>
3. 测试函数:打印所有哈希
void test_transform_hash() {vector<string> v{ "one", "two", "three", "four", "five" };auto res = hash_each_string(v);cout << "Hashes: ";for_each(res.begin(), res.end(),[](uint32_t rh){ cout << rh << " "; });cout << endl;
}
- 创建字符串数组
v
- 调用
hash_each_string
得到每个字符串的 hash 值数组 - 用
for_each
+ lambda 打印所有 hash
总结对比:for_each
vs transform
对比项 | for_each | transform |
---|---|---|
类型 | 非变异算法(non-modifying) | 变异算法(mutating) |
结果输出 | 不生成输出,靠函数对象副作用 | 直接生成结果序列 |
推荐用途 | 有副作用的迭代处理 | 显式地生成新的值序列 |
“副作用”(side effect)是编程中的一个重要概念,指的是在执行某段代码时,除了返回结果之外,还对程序的状态或外部环境产生了影响。简单说,就是代码执行过程中除了计算返回值外,还“改变了什么东西”。
常见的副作用包括:
- 修改变量(特别是全局变量或外部变量)
- 修改传入的参数(通过引用或指针)
- 修改数据结构(比如容器里的元素)
- 写文件、写数据库
- 输出到屏幕(比如
std::cout
) - 发送网络请求
- 抛出异常
举个简单的例子:
int add(int a, int b) {return a + b; // 纯函数,无副作用
}
int x = 0;
int add_and_increment(int a) {x += a; // 修改了外部变量 x,产生副作用return x;
}
add
函数只计算和返回结果,没有修改任何外部状态,因此无副作用。add_and_increment
函数修改了外部变量x
,这就是有副作用。
为什么关注副作用?
- 代码可预测性:没有副作用的函数,输入相同就总有相同输出,更容易调试和理解。
- 并行执行:无副作用的代码更容易安全并发执行。
- 测试方便:无副作用的函数更容易单元测试。
any_of
、all_of
和 none_of
是 STL 中用于判断序列中元素是否满足某种条件的算法:
- any_of:序列中 至少有一个元素满足条件,返回
true
,否则返回false
。 - all_of:序列中 所有元素都满足条件,返回
true
,否则返回false
。 - none_of:序列中 没有任何元素满足条件,返回
true
,否则返回false
。
它们的特点是: - 都接受一个函数对象(或者谓词),用来判断元素是否“满足条件”。
- 在判断过程中,一旦结果已经确定(比如
any_of
找到第一个满足条件的元素),就会提前返回,不用遍历剩下的元素,提高效率。
举个简单例子:
std::vector<int> v = {1, 2, 3, 4, 5};
// 判断是否序列中有元素大于3
bool any = std::any_of(v.begin(), v.end(), [](int x){ return x > 3; }); // true
// 判断是否所有元素都大于0
bool all = std::all_of(v.begin(), v.end(), [](int x){ return x > 0; }); // true
// 判断是否没有元素小于0
bool none = std::none_of(v.begin(), v.end(), [](int x){ return x < 0; }); // true
你这段代码用 all_of
和 any_of
检查 HTTP 头是否有效和搜索特定的头部,示例很经典。
#include <iostream>
#include <vector>
#include <string>
#include <regex>
#include <algorithm>
using namespace std;
// all_of 示例:验证 HTTP headers 格式是否正确
static const regex reHeader("([A-Za-z0-9!#$%&'*+.^_`|~-]+): *(.+) *");
inline bool headers_valid(const vector<string>& headers) {return all_of(headers.begin(), headers.end(), [](const auto& header) -> bool {smatch matches;return regex_match(header, matches, reHeader);});
}
// any_of 示例:在 HTTP headers 中查找特定的 header 和对应值
inline bool header_search(const vector<string>& headers, const string& find_header,const string& find_value) {return any_of(headers.begin(), headers.end(), [&find_header, &find_value](const auto& header) -> bool {const regex reHeader("(" + find_header + "): *(" + find_value + ") *", regex::icase);smatch matches;return regex_match(header, matches, reHeader);});
}
void test_all_of_headers() {vector<string> h1 = {"Foo: bar", "Content-type: application/json","Accept: text/html,text/json,application/json"};cout << "headers valid (h1): " << boolalpha << headers_valid(h1) << endl;vector<string> h2 = {"Foo : bar", "Content-type: application/json","Accept: text/html,text/json,application/json"};cout << "headers valid (h2): " << boolalpha << headers_valid(h2) << endl;vector<string> h3 = {"Foo: bar", "Content-type: application/json",":Accept: text/html,text/json,application/json"};cout << "headers valid (h3): " << boolalpha << headers_valid(h3) << endl;vector<string> h4 = {"Foo: bar", " Content-type: application/json","Accept: text/html,text/json,application/json"};cout << "headers valid (h4): " << boolalpha << headers_valid(h4) << endl;
}
void test_any_of_headers() {vector<string> h1 = {"Foo: bar", "Content-type: application/json", "X-SuperPower: toestrength","Accept: text/html,text/json,application/json"};cout << "header_search (h1): " << boolalpha << header_search(h1, "X-SuperPower", "toestrength")<< endl;vector<string> h2 = {"Foo: bar", "Content-type: application/json", "X-SuperPower: supersmell","Accept: text/html,text/json,application/json"};cout << "header_search (h2): " << boolalpha << header_search(h2, "X-SuperPower", "toestrength")<< endl;vector<string> h3 = {"Foo : bar", "Content-type: application/json", "X-SuperPower: toestrength","Accept: text/html,text/json,application/json"};cout << "header_search (h3): " << boolalpha << header_search(h3, "X-Superpower", "toeStrength")<< endl;
}
int main() {cout << "Testing all_of headers validation:" << endl;test_all_of_headers();cout << "\nTesting any_of header search:" << endl;test_any_of_headers();return 0;
}
总结下:
all_of 示例:验证 HTTP headers 格式是否正确
- 用正则表达式匹配每个 header 是否符合
"key: value"
格式。 all_of
只要有一个 header 不匹配,就返回false
,全部匹配才返回true
。
static const regex reHeader("([A-Za-z0-9!#$%&'*+.^_`|~-]+): *(.+) *");
inline bool headers_valid(const vector<string>& headers) {return all_of(headers.begin(), headers.end(),[](const auto& header) -> bool {smatch matches;return regex_match(header, matches, reHeader);});
}
测试时发现格式不对就会返回 false
,非常直观。
any_of 示例:在 HTTP headers 里查找某个特定的 header 是否存在且值匹配
- 构造正则表达式动态匹配具体的 header 名和对应的值(忽略大小写)。
- 只要找到一个匹配的 header 即返回
true
。
inline bool header_search(const vector<string>& headers,const string& find_header,const string& find_value)
{return any_of(headers.begin(), headers.end(),[&find_header, &find_value](const auto& header) -> bool {const regex reHeader("(" + find_header + "): *(" + find_value + ") *", regex::icase);smatch matches;return regex_match(header, matches, reHeader);});
}
这两个例子非常好地体现了 all_of
和 any_of
的语义和用法:
all_of
用于“全部满足”,通常是验证性质的判断。any_of
用于“存在至少一个满足”,通常是查找或搜索性质的判断。
这段代码展示了如何使用 std::for_each
同时进行两个操作:
- 验证 HTTP Header 格式是否合法(统计合法与不合法的 header 数量)
- 查找是否包含特定的 header 和对应的值(模糊大小写匹配)
主要结构解析
HeaderData
结构体
struct HeaderData {int good_headers = 0;int bad_headers = 0;multimap<string, string> found_headers;string find_header;string find_value;operator bool() const {return !bad_headers && good_headers > 0;}void operator()(const string& header) {static const regex reValid("([A-Za-z0-9!#$%&'*+.^_`|~-]+): *(.+) *");smatch matches;bool match = regex_match(header, matches, reValid);if (match) {++good_headers;// 构造查找用的正则(忽略大小写)const regex reHeader("(" + find_header + "): *(" + find_value + ") *", regex::icase);if (regex_match(header, matches, reHeader)) {found_headers.emplace(matches[1], matches[2]);}} else {++bad_headers;}}
};
- 它是一个函数对象(重载了
operator()
),可以传给std::for_each
。 operator bool()
使得你可以直接用if (hd)
判断:- 至少有一个合法 header
- 没有非法 header
header_parse
函数
const HeaderData header_parse(const vector<string>& headers, const string& find_header, const string& find_value) {HeaderData hd;hd.find_header = find_header;hd.find_value = find_value;return for_each(headers.begin(), headers.end(), hd);
}
- 使用
std::for_each
遍历 headers,自动调用HeaderData::operator()
- 返回最终结果:验证情况 + 找到的 header/value 键值对
测试函数 any_of_headers_full()
这个函数运行三个测试:
- 所有 header 合法,能找到目标 header/value
- 所有 header 合法,但找不到目标值
- 有一个非法 header(空格在冒号前),但依然能找到值
示例输出解释:
headers parse: true, good 4, bad 0'X-SuperPower', 'toestrength'
headers parse: true, good 4, bad 0
headers parse: false, good 3, bad 1'X-Superpower', 'toestrength'
- 第一组:找到 header,全部合法
- 第二组:没找到目标值,但 header 格式都合法
- 第三组:找到值,但有一个 header 不合法,所以整体解析失败(operator bool() 返回 false)
总结:你需要理解的 STL 技巧
功能 | 使用的 STL 技术 |
---|---|
遍历并累积状态 | std::for_each + 函数对象 |
判断是否合法 | std::regex_match |
模糊查找(忽略大小写) | std::regex(..., regex::icase) |
查找结果收集 | std::multimap |
简化布尔判断 | operator bool() |
关于 C++ STL 算法 std::adjacent_find
的用法,特别是用它来检测排序或数值偏差的情况。下面我会一步一步解释它的作用、示例代码,以及输出的含义,帮助你真正理解。
std::adjacent_find
是什么?
这个算法在一个序列中寻找相邻的两个元素,看它们是否满足某种条件。
- 默认条件是:两个相邻元素是否相等
- 你也可以传入自定义比较函数(例如是否
a > b
,是否偏差太大等)
示例 1:判断是否排序(非降序)
vecInt_t v{ 1, 2, 3, 4, 5, 5, 6, 7, 8 };
auto it = adjacent_find(v.begin(), v.end(), greater<int>());
if (it == v.end())cout << "Vector is sorted" << endl;
elsecout << "Vector not sorted, value " << *(it + 1)<< ", at position " << it - v.begin() + 1 << endl;
解释:
greater<int>()
是比较函数:判断 前一个值是否比后一个值大。- 如果
v[i] > v[i+1]
,说明不再升序,即排序错了。 - 如果没有出现这种情况,说明整个序列是升序的。
示例输出:
Vector is sorted
(意味着没有两个相邻值违反升序)
示例 2:检测是否有“数值偏差”超出限制
这个例子更实际,模拟了“值是否变化太快”:
template<typename Cont>
typename Cont::const_iterator checkDeviation(const Cont& cont, double allowed_dev) {return adjacent_find(cont.begin(), cont.end(),[allowed_dev](const typename Cont::value_type& v1, const typename Cont::value_type& v2) {auto limit = v1 * allowed_dev;return (v2 > v1 + limit) || (v2 < v1 - limit);});
}
解释:
- 判断两个相邻值
v1
和v2
之间的变化是否超出允许的偏差allowed_dev
。 limit = v1 * allowed_dev
表示允许变化的范围。- 如果
v2
比v1
大或小太多,就算 deviation。
示例 3:检测偏差的测试用例
vecDbl_t v{ 1.0, 1.05, 1.06, 1.04, 1.09, 1.15, 1.2 };
auto it = checkDeviation(v, 0.1); // 允许 ±10% 偏差
第一个测试:
- 所有相邻值变化都在 ±10% 范围内
- 输出:
Vector is within deviation limits
第二个测试:
v.push_back(2.0);
it = checkDeviation(v, 0.1);
1.2
到2.0
是 +66%,超过了 ±10%- 输出:
Vector outside deviation limits, values 1.2 and 2, at position 7
总结
功能 | 方法 |
---|---|
检查是否排序 | adjacent_find(..., greater<>) |
查找相邻重复元素 | adjacent_find(...) |
检测相邻值是否变化过大 | adjacent_find(..., 自定义比较器) |
remove_if (with erase)
是 C++ 中处理容器中批量删除满足某条件的元素的经典做法,通常称为 “erase-remove 惯用法”(erase-remove idiom)。下面我来详细解释其原理、为什么推荐它,以及具体例子,帮助你深入理解。
为什么使用 remove_if + erase
?
问题:
你想删除一个容器中所有满足某个条件的元素,比如删除所有值为负数的整数:
vector<int> v = { 1, -2, 3, -4, 5 };
你不能这样写:
for (auto it = v.begin(); it != v.end(); ++it) {if (*it < 0)v.erase(it); // 会导致迭代器失效
}
因为 erase
会修改容器大小和结构,破坏迭代器的稳定性。
正确做法:erase-remove 惯用法
v.erase(remove_if(v.begin(), v.end(),[](int n){ return n < 0; }), // 条件:删除负数v.end());
含义:
remove_if
不真的删除元素,它会:- 把不满足条件的元素前移
- 返回一个新的迭代器
new_end
,指向有效范围末尾
erase(new_end, v.end())
:- 从这个位置起把尾部那些“无效元素”真正删除掉。
举例说明:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {vector<int> nums = { 1, -2, 3, -4, 5 };nums.erase(remove_if(nums.begin(), nums.end(),[](int n){ return n < 0; }),nums.end());for (int n : nums)cout << n << " ";return 0;
}
输出:
1 3 5
和 std::list
的区别?
std::list
提供了 remove_if
成员函数:
list<int> lst = { 1, -2, 3, -4, 5 };
lst.remove_if([](int n){ return n < 0; }); // 直接删除,无需 erase
总结:为什么推荐使用 erase-remove?
方法 | 优点 |
---|---|
remove_if + erase | 高效、安全,不会造成迭代器失效 |
手动循环+erase | 易错、易造成迭代器失效 |
Scott Meyers 在《Effective STL》中提到这个习惯用法作为删除元素的首选方式(第 9 和第 32 条),因为它性能好、写法安全、行为明确。 |
你给出的这个 remove_if
(结合 erase
)示例,完美展示了 “erase-remove 惯用法” 在实际应用中的用法,特别是针对自定义类型(如 struct Foo
)的情况。下面我将为你逐步拆解和解释这段代码,让你完全理解它的核心思想与执行流程。
场景背景
你有一个结构体 Foo
,表示某种对象,其中一个成员变量 expired
用于表示对象是否“已过期”:
struct Foo {bool expired;// ... 其他成员
};
然后你有一个容器:
vector<Foo> v;
你想要从这个 vector 中删除所有 expired 为 true 的对象。
正确写法(使用 erase-remove 惯用法):
v.erase(remove_if(v.begin(), v.end(),[](const Foo& foo) { return foo.expired; }),v.end());
步骤详解:
-
remove_if
会对容器进行“重排”,把所有foo.expired == false
的元素移到前面,
然后返回一个迭代器new_end
指向“有效数据的末尾”。它不会真正删除元素,只是把不符合条件的元素留下来,其他的挪到后面。
-
erase(new_end, v.end())
删除从new_end
开始到v.end()
范围的元素,完成真正的删除。
示例输出前后对比
// 假设初始数据:
v = [{ val: 1, expired: false },{ val: 2, expired: true },{ val: 3, expired: false },{ val: 4, expired: false },{ val: 5, expired: true }
];
// 执行后:
v = [{ val: 1, expired: false },{ val: 3, expired: false },{ val: 4, expired: false }
];
expired 为 true 的项被清除,其他顺序不变。
总结重点
点 | 说明 |
---|---|
remove_if | 不删元素,只做重排 |
erase | 真正删除尾部的“垃圾数据” |
安全性 | 不会导致 iterator 失效(在组合使用时) |
效率 | 比一个个调用 erase 更快更安全 |
推荐理由 | Scott Meyers 在《Effective STL》中强烈推荐的惯用法(Item 9 & 32) |
Bonus:完整示例代码(带输出)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Foo {int val;bool expired;
};
void print(const vector<Foo>& v) {for (const auto& f : v)cout << "[val: " << f.val << ", expired: " << boolalpha << f.expired << "] ";cout << endl;
}
int main() {vector<Foo> v = {{1, false},{2, true},{3, false},{4, false},{5, true}};cout << "Before: ";print(v);v.erase(remove_if(v.begin(), v.end(),[](const Foo& foo) { return foo.expired; }),v.end());cout << "After: ";print(v);
}
输出:
Before: [val: 1, expired: false] [val: 2, expired: true] [val: 3, expired: false] [val: 4, expired: false] [val: 5, expired: true]
After: [val: 1, expired: false] [val: 3, expired: false] [val: 4, expired: false]
提到的是 Scott Meyers《Effective STL》Item 31 中关于 STL 排序相关算法的总结。这个条目主要区分了两类排序相关的算法:
分类一:真正排序的算法(Sorting algorithms)
这些算法会完全或部分地对元素 排序(sort)。
算法 | 特点 | 要求 |
---|---|---|
sort | 高效的快速排序(不稳定) | 随机访问迭代器 |
stable_sort | 稳定排序(保持相等元素顺序) | 随机访问迭代器 |
partial_sort | 只排序前 n 个最小元素 | 随机访问迭代器 |
partial_sort_copy | 拷贝并排序前 n 个最小元素到新容器 | 随机访问迭代器 |
分类二:“排序样子”的算法(Sorta-sorts)
这些算法并不保证全体元素有序,但局部有序或分区成功,因此更快。
算法 | 特点 | 要求 |
---|---|---|
nth_element | 使得第 n 个位置是其最终位置(前面都 ≤ 它,后面都 ≥ 它) | 随机访问迭代器 |
partition | 将元素分成两组(满足谓词的在前,不满足的在后,顺序不保证) | 双向迭代器 |
stable_partition | 同 partition ,但保持元素原有顺序 | 双向迭代器 |
partition_copy | 拷贝到两个输出序列,分别保存满足和不满足谓词的元素 | 输入 + 输出迭代器组合 |
示例对比总结
场景 | 适合使用 | 说明 |
---|---|---|
全排序 | sort 或 stable_sort | 最常见,需要排序整个容器 |
只想找前 n 个最小的元素 | partial_sort 或 nth_element | partial_sort 会有序,nth_element 不保证有序 |
需要排序并保留相等元素的顺序 | stable_sort | 稳定排序,适用于比如记录按多个字段排序的场景 |
仅想分区元素满足条件与否 | partition 或 stable_partition | 更快,但不是排序 |
想将元素按条件分别保存到两个容器 | partition_copy | 复制版本的 partition ,保持原容器不变 |
Scott Meyers《Effective STL》Item 31 的核心建议:
选择排序相关算法时,不要盲目使用
sort
,而要根据需求选择最合适的排序或“伪排序”算法。
例如:
- 如果你只关心前 5 名的成绩,用
partial_sort
。 - 如果你只想找第 10 大的值,不用全排序,用
nth_element
。 - 如果你只想把大于某值的元素放一边而不关心顺序,用
partition
。 - 如果你要分组并保留原始顺序,用
stable_partition
。
你引用的是 Scott Meyers《Effective STL》Item 31 中关于几种常见排序算法的核心比较。下面是对这些排序算法的简洁总结与理解:
1. sort
用途:最通用的排序算法
特性:
- 效率高,适用于大多数排序场景。
- 不稳定排序:相等元素的相对顺序可能被打乱(由实现决定)。
- 原地排序:在容器本身内部进行,不额外分配空间。
适合:追求排序性能,对元素顺序无特殊要求。
2. stable_sort
用途:稳定排序,保留相等元素原始顺序
特性:
- 稍慢于
sort
,尤其在大数据上。 - 对于多级排序非常有用(比如:先按姓排,再按名排)。
适合:你关心排序后的元素顺序稳定性。
3. partial_sort
用途:只对前一部分元素排序
特性:
- 排序部分序列,但从更大的序列中选取。
- 更快,因为它不会排序所有元素。
- 不稳定(没有
stable_partial_sort
)。 - 原地排序。
适合:比如只要前 10 个最大值/最小值,而不关心其余顺序。
4. partial_sort_copy
用途:从一个序列中拷贝部分排序结果到另一个序列
特性:
- 输入序列不变。
- 输出排序好的前几个元素。
- 用于排序但不修改原容器。
适合:想要一个排序后的副本,不破坏原始数据结构。
简明对比表:
算法 | 是否稳定 | 是否原地排序 | 修改原数据 | 用途 |
---|---|---|---|---|
sort | 完全排序,性能优先 | |||
stable_sort | 完全排序,保持相等元素顺序 | |||
partial_sort | 排前 N 个最小/最大值 | |||
partial_sort_copy | (输出到新序列) | 拷贝排序前 N 项,原数据不变 |
例子场景:
- 全排序且无序要求 →
sort
- 全排序且要稳定顺序 →
stable_sort
- 只要前 3 名学生 →
partial_sort
- 保留成绩原样,只另拷贝前 5 名 →
partial_sort_copy
你提到的这个排序场景是一个典型的多字段排序问题。我们来逐步理解这段内容,并举例说明 sort
和 stable_sort
如何用于按照多个字段排序。
场景说明
我们有一个结构体 Person
,包含:
struct Person {std::string first;std::string middle;std::string last;// ... 其他成员 ...
};
目标是:
对一个 std::vector<Person>
容器进行排序,按照:
last name > first name > middle name 的优先级升序排序。
也就是说:
- 如果
last
不同,就按last
排序。 - 如果
last
相同,再按first
排序。 - 如果
first
也相同,再按middle
排序。
排序代码示例(使用 std::sort
)
#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
struct Person {std::string first;std::string middle;std::string last;
};
bool comparePersons(const Person& a, const Person& b) {if (a.last != b.last)return a.last < b.last;if (a.first != b.first)return a.first < b.first;return a.middle < b.middle;
}
int main() {std::vector<Person> people = {{"John", "H.", "Smith"},{"John", "A.", "Smith"},{"Alice", "B.", "Adams"},{"John", "H.", "Adams"}};std::sort(people.begin(), people.end(), comparePersons);for (const auto& p : people)std::cout << p.last << ", " << p.first << " " << p.middle << '\n';
}
std::stable_sort
和 std::sort
的区别
在这个例子中,你可以使用 std::sort
或 std::stable_sort
。区别在于:
- 如果只排序一次且
comparePersons
定义完整,没有相等的比较,那么sort
或stable_sort
效果一样。 - 如果多次排序用于“分层”排序(例如先按 middle,再按 first,最后按 last),就必须用
stable_sort
才能保持之前的顺序。
分层稳定排序示例(使用 stable_sort
)
std::stable_sort(people.begin(), people.end(), [](const Person& a, const Person& b) {return a.middle < b.middle;});
std::stable_sort(people.begin(), people.end(), [](const Person& a, const Person& b) {return a.first < b.first;});
std::stable_sort(people.begin(), people.end(), [](const Person& a, const Person& b) {return a.last < b.last;});
这种方式相当于从最低优先级(middle)开始排,再稳定地按 first、再按 last。最终顺序就是按 last > first > middle
排好的。
总结
排序方式 | 优点 | 用法 |
---|---|---|
sort | 高性能,适用于一次性排序 | 自定义比较函数时使用 |
stable_sort | 保持相同元素的原始顺序 | 分层多次排序(优先级排序)时使用 |
这段代码展示了如何使用 std::sort
和 std::stable_sort
对一组 Person
对象按照多个字段进行分层排序(lexicographical sort),其中排序优先级是:
last name
(最高优先级) >first name
>middle name
(最低优先级)
背景知识
假设结构体如下:
struct Person {std::string first;std::string middle;std::string last;
};
排序过程分析
std::vector<Person> v{{{ "Joe", "P", "Smith" },{ "Jane", "Q", "Jones" },{ "Frank", "P", "Johnson" },{ "Sarah", "B", "Smith" },{ "Joe", "X", "Jones" },{ "Joe", "A", "Smith" }
}};
你希望排序为:
先按
last
升序,若相同再按first
升序,若仍相同再按middle
升序。
✳ 分三步完成:
1⃣ sort
按 middle name 排(最低优先级):
sort(v.begin(), v.end(),[](const Person& a, const Person& b){ return a.middle < b.middle; });
用 sort
是可以的,因为排序顺序不会影响上层排序——我们之后还会稳定地再排一次。
2⃣ stable_sort
按 first name 排:
stable_sort(v.begin(), v.end(),[](const Person& a, const Person& b){ return a.first < b.first; });
用 stable_sort
保证相同 first
的 middle
排序结果不变。
3⃣ stable_sort
按 last name 排(最高优先级):
stable_sort(v.begin(), v.end(),[](const Person& a, const Person& b){ return a.last < b.last; });
再次使用 stable_sort
,在最后一步保证同 last
的 first
、middle
顺序仍被保留。
最终排序结果
执行后,排序结果为:
Frank P Johnson
Jane Q Jones
Joe X Jones
Joe A Smith
Joe P Smith
Sarah B Smith
按字段拆解验证:
last | first | middle |
---|---|---|
Johnson | Frank | P |
Jones | Jane | Q |
Jones | Joe | X |
Smith | Joe | A |
Smith | Joe | P |
Smith | Sarah | B |
满足 last < first < middle 的排序规则。 |
总结
步骤 | 方法 | 用途 |
---|---|---|
排最低字段 | sort | 可打乱顺序,之后再稳定地排序 |
排中间字段 | stable_sort | 保持低优先级字段顺序 |
排最高字段 | stable_sort | 保持所有之前排序顺序 |
顺序很重要!:最低优先级先排,最高优先级最后排。 |
多字段排序示例数据
初始数据:
First | Middle | Last |
---|---|---|
Joe | P | Smith |
Jane | Q | Jones |
Frank | P | Johnson |
Sarah | B | Smith |
Joe | X | Jones |
Joe | A | Smith |
排序步骤及正确结果
Step 1:按 middle 用 sort
排序(不稳定)
sort(v.begin(), v.end(), [](auto& a, auto& b){ return a.middle < b.middle; });
排序结果(仅保证 middle 字段排序正确,其他字段顺序不稳定):
First | Middle | Last |
---|---|---|
Joe | A | Smith |
Sarah | B | Smith |
Joe | P | Smith |
Frank | P | Johnson |
Jane | Q | Jones |
Joe | X | Jones |
Step 2:按 first 用 stable_sort
排序(稳定)
stable_sort(v.begin(), v.end(), [](auto& a, auto& b){ return a.first < b.first; });
排序结果(保持了 Step 1 中 middle 字段顺序):
First | Middle | Last |
---|---|---|
Frank | P | Johnson |
Jane | Q | Jones |
Joe | A | Smith |
Joe | P | Smith |
Joe | X | Jones |
Sarah | B | Smith |
Step 3:按 last 用 stable_sort
排序(稳定)
stable_sort(v.begin(), v.end(), [](auto& a, auto& b){ return a.last < b.last; });
最终排序结果:
First | Middle | Last |
---|---|---|
Frank | P | Johnson |
Jane | Q | Jones |
Joe | X | Jones |
Joe | A | Smith |
Joe | P | Smith |
Sarah | B | Smith |
重点
- Step 1
sort
:middle 字段排序完成,但其他字段顺序不稳定。 - Step 2
stable_sort
:first 字段排序,保持了 Step 1 中的 middle 顺序。 - Step 3
stable_sort
:last 字段排序,保持了前两步的稳定顺序。
总结一下 partial_sort
和 partial_sort_copy
的关键点:
partial_sort
- 作用:对输入序列中的前 n 个元素进行排序,确保这部分元素是整体中最小(或最大)的 n 个元素,且这 n 个元素按顺序排列。
- 输入:整个序列 + 需要排序的前 n 个元素的范围(通常用迭代器指定)。
- 输出:序列中前 n 个元素是排好序的最小元素,剩余元素位置顺序不确定(实现定义)。
- 特点:就地排序(in-place),不会额外复制。
- 效率:比完全排序效率高,因为不需要对整个序列排序。
partial_sort_copy
- 作用:将输入序列中最小(或最大)的 n 个元素排序后,复制到另一个序列中。
- 输入:整个序列 + 目标输出序列(通常较短)。
- 输出:输出序列包含排序好的前 n 个元素,输入序列不变。
- 用途:当想要得到部分排序结果且保存到不同容器时使用。
partial_sort
例子:
代码示例
vector<int> v1{ 42, 17, 89, 22, 34, 78, 63, 12, 57, 99 };
// 对 v1[0) 到 v1[8) 这段范围内,执行 partial_sort
// 目标是把前 5 个元素(v1.begin() 到 v1.begin() + 5) 排成从大到小(greater<int>)排序
partial_sort(v1.begin(), v1.begin() + 5, v1.begin() + 8, greater<int>());
解释:
- 输入范围是
v1.begin()
到v1.begin() + 8
(即元素 0~7,共8个元素):
{42, 17, 89, 22, 34, 78, 63, 12}
- 目标:对这8个元素中,找出最大的5个元素,并按从大到小排序,放到
v1.begin()
到v1.begin() + 5
的位置。 - 运行完后,
v1
前5个元素是这8个元素中最大的5个,且有序(降序排列,因为用的是greater<int>()
),其余元素位置不保证排序。
举例执行结果(可能是)
v1前8元素可能变成:
{ 89, 78, 63, 42, 34, 17, 22, 12 }
- 这里
89, 78, 63, 42, 34
是这8个元素中最大的5个,且降序排列。 - 后面
17, 22, 12
这三个元素在末尾,顺序不定(实现定义)。
总结:
partial_sort
只保证部分排序:这里是排序了前5个最大元素。- 输入序列的剩余元素(第6个以后)顺序不保证。
- 比完全排序更高效,因为不用对全部元素排序。
Scott Meyers “Effective STL” Item 31 关于 partition 系列算法的关键点:
partition 系列算法概览
算法 | 功能描述 | 稳定性 | 额外说明 |
---|---|---|---|
partition | 将序列重新排列,使满足条件的元素在前,不满足的在后 | 不稳定(元素相对顺序不保证) | 原地操作,分割点前是满足条件元素,分割点后是不满足元素 |
stable_partition | 功能同 partition,但保持满足条件元素及不满足元素的原始顺序 | 稳定 | 原地操作 |
partition_copy | 根据条件将元素分别拷贝到两个不同输出序列中 | 不稳定 | 不原地操作,无法保持元素顺序 |
详细说明
- partition
把序列中满足给定条件的元素放到序列前部,不满足的放到后部。
不保证满足条件元素之间或不满足条件元素之间的原有顺序。
在某些算法中能提升性能,但对顺序敏感时需谨慎。 - stable_partition
作用与partition
相同,但保证满足条件的元素保持其在原序列中的相对顺序,
不满足条件的元素相对顺序也保持不变。
这对于需要保留顺序信息的场景非常重要。 - partition_copy
类似于partition
,但不会原地修改输入序列,
而是根据条件分别复制元素到两个不同的输出容器。
不保证元素的相对顺序。
关于 Scott Meyers “Effective STL” Item 31 中的 nth_element
,我帮你整理总结如下:
nth_element 详解
- 功能
重新排列序列,使得序列中第n
个位置上的元素,恰好是如果序列完全排序后该位置的元素。 - 效果
- 在第
n
个元素之前的所有元素都小于或等于该元素(实现定义可能包括等于元素的位置不同)。 - 第
n
个元素之后的所有元素都大于或等于该元素。 - 但序列中第
n
元素之前和之后的元素之间顺序是未定义的(不稳定)。
- 在第
- 特点
- 只进行部分排序,不像
sort
会完全排序整个序列,因此在寻找第n
小元素时比完整排序更高效。 - 原地操作,不会额外分配大量内存。
- 只进行部分排序,不像
应用场景举例
- 找第 k 小元素(或第 k 大元素)的值。
- 只关心部分排序,比如找到前 k 大的元素但不关心顺序。
示例代码(C++)
#include <iostream>
#include <vector>
#include <algorithm>
int main() {std::vector<int> v{42, 17, 89, 22, 34, 78, 63, 12, 57, 99};// 将第5个位置的元素放到排序后它应该在的位置std::nth_element(v.begin(), v.begin() + 4, v.end());std::cout << "The 5th smallest element is " << v[4] << "\n";std::cout << "Elements before 5th element (unsorted): ";for (int i = 0; i < 4; ++i) std::cout << v[i] << " ";std::cout << "\nElements after 5th element (unsorted): ";for (int i = 5; i < v.size(); ++i) std::cout << v[i] << " ";std::cout << "\n";return 0;
}
对 partition
和 nth_element
的理解很准确,下面帮你做个清晰的对比总结,方便记忆和理解:
partition vs nth_element 比较
特性 | partition | nth_element |
---|---|---|
功能 | 根据条件将序列分成两部分(满足条件和不满足条件) | 将序列部分排序,使第 nth 个元素位于排序后正确位置 |
分割点 | 分割点不保证对应排序后序列的值 | nth 元素是排序后在该位置的值 |
输入参数 | 起始迭代器、结束迭代器、判断条件函数 | 起始迭代器、结束迭代器、第 nth 位置迭代器、(可选)比较函数 |
输出 | 重排的序列和指向分割点的迭代器 | 重排的序列,nth 元素处于正确位置,序列围绕该元素分区 |
稳定性 | 不保证稳定(顺序未定义) | 不保证稳定(顺序未定义) |
用途 | 快速把满足某条件的元素移动到序列一端 | 找出序列中第 nth 小(或大)元素,部分排序 |
示例 | 将序列中所有奇数放在偶数前面 | 找出序列中第 5 小元素,并使其左边都是更小的元素 |
简单类比
- partition:只关心“满足条件”和“不满足条件”的分类,顺序无所谓,就像洗牌时把红牌和黑牌分开。
- nth_element:关心“第 nth 个元素”的正确排序位置,左边全是更小的,右边全是更大的,但不保证其他元素顺序。
用 partition
将 vector 中小于 50 的元素放到前面,大于等于 50 的元素放到后面。关键点是:
partition
会就地重排容器元素,- 满足条件
i < 50
的元素被放在序列前部, - 返回值
part_it
是分区点迭代器,指向第一个不满足条件的元素。
例如,给定:
vector<int> v{ 12, 89, 31, 18, 7, 72, 69, 50, 49, 50, 51, 49 };
auto part_it = partition(v.begin(), v.end(), [](int i) { return i < 50; });
重排后,v
中满足 < 50
的元素会聚集在前面,part_it
指向第一个 >= 50
的元素,元素内部顺序未定义。
下面是一个简单的 C++ 代码示例,用来直观对比 partition
和 nth_element
的区别:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {vector<int> v1{12, 89, 31, 18, 7, 72, 69, 50, 49, 50, 51, 49};vector<int> v2 = v1; // 复制一份给 nth_element 使用// partition:把小于50的放前面,大于等于50的放后面,不保证排序auto part_it = partition(v1.begin(), v1.end(), [](int x){ return x < 50; });cout << "partition result:\n";for (int x : v1) cout << x << ' ';cout << "\nPartition point value: " << *part_it << endl;// nth_element:调整序列,使第6个元素是排序后的第6小元素nth_element(v2.begin(), v2.begin() + 5, v2.end());cout << "\nnth_element result:\n";for (int x : v2) cout << x << ' ';cout << "\nValue at nth position (index 5): " << v2[5] << endl;return 0;
}
运行结果(示例)
partition result:
12 31 18 7 49 49 89 72 69 50 50 51
Partition point value: 89
nth_element result:
12 18 7 31 49 49 72 69 50 50 51 89
Value at nth position (index 5): 49
说明
partition
保证所有小于 50 的数都在前面,后面是大于等于 50 的数,但顺序无规律,分界点的值不一定是第几个最小元素。nth_element
保证第 6 个位置的元素是排序后第 6 小的元素,前面都比它小,后面都比它大,但两边的元素顺序没有完全排序。
partition 系列算法总结(来自 Scott Meyers《Effective STL》Item 31)
算法名 | 功能描述 | 额外说明 | 运行方式 |
---|---|---|---|
partition | 重排序列,使满足条件的元素在前,不满足的元素在后 | 子序列内元素的相对顺序是不确定的(实现定义),等价元素顺序也不确定 | 就地操作(in-place) |
stable_partition | 类似 partition,但保持满足条件元素及不满足元素的相对顺序 | 保持相对顺序 | 就地操作 |
partition_copy | 根据条件分两部分复制到两个不同的输出序列 | 子序列内元素顺序实现定义,不保证稳定性;没有 stable_partition_copy | 复制操作 |
额外说明
partition
和stable_partition
都是原地修改序列,不会分配额外空间。stable_partition
稍微慢一点,但它保证了稳定性(元素相对顺序不变)。partition_copy
是把满足和不满足条件的元素分别复制到两个不同的序列,不修改原序列。- STL没有提供
stable_partition_copy
。
总结一下 nth_element
的核心要点:
nth_element
详解(来自 Scott Meyers《Effective STL》Item 31)
- 功能:
重排序列,使得位于第nth
位置的元素正好是如果序列完全排序后该位置上的元素。 - 具体表现:
nth
位置之前的所有元素都不大于该元素(如果是升序排序),nth
位置之后的所有元素都不小于该元素。- 但是,这些分割前后的元素本身不保证有序。
- 性质:
- 不需要完全排序,因此效率更高。
- 对等价元素的相对顺序和分区内的顺序,都是实现定义,没有稳定性保证。
- 工作方式:
- 就地修改(in-place),不额外分配大量内存。
partition_copy
详解
- 功能:
从输入序列中根据条件函数分割元素,
并分别拷贝到两个不同的输出序列中。 - 示例代码:
vector<int> v{12, 89, 31, 18, 7, 72, 69, 50, 49, 50, 51, 49};
vector<int> smaller; // 用来存放小于50的元素
vector<int> larger; // 用来存放不小于50的元素
partition_copy(v.begin(), v.end(), back_inserter(smaller), // 输出小于50的元素back_inserter(larger), // 输出大于等于50的元素[](const int i) { return i < 50; }
);
- 工作过程:
- 遍历
v
中所有元素,判断是否满足i < 50
。 - 满足的元素放入
smaller
。 - 不满足的元素放入
larger
。 smaller
和larger
两个容器分别收集对应的元素,输入序列保持不变。
- 遍历
- 区别:
- 与
partition
不同,partition_copy
是复制操作,不会修改原序列。 - 输出结果是两个独立序列,分别对应满足和不满足条件的元素。
partition_copy
不是原地操作。
- 与
这个示例很好地展示了 partition_copy
、partition
和 stable_partition
混合使用的实际场景,特别是在对员工(Employee)分类管理时的多层次分区处理。下面我帮你总结和解析这段流程:
示例场景概览
定义了一个 Employee
结构体继承自 Person
,并添加了枚举 EmployeeType
用于标识员工类型:
struct Employee: public Person {enum class EmployeeType {Executive, Manager, Architect, Senior, Junior,Management = Manager,Individual = Architect};EmployeeType type;// 其他成员
};
员工数据示例:
vector<Employee> v{{ "Joe", "P", "Smith", Employee::EmployeeType::Manager },{ "Jane", "Q", "Jones", Employee::EmployeeType::Junior },{ "Frank", "P", "Johnson", Employee::EmployeeType::Architect },{ "Sarah", "B", "Smith", Employee::EmployeeType::Executive },{ "Joe", "X", "Jones", Employee::EmployeeType::Senior },{ "Joe", "A", "Smith", Employee::EmployeeType::Junior },{ "Chris", "M", "Williams", Employee::EmployeeType::Manager }
};
1. 使用 partition_copy
分离管理层和普通员工
vector<Employee> management, individuals;
partition_copy(v.begin(), v.end(), back_inserter(management), back_inserter(individuals), [](const Employee& e) { return e.type <= Employee::EmployeeType::Manager; });
management
容器存放Executive
和Manager
类型(EmployeeType
小于等于Manager
的)。individuals
容器存放其他类型(如 Architect、Senior、Junior)。- 原始序列
v
保持不变。 - 这里是复制操作。
2. 在 management
中使用 partition
分区,区分执行官和经理
vector<Employee>::iterator car_it = partition(management.begin(), management.end(), [](const Employee& e) { return e.type == Employee::EmployeeType::Executive; });
- 将
management
容器中所有执行官 (Executive
) 放在前面,经理 (Manager
) 放后面。 - 原地操作,容器
management
被重新排序。 - 返回的迭代器
car_it
指向第一个经理的位置。
3. 在 individuals
中使用 partition
分区,区分建筑师和其他个人员工
vector<Employee>::iterator bike_it = partition(individuals.begin(), individuals.end(), [](const Employee& e) { return e.type == Employee::EmployeeType::Architect; });
- 建筑师(
Architect
)被放在前面,其余员工放在后面。 - 依然是原地操作。
4. 对非建筑师(后面部分)使用 stable_partition
按资历分区
vector<Employee>::iterator old_bike_it = stable_partition(individuals.begin(), individuals.end(), [](const Employee& e) { return e.type <= Employee::EmployeeType::Senior; });
- 对整个
individuals
容器进行稳定分区,将资历较高的 (Senior
及以上) 放在前面,较低的放后面。 - 稳定分区保证了已经排好序的建筑师(前面部分)顺序不被破坏。
- 迭代器
old_bike_it
指向资历较低员工的起始位置。
输出示意(以管理层为例)
Management partitioned:
jet Sarah B Smith: Executive
car Joe P Smith: Manager
car Chris M Williams: Manager
- 执行官(jet)优先,经理(car)次之。
核心理解
- partition_copy 用于将输入序列按条件分成两个独立容器,保留原序列不变。
- partition 对容器进行原地分区,元素被重新排列,分区内顺序不保证稳定。
- stable_partition 同样原地分区,但保持等价元素的相对顺序,适合在已有分区结果基础上继续细分。
这个内容讲的是 nth_element
和 partial_sort
的用法及区别,下面帮你总结并举个简单示例,方便理解:
1. nth_element
基本作用
- 将序列重排,使得第
nth
个元素就是如果排序后该位置的元素。 nth
之前的元素都 <= 它,nth
之后的元素都 >= 它(具体顺序不保证)。- 典型应用:快速找到中位数或百分位数。
- 操作是原地进行,效率高于完全排序。
示例:找中位数
vector<int> v{12, 2, 89, 78, 18, 7, 72, 69, 81, 50, 49, 50, 51, 49};
size_t nth = v.size() / 2;
nth_element(v.begin(), v.begin() + nth, v.end());
cout << "Median: " << v[nth] << endl; // 输出中位数
2. partial_sort
基本作用
- 将序列的前
n
个元素排序好(从小到大或自定义排序)。 - 后面的元素不保证顺序。
- 适合需要前
n
个元素有序的情况,比如排行榜。 - 比完全排序效率高,但比
nth_element
低。
示例:找前 5 大元素(降序)
vector<int> v{42, 17, 89, 22, 34, 78, 63, 12, 57, 99};
partial_sort(v.begin(), v.begin() + 5, v.end(), greater<int>());
for (int i = 0; i < 5; i++) cout << v[i] << " "; // 输出前5个最大元素,且有序
3. nth_element
+ sort
vs partial_sort
你也可以用 nth_element
找出前 n
个最大元素的分区位置,再对这部分排序:
vector<int64_t> v = init_vec();
vector<int64_t> v2 = v; // 复制一份
partial_sort(v.begin(), v.begin() + 20, v.end(), greater<int64_t>());
nth_element(v2.begin(), v2.begin() + 20, v2.end(), greater<int64_t>());
sort(v2.begin(), v2.begin() + 20, greater<int64_t>());
// 比较两者排序的前20个元素是否相同(通常相同)
cout << boolalpha << equal(v.begin(), v.begin() + 20, v2.begin()) << endl;
- 结果:前 20 个元素排序相同(
true
) - 但后面的未排序部分可能不同,
partial_sort
之后尾部顺序不保证,nth_element
+sort
的尾部顺序更随机。
总结
算法 | 作用 | 复杂度(平均) | 是否排序前n元素 | 是否稳定(等价元素顺序) |
---|---|---|---|---|
nth_element | 找出第 n 个元素,分区 | O(n) | 不保证排序 | 否 |
partial_sort | 排序前 n 个元素 | O(n log n) | 是 | 否 |
sort | 完全排序 | O(n log n) | 是 | 否(除非 stable_sort) |
你对 rotate
的理解很正确:
rotate
是在[first, last)
范围内,将[first, middle)
这一段“左移”到[middle, last)
之后,实现元素的原地循环移动。- 这个操作不需要用
erase
和insert
,避免了不必要的内存重新分配和拷贝,效率更高。 rotate_copy
和它类似,但会把结果拷贝到另一个容器中,不修改原序列。- 需要保证三个迭代器的顺序正确:
first <= middle <= last
。
1. Naïve Implementation(笨方法)用 erase + insert 移动一个元素
vector<int> v = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
vector<int>::iterator it = v.begin() + 2; // 指向元素2
int i = *it; // 取出元素2的值
v.erase(it); // 删除元素2,后面的元素都往前移一格
vector<int>::iterator it2 = v.begin() + 6 - 1; // 计算插入位置
v.insert(it2, i); // 把2插入到位置5(即原来索引6的位置往前一格)
输出:
before: 0 1 2 3 4 5 6 7 8 9
after: 0 1 3 4 5 2 6 7 8 9
分析:
- 先
erase
删除元素2,这会导致容器内后续元素左移,容器大小减少1。 - 再
insert
元素2 到目标位置前面,容器大小变回原来大小,元素又右移。 - 整体涉及两次数据搬移,效率较低,且可能导致内存重新分配。
2. Better Implementation(更好方法)用 std::rotate
移动一个元素
vector<int> v = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
vector<int>::iterator it = v.begin() + 2; // 指向元素2
vector<int>::iterator it2 = v.begin() + 6; // 目标位置后面一个元素
rotate(it, it + 1, it2);
输出:
before: 0 1 2 3 4 5 6 7 8 9
after: 0 1 3 4 5 2 6 7 8 9
分析:
rotate(it, it + 1, it2)
是对区间[it, it2)
执行“左旋1位”:将区间开头元素移到末尾,区间中剩余元素全部向左移动一格。- 这里,区间是
[2, 6)
即元素{2,3,4,5,6}
。 - 旋转后变为
{3,4,5,6,2}
,完成了把2
移动到原索引6的位置。 - 只涉及一次元素搬移,效率明显高于第一种方法。
- 该操作在原地完成,不改变容器大小,不涉及内存重新分配。
3. Rotate a range of items(旋转一个区间的多个元素)
vector<int> v = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
vector<int>::iterator it = v.begin() + 2; // 指向元素2
vector<int>::iterator it2 = v.begin() + 7; // 目标区间结束
rotate(it, it + 3, it2);
输出:
before: 0 1 2 3 4 5 6 7 8 9
after: 0 1 5 6 2 3 4 7 8 9
分析:
- 这里的区间是
[2, 7)
,包含元素{2,3,4,5,6}
。 middle
迭代器是it + 3
,即指向元素5
。rotate(it, it + 3, it2)
把[it, it+3)
的元素{2,3,4}
移到区间末尾,元素{5,6}
前移。- 旋转结果是
{5,6,2,3,4}
,对应索引2到6变为5 6 2 3 4
。 - 这种用法可以一次性将一段连续元素“整体往后挪”,把另一段挪到前面,非常灵活。
总结它们之间的关系
方法 | 操作对象 | 元素搬移次数 | 容器大小改变 | 备注 |
---|---|---|---|---|
笨方法(erase+insert) | 单个元素 | 两次 | 是 | 容器大小变,效率低 |
std::rotate 单元素 | 一个区间内的单个元素 | 一次 | 否 | 原地算法,效率高 |
std::rotate 区间 | 区间内多个元素 | 一次 | 否 | 原地算法,区间灵活旋转 |
结论
- 推荐使用
std::rotate
实现元素移动,特别是连续区间内的元素。 rotate
是原地算法,操作高效且安全,不改变容器大小。- 对比笨方法,避免多次搬移和可能的内存重新分配。
它描述了一种通用的、方向无关的元素移动方式 —— 即:
将容器中一段范围
[first, last)
的元素移动到另一个位置pos
,不管pos
在原范围的前面还是后面。
核心问题
标准库中的 std::rotate(first, middle, last)
要求:
first <= middle <= last
这意味着:
- 你必须判断
pos
相对于[first, last)
的位置,才能决定如何调用rotate
。 - 这对用户来说很容易出错,尤其在不清楚关系的时候。
因此,Sean Parent 提出一个更易用的抽象:slide
slide 的行为定义
假设有容器:
std::vector<char> v = { 'a', 'b', 'c', 'd', 'e', 'f', 'g' };
我们调用:
slide(v.begin() + 1, v.begin() + 4, v.begin() + 6);
目标是把 [b, c, d)
—— 即 {'b', 'c', 'd'}
移动到 f
之前:
原始: a b c d e f g^ ^
slide(1, 4, 6)
结果: a e b c d f g^-----^
- 所选区间 整体前移或后移 到新的位置
pos
。 - 保证最终顺序正确。
- 其底层实现仍然依赖
std::rotate
,但封装了逻辑,用户不用关心first <= pos <= last
的关系。
slide 实现代码(基于 rotate)
template<typename Iterator>
std::pair<Iterator, Iterator> slide(Iterator first, Iterator last, Iterator pos) {if (pos < first) {std::rotate(pos, first, last);return {pos, pos + (last - first)};}if (last < pos) {std::rotate(first, last, pos);return {pos - (last - first), pos};}return {first, last}; // 如果 pos 落在原区间内,不做任何旋转
}
函数返回值意义
std::pair<Iterator, Iterator>
:返回新的 [new_first, new_last)
区间,方便后续操作,比如:
auto new_range = slide(...);
std::for_each(new_range.first, new_range.second, highlight);
slide 的实际用途
适用于GUI列表操作、拖拽排序、任务队列重排等场景:
- 用户选中若干项,拖到另一个位置
- 无需担心起点和目标位置谁在前
- 保持逻辑清晰、语义明确
示例完整代码
#include <vector>
#include <algorithm>
#include <iostream>
template<typename Iterator>
std::pair<Iterator, Iterator> slide(Iterator first, Iterator last, Iterator pos) {if (pos < first) {std::rotate(pos, first, last);return {pos, pos + (last - first)};}if (last < pos) {std::rotate(first, last, pos);return {pos - (last - first), pos};}return {first, last};
}
int main() {std::vector<char> v = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};auto result = slide(v.begin() + 1, v.begin() + 4, v.begin() + 5);for (char c : v)std::cout << c << " "; // 输出: a e b c d f gstd::cout << "\nMoved range: ";for (auto it = result.first; it != result.second; ++it)std::cout << *it << " "; // 输出: b c d
}
总结理解
特性 | 描述 |
---|---|
slide | 封装了 rotate ,自动处理 pos 相对位置 |
输入参数 | [first, last) 为要移动的元素范围,pos 为目标插入点 |
操作方向 | 可以左移,也可以右移,取决于 pos 与 [first, last) 的关系 |
返回值 | 移动后新位置的 [first, last) 迭代器对 |
应用场景 | 拖拽移动、列表重排、队列调整、视图刷新等 |
这是一个很经典的、优雅的“滑动窗口”操作。我们来逐句拆解和图解理解这个版本的 slide
实现。
原型回顾:
template <typename I> // I models RandomAccessIterator
auto slide(I f, I l, I p) -> std::pair<I, I> {if (p < f)return { p, rotate(p, f, l) };if (l < p)return { rotate(f, l, p), p };return { f, l };
}
基本概念:
f
:要移动区间的开始l
:要移动区间的结束(不包括l
)p
:你想把[f, l)
插入到的位置- 返回值是一个 pair:新的
[f', l')
区间表示移动后的位置
情况 1:目标位置在左边(p < f
)
if (p < f)return { p, rotate(p, f, l) };
这时你希望把 [f, l)
这个区间往前移动到 p
,原位置区间之间的 [p, f)
就是你要腾出的空间。
rotate(p, f, l) 会把 [p, f)
挪到 [l - (f - p), l)
,而 [f, l)
会移动到 [p, p + (l - f))
。
示例:
原始: a b c d e f g
范围: [c d e] // f=2, l=5
目标: ^ // p=1 (即 'b')
rotate(p, f, l): rotate(1, 2, 5)
变成: a c d e b f g
新范围: [1, 4)
所以返回:{p, rotate(p, f, l)}
就是 [1, 4)
情况 2:目标位置在右边(l < p
)
if (l < p)return { rotate(f, l, p), p };
这时是把 [f, l)
插入到 p
之前,所以 rotate 的区间是 [f, p)
,中间的 [l, p)
是你要移开的空间。
rotate 会把 [f, l)
移到 [p - (l - f), p)
,中间的空隙填入 [l, p)
。
示例:
原始: a b c d e f g
范围: [b c d] // f=1, l=4
目标: ^ // p=5 (即 'f')
rotate(f, l, p): rotate(1, 4, 5)
结果: a e f b c d g
新范围: [2, 5)
返回:{rotate(f, l, p), p}
即:{2, 5}
情况 3:目标位置在内部(p ∈ [f, l)
)
return { f, l };
这种情况说明你想插入的位置就在你移动的区间内,相当于不需要动,直接返回原区间。
总结:Sean Parent 的 slide 函数要点
情况 | 判断条件 | 旋转调用 | 含义 |
---|---|---|---|
往前移 | p < f | rotate(p, f, l) | 把 [f,l) 移到 p 前面,左移 |
往后移 | l < p | rotate(f, l, p) | 把 [f,l) 移到 p 前面,右移 |
重叠 | p ∈ [f, l) | 无 | 不需要移动 |
std::rotate
小复习:
std::rotate(begin, middle, end)
- 把
[begin, middle)
移到[middle, end)
后面 - 最终顺序:
[middle, end) + [begin, middle)
- 返回新
begin
,即middle
最后的建议
Sean Parent 的这个版本非常精炼,前提是你熟悉 std::rotate
的行为。如果你刚入门,它可能显得太压缩;但一旦你理解 rotate
,这个版本是极高效且表达力强的写法。
使用标准库算法组合完成复杂的操作,而不是写显式的循环和特殊处理逻辑。
问题:gather
是什么?
把满足条件的一些分散的元素,按照它们原来的相对顺序,集中到一个指定的位置。
比如在 GUI 列表中,把你鼠标框选的多个项目聚合到目标位置,而不改变它们之间的顺序。
gather 的目标功能:
- 输入:一个范围
[f, l)
,一个谓词pred
表示选中的项,一个中心位置p
。 - 输出:把所有满足
pred
的元素收集起来,并围绕位置p
聚合。 - 返回值:新位置区间(满足
pred
的元素的新位置)
实现方法:利用 stable_partition
+ rotate
template <typename I, typename Pred>
std::pair<I, I> gather(I f, I l, I p, Pred pred) {return {std::stable_partition(f, p, [&](const auto& x) { return !pred(x); }),std::stable_partition(p, l, pred)};
}
怎么理解这个实现?
核心思想:
把整个序列分成两段:
[f, p) 和 [p, l)
我们希望:
- 在
[f, p)
这段,把 不是选中的元素 放到前面,让选中的元素移动到接近p
; - 在
[p, l)
这段,把 选中的元素 放到前面(即靠近p
)。
最终效果就是:选中的元素都被“吸引”到p
位置周围。
图解说明
示例:
std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto pred = [](int x) { return x % 2 == 1; }; // odd numbers
auto result = gather(v.begin(), v.end(), v.begin() + 5, pred);
目标是把所有 奇数(1,3,5,7,9
)聚集到索引 5
附近。
步骤说明:
std::stable_partition(f, p, !pred)
:- 把前半段
[0, 1, 2, 3, 4]
中 不是奇数 的元素(即偶数)移到前面 - 结果:[0, 2, 4, 1, 3, …]
- 把前半段
std::stable_partition(p, l, pred)
:- 把后半段
[5, 6, 7, 8, 9]
中的 奇数 移到前面 - 结果:[0, 2, 4, 1, 3, 5, 7, 9, 6, 8]
最终聚合结果是:
- 把后半段
[非选中 | 选中(聚集) | 非选中]
[0 2 4 | 1 3 5 7 9 | 6 8]
gather 的优势:
- 不写任何循环
- 使用标准库
stable_partition
保证原始顺序不变 - 支持任意选择条件
pred
- 返回值指示了新聚集区间的位置(
pair<I, I>
)
实战例子(带输出)
#include <vector>
#include <algorithm>
#include <iostream>
template <typename I, typename Pred>
std::pair<I, I> gather(I f, I l, I p, Pred pred) {return {std::stable_partition(f, p, [&](const auto& x) { return !pred(x); }),std::stable_partition(p, l, pred)};
}
int main() {std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};auto pred = [](int x) { return x % 2 == 1; }; // odd numbersauto result = gather(v.begin(), v.end(), v.begin() + 5, pred);std::cout << "After gather: ";for (int x : v) std::cout << x << " ";std::cout << "\nGathered range: ";for (auto it = result.first; it != result.second; ++it) std::cout << *it << " ";
}
输出:
After gather: 0 2 4 1 3 5 7 9 6 8
Gathered range: 1 3 5 7 9
总结:gather 思维模式
特性 | 说明 |
---|---|
输入 | [f, l) , 中心 p , 条件 pred |
过程 | 对 [f, p) 保留非选中;对 [p, l) 保留选中 |
输出 | 把所有满足 pred 的元素聚集到 p 位置周围 |
原始代码回顾:
template <typename I, // models BidirectionalIterator typename S> // models UnaryPredicate
auto gather(I f, I l, I p, S s) -> std::pair<I, I>
{ return { std::stable_partition(f, p, std::not1(s)), std::stable_partition(p, l, s) };
}
概念解释
功能:
将序列 [f, l)
中满足条件 s
的元素,在不改变它们相对顺序的前提下,收集在中心点 p
附近。
参数说明:
参数 | 含义 |
---|---|
f | 起始迭代器(inclusive) |
l | 结束迭代器(exclusive) |
p | 聚集中心位置(迭代器) |
s | 一元谓词,判断元素是否应被选中(例如:是否是奇数) |
返回值 | 被选中元素的新位置范围 [new_first, new_last) |
核心思想:分成两段 partition
将整个区间 [f, l)
分成两段:
[f, p) 和 [p, l)
分别对两段应用 std::stable_partition
,这样就可以将所有满足 s
的元素围绕中心 p
稳定地收集起来:
std::stable_partition(f, p, not1(s)); // 把不满足条件的放前面
std::stable_partition(p, l, s); // 把满足条件的放前面
最终效果:选中的元素都集中在 p
附近。
示例讲解
std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto is_odd = [](int x) { return x % 2 == 1; };
auto result = gather(v.begin(), v.end(), v.begin() + 5, is_odd);
过程图解:
原始序列: 0 1 2 3 4 | 5 6 7 8 9↑ p = v.begin() + 5
第一段 stable_partition(f, p, not1(s)):
→ 把非奇数的元素 [0, 2, 4] 留在前面,奇数 [1, 3] 被移到 p 前
→ 结果变为: 0 2 4 1 3 | 5 6 7 8 9
第二段 stable_partition(p, l, s):
→ 把奇数 [5, 7, 9] 移到前,结果如下:
→ 最终序列: 0 2 4 1 3 | 5 7 9 6 8↑ gathered at [3, 8)
返回值:
返回的是 std::pair<I, I>
,表示聚集后满足条件元素的范围:
result.first = v.begin() + 3; // points to 1
result.second = v.begin() + 8; // points past 9
即:{1, 3, 5, 7, 9}
被集中在 [3, 8)
。
为何使用 stable_partition
?
- 保留原来元素的相对顺序
- 比
partition
更可控,适合 GUI、用户列表等逻辑 - 不需要你写显式循环,非常函数式的写法
总结
点 | 说明 |
---|---|
gather 目的 | 把满足条件的元素集中在某个中心点 |
用法 | 分别对 [f, p) 与 [p, l) 使用 stable_partition |
效果 | 元素按谓词分组、相对顺序不变、围绕 p 聚集 |
返回值 | 满足条件的元素新范围 [new_first, new_last) |
使用 std::set_difference
来快速对比两个有序集合的差异,用于处理配置更新(比如服务器设置、用户权限、菜单选项等)的一种非常典型而优雅的方法。
使用场景理解
场景:
- 你有一个 当前配置集合(
current
),是有序的容器(如std::set
)。 - 系统接收到一份 更新后的配置集合(
update
)。 - 你想快速知道:
- 哪些旧配置项不再存在了(→ 要移除)
- 哪些新配置项被添加进来了(→ 要添加)
STL 解法:std::set_difference
C++ 提供了专门的算法 std::set_difference
:
set_difference(a.begin(), a.end(),b.begin(), b.end(),output);
它会把 在 a 中但不在 b 中的元素 放进 output
中。
示例代码讲解
#include <set>
#include <vector>
#include <algorithm>
#include <iostream>
int main() {std::set<std::string> current{ "one", "two", "three", "four", "five" };std::set<std::string> update{ "one", "three", "four", "six", "seven" };std::vector<std::string> removed_items, new_items;// 找出当前配置中被移除的项目(在 current 中,但不在 update 中)std::set_difference(current.begin(), current.end(),update.begin(), update.end(),std::back_inserter(removed_items));// 找出新加入的项目(在 update 中,但不在 current 中)std::set_difference(update.begin(), update.end(),current.begin(), current.end(),std::back_inserter(new_items));// 模拟操作std::cout << "Removed: ";for (const auto& item : removed_items) std::cout << item << " ";std::cout << "\nAdded: ";for (const auto& item : new_items) std::cout << item << " ";
}
输出:
Removed: five two
Added: seven six
解释原理:
current (旧) | update (新) |
---|---|
one | one |
two 被移除 | three |
three | four |
four | six 被新增 |
five 被移除 | seven 被新增 |
要点总结:
功能 | 方法 | 说明 |
---|---|---|
获取被移除项 | set_difference(current, update) | 旧集合中存在,更新中没有的 |
获取新增项 | set_difference(update, current) | 更新中存在,旧集合没有的 |
容器要求 | 有序(如 set 、vector 排好序) | set_difference 只对有序容器有效 |
稳定性 | 元素相对顺序不保证 | 但结果是升序 |
实际应用场景
- 配置文件更新比对
- 权限列表(新增/移除权限)
- GUI 多选项同步
- 云同步差异分析
Bonus:可读性更高的封装(推荐)
template <typename Container>
std::vector<typename Container::value_type>
difference(const Container& a, const Container& b) {std::vector<typename Container::value_type> result;std::set_difference(a.begin(), a.end(),b.begin(), b.end(),std::back_inserter(result));return result;
}
这样你就可以直接这样调用:
auto removed = difference(current, update);
auto added = difference(update, current);
C++ STL(标准模板库)算法的分类总览与学习建议,
一、总纲:学好 STL 算法的建议
建议 | 说明 |
---|---|
熟悉所有算法 | 不仅知道有这些算法,还要知道它们的特点与用途 |
编写练习代码 | 用小例子尝试每个算法,了解它的行为与边界 |
改造已有算法 | 学会组合 STL 算法或用自定义谓词适配需求 |
编写你自己的算法 | STL 不一定覆盖你所有场景,自己实现是终极理解方式 |
二、STL 算法总览(按类型分类)
1⃣ 非修改类(Non-Modifying):用于查看/查找/判断
功能 | 代表算法 |
---|---|
范围检查 | all_of , any_of , none_of |
遍历操作 | for_each |
查找元素 | find , find_if , find_if_not , find_end , find_first_of , adjacent_find |
数量相关 | count , count_if |
比较元素 | mismatch , equal , is_permutation |
匹配子序列 | search , search_n |
特点:不修改容器内容,只做“观察”。 |
2⃣ 修改类(Mutating):用于复制、替换、移动、变换
功能 | 代表算法 |
---|---|
复制 | copy , copy_if , copy_n , copy_backward |
移动 | move , move_backward |
替换 | replace , replace_if , replace_copy , replace_copy_if |
填充 | fill , fill_n , generate , generate_n |
删除 | remove , remove_if , remove_copy , remove_copy_if |
去重 | unique , unique_copy |
反转/旋转/打乱 | reverse , rotate , shuffle , random_shuffle |
分区 | partition , stable_partition , partition_copy , partition_point |
注意:很多算法“看似”删除,其实只是“重新排列 + 返回末尾”,比如 remove_if + erase 组合成“真正删除”。 |
3⃣ 排序与相关(Sorting & Set Operations)
功能 | 代表算法 |
---|---|
排序 | sort , stable_sort , partial_sort , partial_sort_copy , nth_element |
二分查找 | lower_bound , upper_bound , binary_search , equal_range |
合并 | merge , inplace_merge |
集合操作 | includes , set_union , set_intersection , set_difference , set_symmetric_difference |
堆操作 | make_heap , push_heap , pop_heap , sort_heap |
最小最大 | min , max , min_element , max_element , minmax , minmax_element |
字典序比较 | lexicographical_compare |
排列生成 | next_permutation , prev_permutation |
用于有序结构操作、排序优化、集合比对(如权限、配置差异)等。 |
4⃣ 数值操作(Numeric)
功能 | 代表算法 |
---|---|
累加 | accumulate |
点积 | inner_product |
局部和 | partial_sum |
相邻差 | adjacent_difference |
整数序列生成 | iota |
常用于数学运算、数据分析、图算法等场景。 |