STL算法(Standard Template Library Algorithms)
目录
32.2.1 序列(Sequences)(译注:顺序访问的抽象)
32.4 非修改序列算法(Nonmodifying Sequence Algorithms)
32.4.2 序列谓词(Sequence Predicates)
32.5 修改序列算法(Modifying Sequence Algorithms)
32.5.4 rotate() , random_shuffle() ,和 partition()
32.6 排序和搜索(Sorting and Searching)
32.6.5 lexicographical_compare() (词典比较)
32.1 引言(Introduction)
本章介绍 STL 算法。STL 由标准库中的迭代器、容器、算法和函数对象部分组成。STL 的其余部分在第 31 章和第 33 章中介绍。
32.2 算法(Algorithms)
<algorithm> 中定义了大约 80 种标准算法。它们对由一对迭代器(用于输入)或单个迭代器(用于输出)定义的序列进行操作。在对两个序列进行复制、比较等操作时,第一个序列由一对迭代器 [b:e)表示,而第二个序列仅由单个迭代器 b2 表示,该迭代器被视为包含足够元素的序列的开头,例如,元素数量与第一个序列相同:[b2:b2+(e−b))。某些算法(例如 sort())需要随机访问迭代器,而许多算法(例如 find())仅按顺序读取元素,因此可以使用前向迭代器。许多算法遵循通常的惯例,返回序列的末尾来表示“未找到”(§4.5)。我不会对每一个算法都提及这一点。
算法(包括标准库算法和用户自己的算法)都很重要:
• 每一个方法都命名了一个特定的操作,记录了一个接口,并指定了语义。
• 每一个方法都可以被许多程序员广泛使用和了解。
就正确性、可维护性和性能而言,与函数和依赖关系定义不明确的“随机代码”相比,这些代码具有巨大的优势。如果您发现自己编写的代码包含多个循环、看似彼此无关的局部变量或复杂的控制结构,请考虑是否可以通过将其中一部分代码简化为一个函数/算法,使其具有描述性的名称、明确定义的用途、明确定义的接口和明确定义的依赖关系。
STL 算法风格的数值算法在§40.6 中介绍。
32.2.1 序列(Sequences)(译注:顺序访问的抽象)
标准库算法的理想状态是提供最通用、最灵活的接口,以便实现最优实现。基于迭代器的接口虽然不错,但并非完美地接近了这一理想状态(§33.1.1)。例如,基于迭代器的接口并不直接表示序列的概念,这可能会导致混淆,并难以检测某些范围错误:
void user(vector<int>& v1, vector<int>& v2)
{
copy(v1.begin(),v1.end(),v2.begin()); // 可能会溢出 v2
sort(v1.begin(),v2.end()); // oops!
}
通过提供标准库算法的容器版本,可以缓解许多此类问题。例如:
template<typename Cont>
void sort(Cont& c)
{
static_assert(Range<Cont>(), "sort(): Cont argument not a Range");
static_assert(Sortable<Iterator<Cont>>(), "sort(): Cont argument not Sortable");
std::sort(begin(c),end(c));
}
template<typename Cont1, typename Cont2>
void copy(const Cont1& source , Cont2& target)
{
static_assert(Range<Cont1>(), "copy(): Cont1 argument not a Range");
static_assert(Range<Cont2>(), "copy(): Cont2 argument not a Range");
if (target.siz e()<source .size()) throw out_of_rang e{"copy target too small"};
std::copy(source .begin(),source .end(),target.begin());
}
这将简化 user() 的定义,使第二个错误无法表达,并在运行时捕获第一个错误:
void user(vector<int>& v1, vector<int>& v2)
{
copy(v1,v2); // overflows will be caught
sort(v1);
}
然而,容器版本也比直接使用迭代器的版本通用性差。具体来说,你不能使用容器的 sort() 方法对半个容器进行排序,也不能使用容器的 copy() 方法写入输出流。
一种补充方法是定义一个“范围(range)”或“序列(sequence)”抽象(译注:即顺访问的一种抽象),以便我们在需要时定义序列。我使用“范围”概念来表示任何带有 begin() 和 end() 迭代器(§24.4.4)的对象。也就是说,没有 Range 类来保存数据——正如 STL 中没有 Iterator 类或 Container 类一样。因此,在“容器 sort()”和“容器 copy()”的示例中,我将模板参数命名为 Cont(代表“容器”),但它们可以接受任何带有 begin() 和 end() 迭代器且满足算法其余要求的序列。
标准库容器大多返回迭代器。具体来说,它们不返回结果容器(除了少数几个罕见的例子,返回一对结果)。其中一个原因是,在STL设计之初,并没有直接支持移动语义。因此,没有一种显而易见且高效的方法可以从算法中返回大量数据。一些程序员使用了显式的间接访问(例如,指针、引用或迭代器)或一些巧妙的技巧。如今,我们可以做得更好:
template<typename Cont, typename Pred>
vector<Value_type<Cont>∗>
find_all(Cont& c, Pred p)
{
static_assert(Range<Cont>(), "find_all(): Cont argument not a Range");
static_assert(Predicate<Pred>(), "find_all(): Pred argument not a Predicate");
vector<Value_type<Cont>∗> res;
for (auto& x : c)
if (p(x)) res.push_back(&x);
return res; 这时候其实系统分配了全局内存来存放 res 中的内容
}
译注:以 int 为例看看 return res 都做了什么事:
return res;
00007FF66FC03B41 lea rdx,[res]
00007FF66FC03B46 mov rcx,qword ptr [rsp+70h]
;下列代码调用 vector 构造函数创建一个新的对象 vector 对象
00007FF66FC03B4B call
std::vector<int,std::allocator<int> >::vector<int,std::allocator<int> > (07FF66FBF30E4h)
00007FF66FC03B50 mov eax,dword ptr [rsp+24h]
00007FF66FC03B54 or eax,1
00007FF66FC03B57 mov dword ptr [rsp+24h],eax
00007FF66FC03B5B lea rcx,[res]
;释放局部对象
00007FF66FC03B60 call std::vector<int,std::allocator<int> >::~vector<int,std::allocator<int> > (07FF66FBF41D8h)
00007FF66FC03B65 mov rax,qword ptr [rsp+70h] ;返回新生成的对象地址
}
在 C++98 中,每当匹配数量很大时,这个 find_all() 就会成为一个严重的性能缺陷。如果标准库算法的选择显得过于严格或不足,那么使用新版本的 STL 算法或新算法进行扩展通常是一个可行且更优的替代方案,而不是仅仅编写“随机代码”来解决这个问题。
请注意,STL 算法返回的任何内容都不能作为参数容器。STL 算法的参数是迭代器(第 33 章),而算法并不知道这些迭代器指向的数据结构。迭代器的存在主要是为了将算法与其操作的数据结构隔离开来,反之亦然。
32.3 策略参数(Policy Arguments)
大多数标准库算法都有两个版本:
• 使用常规操作(例如 < 和 ==)执行操作的“简单(plain)”版本。
• 将关键操作作为参数的版本。
例如:
template<class Iter>
void sort(Iter first, Iter last)
{
// ... sort using e1<e2 ...
}
template<class Iter, class Pred>
void sort(Iter first, Iter last, Pred pred)
{
// ... sort using pred(e1,e2) ...
}
这大大增加了标准库的灵活性及其使用范围。
算法的两个版本通常可以实现为两个(重载)函数模板,也可以实现为一个带有默认参数的函数模板。例如:
template<typename Ran, typename Pred = less<Value_type<Ran>>> // use a default template argument
sort(Ran first, Ran last, Pred pred ={})
{
// ... use pred(x,y) ...
}
拥有两个函数和拥有一个带有默认参数的函数之间的区别,可以通过使用指向函数的指针来观察。然而,将标准算法的许多变体简单地视为“带有默认谓词的版本”,大约可以将需要记住的模板函数数量减少一半。
在某些情况下,参数可以解释为谓词或值。例如:
bool pred(int);
auto p = find(b,e,pred); // find element pred or apply predicate’pred()? (the latter)
一般来说,编译器无法消除此类例子的歧义,即使在编译器可以消除歧义的情况下,程序员也会感到困惑。
为了简化程序员的任务,通常使用 _if 后缀来表示算法接受谓词。使用两个名称进行区分是为了最大限度地减少歧义和混淆。请考虑:
using Predicate = bool(∗)(int);
void f(vector<Predicate>& v1, vector<int>& v2)
{
auto p1 = find(v1.begin(),v1.end(),pred); // 用值 pred 查找元素
auto p2 = find_if(v2.begin(),v2.end(),pred); // 统计 pred() 返回 true 的元素
}
传递给算法的某些操作作为参数,其目的是修改应用这些操作的元素(例如,传递给 for_each() 的某些操作;§32.4.1),但大多数操作是谓词(例如,sort() 的比较对象)。除非另有说明,否则假定传递给算法的策略参数不应修改元素。尤其不要尝试通过谓词修改元素:
int n_even(vector<int>& v) // 不要这样做
// 统计v中的偶数值
{
return find_if(v.begin(),v.end(),[](int& x) {++x; return x&1; });
}
通过谓词修改元素会掩盖正在执行的操作。如果你真的够狡猾,甚至可以修改序列(例如,使用正在迭代的容器的名称插入或删除元素),从而导致迭代失败(可能以不为人知的方式)。为了避免意外,你可以使用 const 引用将参数传递给谓词。
同样,谓词不应携带会改变其操作含义的状态。算法的实现可能会复制谓词,而我们很少希望对同一值重复使用谓词时产生不同的结果。传递给算法的某些函数对象(例如随机数生成器)确实携带可变状态。除非你确实确定算法不会复制,否则请将函数对象参数的可变状态保存在另一个对象中,并通过指针或引用访问它。
指针元素上的 == 和 < 操作很少适用于 STL 算法:它们比较的是机器地址,而不是指向的值。尤其不要使用默认的 == 和 < (§32.6) 对 C 风格字符串容器进行排序或搜索。
32.3.1 复杂度(Complexity)
对于容器(§31.3),算法的复杂度由标准指定。大多数算法都是线性的,复杂度为 O(n),其中 n 通常是输入序列的长度。
算法复杂度(§iso.25) | |
O(1) | swap(), iter_swap() |
O(log(n)) | lower_bound(), upper_bund(), equal_range(), binary_search(), push_heap(), pop_heap() |
O(n∗log(n)) | inplace_merge() (最差的情况), stable_par tition()(最差的情况),sort(), stable_sor t(), partial_sort(), partial_sort_copy(), sort_heap() |
O(n∗n) | find_end(), find_first_of(), search(), search_n() |
O(n) | 所有以上之外的算法 |
与以往一样,这些都是渐近复杂度,你必须了解n度量什么才能理解其含义。例如,如果 n < 3 ,二次算法可能是最佳选择。每次迭代的成本可能会有很大差异。例如,遍历list可能比遍历vector慢得多,即使这两种情况下的复杂度都是线性的(O(n))。复杂度度量不能替代常识和实际时间测量;它们只是众多确保代码质量的工具之一。
32.4 非修改序列算法(Nonmodifying Sequence Algorithms)
非修改算法仅读取其输入序列元素的值;它不会重新排列序列,也不会更改元素的值。通常,用户对算法提供的操作也不会更改元素的值;这些操作通常是谓词(它们可能不会修改其参数)。
32.4.1 for_each()
最简单的算法是 for_each(),它只是对序列的每一个元素应用一个操作:
for_each() (§iso.25.2.4) | |
f=for_each(b,e ,f) | 对 [b:e) 中的每一个 x 执行 f(x);返回 f |
如果可能的话,最好采用更具体的算法。
传递给 for_each() 的操作可能会修改元素。例如:
void increment_all(vector<int>& v) // 递增 v 中的每一个元素
{
for_each(v.begin(),v.end(), [](int& x) {++x;});
}
译注:[](int& x) {++x;} 为一种Lambda 表达式,在这里为匿名函数。
32.4.2 序列谓词(Sequence Predicates)
序列谓词 (§iso.25.2.1) | |
all_of(b,e,f) | f(x) 对 [b:e) 中的所有 x 都成立吗? |
any_of(b,e ,f) | f(x) 对 [b:e) 中的任意 x 都成立吗? |
none_of(b,e,f) | f(x) 对 [b:e) 中的所有 x 都不成立吗? |
例如:
vector<double> scale(const vector<double>& val, const vector<double>& div)
{
assert(val.size()<div.si e());
assert(all_of(div.begin(),div.end(),[](double x){ return 0<x; });
vector res(val.siz e());
for (int i = 0; i<val.siz e(); ++i)
res[i] = val[i]/div[i];
return res;
}
当其中一个序列谓词失败时,它不会告知哪个元素导致了失败。
32.4.3 count()
count (§iso.25.2.9) | |
x=count(b,e,v) | x 是 [b:e) 中元素 ∗p 的数量,满足 v==∗p |
x=count_if(b,e,v,f) | x 是 [b:e) 中元素 ∗p 的数量,满足 f(∗p) |
例如:
void f(const string& s)
{
auto n_space = count(s.begin(),s.end(),' ');
auto n_whitespace = count_if(s.begin(),s.end(),isspace);
// ...
}
isspace() 谓词(§36.2)让我们计算所有空格字符,而不仅仅是空格。
32.4.4 find()
find() 系列算法对某些元素或谓词匹配进行线性搜索:
find() 系列算法(§iso.25.2.5) | |
p=find(b,e,v) | p 指向 [b:e) 中的第一个元素,使得 ∗p==v |
p=find_if(b,e,f) | p 指向 [b:e) 中的第一个元素,使得 f(∗p)为真 |
p=find_if_not(b,e,f) | p 指向 [b:e) 中的第一个元素,使得 !f(∗p) 为真 |
p=find_first_of(b,e ,b2,e2) | p 指向 [b:e) 中的第一个元素,使得对于[b2:e2)中的某个q有 ∗p==∗q |
p=find_first_of(b,e ,b2,e2,f) | p 指向 [b:e) 中的第一个元素,使得对于[b2:e2)中的某个q有 f(∗p,∗q) 成立 |
p=adjacent_find(b,e) | p 指向 [b:e) 中的第一个元素,使得 ∗p==∗(p+1) |
p=adjacent_find(b,e,f) | p 指向 [b:e) 中的第一个元素,使得f(∗p,∗(p+1)) 为真 |
p=find_end(b,e,b2,e2) | p 指向 [b:e) 中的最后一个元素 *p,使得对于[b2:e2)中的某个元素*q有 ∗p == ∗q |
p=find_end( b,e,b2,e2,f) | p 指向 [b:e) 中的最后一个元素 *p,使得对于[b2:e2)中的一个元素*q有 f(∗p,∗q) 为真 |
算法 find() 和 find_if() 分别返回一个迭代器,指向与某个值和某个谓词匹配的第一个元素。
void f(const string& s)
{
auto p_space = find(s.begin(),s.end(),' ');
auto p_whitespace = find_if(s.begin(),s.end(), isspace);
// ...
}
find_first_of() 算法用于查找一个序列中元素在另一个序列中的首次出现。例如:
array<int> x = {1,3,4 };
array<int> y = {0,2,3,4,5};
void f()
{
auto p = find_first_of(x.begin(),x.end(),y.begin(),y.end); // p = &x[1]
auto q = find_first_of(p+1,x.end(),y.begin(),y.end()); // q = &x[2]
}
迭代器 p 将指向 x[1],因为 3 是 x 中第一个与 y 匹配的元素。同样,q 将指向 x[2] 。
32.4.5 equal() 和 mismatch()
equal() 和 mismatch() 算法比较序更对:
count (§iso.25.2.9) | |
equal(b,e,b2) | 对于 [b:e) 和 [b2:b2+(e−b)) 的所有对应元素,v==v2 吗? |
equal(b,e,b2,f) | 对于 [b:e) 和 [b2:b2+(e−b)) 的所有对应元素,f(v,v2) 是否成立? |
pair(p1,p2)=mismatch(b,e ,b2) | p1 指向 [b:e) 中的第一个元素,p2 指向 [b2:b2+(e−b)) 中的第一个元素,使得 !(∗p1==∗p2) 或 p1==e |
pair(p1,p2)=mismatch(b,e ,b2,f) | p1 指向 [b:e) 中的第一个元素,p2 指向 [b2:b2+(e−b)) 中的第一个元素,使得 !f(∗p1,∗p2) 或 p1==e |
mismatch() 查找两个序列中比较结果不相等的第一对元素,并返回指向这些元素的迭代器。第二个序列没有指定结束点;也就是说,没有 last2 元素。相反,它假设第二个序列中的元素数量至少与第一个序列中的元素数量相同,并且 first2 + (last-first) 用作 last2 元素。这种技术在整个标准库中都使用,其中序列对用于对元素对进行操作。我们可以像这样实现 mismatch():
template<class In, class In2, class Pred = equal_to<Value_type<In>>>
pair<In, In2> mismatch(In first, In last, In2 first2, Pred p ={})
{
while (first != last && p(∗first,∗first2)) {
++first;
++first2;
}
return {first,first2};
}
我使用了标准函数对象 equal_to (§33.4) 和类型函数 Value_type (§28.2.1)。
32.4.6 search()
search() 和 search_n() 算法将一个序列查找为另一个序列的子序列:
搜索序列(§iso.25.2.13) | |
p=search(b,e ,b2,e2) | p 指向 [b:e) 中的第一个 ∗p,使得 [p:p+(e−b)) 等于 [b2:e2) |
p=search(b.e ,b2,e2,f) | p 指向 [b:e) 中的第一个 ∗p,使得 [p:p+(e−b)) 等于 [b2:e2),使用 f 进行元素比较 |
p=search_n(b,e ,n,v) | p 指向 [b:e) 的第一个元素,使得 [p:p+n) 的每个元素都具有值 v |
p=search_n(b,e ,n,v,f) | p 指向 [b:e) 的第一个元素,使得对于 [p:p+n) 中的每个元素 ∗q,我们都有 f(∗p,v) |
search() 算法会查找第二个序列作为第一个序列的子序列。如果找到了第二个序列,则返回第一个序列中第一个匹配元素的迭代器。通常,序列的末尾会表示“未找到”。例如:
string quote {"Why waste time learning, when ignorance is instantaneous?"};
bool in_quote(const string& s)
{
auto p = search(quote .begin(),quote.end(),s.begin(),s.end()); // find s in quote
return p!=quote.end();
}
void g()
{
bool b1 = in_quote("learning"); // b1 = true
bool b2 = in_quote("lemming"); // b2 = false
}
因此,search() 是一种用于查找适用于所有序列的子字符串的有用算法。
使用 find() 或 binary_search() (§32.6) 来查找单个元素。
32.5 修改序列算法(Modifying Sequence Algorithms)
修改算法(也称为变化序列算法)可以(并且经常)修改其参数序列的元素。
transform (§iso.25.3.4) | |
p=transform(b,e ,out,f) | 将 ∗q=f(∗p1) 应用于 [b:e) 中的每一个 ∗p1,写入[out:out+(e−b)) 中的对应 ∗q;p=out+(e−b) |
p=transform(b,e ,b2,out,f) | 将 ∗q=f(∗p1,∗p2) 应用于 [b:e) 中 ∗p1 的每一个元素以及 [b2:b2+(e−b)) 中对应的 ∗p2,写入 [out:out+(e−b)); 中对应的 ∗q p=out+(e−b) |
有点令人困惑的是,transform() 并不一定会改变其输入。相反,它会根据用户提供的操作,生成一个输出,该输出是对输入的变换。transform() 的单输入序列版本可以定义如下:
template<class In, class Out, class Op>
Out transform(In first, In last, Out res, Op op)
{
while (first!=last)
∗res++ = op(∗first++);
return res;
}
输出序列可能与输入序列相同:
void toupper(string& s) // remove case
{
transform(s.begin(),s.end(),s.begin(),toupper);
}
这确实改变了输入 s 。
32.5.1 copy()
copy() 系列算法将元素从一个序列复制到另一个序列。以下章节列出了 copy() 与其他算法(例如 replace_copy() (§32.5.3))结合使用的版本。
copy算法族 (§iso.25.3.1) | |
p=copy(b,e ,out) | 将[b:e)中的所有元素复制到[out:p); p=out+(e−b) |
p=copy_if(b,e ,out,f) | 将 [b:e) 中满足 f(x) 的元素 x 复制到 [out:p) |
p=copy_n(b,n,out) | 将[b:b+n)中的前n个元素复制到[out:p); p=out+n |
p=copy_backward(b,e ,out) | 从最后一个元素 p=out+(e−b) 开始,将 [b:e) 中的所有元素复制到 [out:p); |
p=move(b,e ,out) | 将[b:e)中的所有元素移动到[out:p); p=out+(e−b) |
p=move_backward(b,e ,out) | 从最后一个元素 p=out+(e−b) 开始,将 [b:e) 中的所有元素移动到 [out:p); |
复制算法的目标不必是容器。任何能够用输出迭代器(见 38.5 节)描述的对象都可以。例如:
void f(list<Club>& lc, ostream& os)
{
copy(lc.begin(),lc.end(),ostream_iterator<Club>(os));
}
要读取序列,我们需要一对迭代器来描述起始位置和终止位置。要写入,我们只需要一个迭代器来描述写入位置。但是,我们必须注意不要写入超出目标的末尾。确保不超出目标末尾的一种方法是使用插入器(§33.2.2)根据需要扩展目标。例如:
void f(const vector<char>& vs, vector<char>& v)
{
copy(vs.begin(),vs.end(),v.begin()); // 可能重写v的尾
copy(vs.begin(),vs.end(),back_inserter(v)); // 从vs填加元素到v的尾
}
输入序列和输出序列可能重叠。当序列不重叠或输出序列的末尾位于输入序列中时,我们使用 copy() 。
我们使用 copy_if() 来仅复制满足某些条件的元素。例如:
void f(list<int>&ld, int n, ostream& os)
{
copy_if(ld.begin(),ld.end(),
ostream_iterator<int>(os),
[](int x) { return x>n); });
}
另见 remove_copy_if() 。
32.5.2 unique()
unique() 算法从序列中删除相邻的重复元素:
unique 算法族 (§iso.25.3.9) | |
p=unique(b,e) | 移动 [b:e) 中的元素,使得 [b:p) 没有相邻的重复项 |
p=unique(b,e,f) | 移动 [b:e) 中的元素,使得 [b:p) 没有相邻的重复项;f(∗p,∗(p+1)) 定义“重复项” |
p=unique_copy(b,e ,out) | 将 [b:e) 复制到 [out:p); 不要复制相邻的重复项 |
p=unique_copy(b,e ,out,f) | 将 [b:e) 复制到 [out:p);不复制相邻的重复项; f(∗p,∗(p+1)) 定义“重复项” |
unique() 和 unique_copy() 算法可以消除相邻的重复值。例如:
void f(list<string>& ls, vector<string>& vs)
{
ls.sort(); // list sort (§31.4.2)
unique_copy(ls.begin(),ls.end(),back_inser ter(vs));
}
这会将 ls 复制到 vs,从而消除重复项。我使用了 sort() 来获取相邻的相同字符串。
与其他标准算法一样,unique() 操作的是迭代器。它不知道这些迭代器指向哪个容器,所以无法修改该容器。它只能修改元素的值。这意味着 unique() 不会像我们天真地期望的那样,从输入序列中消除重复项。因此,它不会消除vector中的重复项:
void bad(vector<string>& vs) // warning: 没有按照它看起来的那样执行!
{
sort(vs.begin(),vs.end()); // sort vector
unique(vs.begin(),vs.end()); // 消除重复(非也!)
}
相反,unique() 会将唯一元素移至序列的前端(头部),并返回一个指向唯一元素子序列末尾的迭代器。例如:
void main()
{
string s ="abbcccde";
auto p = unique(s.begin(),s.end());
cout << s << ' ' << p−s.begin() << '\n';
}
产生输出:
abcdecde 5
即,p 指向第二个 c (即重复项中的第一个位置)。
可能删除元素(但不能删除)的算法通常有两种形式:以类似于 unique() 的方式重新排序元素的“普通”版本,以及以类似于 unique_copy() 的方式生成新序列的 _copy 版本。
为了消除容器中的重复项,我们必须显式地缩小它:
template<class C>
void eliminate_duplicates(C& c)
{
sort(c.begin(),c.end()); // sort
auto p = unique(c.begin(),c.end()); // compact
c.erase(p,c.end()); // shrink
}
我们可以等效地写成 c.erase(unique(c.begin(),c.end()),c.end()),但我认为这种简洁并不能提高可读性或可维护性。
32.5.3 remove() 和 replace()
remove() 算法将直至序列末尾的元素“删除”:
remove (§iso.25.3.8) | |
p=remove(b,e ,v) | 从 [b:e) 中删除值为 v 的元素,使得 [b:p) 成为满足 !(∗q==v) 的元素 |
p=remove_if(b,e ,v,f) | 从 [b:e) 中移除元素 ∗q,使得 [b:p) 成为满足 !f(∗q) 的元素 |
p=remove_copy(b,e ,out,v) | 将元素从 [b:e) (其中 !(∗q==v)) 复制到 [out:p) |
p=remove_copy_if(b,e ,out,f) | 将元素从 [b:e) 复制到 [out:p) |
reverse(b,e) | 反转 [b:e) 中元素的顺序 |
p=reverse_copy(b,e ,out) | 将 [b:e) 以相反的顺序复制到 [out:p) |
replace()算法给选定的元素分配新值:
replace (§iso.25.3.5) | |
replace(b,e,v,v2) | 将 [b:e) 中 ∗p==v 的元素 ∗p 替换为 v2 |
replace_if(b,e,f,v2) | 替换 [b:e) 中的元素 ∗p,其中 f(∗p) 为 v2 |
p=replace_copy(b,e ,out,v,v2) | 将 [b:e) 复制到 [out:p), 用 v2 替换 ∗p==v 的元素 |
p=replace_copy_if(b,e ,out,f,v2) | 将 [b:e) 复制到 [out:p), 用 v2 替换 f(∗p,v) 中的元素 |
这些算法无法改变输入序列的大小,因此即使 remove() 也不会改变输入序列的大小。与 unique() 类似,它通过将元素向左移动来实现“移除”。例如:
string s {"∗CamelCase∗IsUgly∗"};
cout << s << '\n'; // *CamelCase*IsUgly*
auto p = remove(s.begin(),s.end(),'∗');
copy(s.begin(),p,ostream_iterator<char>{cout}); // CamelCaseIsUgly
cout << s << '\n'; // CamelCaseIsUglyly*
32.5.4 rotate() , random_shuffle() ,和 partition()
rotate(), random_shuffle() 和 partion() 算法提供了在序列中移动元素的系统方法:
Rotate() (§iso.25.3.11) | |
p=rotate(b,m,e) | 左旋转元素:将 [b:e) 视为一个圆,第一个元素紧跟在最后一个元素之后;将 ∗(b+i) 移动到 ∗((b+(i+(e−m))%(e−b));注意:∗b 移动到 ∗m;p=b+(e−m) |
p=rotate_copy(b,m,e ,out) | 将 [b:e) 复制到旋转序列 [out:p) |
由rotate()(以及 shuffle 和分割算法)完成的元素移动是使用 swap() 实现的。
random_shuffle() (§iso.25.3.12) | |
random_shuffle(b,e) | 使用默认随机数生成器对 [b:e) 中的元素进行随机排列 |
random_shuffle(b,e,f) | 使用随机数生成器 f 对 [b:e) 中的元素进行打乱 |
shuffle(b,e,f) | 使用均匀随机数生成器 f 对 [b:e) 中的元素进行打乱 |
(译注:单词shuffle[ˈʃʌf(ə)l ] n & v 词义为“洗牌,搅乱,变换位置” 。)
洗牌(shuffle)算法洗牌的方式与我们洗牌的方式非常相似。也就是说,洗牌后,元素的顺序是随机的,这里的“随机”是由随机数生成器产生的分布定义的。
默认情况下,random_shuffle() 使用均匀分布随机数生成器来打乱其序列。也就是说,它会选择序列元素的排列,使得每个排列被选中的概率相同。如果您需要不同的分布或更好的随机数生成器,您可以自行提供。对于 random_shuffle(b,e,r) 调用,生成器会将序列(或子序列)中的元素数量作为参数进行调用。例如,对于 r(e−b) 调用,生成器必须返回 [0,e−b) 范围内的值。如果 My_rand 就是这样一个生成器,我们可能会像这样洗牌:
void f(deque<Card>& dc, My_rand& r)
{
random_shuffle(dc.begin(),dc.end(),r);
// ...
}
分割算法根据分割标准将序列分成两部分:
Partion() (§iso.25.3.12) | |
p=partition(b,e,f) | 将 f(∗p1) 的元素放在 [b:p) 中,将其他元素放在 [p:e) 中 |
p=stable_partition(b,e,f) | 将 f(∗p1) 的元素放在 [b:p) 中,将其他元素放在 [p:e) 中;保持相对顺序 |
pair(p1,p2)=partition_copy(b,e ,out1,out2,f) | 将 [b:e) 中 f(∗p) 的元素复制到 [out1:p1) 中,并将 [b:e) 中 !f(∗p) 的元素复制到 [out2:p2) 中 |
p=partition_point(b,e,f) | 对于 [b:e) ,p 是满足 all_of(b,p,f) 和 none_of(p,e,f) 的点 |
is_partitioned(b,e,f) | [b:e) 中满足 f(∗p) 的每一个元素是否都位于满足 !f(∗p) 的每个元素之前? |
32.5.5 排例(Permutations)
排列算法提供了一种生成序列所有排列的系统方法。
排列 (§iso.25.4.9 , §iso.25.2.12 ) 如果 next_∗ 操作成功,则 x 为 true,否则为 false | |
x=next_permutation(b,e) | 按字典顺序将 [b:e) 设为下一个排列 |
x=next_permutation(b,e ,f) | 使 [b:e) 成为下一个排列,使用 f 进行比较 |
x=prev_permutation(b,e) | 使 [b:e) 按字典顺序排列为前一个排列 |
x=prev_permutation(b,e ,f) | 使 [b:e) 成为前一个排列,使用 f 进行比较 |
is_permutation(b,e ,b2) | [b2:b2+(e−b)) 的排列是否存在,与 [b,e) 相等? |
is_permutation(b,e ,b2,f) | 是否存在一个排列 [b2:b2+(e−b)),使得其比较结果等于 [b,e),以 f(∗p,∗q) 作为元素比较? |
排列用于生成序列元素的组合。例如,abc 的排列为 acb,bac,bca,cab 和 cba。
next_permutation() 函数接受一个序列 [b:e),并将其转换为下一个排列。下一个排列的查找方法是假设所有排列的集合按字典顺序排列。如果存在这样的排列,next_permutation() 函数返回 true;否则,它将序列转换为最小的排列,即按升序排列(示例中为 abc),并返回 false。因此,我们可以像这样生成 abc 的排列:
vector<char> v {'a','b','c'};
while(next_permutation(v.begin(),v.end()))
cout << v[0] << v[1] << v[2] << ' ';
类似地,如果 [b:e) 已经包含第一个排列(示例中为 abc),则 prev_permutation() 的返回值为 false;在这种情况下,它返回最后一个排列(示例中为 cba)。
32.5.6 fill()
fill() 系列算法提供了分配和初始化序列元素的方法:
fill算法族 (§iso.25.3.6 , §iso.25.3.7, §iso.25.6.12 ) | |
fill(b,e,v) | 将 v 分配给 [b:e) 的每一个元素 |
p=fill_n(b,n,v) | 将 v 分配给 [b:b+n); p=b+n 的每一个元素 |
generate(b,e ,f) | 将 f() 分配给 [b:e) 的每一个元素 |
p=generate_n(b,n,f) | 将 f() 分配给 [b:b+n); p=b+n 的每一个元素 |
uninitialized_fill(b,e,v) | 用 v 初始化 [b:e) 中的每一个元素 |
p=uninitialized_fill_n(b,n,v) | 用 v;p=b+n 初始化 [b:b+n) 中的每一个元素 |
p=uninitialized_copy(b,e ,out) | 将 [out:out+(e−b)); p=b+n 中的每一个元素初始化为 [b:e) 中其对应的元素 |
p=uninitialized_copy_n(b,n,out) | 将 [out:out+n) 中的每一个元素初始化为 [b:b+n) 中其对应的元素;p=b+n |
fill() 算法重复赋值指定的值,而 generate() 算法则通过重复调用其函数参数来赋值。例如,使用§40.7中的随机数生成器 Randint 和 Urand:
int v1[900];
array<int,900> v2;
vector v3;
void f()
{
fill(begin(v1),end(v1),99); //将 v1 的所有元素设置为 99
generate(begin(v2),end(v2),Randint{}); //设置为随机值 (§40.7)
// 输出区间 [0:100) 内的 200 个随机整数:
generate_n(ostream_iterator<int>{cout},200,Urand{100}); // 见§40.7
fill_n(back_inser ter{v3},20,99); // 将 20 个值为 99 的元素添加到 v3
}
generate() 和 fill() 函数用于赋值而非初始化。如果你需要操作原始存储,例如,将一块内存区域转换为类型和状态明确的对象,则可以使用 uninitialized_ 版本(在 <memory> 中显示)。
未初始化序列应该只出现在编程的最低层级,通常是在容器的实现内部。uninitialized_fill() 或 uninitialized_copy() 的目标元素必须是内置类型或未初始化的。例如:
vector<string> vs {"Breugel","El Greco","Delacroix","Constable"};
vector<string> vs2 {"Hals","Goya","Renoir","Turner"};
copy(vs.begin(),vs.end(),vs2.begin()); // OK
uninitialized_copy(vs.begin(),vs.end(),vs2.begin()); // 内存泄漏!
§34.6 中描述了更多处理未初始化内存的工具。
32.5.7 swap()
swap() 算法交换两个对象的值:
swap 族 (§iso.25.3.3) | |
swap(x,y) | 交换x和y的值 |
p=swap_ranges(b,e,b2) | swap(v,v2) 在 [b:e) 和 [b2,b2+(e−b)) 中的对应元素 |
iter_swap(p,q) | swap(∗p,∗q) |
例如:
void use(vector<int>& v, int∗ p)
{
swap_ranges(v.begin(),v.end(),p); // 交换值
}
指针 p 最好指向至少具有 v.size() 个元素的数组。
swap() 算法可能是标准库中最简单、也可以说是最关键的算法。它被用作许多最广泛使用的算法的实现的一部分。它的实现在§7.7.2 中作为示例,其标准库版本在§35.5.2 中介绍。
32.6 排序和搜索(Sorting and Searching)
在已排序序列中进行排序和搜索是基础操作,程序员的需求也多种多样。比较操作默认使用 < 运算符,而值 a 和 b 的等价性则通过 !(a<b)&&!(b<a) 来判断,而无需使用 == 运算符。
“简单”sort 族 (§iso.25.4.1) | |
sort(b,e) | 排序 [b:e) |
sort(b,e,f) | 对 [b:e) 进行排序,使用 f(∗p,∗q) 作为排序标准 |
简单排序的其它变体:
“简单”sort 族 (§iso.25.4.1) | |
stable_sort(b,e) | 排序 [b:e) 保持相等元素的顺序 |
stable_sor t(b,e,f) | 对 [b:e) 进行排序,使用 f(∗p,∗q) 作为排序标准, 保持相等元素的顺序 |
partial_sort(b,m,e) | 对 [b:e) 进行足够排序,使 [b:m) 有序;[m:e) 无需排序 |
partial_sort(b,m,e,f) | 对 [b:e) 进行足够多的排序,使 [b:m) 有序化,使用 f(∗p,∗q) 作为排序标准;[m:e) 无需排序 |
p=partial_sort_copy(b,e ,b2,e2) | 对 [b:e) 进行全排序,以便将前 e2-b2 个元素复制到 [b2:e2);p 是 b2 和 b2+(e-b) 中较小的一个 |
p=partial_sort_copy(b,e ,b2,e2,f) | 对 [b:e) 进行全排序,将前 e2−b2 个元素复制到 [b2:e2) 中,使用 f 进行比较;p 是 b2 和 b2+(e−b) 中较小的一个 |
is_sorted(b,e) | [b:e) 已排序吗? |
is_sorted(b,e,f) | 使用 f 进行比较,[b:e) 是否已排序? |
p=is_sorted_until(b,e) | p 指向 [b:e) 中第一个不按顺序排列的元素 |
p=is_sorted_until(b,e,f) | p指向 [b:e) 中第一个无序的元素,使用 f 进行比较 |
nth_element(b,n,e) | 如果 [b:e) 已排序,∗n 所处的位置应与 [b:n) 中元素的 ∗n <= ∗n,且 ∗n <= [n:e) 中的元素的 ∗n |
nth_element(b,n,e,f) | 如果 [b:e) 已排序,∗n 所处的位置应与 [b:n) 中元素的 ∗n <= ∗n,且 ∗n <= [n:e) 中的元素,使用 f 进行比较 |
sort() 算法需要随机访问迭代器(§33.1.2)。
尽管名字如此,is_sorted_until() 返回的是一个迭代器,而不是布尔值。
标准list (§31.3) 不提供随机访问迭代器,因此list应使用特定的list操作 (§31.4.2) 进行排序,或者将其元素复制到vector中,对该vector进行排序,然后将元素复制回列表:
template<typename List>
void sort_list(List& lst)
{
vector v {lst.begin(),lst.end()}; // initialize from lst
sort(v); // use container sort (§32.2)
copy(v,lst);
}
基本 sort() 效率较高(平均为N∗log(N))。如果需要稳定排序,则应使用 stable_sort(),即一个 N∗log(N)∗log(N) 算法,当系统拥有足够的额外内存时,该算法会改进至 N∗log(N)。get_temporary_buffer() 函数可用于获取此类额外内存(§34.6)。stable_sort() 会保留比较相等元素的相对顺序,但 sort() 不会。
有时,只需要排序序列的前几个元素。在这种情况下,只需对序列进行必要的排序,使第一部分按顺序排列,即部分排序。简单的 partial_sort(b,m,e) 算法将元素按顺序排列在 [b:m) 范围内。partial_sort_copy() 算法会生成 N 个元素,其中 N 是输出序列中元素数量与输入序列中元素数量中较小的一个。我们需要指定结果序列的起始和结束位置,因为这决定了我们需要排序的元素数量。例如:
void f(const vector<Book>& sales) //求十本最佳书籍
{
vector<Book> bestsellers(10);
partial_sort_copy(sales.begin(),sales.end(),
bestsellers.begin(),bestsellers.end(),
[](const Book& b1, const Book& b2) { return b1.copies_sold()>b2.copies_sold(); });
copy(bestsellers.begin(),bestsellers.end(),ostream_iterator<Book>{cout,"\n"});
}
因为 partial_sort_copy() 的目标必须是随机访问迭代器,所以我们不能直接对 cout 进行排序。
如果 partial_sort() 需要排序的元素数量相对于元素总数较小,则这些算法的速度会比完整sort() 快得多。此时,它们的复杂度接近 O(N),而 sort() 的复杂度为 O(N∗log(N))。
nth_element() 算法仅对第 N 个元素进行必要的排序,以确保其位于正确的位置,且序列中不存在比其后的第 N 个元素更小的元素。例如:
vector<int> v;
for (int i=0; i<1000; ++i)
v.push_back(randint(1000)); // §40.7
constexpr int n = 30;
nth_element(v.begin(), v.begin()+n, v.end());
cout << "nth: " << v[n] < '\n';
for (int i=0; i<n; ++i)
cout << v[i] << ' ';
这将产生:
nth: 24
10 8 15 19 21 15 8 7 6 17 21 2 18 8 1 9 3 21 20 18 10 7 3 3 8 11 11 22 22 23
nth_element() 与 partial_sort() 的区别在于,n 之前的元素不一定是排序的,只要小于第 n 个元素即可。将上述示例中的 nth_element 替换为 partial_sort(并使用相同的随机数生成器种子来获得相同的序列),我得到了:
nth: 995
1 2 3 3 3 6 7 7 8 8 8 8 9 10 10 11 11 15 15 17 18 18 19 20 21 21 21 22 22 23
nth_element() 算法对于需要查找中位数、百分位数等的人士(例如经济学家、社会学家和教师)特别有用。
对 C 风格字符串进行排序需要明确的排序标准。原因是 C 风格字符串只是指针,具有一组使用约定,因此指针上的 < 比较的是机器地址,而不是字符序列。例如:
vector<string> vs = {"Helsinki","Copenhagen","Oslo","Stockholm"};
vector<char∗> vcs = {"Helsinki","Copenhagen","Oslo","Stockholm"};
void use()
{
sort(vs); // 我已经定义了sort()的一个范围版本
sort(vcs);
for (auto& x : vs)
cout << x << ' '
cout << '\n';
for (auto& x : vcs)
cout << x << ' ';
}
这会打印输出:
Copenhagen Helsinki Stockholm Oslo
Helsinki Copenhagen Oslo Stockholm
我们可能天真地期望两个vector的输出相同。然而,要按字符串值而不是地址对 C 风格的字符串进行排序,我们需要一个合适的排序谓词。例如:
sort(vcs, [](const char∗ p, const char∗ q){ return strcmp(p,q)<0; });
标准库函数 strcmp() 在§43.4 中描述。
请注意,我不需要提供 == 来对 C 风格的字符串进行排序。为了简化用户接口,标准库使用 !(x<y>||y<x) 而不是 x==y 来比较元素(§31.2.2.2)。
32.6.1 二分搜索(Binary Search)
binary_search() 族算法提供有序(排序)序列的二分搜索:
二分搜索 (§iso.25.4.3) | |
p=lower_bound(b,e ,v) | p 指向 [b:e) 中第一次出现的 v |
p=lower_bound(b,e ,v,f) | p 指向 [b:e) 中第一次出现的 v,使用 f 进行比较 |
p=upper_bound(b,e,v) | p 指向 [b:e) 中第一个大于 v 的值 |
p=upper_bound(b,e,v,f) | p 指向 [b:e) 中第一个大于 v 的值,使用 f 进行比较 |
binary_search(b,e ,v) | v 是否在排序序列 [b:e) 中? |
binary_search(b,e ,v,f) | 使用 f 进行比较,v 是否在排序序列 [b:e) 中? |
pair(p1,p2)=equal_range(b,e,v) | [p1,p2) 是 [b:e) 的子序列,值为 v;本质上是对 v 进行二分查找 |
pair(p1,p2)=equal_range(b,e,v,f) | [p1,p2) 是 [b:e) 的子序列,其值为 v,使用 f 进行比较;本质上是对 v 进行二分查找 |
对于大型序列,诸如 find() (§32.4) 之类的顺序搜索效率极低,但它几乎是我们在没有排序或哈希 (§31.4.3.2) 的情况下所能做到的最好的了。然而,一旦序列排序完毕,我们就可以使用二分查找来确定某个值是否在序列中。例如:
void f(vector<int>& c)
{
if (binary_search(c.begin(),c.end(),7)) { // is 7 in c?
// ...
}
// ...
}
binary_search() 返回一个bool值,指示某个值是否存在。与 find() 类似,我们通常也想知道具有该值的元素在序列中的位置。然而,序列中可能存在多个具有给定值的元素,我们通常需要找到第一个或所有这样的元素。因此,提供了用于查找相等元素范围的算法 equal_range(),以及用于查找该范围的 lower_bound() 和 upper_bound() 的算法。这些算法对应于multimap上的操作(§31.4.3)。我们可以将 lower_bound() 视为对已排序序列的快速 find() 和 find_if()。例如:
void g(vector<int>& c)
{
auto p = find(c.begin(),c.end(),7); // 可能会慢: O(N); c无需排序
auto q = lower_bound(c.begin(),c.end(),7); // 可能快: O(log(N)); c必须排序
// ...
}
如果 lower_bound(first,last,k) 未找到 k,则返回指向第一个键大于 k 的元素的迭代器,如果不存在这样的大于 k 的元素,则返回 last。upper_bound() 和 equal_range() 也使用了这种报告失败的方式。这意味着我们可以使用这些算法来确定在已排序序列中插入新元素的位置,以确保序列保持排序:只需在返回对的第二个元素之前插入即可。
奇怪的是,二分搜索算法不需要随机访问迭代器:前向迭代器就足够了。
32.6.2 merge()
merge()算法将两个有序(排序)序列合并为一个:
merge 族 (§iso.25.4.4) | |
p=merge(b,e ,b2,e2,out) | 将两个已排序的序列 [b2:e2) 和 [b:e) 合并为 [out:p) |
p=merge(b,e ,b2,e2,out,f) | 将两个已排序的序列 [b2:e2) 和 [b:e) 合并为 [out,out+p),以 f 为比较对象 |
inplace_merge(b,m,e) | 将两个已排序子序列 [b:m) 和 [m:e) 合并为一个已排序序列 [b:e) |
inplace_merge(b,m,e ,f) | 将两个已排序子序列 [b:m) 和 [m:e) 合并为一个已排序序列 [b:e),以 f 为比较项 |
merge() 算法可以接受不同类型的序列和不同类型的元素。例如:
vector<int> v {3,1,4,2};
list<double> lst {0.5,1.5,2,2.5}; // lst is in order
sort(v.begin(),v.end()); //排序v
vector<double> v2;
merge(v.begin(),v.end(),lst.begin(),lst.end(),back_inserter(v2)); // 将 v 和 lst 并入 v2
for (double x : v2)
cout << x << ", ";
对于插入器,请参阅§33.2.2。输出为:
0.5, 1, 1.5, 2, 2, 2.5, 3, 4,
32.6.3 集合(Set)算法
这些算法将序列视为元素的集合,并提供基本的集合运算。输入序列应已排序,输出序列也应已排序。
集合算法 (§iso.25.4.5) | |
includes(b,e ,b2,e2) | [b:e) 的所有元素也都在 [b2:e2) 中吗? |
includes(b,e ,b2,e2,f) | 使用 f 进行比较,[b:e) 中的所有元素是否也都在 [b2:e2) 中? |
p=set_union(b,e,b2,e2,out) | 构造一个排序序列 [out:p) 其元素要么在 [b:e) 中,要么在 [b2:e2) 中 |
p=set_union(b,e,b2,e2,out,f) | 构建一个排序序列 [out:p),其元素位于 [b:e) 或 [b2:e2) 中,使用 f 进行比较 |
p=set_intersection(b,e ,b2,e2,out) | 构建一个排序序列 [out:p),其元素同时存在于 [b:e) 和 [b2:e2) 中 |
p=set_intersection(b,e ,b2,e2,out,f) | 构建一个排序序列 [out:p),其元素同时存在于 [b:e) 和 [b2:e2) 中,使用 f 进行比较 |
p=set_difference(b,e ,b2,e2,out) | 构建一个排序序列 [out:p),其元素位于 [b:e) 中,但不位于 [b2:e2) 中 |
p=set_difference(b,e ,b2,e2,out,f) | 构建一个排序序列 [out:p),其元素位于 [b:e) 中,但不位于 [b2:e2) 中,使用 f 进行比较 |
p=set_symmetric_difference(b,e ,b2,e2,out) | 构造一个排序序列 [out:p),其元素位于 [b:e) 或 [b2:e2) 中,但不同时位于两者中 |
p=set_symmetric_difference(b,e ,b2,e2,out,f) | 构建一个排序序列 [out:p),其元素位于 [b:e) 或 [b2:e2) 中,但不同时位于两者中,并使用 f 进行比较 |
例如:
string s1 = "qwertyasdfgzxcvb";
string s2 = "poiuyasdfg/.,mnb";
sort(s1.begin(),s1.end()); // set算法要求有序序列
sort(s2.begin(),s2.end());
string s3(s1.size()+s2.siz e(),'∗'); //为最大可能的结果留出足够的空间
cout << s3 << '\n';
auto up = set_union(s1.begin(),s1.end(),s2.begin(),s2.end(),s3.begin());
cout << s3 << '\n';
for (auto p = s3.begin(); p!=up; ++p)
cout << ∗p;
cout << '\n';
s3.assign(s1.size()+s2.size(),'+');
up = set_difference(s1.begin(),s1.end(),s2.begin(),s2.end(),s3.begin());
cout << s3 << '\n';
for (auto p = s3.begin(); p!=up; ++p)
cout << ∗p;
cout << '\n';
这个小测试产生的结果如下:
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
,./abcdefgimnopqrstuvxyz
ceqrtvwxz++++++++++++++++++++++
ceqrtvwxz
32.6.4 堆算法(Heaps)
(译注:此堆非内存管理的堆!)
堆是一种紧凑的数据结构,它将值最高的元素放在最前面。可以把堆想象成二叉树的表示形式。堆算法允许程序员将随机访问序列视为堆:
堆运算 (§iso.25.4.6) | |
make_heap(b,e) | 使 [b:e) 准备好用作堆 |
make_heap(b,e,f) | 使 [b:e) 准备好用作堆,使用 f 进行比较 |
push_heap(b,e) | 将 ∗(e−1) 添加到堆 [b:e−1); 之后 [b:e) 是一个堆 |
push_heap(b,e,f) | 向堆[b:e−1)中添加一个元素,使用f进行比较 |
pop_heap(b,e) | 从堆 [b:e) 中移除 ∗(e−1);之后,[b:e−1) 是一个堆 |
pop_heap(b,e,f) | 从堆中移除元素[b:e),使用f进行比较 |
sort_heap(b,e) | 排序堆 [b:e) |
sort_heap(b,e,f) | 排序堆 [b:e),使用f进行比较 |
is_heap(b,e) | [b:e) 是一个推吗? |
is_heap(b,e,f) | [b:e) 是一个推吗?使用f进行比较 |
p=is_heap_until(b,e) | p 是最大的 p,使得 [b:p) 是一个堆 |
p=is_heap_until(b,e,f) | p 是最大的 p,使得 [b:p) 是一个堆,使用f进行比较 |
将堆 [b:e) 的末尾 e 视为一个指针,通过 pop_heap() 将其递减,通过 push_heap() 将其递增。通过读取 b(例如,x=∗b)然后执行 pop_heap() 来提取最大元素。通过写入 e(例如,∗e=x)然后执行 push_heap() 来插入新元素。例如:
string s = "herewego";
make_heap(s.begin(),s.end()); // rogheeew
pop_heap(s.begin(),s.end()); // rogheeew
pop_heap(s.begin(),s.end()−1); // ohgeeerw
pop_heap(s.begin(),s.end()−2); // hegeeorw
∗(s.end()−3)='f';
push_heap(s.begin(),s.end()−2); // hegeefrw
∗(s.end()−2)='x';
push_heap(s.begin(),s.end()−1); // xeheefge
∗(s.end()−1)='y';
push_heap(s.begin(),s.end()); // yxheefge
sort_heap(s.begin(),s.end()); // eeefghxy
reverse(s.begin(),s.end()); // yxhgfeee
理解 s 变化的方式是:用户只读取 s[0] 并只写入 s[x],其中 x 是堆当前末端的索引。堆通过与 s[x] 交换来删除元素(始终是 s[0])。
堆的目的是提供快速添加元素和快速访问具有最高值的元素的功能。堆的主要用途是实现优先级队列。
32.6.5 lexicographical_compare() (词典比较)
词典比较是我们在词典中对单词进行排序的规则。
词典比较 (§iso.25.4.8) | |
lexicographical_compare(b,e ,b2,e2) | [b:e)<[b2:e2) 吗? |
lexicographical_compare(b,e ,b2,e2,f) | [b:e)<[b2:e2) 吗?使用f进行元素的比较 |
我们可以像这样实现 lexicographical_compare(b,e,b2,e2):
template<class In, class In2>
bool lexicographical_compare(In first, In last, In2 first2, In2 last2)
{
for (; first!=last && first2!=last2; ++first,++last) {
if (∗first<∗first2)
return true; // [first:last)<[first2:last2)
if (∗first2<∗first)
return false; // [first2:last2)<[first:last)
}
return first==last && first2!=last2; // [first:last)<[first2:last2) if [first:last) is shorter
}
也就是说,字符串作为字符序列进行比较。例如:
string n1 {"10000"};
string n2 {"999"};
bool b1 = lexicographical_compare(n1.begin(),n1.end(),n2.begin(),n2.end()); // b1 == true
n1 = "Zebra";
n2 = "Aardvark";
bool b2 = lexicographical_compare(n1.begin(),n1.end(),n2.begin(),n2.end()); // b2 == false
32.7 最小值和最大值(Min and Max)
值比较在许多情况下都很有用:
min和max族 (§iso.25.4.7) | |
x=min(a,b) | x 是 a 和 b 中较小的一个 |
x=min(a,b,f) | x 是 a 和 b 中较小的一个,使用 f 进行比较 |
x=min({elem}) | x 是 {elem} 中的最小元素 |
x=min({elem},f) | x 是 {elem} 中最小的元素,使用 f 进行元素比较 |
x=max(a,b) | x 是 a 和 b 中较大的一个 |
x=max(a,b,f) | x 是 a 和 b 中较大的一个,使用 f 进行比较 |
x=max({elem}) | x 是 {elem} 中的最大元素 |
x=max({elem},f) | x 是 {elem} 中的最大元素,使用 f 进行元素比较 |
pair(x,y)=minmax(a,b) | x 是 min(a,b) 且 y 是 max(a,b) |
pair(x,y)=minmax(a,b,f) | x 是 min(a,b,f) 且 y 是 max(a,b,f) |
pair(x,y)=minmax({elem}) | x 为最小值({elem}),y 为最大值({elem}) |
pair(x,y)=minmax({elem},f) | x 是 min({elem},f) 且 y 是 max({elem},f) |
p=min_element(b,e) | p 指向 [b:e) 或 e 的最小元素 |
p=min_element(b,e,f) | p 指向 [b:e) 或 e 的最小元素,使用 f 进行元素比较 |
p=max_element(b,e) | p 指向 [b:e) 或 e 的最大元素 |
p=max_element(b,e,f) | p 指向 [b:e) 或 e 中的最大元素,使用 f 进行元素比较 |
pair(x,y)=minmax_element(b,e) | x 是 min_element(b,e) 并且 y 是 max_element(b,e) |
pair(x,y)=minmax_element(b,e,f) | x 是 min_element(b,e,f) 并且 y 是 max_element(b,e,f) |
如果我们比较两个左值,则结果是一个指向结果的引用;否则,返回一个右值。遗憾的是,接受左值的版本接受 const 左值,因此你永远无法修改这些函数的结果。例如:
int x = 7;
int y = 9;
++min(x,y); // min(x,y)的结果一个 const int&
++min({x,y}); // 错: t min({x,y}) 的结果一个右值 (一个initializer_list不可变)
_element 函数返回迭代器,minmax 函数返回数对,所以我们可以写出:
string s = "Large_Hadron_Collider";
auto p = minmax_element(s.begin(),s.end(),
[](char c1,char c2) { return toupper(c1)<toupper(c2); });
cout << "min==" << ∗(p.first) << ' ' << "max==" << ∗(p.second) << '\n';
使用我的机器上的 ASCII 字符集,这个小测试产生:
min==a max==_
32.8 建议(Advice)
[1] STL 算法对一个或多个序列进行操作;§32.2。
[2] 输入序列是半开的,由一对迭代器定义;§32.2。
[3] 搜索时,算法通常返回输入序列的末尾以指示“未找到”;§32.2。
[4] 优先使用精心指定的算法,而不是“随机代码”;§32.2。
[5] 编写循环时,考虑它是否可以表示为通用算法;§32.2。
[6] 确保一对迭代器参数确实指定了一个序列;§32.2。
[7] 当迭代器对样式变得繁琐时,引入容器/范围算法;§32.2。
[8] 使用谓词和其他函数对象赋予标准算法更广泛的含义;§32.3。
[9] 谓词不能修改其参数;§32.3。
[10] 指针上的默认 == 和 < 很少适用于标准算法;§32.3。
[11] 了解所用算法的复杂度,但请记住,复杂度度量只是性能的粗略指南;§32.3.1。
[12] 仅在没有更具体的算法时才使用 for_each() 和 transform();§32.4.1。
[13] 算法不会直接从其参数序列中添加或减去元素;§32.5.2,§32.5.3。
[14] 如果必须处理未初始化的对象,请考虑 uninitialized_∗ 算法;§32.5.6。
[15] STL 算法使用由其排序比较生成的相等性比较,而不是 ==;§32.6。
[16] 请注意,对 C 风格字符串进行排序和搜索需要用户提供字符串比较操作;§32.6。
内容来源:
<<The C++ Programming Language >> 第4版,作者 Bjarne Stroustrup