六、排序、合并、搜索和分区
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-0004-9_6) contains supplementary material, which is available to authorized users.
本章描述了与排序和合并范围松散相关的算法。其中有两组专门提供排序和合并功能。另一组提供了相对于给定元素值划分范围的机制。另外两个组提供了在一个范围内查找一个或多个元素的方法。在本章中,您将了解:
- 如何将随机访问迭代器定义的范围按升序或降序排序?
- 如何防止相等的元素在排序操作中被重新排序?
- 如何合并有序范围?
- 如何搜索一个无序的范围来找到一个或多个给定的元素。
- 划分一个范围意味着什么,以及如何使用 STL 提供的划分算法。
- 如何使用二分搜索法算法?
对范围进行排序
排序在许多应用程序中是必不可少的,许多 STL 算法只能处理有序的对象范围。默认情况下,algorithm
头中定义的sort<Iter>()
函数模板将一系列元素按升序排序,这意味着假定要排序的对象类型支持<
操作符。这些对象还必须是可交换的,这意味着必须能够使用在utility
头中定义的swap()
函数模板交换两个对象。这进一步意味着对象的类型必须实现一个移动构造函数和一个移动赋值操作符。sort()
函数模板类型参数Iter,
是范围内迭代器的类型,它们必须是随机访问迭代器。这意味着只有提供随机访问迭代器的容器中的元素才能被sort()
算法排序,这意味着只有array
、vector
或deque
容器中的元素或标准数组中的元素是可接受的。你在第二章中看到list
和forward_list
容器有sort()
功能成员;这些用于排序的特殊成员是必要的,因为list
只提供双向迭代器,而forward_list
只提供正向迭代器。
sort()
的模板类型参数将从函数调用参数中推导出来,这些参数将是定义要排序的对象范围的迭代器。当然,迭代器类型隐式定义了范围内对象的类型。这里有一个使用sort() algorithm:
的例子
std::vector<int> numbers {99, 77, 33, 66, 22, 11, 44, 88};
std::sort(std::begin(numbers), std::end(numbers));
std::copy(std::begin(numbers), std::end(numbers),
std::ostream_iterator<int> {std::cout, " "}); // Output: 11 22 33 44 66 77 88 99
sort()
调用将numbers
容器中的所有元素按升序排序,copy()
算法输出结果。您不必对容器中的所有内容进行排序。该语句对numbers
中的元素进行排序,不包括第一个和最后一个:
std::sort(++std::begin(numbers), --std::end(numbers));
要按降序排序,您需要将用于比较元素的函数对象作为第三个参数提供给sort()
:
std::sort(std::begin(numbers), std::end(numbers), std::greater<>());
比较函数必须返回一个bool
值,并有两个参数,要么是解引用迭代器产生的类型,要么是解引用迭代器可以隐式转换的类型。参数可以是不同的类型。只要比较函数满足要求,它可以是你喜欢的任何东西,包括一个 lambda 表达式。例如:
std::deque<string> words {"one", "two", "nine", "nine", "one", "three", "four", "five", "six"};
std::sort(std::begin(words), std::end(words),
[](const string& s1, const string& s2){ return s1.back() > s2.back(); });
std::copy(std::begin(words), std::end(words),
std::ostream_iterator<string> {std::cout, " "}); // six four two one nine nine one three five
这个语句序列对deque
容器words
中的string
元素进行排序,并输出结果。这里的比较函数是一个 lambda 表达式,它比较每个单词的最后一个字母来确定排序顺序。结果是元素按其最后一个字母的降序排列。
让我们用一个简单的工作示例来看看sort()
的运行,这个示例将从键盘读取Name
对象,按升序对它们进行排序,然后输出结果。Name
类将在Name.h
头中定义,它将包含以下代码:
#ifndef NAME_H
#define NAME_H
#include <string> // For string class
class Name
{
private:
std::string first{};
std::string second{};
public:
Name(const std::string& name1, const std::string& name2) : first(name1), second(name2){}
Name()=default;
std::string get_first() const {return first;}
std::string get_second() const { return second; }
friend std::istream& operator>>(std::istream& in, Name& name);
friend std::ostream& operator<<(std::ostream& out, const Name& name);
};
// Stream input for Name objects
inline std::istream& operator>>(std::istream& in, Name& name)
{
return in >> name.first >> name.second;
}
// Stream output for Name objects
inline std::ostream& operator<<(std::ostream& out, const Name& name)
{
return out << name.first << " " << name.second;
}
#endif
流插入和提取操作符为Name
对象定义为friend
函数。您可以将operator<()
定义为一个类成员,但是我省略了它,以显示将比较指定为sort()
的一个参数。程序是这样的:
// Ex6_01.cpp
// Sorting class objects
#include <iostream> // For standard streams
#include <string> // For string class
#include <vector> // For vector container
#include <iterator> // For stream and back insert iterators
#include <algorithm> // For sort() algorithm
#include "Name.h"
int main()
{
std::vector<Name> names;
std::cout << "Enter names as first name followed by second name. Enter Ctrl+Z to end:";
std::copy(std::istream_iterator<Name>(std::cin), std::istream_iterator<Name>(),
std::back_insert_iterator<std::vector<Name>>(names));
std::cout << names.size() << " names read. Sorting in ascending sequence...\n";
std::sort(std::begin(names), std::end(names), [](const Name& name1, const Name& name2)
{return name1.get_second() < name2.get_second(); });
std::cout << "\nThe names in ascending sequence are:\n";
std::copy(std::begin(names), std::end(names), std::ostream_iterator<Name>(std::cout, "\n"));
}
main()
中几乎所有的东西都是使用 STL 模板完成的。names
容器将存储从cin
中读取的名字。输入由copy()
算法执行,该算法使用istream_iterator<Name>
实例读取Name
对象。istream_iterator<Name>
的默认构造函数为流创建结束迭代器。copy()
函数使用由back_insert_iterator<Name>()
函数创建的back_inserter<Name>
迭代器将每个输入对象复制到names
。重载Name
类的流操作符允许流迭代器用于Name
对象的输入和输出。
对象的比较函数由 lambda 表达式定义,它是算法的第三个参数。如果要将operator<()
定义为Name
类的成员,可以省略这个参数。排序后的名称由copy()
算法写入标准输出流,该算法将前两个参数指定的元素范围复制到第三个参数ostream_iterator<Name>
对象。
以下是一些输出示例:
Enter names as first name followed by second name. Enter Ctrl+Z to end:
Jim Jones
Bill Jones
Jane Smith
John Doe
Janet Jones
Willy Schaferknaker
^Z
6 names read. Sorting in ascending sequence...
The names in ascending sequence are:
John Doe
Jim Jones
Bill Jones
Janet Jones
Willy Schaferknaker
Jane Smith
姓名的排序只考虑第二个姓名。当第二个名字相同时,您可以扩展 lambda 表达式来比较第一个名字。
你可能想知道为什么我没有在这个例子中使用pair<string,string>
对象来表示名字;这将比定义一个新类更简单。显然,这是可能的,但却不太清楚。
排序和相等元素的顺序
sort()
算法可能会改变相等元素的顺序,这有时不是您想要的。假设您有一个存储某种交易的容器,可能是银行账户。进一步假设您希望在处理交易之前按帐号对交易进行排序,以便可以按顺序更新帐户。如果 equal 元素出现的顺序反映了它们被添加到容器中的时间顺序,您将需要保留该顺序。允许给定账户的交易被重新安排可能导致账户看起来已经透支,而事实并非如此。
在这种情况下,stable_sort()
算法提供了您需要的东西。它对一个范围内的元素进行排序,并确保相等的元素保持其原始顺序。有两个版本;一个接受两个指定排序范围的迭代器参数,另一个接受一个用于比较的附加参数。您可以修改对Ex6_01.cpp
中的names
容器进行排序的语句,以查看stable_sort()
的工作情况:
std::stable_sort(std::begin(names), std::end(names),
[](const Name& name1, const Name& name2) { return name1.get_second() < name2.get_second(); });
当然,我为使用了sort()
的Ex6_01
展示的输出没有打乱相同元素的顺序,所以使用stable_sort()
不会改变相同输入的输出。不同之处在于,使用stable_sort()
可以保证相同元素的顺序不会改变,而使用sort()
算法则不是这种情况。当您想确定相同元素的顺序保持不变时,使用stable_sort()
。
部分排序
通过一个例子最容易理解部分排序是什么意思。假设您有一个容器,其中收集了几百万个值,但您只对其中最低的 100 个值感兴趣。您可以对容器的全部内容进行排序,并选择前 100 个,但这可能会有点耗时。您需要的是部分排序,在这种情况下,一个范围内的大量值中只有最低的n
按顺序排列。这里有一个特殊的算法,partial_sort()
算法,它期望三个参数是随机访问迭代器。如果功能参数为first
、second
和last
,则算法适用于范围[first,last)
内的元素。执行算法后,范围[first,second)
将包含范围[first,last)
中升序排列的最低second-first
元素。
Note
如果你以前没有遇到过,我用来表示范围的符号[first,last)
来源于数学,它定义了区间,区间定义了数字的范围。这两个值称为端点。在该符号中,方括号表示相邻端点包括在范围内,圆括号表示相邻端点不包括在内。例如,如果(2,5)
是一个整数区间,2 和 5 被排除在外,所以它只代表整数3,4
;这被称为开放区间,因为两个端点都不包括在内。区间[2,5)
包括 2 但不包括 5,所以它代表2,3,4
。(2,5)代表 3,4,5。[2,5]代表 2,3,4,5,被称为封闭区间,因为两端点都包括在内。当然,first
和last
是迭代器,first,last)
表示first
指向的被包含,而last
指向的不被包含——所以它在 C++ 中精确地表达了一个范围。
这里有一些代码展示了partial_sort()
算法是如何工作的:
size_t count {5}; // Number of elements to be sorted
std::vector<int> numbers {22, 7, 93, 45, 19, 56, 88, 12, 8, 7, 15, 10};
std::partial_sort(std::begin(numbers), std::begin(numbers) + count, std::end(numbers));
执行上述partial_sort()
的效果如图 [6-1 所示。
图 6-1。
Operation of the partial_sort()
algorithm
最低的count
元素按顺序排列。在范围first,second)
中,second
指向的元素不包含在内,因为second
是最后一个迭代器。图 [6-1 显示了在我的系统上执行语句的结果;在您的系统上可能有所不同。请注意,没有排序的元素的原始顺序没有保持。执行partial_sort()
后这些元素的顺序是不确定的,取决于您的实现。
如果您希望partial_sort()
算法使用不同于<
操作符的比较,您可以提供一个函数对象作为附加参数。例如:
std::partial_sort(std::begin(numbers), std::begin(numbers) + count, std::end(numbers),
std::greater<>());
现在,numbers
中最大的count
元素将在容器的开头按降序排列。在我的系统上,执行这条语句的结果是:
93 88 56 45 22 7 19 12 8 7 15 10
同样,numbers
中未排序的元素的原始顺序也不会保留。
除了将排序后的元素复制到另一个容器中的不同区域之外,partial_sort_copy()
算法与partial_sort()
基本相同。前两个参数是迭代器,指定部分排序要应用的范围;第三个和第四个参数是迭代器,用于标识存储结果的范围。目标范围中的元素数量决定了输入范围中将要排序的元素数量。这里有一个例子:
std::vector<int> numbers {22, 7, 93, 45, 19, 56, 88, 12, 8, 7, 15, 10};
size_t count {5}; // Number of elements to be sorted
std::vector<int> result(count); // Destination for the results - count elements
std::partial_sort_copy(std::begin(numbers), std::end(numbers), std::begin(result), std::end(result));
std::copy(std::begin(numbers), std::end(numbers), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl;
std::copy(std::begin(result), std::end(result), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl;
这些语句实现了numbers
容器的部分排序。这个想法是将来自numbers
的最底层的count
元素按顺序放置,并将它们存储在result
容器中。您指定为目的地的范围必须存在,因此在目的地容器result
中必须至少有count
个元素,在本例中,我们分配的正好是所需的数量。执行这些语句的输出是:
22 7 93 45 19 56 88 12 8 7 15 10
7 7 8 10 12
你可以看到numbers
中的元素序列没有被打乱,result
包含了从numbers
开始按升序排列的最低count
元素的副本。
当然,您可以通过额外的参数指定不同的比较函数:
std::partial_sort_copy(std::begin(numbers), std::end(numbers), std::begin(result), std::end(result),
std::greater<>());
将greater<>
的一个实例指定为函数对象将导致最大的count
元素按降序被复制到result
。如果该语句后面跟有前面代码片段的输出语句,输出将是:
22 7 93 45 19 56 88 12 8 7 15 10
93 88 56 45 22
和以前一样,原始容器中的元素顺序是不受干扰的。
nth_element()
算法不同于partial_sort()
。它适用的范围由函数的第一个和第三个参数定义,第二个参数是一个指向第n
个元素的迭代器。执行nth_element()
将导致第 n 个元素被设置为如果该范围被完全排序时应该存在的元素。范围中第n
个元素之前的所有元素都将小于第n
个元素,之后的所有元素都将大于第n
个元素。默认情况下,该算法使用<
运算符来产生结果。下面是一些要练习的代码nth_element()
:
std::vector<int> numbers {22, 7, 93, 45, 19, 56, 88, 12, 8, 7, 15, 10};
size_t count {5}; // Index of nth element
std::nth_element(std::begin(numbers), std::begin(numbers) + count, std::end(numbers));
第 n 个元素是numbers
容器中的第 6 个,对应的是numbers[5]
。图 6-2 说明了这是如何工作的。
图 6-2。
Operation of the nth_element()
algorithm
第n
个元素之前的元素将少于第n
个元素,但不一定是有序的。同样,第n
个元素之后的元素会比它大,但不一定是有序的。如果第二个参数与第三个参数相同(范围的结尾),则该算法无效。
与本章前面的算法一样,您可以提供一个函数对象,将比较定义为第四个参数:
std::nth_element(std::begin(numbers), std::begin(numbers) + count, std::end(numbers),
std::greater<>());
这使用了>
操作符来比较元素,所以如果元素是降序的,那么第n
个元素将是它应该是的。第n
个元素前面的元素会更大,后面的元素会更小。使用前面numbers
容器中的初始值,结果将是:
45 56 93 88 22 19 10 12 15 7 8 7
在您的系统中,第 n 个元素两边的元素顺序可能不同,但是左边的应该比它大,右边的应该比它小。
排序范围的测试
排序非常耗时,尤其是当您有大量元素时。测试一个范围是否已经排序可以避免不必要的排序操作。如果由两个迭代器参数指定的范围内的元素是升序,那么is_sorted()
函数模板返回true
。迭代器必须至少是前向迭代器,以允许元素被顺序处理。提醒你一下——前向迭代器支持前缀和后缀增量操作。这里有一个使用is_sorted()
的例子:
std::vector<int> numbers {22, 7, 93, 45, 19};
std::vector<double> data {1.5, 2.5, 3.5, 4.5};
std::cout << "numbers is "
<< (std::is_sorted(std::begin(numbers), std::end(numbers)) ? "": "not ")
<< "in ascending sequence.\n";
std::cout << "data is "
<< (std::is_sorted(std::begin(data), std::end(data)) ? "": "not ")
<< "in ascending sequence." << std::endl;
使用的默认比较是<
操作符。输出将显示numbers
不在升序中,而data
在。有一个版本允许你提供一个函数对象来比较元素:
std::cout << "data reversed is "
<< (std::is_sorted(std::rbegin(data), std::rend(data), std::greater<>()) ? "": "not ")
<< "in descending sequence." << std::endl;
该语句的输出将表明data
中逆序的元素按降序排列。
您还可以使用is_sorted_until()
函数模板来确定某个范围内的元素的顺序。参数是定义测试范围的迭代器。这个函数返回一个迭代器,这个迭代器是这个范围中按升序排列的元素的上限。这里有一个例子:
std::vector<string> pets {"cat", "chicken", "dog", "pig", "llama", "coati", "goat"};
std::cout << "The pets in ascending sequence are:\n";
std::copy(std::begin(pets), std::is_sorted_until(std::begin(pets), std::end(pets)),
std::ostream_iterator<string>{std::cout, " "});
copy()
算法的前两个参数是pets
容器的 begin 迭代器和is_sorted_until()
应用于 pets 中所有元素时返回的迭代器。is_sorted_until()
算法将返回pets
中升序排列的元素的上限——这将是一个迭代器,指向小于其前任的第一个元素,或者是序列的结束迭代器(如果排序的话)。该代码的输出将是:
The pets in ascending sequence are:
cat chicken dog pig
"llama"
是小于其前身的第一个元素,因此"pig"
是升序序列中的最后一个元素。
您可以选择提供一个函数对象来比较元素:
std::vector<string> pets {"dog", "coati", "cat", "chicken", "pig", "llama", "goat"};
std::cout << "The pets in descending sequence are:\n";
std::copy(std::begin(pets),
std::is_sorted_until(std::begin(pets), std::end(pets), std::greater<>()),
std::ostream_iterator<string>{std::cout, " "});
这次我们寻找一个降序的元素序列,因为string
类的operator>()
成员将用于比较元素。输出将是:
The pets in descending sequence are:
dog coati cat
"chicken"
是第一个大于其前任的元素,所以由is_sorted_until()
返回的迭代器将指向这个元素。因此"cat"
是降序排列的最后一个元素。
合并范围
合并操作将两个范围中以相同方式排序的元素组合在一起——要么都是升序,要么都是降序。结果是包含来自两个输入范围的元素副本的范围,其排序方式与原始范围相同。图 6-3 说明了这是如何工作的。
图 6-3。
Merging elements from two vector
containers
merge()
算法合并两个范围,并将结果存储在第三个范围中。它使用<
操作符来比较元素。图 6-3 显示了应用于these
和those
容器内容的合并操作,其中结果范围存储在both
容器中。merge()
算法需要五个参数。前两个是迭代器,指定第一个输入范围—these
,在本例中,后两个是迭代器,标识第二个输入范围—those
,最后一个参数是迭代器,标识第一个合并的元素范围应该放在哪里—both
容器。标识要合并的范围的迭代器至少只需要是输入迭代器,用于保存结果的目标范围的迭代器只需要是输出迭代器。
merge()
算法没有关于合并元素范围的容器的信息,所以它不能创建元素——它只能使用您作为第五个参数提供的迭代器来存储元素。因此,示例中目标范围内的元素必须已经存在。这在图 6-3 中通过创建both
容器来确保,容器中的元素数量指定为每个输入容器的元素总数。目标区域可以在任何地方,甚至可以在一个源区域容器中,但是源区域和目标区域不能重叠。如果他们这样做,后果是不确定的,但你可以肯定的是,效果不会好。当然,如果通过插入迭代器指定目的地,元素将被自动创建。
merge()
算法返回一个迭代器,该迭代器指向合并范围中最后一个元素之后的一个元素,因此您可以通过函数调用中使用的第五个迭代器参数加上函数返回的迭代器来标识合并范围。
当比较需要不同于<
操作符时,您可以提供一个函数对象作为第六个参数。例如:
std::vector<int> these {2, 15, 4, 11, 6, 7}; // 1st input to merge
std::vector<int> those {5, 2, 3, 2, 14, 11, 6}; // 2nd input to merge
std::stable_sort(std::begin(these), std::end(these), // Sort 1st range in...
std::greater<>()); // ...descending sequence
std::stable_sort(std::begin(those), std::end(those), // Sort 2nd range
std::greater<>());
std::vector<int> result(these.size() + those.size() + 10); // Plenty of room for results
auto end_iter = std::merge(std::begin(these), std::end(these), // Merge 1st range...
std::begin(those), std::end(those), // ...and 2nd range...
std::begin(result), std::greater<>()); // ...into result
std::copy(std::begin(result), end_iter, std::ostream_iterator<int>{std::cout, " "});
这个语句序列首先使用stable_sort()
将两个vector
容器的内容按降序排序,这保证了 equal 元素的原始顺序将被保持。合并操作将两个容器的内容合并到第三个容器result
中,这个容器比需要的多创建了 10 个元素——只是为了演示merge()
返回的迭代器的使用。copy()
算法复制由result
的 begin 迭代器和merge()
返回的end_iter
迭代器指定的范围到输出流迭代器。输出将是:
15 14 11 11 7 6 6 5 4 3 2 2 2
inplace_merge()
算法就地合并相同范围内的两个连续排序的元素序列。有三个参数,first
、second
和last
是双向迭代器。第一个输入序列的范围是first,second)
,第二个输入序列的范围是[second,last)
,因此second
指向的元素在第二个输入范围内。结果将是范围[first,last)
。图 [6-4 显示了该操作。
图 6-4。
inplace_merge()
operation
图 6-4 中的data
容器包含两个范围,都是升序排列。inplace_merge()
操作将这些组合起来,在同一个容器中产生一个升序范围。
我们可以将你在本章中看到的几个算法合并成一个单一的工作示例。这个有些做作的示例将处理从键盘输入的信贷和借记交易,并将它们应用到根据需要创建的一组帐户。我们将始终创建零余额账户。交易将是一个包含账号、金额以及金额是贷方还是借方的对象。为不存在的账户处理交易将导致该账户被创建。帐户对象将包含标识唯一帐号、所有者姓名和当前余额的成员。账户持有人的名字将是一个包含名字和第二个名字的pair
对象。帐号将是一个无符号整数。贷记将被表示为一个bool
值,账户余额和要借记或贷记的金额将属于double
类型。
Transaction
类型将在Transaction.h
头文件中定义如下:
#ifndef TRANSACTION_H
#define TRANSACTION_H
#include <iostream> // For stream class
#include <iomanip> // For stream manipulators
#include "Account.h"
class Transaction
{
private:
size_t account_number {}; // The account number
double amount {}; // The amount
bool credit {true}; // credit = true debit=false
public:
Transaction()=default;
Transaction(size_t number, double amnt, bool cr) : account_number {number}, amount {amnt},
credit {cr}{}
size_t get_acc_number() const { return account_number; }
// Less-than operator - compares account numbers
bool operator<(const Transaction& transaction) const { return account_number < transaction.account_number; }
// Greater-than operator - compares account numbers
bool operator>(const Transaction& transaction) const { return account_number > transaction.account_number; }
friend std::ostream& operator<<(std::ostream& out, const Transaction& transaction);
friend std::istream& operator>>(std::istream& in, Transaction& transaction);
// Making the Account class a friend allows Account objects
// to access private members of Transaction objects
friend class Account;
};
// Stream insertion operator for Transaction objects
std::ostream& operator<<(std::ostream& out, const Transaction& transaction)
{
return out << std::right << std::setfill('0') << std::setw(5) << transaction.account_number
<< std::setfill(' ') << std::setw(8) << std::fixed << std::setprecision(2) << transaction.amount
<< (transaction.credit ? " CR" : " DR");
}
// Stream extraction operator for Transaction objects
std::istream& operator>>(std::istream& in, Transaction& tr)
{
if((in >> std::skipws >> tr.account_number).eof())
return in;
return in >> tr.amount >> std::boolalpha >> tr.credit;
}
#endif
默认构造函数通常是允许在容器中创建默认元素所必需的。在类中同时包含<
和>
操作符允许对Transaction
对象进行升序或降序排序,尽管这个例子不会同时使用这两个选项。我们接下来要讨论的Account
类是Transaction
类的friend
,因此Account
类的函数成员可以访问传递给它的Transaction
对象的private
数据成员。定义了重载的流输入和输出操作符后,我们将能够结合 STL 提供的流迭代器使用copy()
算法来读写Transaction
对象。
Account
类将在Account.h
头中定义:
#ifndef ACCCOUNT_H
#define ACCCOUNT_H
#include <iostream> // For stream class
#include <iomanip> // For stream manipulators
#include <string> // For string class
#include <utility> // For pair template type
#include "Transaction.h"
using first_name = std::string;
using second_name = std::string;
using Name = std::pair<first_name, second_Name>;
class Account
{
private:
size_t account_number {}; // 5-digit account number
Name name {"", ""}; // A pair containing 1st & 2nd names
double balance {}; // The account balance - negative when overdrawn
public:
Account()=default;
Account(size_t number, const Name& nm) : account_number {number}, name {nm}{}
double get_balance() const { return balance; }
void set_balance(double bal) { balance = bal; }
size_t get_acc_number() const {return account_number;}
const Name& get_name() const { return name; }
// Apply a transaction to the account
bool apply_transaction(const Transaction& transaction)
{
if(transaction.credit) // For a credit...
balance += transaction.amount; // ...add the mount
else // For a debit...
balance -= transaction.amount; // ...subtract the amount
return balance < 0.0; // Return true when overdrawn
}
// Less-than operator - compares by account number
bool operator<(const Account& acc) const { return account_number < acc.account_number; }
friend std::ostream& operator<<(std::ostream& out, const Account& account);
};
// Stream insertion operator for Account objects
std::ostream& operator<<(std::ostream& out, const Account& acc)
{
return out << std::left << std::setw(20) << acc.name.first + " " + acc.name.second
<< std::right << std::setfill('0') << std::setw(5) << acc.account_number
<< std::setfill(' ') << std::setw(8) << std::fixed << std::setprecision(2) << acc.balance;
}
#endif
除了帐号之外,该类还有一个Name
成员来标识帐户的所有者。一个Name
只是一个pair<string,string>
类型的别名,first_name
和second_name
别名仅仅是为了标识每个pair
成员的重要性。类型别名通常有助于将特定于应用程序的含义赋予一般类型。
为Account
对象重载流插入操作符允许使用<<
将对象写入输出流。operator<()
成员定义允许Account
对象在排序或存储到有序容器中时按账号排序。如果你想让Account
对象以不同的方式排序——比如通过名字,你可以定义一个提供比较功能的函数对象。该示例将按名称对Account
对象进行排序,启用该功能的函数对象将在Compare_Names.h
中定义,如下所示:
#ifndef COMPARE_NAMES_H
#define COMPARE_NAMES_H
#include "Account.h"
// Order Account objects in ascending sequence by Name
class Compare_Names
{
public:
bool operator()(const Account& acc1, const Account& acc2)
{
const auto& name1 = acc1.get_name();
const auto& name2 = acc2.get_name();
return (name1.second < name2.second) ||
((name1.second == name2.second) && (name1.first < name2.first));
}
};
#endif
你应该不难理解这是如何工作的。函数调用操作符定义比较两个Account
对象的Name
成员,首先通过名字,其次通过名字。
使用我们定义的类的main()
程序将在Ex6_02.cpp
中:
// Ex6_02.cpp
// Sorting and inplace merging
#include <iostream> // For standard streams
#include <string> // For string class
#include <algorithm> // For sort(), inplace_merge()
#include <functional> // For greater<T>
#include <vector> // For vector container
#include <utility> // For pair template type
#include <map> // For map container
#include <iterator> // For stream and back insert iterators
#include "Account.h"
#include "Transaction.h"
#include "Compare_Names.h"
using std::string;
using first_name = string;
using second_name = string;
using Name = std::pair<first_name, second_Name>;
using Account_Number = size_t;
// Read the name of an account holder
Name get_holder_name(Account_Number number)
{
std::cout << "Enter the holder’s first and second names for account number " << number << ": ";
string first {};
string second {};
std::cin >> first >> second;
return std::make_pair(first, second);
}
int main()
{
std::vector<Transaction> transactions;
std::cout << "Enter each transaction as:\n"
<< " 5 digit account number amount credit(true or false).\n"
<< "Enter Ctrl+Z to end.\n";
// Read 1st set of transactions
std::copy(std::istream_iterator<Transaction> {std::cin}, std::istream_iterator<Transaction> {},
std::back_inserter(transactions));
std::cin.clear(); // Clear the EOF flag for the stream
// Sort 1st set``in
std::stable_sort(std::begin(transactions), std::end(transactions), std::greater<>());
// List the transactions
std::cout << "First set of transactions after sorting...\n";
std::copy(std::begin(transactions), std::end(transactions), std::ostream_iterator<Transaction>{std::cout, "\n"});
// Read 2nd set of transactions
std::cout << "\nEnter more transactions:\n";
std::copy(std::istream_iterator<Transaction> {std::cin}, std::istream_iterator<Transaction> {},
std::back_inserter(transactions));
std::cin.clear(); // Clear the EOF flag for the stream
// List the transactions
std::cout << "\nSorted first set of transactions with second set appended...\n";
std::copy(std::begin(transactions), std::end(transactions), std::ostream_iterator<Transaction>{std::cout, "\n"});
// Sort second set into descending account sequence
auto iter = std::is_sorted_until(std::begin(transactions), std::end(transactions),
std::greater<>());
std::stable_sort(iter, std::end(transactions), std::greater<>());
// List the transactions
std::cout << "\nSorted first set of transactions with sorted second set appended...\n";
std::copy(std::begin(transactions), std::end(transactions), std::ostream_iterator<Transaction>{std::cout, "\n"});
// Merge transactions in place
std::inplace_merge(std::begin(transactions), iter, std::end(transactions), std::greater<>());
// List the transactions
std::cout << "\nMerged sets of transactions...\n";
std::copy(std::begin(transactions), std::end(transactions), std::ostream_iterator<Transaction>{std::cout, "\n"});
// Process transactions creating Account objects when necessary
std::map<Account_Number, Account> accounts;
for(const auto& tr : transactions)
{
Account_Number number = tr.get_acc_number();
auto iter = accounts.find(number);
if(iter == std::end(accounts))
iter = accounts.emplace(number, Account {number, get_holder_name(number)}).first;
if(iter->second.apply_transaction(tr))
{
auto name = iter->second.get_name();
std::cout << "\nAccount number " << number
<< " for " << name.first << " " <<name.second << " is overdrawn!\n"
<< "The concept is that you bank with us - not the other way round, so fix it!\n"
<< std::endl;
}
}
// Copy accounts to a vector container
std::vector<Account> accs;
for(const auto& pr :accounts)
accs.push_back(pr.second);
// List accounts after sorting in name sequence
std::stable_sort(std::begin(accs), std::end(accs), Compare_Names());
std::copy(std::begin(accs), std::end(accs), std::ostream_iterator < Account > {std::cout, "\n"});
}
get_holder_name()
是一个助手函数,它从cin
中读取给定账号的名称。这是在为一个给定的账号处理一个交易并且没有Account
对象时使用的。返回的Name
对象将用于创建Account
对象。
事务作为Transaction
对象被读取并存储在vector<Transaction>
容器transactions
中。代码读取一个使用stable_sort()
按降序排序的事务序列。然后,第二个事务序列被读入同一个容器,并以同样的方式进行排序。通过设法创建一个包含两个排序的事务序列的vector
,我们可以利用inplace_merge()
创建两个序列的有序组合。
下面是针对五个帐户的七笔交易的输出示例。我选择事务数量来演示排序和合并操作的行为方式。
Enter each transaction as:
5 digit account number amount credit(true or false).
Enter Ctrl+Z to end.
12345 40 true
12344 50 true
12346 75.5 true
^Z
First set of transactions after sorting...
12346 75.50 CR
12345 40.00 CR
12344 50.00 CR
Enter more transactions:
12344 25.25 true
12345 75 false
12345 100 true
12346 100 true
^Z
Sorted first set of transactions with second set appended...
12346 75.50 CR
12345 40.00 CR
12344 50.00 CR
12344 25.25 CR
12345 75.00 DR
12345 100.00 CR
12346 100.00 CR
Sorted first set of transactions with sorted second set appended...
12346 75.50 CR
12345 40.00 CR
12344 50.00 CR
12344 25.25 CR
12346 100.00 CR
12345 75.00 DR
12345 100.00 CR
Merged sets of transactions...
12346 75.50 CR
12346 100.00 CR
12345 40.00 CR
12345 75.00 DR
12345 100.00 CR
12344 50.00 CR
12344 25.25 CR
Enter the holder’s first and second names for account number 12346: Stan Dupp
Enter the holder’s first and second names for account number 12345: Ann Ounce
Account number 12345 for Ann Ounce is overdrawn!
The concept is that you bank with us - not the other way round, so fix it!
Enter the holder’s first and second names for account number 12344: Dan Druff
Dan Druff 12344 75.25
Stan Dupp 12346 175.50
Ann Ounce 12345 65.00
在每个阶段都列出了Transaction
对象的序列,所以你可以看到stable_sort()
和inplace_merge()
算法像我描述的那样工作。特别是,等价交易的顺序是保持不变的,所以借项和贷项是按照它们产生的顺序来应用的。最后,帐户按名称顺序列出,以表明交易已被正确应用。这是通过将map
容器中的Account
对象复制到vector<Account>
容器中,并将stable_sort()
算法应用于vector
中的元素,其中Compare_Names
函数对象提供比较。您可以将Account
对象复制到一个set<Account, Compare_Names>
容器而不是vector<Account>
容器中,让对象自动排序,但是这样您就会错过使用stable_sort()
的机会。
搜索范围
STL 提供了多种多样的算法,用于以各种方式搜索一系列对象。这些方法大多处理无序序列。但是有些,我稍后会讲到,需要对序列进行排序。
查找范围中的元素
有三种算法可以在两个输入迭代器定义的范围内找到单个对象。
find()
算法在由前两个参数指定的范围内找到第一个等于第三个参数的对象。find_if()
算法在前两个参数指定的范围内找到第一个对象,第三个参数指定的谓词为该对象返回true
。谓词不能修改传递给它的对象。find_if_not()
算法在前两个参数指定的范围内找到第一个对象,第三个参数指定的谓词为该对象返回false
。谓词不能修改传递给它的对象。
每个算法返回一个指向找到的对象的迭代器,如果没有找到对象,则返回该范围的结束迭代器。以下是如何使用find()
的示例:
std::vector<int> numbers {5, 46, -5, -6, 23, 17, 5, 9, 6, 5};
int value {23};
auto iter = std::find(std::begin(numbers), std::end(numbers), value);
if(iter != std::end(numbers)) std::cout << value << " was found.\n";
这段代码将输出消息,表明在numbers
向量中确实找到了23
。当然,您可以重复使用find()
来查找给定元素在一个范围内的所有出现:
size_t count {};
int five {5};
auto start_iter = std::begin(numbers);
auto end_iter = std::end(numbers);
while((start_iter = std::find(start_iter, end_iter, five)) != end_iter)
{
++count;
++start_iter;
}
std::cout << five << " was found " << count << " times." << std::endl; // 3 times
在while
循环中递增的count
变量计算在numbers
向量中找到five
的次数。循环表达式调用find()
在start_iter
和end_iter
定义的范围内寻找five
。find()
返回的迭代器存储在start_iter
中,覆盖变量之前的值。最初,搜索的范围是numbers
中的所有元素,因此find()
将返回一个迭代器,指向第一次出现的five
。每次找到five
时,start_iter
在循环中递增,因此它将指向所找到的元素之后的元素。因此,下一次迭代将从该点搜索到序列的末尾。当five
不再存在时,find()
将返回end_iter
,循环结束。
您可以使用find_if()
来查找numbers
中大于value
的第一个元素,如下所示:
int value {5};
auto iter1 = std::find_if(std::begin(numbers), std::end(numbers), value { return n > value; });
if(iter1 != std::end(numbers)) std::cout << *iter1 << " was found greater than " << value << ".\n";
find_if()
的第三个参数是一个由 lambda 表达式定义的谓词。lambda 表达式通过值捕获value
,并在 lambda 的参数大于value
时返回true
。这个片段将找到值为46
的元素。您可以在一个循环中使用find_if()
来查找所有大于value
的数字,就像前面的代码片段一样。
您可以使用find_if_not()
算法来查找谓词为false
的元素,如下所示:
size_t count {};
int five {5};
auto start_iter = std::begin(numbers);
auto end_iter = std::end(numbers);
while((start_iter = std::find_if_not(start_iter, end_iter,
five {return n > five; })) != end_iter)
{
++count;
++start_iter;
}
std::cout << count << " elements were found that are not greater than "<< five << std::endl;
作为find_if_not()
的第三个参数的谓词是一个 lambda 表达式,类似于我之前在find_if()
算法中使用的表达式。只有当一个元素大于five
时,才会返回true
。当谓词返回false
时找到一个元素,因此操作实际上是找到小于或等于five
的元素。将找到与该范围中的值5
、-5
、-6
、5
和5
相对应的五个元素。
在一个范围中查找一系列元素中的任何一个
find_first_of()
算法在一个范围中搜索第二个范围中任何元素的第一个匹配项。要搜索的范围可以仅由输入迭代器指定,但是标识所搜索内容的范围必须至少是前向迭代器。来自两个范围的元素使用==
操作符进行比较,所以如果范围指定了一个类类型的对象,这个类必须实现operator==()
。这里有一个使用find_first_of()
的例子:
string text {"The world of searching"};
string vowels {"aeiou"};
auto iter = std::find_first_of(std::begin(text), std::end(text), std::begin(vowels), std::end(vowels));
if(iter != std::end(text)) std::cout << "We found '" << *iter << "'." << std::endl; // We found 'e'.
这段代码在text
中搜索第一次出现的vowels
中的任何字符。指向"The"
中第三个字母的迭代器在这个实例中被返回。您可以使用一个循环来查找来自vowels
的任何字符在text
中的所有出现:
string found {}; // Records characters that are found
for(auto iter = std::begin(text);
(iter = std::find_first_of(
iter, std::end(text), std::begin(vowels), std::end(vowels))) != std::end(text); )
found += *(iter++);
std::cout << "The characters \"" << found << "\" were found in text." << std::endl;
这使用了一个for
循环——只是为了证明你可以。第一个循环控制表达式用初始值定义了iter
,作为text
的开始迭代器。第二个控制表达式调用find_first_of()
在范围[iter, std::end(text))
中搜索从vowels
开始的任何字符的第一次出现。find_first_of()
返回的迭代器存储在iter
中,然后与text
的结束迭代器进行比较。如果iter
现在是text
的结束迭代器,循环结束。如果iter
不是text
的结束迭代器,则循环体执行将iter
指向的字符追加到found
字符串,并递增iter
指向下一个字符。该字符将用作下一次搜索范围的起始位置。这个片段产生的输出将是:
The characters "eooeai" were found in text.
另一个版本的find_first_of()
使您能够从第二个范围中搜索任何元素的第一次出现,其中由第五个参数指定的二元谓词返回true
。范围中的元素不需要属于同一类型。当要比较的元素不支持==
操作符时,您可以使用这个版本的算法来定义相等比较,但是您也可以以其他方式使用它。例如:
std::vector<long> numbers {64L, 46L, -65L, -128L, 121L, 17L, 35L, 9L, 91L, 5L};
int factors[] {7, 11, 13};
auto iter = std::find_first_of(std::begin(numbers), std::end(numbers), // The range to be searched
std::begin(factors), std::end(factors), // Elements sought
[](long v, long d) { return v % d == 0;}); // Predicate - true for a match
if(iter != std::end(numbers)) std::cout << *iter << " was found." << std::endl;
谓词是一个 lambda 表达式,当第一个参数被第二个参数整除时,它返回true
。因此这段代码找到了-65
,因为这是numbers
中第一个能被factors
数组中的一个元素整除的元素,在本例中是13
。谓词中的参数类型可以不同于范围中的元素类型,只要每个范围中的元素可以隐式转换为相应的参数类型。这里,factors
数组中的元素被隐式转换为类型long
。
当然,您可以使用循环来查找谓词返回true
的所有元素:
std::vector<long> numbers {64L, 46L, -65L, -128L, 121L, 17L, 35L, 9L, 91L, 5L};
int factors[] {7, 11, 13};
std::vector<long> results; // Stores elements found
auto iter = std::begin(numbers);
while((iter = std::find_first_of(iter, std::end(numbers), // Range searched
std::begin(factors), std::end(factors), // Elements sought
[](long v, long d) { return v % d == 0; })) // Predicate
!= std::end(numbers))
results.push_back(*iter++);
std::cout << results.size() << " values were found:\n";
std::copy(std::begin(results), std::end(results), std::ostream_iterator < long > {std::cout, " " });
std::cout << std::endl;
这个代码片段在numbers
中找到所有将来自factors
的元素作为因子的元素。只要find_first_of()
返回的迭代器不是numbers
的结束迭代器,while
循环就会继续。iter
变量开始指向numbers
中的第一个元素,指向找到的元素的迭代器存储在iter
中,覆盖先前的值。在循环体中,iter
指向的元素存储在results
容器中,然后iter
递增指向后面的元素。当循环结束时,results
包含所有找到的元素,并使用copy()
算法输出。
从一个范围中查找多个元素
adjacent_find()
算法在一个范围内搜索两个相同的连续元素。使用==
操作符比较连续的元素对,并返回指向前两个相等元素中第一个的迭代器。如果没有相等的元素对,该算法返回该范围的结束迭代器。例如:
string saying {"Children should be seen and not heard."};
auto iter = std::adjacent_find(std::begin(saying), std::end(saying));
if(iter != std::end(saying))
std::cout << "In the following text:\n\"" << saying << "\"\n'"
<< *iter << "' is repeated starting at index position "
<< std::distance(std::begin(saying), iter) << std::endl;
这将在saying
字符串中搜索前两个相同的连续字符,因此代码将产生以下输出:
In the following text:
"Children should be seen and not heard."
'e' is repeated starting at index position 20
第二个版本的adjacent_find()
算法允许您提供一个应用于连续元素的谓词。下面是如何使用它来查找一个范围内的第一对连续的奇数整数:
std::vector<long> numbers {64L, 46L, -65L, -128L, 121L, 17L, 35L, 9L, 91L, 5L};
auto iter = std::adjacent_find(std::begin(numbers), std::end(numbers),
[](long n1, long n2){ return n1 % 2 && n2 % 2; });
if(iter != std::end(numbers))
std::cout << "The first pair of odd numbers is "
<< *iter << " and " << *(iter+1) << std::endl;
当两个参数都不是 2 的倍数时,lambda 表达式返回true
,因此这段代码将找到数字121
和17
。
find end()算法
find_end()
算法查找第二个元素范围中的最后一个匹配项。您可以将此想象为在任何类型的元素序列中查找子序列的最后一个匹配项。该算法返回一个指向子序列最后一次出现的第一个元素的迭代器,或者是正在搜索的范围的结束迭代器。这里有一个使用它的例子:
string text {"Smith, where Jones had had \"had\", had had \"had had\"."
" \"Had had\" had had the examiners\' approval."};
std::cout << text << std::endl;
string phrase {"had had"};
auto iter = std::find_end(std::begin(text), std::end(text), std::begin(phrase), std::end(phrase));
if(iter != std::end(text))
std::cout << "The last \"" << phrase
<< "\" was found at index " << std::distance(std::begin(text), iter) << std::endl;
这将在text
中搜索最后一次出现的"had had"
,并产生以下输出:
Smith, where Jones had had "had", had had "had had". "Had had" had had the examiners' approval.
The last "had had" was found at index 63
您可以在text
中搜索所有出现的phrase
。这个例子简单地计算了出现的次数:
size_t count {};
auto iter = std::end(text);
auto end_iter = iter;
while((iter = std::find_end(std::begin(text), end_iter, std::begin(phrase),
std::end(phrase))) != end_iter)
{
++count;
end_iter = iter;
}
std::cout << "\n\""<< phrase << "\" was found " << count << " times." << std::endl;
while
循环表达式执行搜索。循环表达式在范围std::begin(text), end_iter)
中搜索phrase
,搜索到的第一个范围是text
中的所有元素。为了帮助澄清这里发生了什么,这个过程如图 [6-5 所示。
图 6-5。
Searching repeatedly with find_end()
由find_end()
返回的迭代器存储在iter
中,当它等于end_iter
—iter
的前一个值时——循环结束。因为find_end()
找到子序列的最后一个出现,所以下一次要搜索的范围的结束迭代器(end_iter
)必须改为算法返回的迭代器。这指向已找到的序列的第一个字符,因此下一次搜索将从text
的开头到这一点,忽略已找到的序列。在循环体内递增count
后,end_iter
被设置为iter
。这是必要的,因为如果没有找到phrase
,下一次搜索将返回这个迭代器。
第二个版本的find_end()
接受一个二元谓词作为第五个参数,用于比较元素。您可以用它来重复前面的搜索忽略情况:
size_t count {};
auto iter = std::end(text);
auto end_iter = iter;
while((iter = std::find_end(std::begin(text), end_iter, std::begin(phrase), std::end(phrase),
[](char ch1, char ch2){ return std::toupper(ch1) == std::toupper(ch2); })) != end_iter)
{
++count;
end_iter = iter;
}
现在,来自两个范围的字符对将在转换成大写字母后进行比较。将在text
中找到phrase
的五个实例,因为将发现"Had had"
等于phrase
。
search()算法
search()
算法与find_end()
相似,它在序列中寻找一个子序列,但它寻找的是第一个而不是最后一个。和find_end()
算法一样,有两个版本——第二个版本接受第五个参数,该参数是用于比较元素的谓词。您可以使用search()
算法通过find_end()
执行之前的搜索。主要的区别在于如何在每次迭代中改变要搜索的范围的规格。代码如下:
string text {"Smith, where Jones had had \"had\", had had \"had had\"."
" \"Had had\" had had the examiners\' approval."};
std::cout << text << std::endl;
string phrase {"had had"};
size_t count {};
auto iter = std::begin(text);
auto end_iter = end(text);
while((iter = std::search(iter, end_iter, std::begin(phrase), std::end(phrase),
[](char ch1, char ch2){ return std::toupper(ch1) == std::toupper(ch2); })) != end_iter)
{
++count;
std::advance(iter, phrase.size()); // Move to beyond end of subsequence found
}
std::cout << "\n\""<< phrase << "\" was found " << count << " times." << std::endl;
执行此代码将产生以下输出:
Smith, where Jones had had "had", had had "had had". "Had had" had had the examiners' approval.
"had had" was found 5 times.
我们仍然在搜索"had had"
忽略的情况,但是在向前的方向找到第一个出现的情况。search()
算法返回的迭代器指向找到的子序列中的第一个元素,因此为了搜索下一个phrase
,iter
必须增加phrase
中的元素数,使其指向找到的子序列之后的第一个元素。
search n()算法
search_n()
算法在一个范围内搜索一个元素给定的连续出现次数。前两个参数是定义要搜索的范围的前向迭代器,第三个参数是要查找的第四个参数的连续出现次数。这里有一个例子:
std::vector<double> values {2.7, 2.7, 2.7, 3.14, 3.14, 3.14, 2.7, 2.7};
double value {3.14};
int times {3};
auto iter = std::search_n(std::begin(values), std::end(values), times, value);
if(iter != std::end(values))
std::cout << times << " successive instances of " << value
<< " found starting index " << std::distance(std::begin(values), iter) << std::endl;
这段代码在values
容器中搜索value
的一系列times
实例的第一次出现。它在索引位置 3 查找序列。请注意,指定计数的第三个参数不能是无符号整数类型;如果是这样,代码将不会在没有警告的情况下编译。
使用==
比较元素,但是您可以提供一个额外的参数来指定要使用的谓词。当然,这不一定需要定义一个相等的比较。下面是一个完整的工作示例,它做了一些不同的事情:
// Ex6_03.cpp
// Searching using search_n() to find freezing months
#include <iostream> // For standard streams
#include <vector> // For vector container
#include <algorithm> // For search_n()
#include <string> // For string class
using std::string;
int main()
{
std::vector<int> temperatures {65, 75, 56, 48, 31, 28, 32, 29, 40, 41, 44, 50};
int max_temp {32};
int times {3};
auto iter = std::search_n(std::begin(temperatures), std::end(temperatures), times, max_temp,
[](double v, double max){return v <= max; } );
std::vector<string> months {"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"};
if(iter != std::end(temperatures))
std::cout << "It was " << max_temp << " degrees or below for " << times
<< " months starting in " << months[std::distance(std::begin(temperatures), iter)]
<< std::endl;
}
容器存储一年中每个月的平均温度。作为search_n()
最后一个参数的谓词是一个 lambda 表达式,当一个元素小于或等于max_temp
时,它将返回true
。months
容器存储月份的名称。表达式std::distance(std::begin(temperatures), iter)
产生temperatures
中元素的索引,这是谓词返回true
的times
元素序列的第一个。该值用于索引months
向量以选择月份名称。因此,该代码将产生以下输出:
It was 32 degrees or below for 3 months starting in May
划分范围
对一个范围内的元素进行分区会重新排列元素,使得给定谓词返回true
的所有元素都在谓词返回false
的所有元素之前。partition()
算法做到了这一点。前两个参数是前向迭代器,用于标识要划分的范围,第三个参数是谓词。下面是如何使用partition()
算法来重新排列一个值序列,使所有小于平均值的值排在所有大于平均值的值之前:
std::vector<double> temperatures {65, 75, 56, 48, 31, 28, 32, 29, 40, 41, 44, 50};
std::copy(std::begin(temperatures), std::end(temperatures), // List the values
std::ostream_iterator<double>{std::cout, " "});
std::cout << std::endl;
auto average = std::accumulate(std::begin(temperatures), // Compute the average value
std::end(temperatures), 0.0)/ temperatures.size();
std::cout << "Average temperature: " << average << std::endl;
std::partition(std::begin(temperatures), std::end(temperatures), // Partition the values
average { return t < average; });
std::copy(std::begin(temperatures), std::end(temperatures), // List the values after partitioning
std::ostream_iterator<double>{std::cout, " "});
std::cout << std::endl;
这些语句产生以下输出:
65 75 56 48 31 28 32 29 40 41 44 50
Average temperature: 44.9167
44 41 40 29 31 28 32 48 56 75 65 50
使用accumulate()
算法生成元素的和,然后除以元素的数量,从而得到temperatures
容器中值的平均值。你以前见过accumulate()
算法,所以你会记得第三个参数是总和的初始值。您可以看到,在执行partition()
算法后,所有小于average
的温度值都在大于average
的温度值之前。
谓词不一定是顺序关系,可以是任何你喜欢的东西。例如,您可以对代表个人的一系列Person
对象进行划分,使所有女性优先于男性,或者所有拥有大学学位的人优先于没有大学学位的人。下面是一个例子,它划分了一系列代表人们并识别他们性别的tuple
对象:
using gender = char;
using first = string;
using second= string;
using Name = std::tuple<first, second, gender>;
std::vector<Name> names {std::make_tuple("Dan", "Old", 'm'), std::make_tuple("Ann", "Old", 'f'),
std::make_tuple("Ed", "Old", 'm'), std::make_tuple("Jan", "Old", 'f'),
std::make_tuple("Edna", "Old", 'f')};
std::partition(std::begin(names), std::end(names), // Partition the names
[](const Name& name) { return std::get<2>(name) == 'f'; });
for(const auto& name : names)
std::cout << std::get<0>(name) << " " << std::get<1>(name) << std::endl;
using
声明解释了tuple
对象成员的重要性。当元组的最后一个成员是'f'
时,谓词返回 true,因此输出将在 Ed 和 Dan 之前呈现 Edna、Ann 和 Jan。您可以使用表达式std::get<gender>(name)
来引用谓词中tuple
的第三个成员。这是可能的,因为第三个成员的类型是唯一的,这允许通过其类型来标识该成员。
partition()
算法不保证保持范围内原始元素的相对顺序。在上面使用它的例子中,元件 44 和41
在原始范围内跟随40
,但是在操作之后不再是这种情况。为了保持元素的相对顺序,可以使用stable_partition()
算法。论点与partition()
相同。您可以用下面的语句替换前面代码中调用partition()
来划分温度的语句:
std::stable_partition(std::begin(temperatures), std::end(temperatures),
average { return t < average; });
经过这一更改后,输出将是:
65 75 56 48 31 28 32 29 40 41 44 50
Average temperature: 44.9167
31 28 32 29 40 41 44 65 75 56 48 50
您可以看到,当不需要重新排序来划分范围时,元素的相对顺序得到了保留。所有小于平均值的元素都按其原始顺序排列,所有不小于平均值的元素也是如此。
partition_copy()算法
partition_copy()
算法以与stable_partition()
相同的方式划分一个范围,但是谓词返回true
的元素被复制到一个单独的范围,谓词返回false
的元素被复制到第三个范围。该操作保持原始范围不变。源范围由前两个参数标识,它们必须是输入迭代器。谓词为其返回true
的元素的目标范围的开始由第三个参数标识,谓词为false
的元素的目标的开始是第四个参数;两者都必须是输出迭代器。第五个参数是用于划分元素的谓词。这里有一个完整的程序展示了partition_copy()
的作用:
// Ex6_04.cpp
// Using partition_copy() to find values above average and below average
#include <iostream> // For standard streams
#include <vector> // For vector container
#include <algorithm> // For partition_copy(), copy()
#include <numeric> // For accumulate()
#include <iterator> // For back_inserter, ostream_iterator
int main()
{
std::vector<double> temperatures {65, 75, 56, 48, 31, 28, 32, 29, 40, 41, 44, 50};
std::vector<double> low_t; // Stores below average temperatures
std::vector<double> high_t; // Stores average or above temperatures
auto average = std::accumulate(std::begin(temperatures),std::end(temperatures), 0.0) /
temperatures.size();
std::partition_copy(std::begin(temperatures), std::end(temperatures),
std::back_inserter(low_t), std::back_inserter(high_t),
average { return t < average; });
// Output below average temperatures
std::copy(std::begin(low_t), std::end(low_t), std::ostream_iterator<double>{std::cout, " "});
std::cout << std::endl;
// Output average or above temperatures
std::copy(std::begin(high_t), std::end(high_t), std::ostream_iterator<double>{std::cout, " "});
std::cout << std::endl;
}
这段代码与您之前看到的stable_partition()
操作相同,但是将temperatures
中低于平均值的元素复制到low_t
容器,将高于平均值的元素复制到high_t
。output 语句验证了这一点,因为它们会产生以下输出:
31 28 32 29 40 41 44
65 75 56 48 50
注意,main()
中的代码使用通过back_inserter()
助手函数创建的back_insert_iterator
对象作为partition_copy()
调用中两个目标容器的迭代器。一个back_insert_iterator
调用push_back()
向容器中添加一个新元素,因此使用这种方法避免了预先知道有多少元素要被存储的必要性。如果对目标范围使用 begin 迭代器,那么在操作之前,目标中必须已经存在足够的元素,以容纳将要复制的元素。请注意,如果输入范围与任一输出范围重叠,该算法将无法正常工作。
partition_point()算法
使用partition_point()
算法获得分区范围中第一个分区的结束迭代器。前两个参数是定义要检查的范围的前向迭代器,最后一个参数是用于划分范围的谓词。您通常不知道每个分区中有多少元素,因此该算法使您能够提取或访问任一分区中的元素。例如:
std::vector<double> temperatures {65, 75, 56, 48, 31, 28, 32, 29, 40, 41, 44, 50};
auto average = std::accumulate(std::begin(temperatures), // Compute the average value
std::end(temperatures), 0.0)/ temperatures.size();
auto predicate = average { return t < average; };
std::stable_partition(std::begin(temperatures), std::end(temperatures), predicate);
auto iter = std::partition_point(std::begin(temperatures), std::end(temperatures), predicate);
std::cout << "Elements in the first partition: ";
std::copy(std::begin(temperatures), iter, std::ostream_iterator<double>{std::cout, " "});
std::cout << "\nElements in the second partition: ";
std::copy(iter, std::end(temperatures), std::ostream_iterator<double>{std::cout, " "});
std::cout << std::endl;
这段代码根据平均温度对temperatures
容器中的元素进行分区,并通过调用范围的partition_point()
找到分区点。这是第一个分区的结束迭代器,存储在iter
中。因此,范围std::begin(temperatures), iter)
对应于第一分区中的元素,而范围[iter, std::end(temperatures))
包含第二分区中的元素。copy()
算法的两个应用程序输出分区,输出将是:
Elements in the first partition: 31 28 32 29 40 41 44
Elements in the second partition: 65 75 56 48 50
在应用partition_point()
之前,你需要确保范围已经被划分。如果对此有疑问,您可以致电is_partitioned()
来确定是否如此。参数是指定范围的输入迭代器和应该用于划分范围的谓词。如果区域被划分,该算法返回 true,否则返回false
。在对其应用partition_point()
算法之前,您可以使用它来验证temperatures
范围是否被划分:
if(std::is_partitioned(std::begin(temperatures), std::end(temperatures),
[average { return t < average; }))
{
auto iter = std::partition_point(std::begin(temperatures), std::end(temperatures),
average { return t < average; });
std::cout << "Elements in the first partition: ";
std::copy(std::begin(temperatures), iter, std::ostream_iterator<double>{std::cout, " "});
std::cout << "\nElements in the second partition: ";
std::copy(iter, std::end(temperatures), std::ostream_iterator<double>{std::cout, " "});
std::cout << std::endl;
}
else
std::cout << "Range is not partitioned." << std::endl;
这段代码只在is_partitioned()
返回true
时执行对partition_point()
的调用。指向分割点的iter
变量位于true
结果的if
模块。如果您希望iter
随后可用,您可以在if
语句之前定义它,如下所示:
std::vector<double>::iterator iter;
在所有使迭代器可用的容器类型模板中定义了iterator
类型别名,它对应于容器类型的begin()
和end()
成员返回的迭代器类型。
二分搜索法算法
到目前为止,您在本章中看到的搜索算法按顺序搜索一个范围,并且对元素没有预先的排序要求。二分搜索法算法通常比顺序搜索更快,但是要求对应用它们的范围内的元素进行排序。这是因为二分搜索法的工作方式。如图 6-6 所示。
图 6-6。
A binary search
图 6-6 显示了22
的二分搜索法从一系列值中按升序排列的事件顺序。因为元素是按升序排列的,所以搜索机制使用小于运算符来查找元素。按降序搜索范围将使用大于号运算符来比较元素。二分搜索法总是从选择范围中间的元素开始,并将其与所寻求的值进行比较。与要查找的元素等价的元素被认为是匹配的,因此当!(x < n) && !(n < x)
时,值n
将匹配值x
。如果被检查的元素不匹配,如果是x < n
,则从左分区的中间元素继续搜索,否则从右分区的中间元素继续搜索。当找到一个等价元素时,或者当被检查的分区只包含一个元素时,搜索结束。如果不匹配,则该元素不在范围内。
二进制搜索()算法
正如您无疑会猜到的那样,binary_search()
算法实现了一个二分搜索法。它在前两个参数指定的范围内搜索与第三个参数等效的元素。指定范围的迭代器必须是前向迭代器,并且使用<
操作符比较元素。该范围内的元素必须按升序排序,或者至少相对于要查找的值进行分区。该算法返回一个bool
值,如果找到第三个参数,该值为true
,否则为false
,所以它只告诉您元素是否存在,而不告诉您它何时在哪里。当然,如果你一定要知道在哪里,可以用你已经看过的 find 算法之一,或者lower_bound()
、upper_bound()
或者equal_range()
。这里有一个使用binary_search()
的例子:
std::list<int> values {17, 11, 40, 36, 22, 54, 48, 70, 61, 82, 78, 89, 99, 92, 43};
values.sort(); // Sort into ascending sequence
int wanted {22}; // What we are looking for
if(std::binary_search(std::begin(values), std::end(values), wanted))
std::cout << wanted << " is definitely in there - somewhere..." << std::endl;
else
std::cout << wanted << " cannot be found - maybe you got it wrong..." << std::endl;
我使用了一个list
来以任意顺序存储一组任意值——只是为了提醒您这个容器。该代码使用binary_search()
算法来搜索wanted
的值。由于binary_search()
只适用于排序范围,我们必须首先确保列表中的元素是有序的。sort()
算法不能应用于list
容器中的一系列元素,因为它需要随机访问迭代器,而list
容器只提供双向迭代器。出于这个原因,list
容器定义了一个sort()
成员,该成员按升序对所有元素进行排序,因此它用于对values
容器进行排序。该代码执行时,将输出确认wanted
在values
中的消息。
第二个版本的binary_search()
接受一个附加的参数,它是一个用于搜索的函数对象;显然,这必须与用于排序被搜索范围的比较有效地相同。以下是你如何按降序排列values
,然后搜索wanted
:
std::list<int> values {17, 11, 40, 36, 22, 54, 48, 70, 61, 82, 78, 89, 99, 92, 43};
auto predicate = [](int a, int b){ return a > b; };
values.sort(predicate); // Sort into descending sequence
int wanted {22};
if(std::binary_search(std::begin(values), std::end(values), wanted, predicate))
std::cout << wanted << " is definitely in there - somewhere..." << std::endl;
else
std::cout << wanted << " cannot be found - maybe you got it wrong..." << std::endl;
这使用了接受定义比较的函数对象的list
容器的sort()
成员。这里,它是由λ表达式定义的。同一个 lambda 表达式被用作binary_search()
的第四个参数。当然,结果将与前面的代码相同。
下界()算法
lower_bound()
算法在由前两个参数指定的范围内查找不小于第三个参数的元素——换句话说,是大于或等于第三个参数的第一个元素。前两个参数必须是前向迭代器。upper_bound()
算法在由它的前两个参数定义的范围内找到大于第三个参数的第一个元素。对于这两种算法,范围必须是有序的,并且假定它们是使用小于运算符进行排序的。这里有一个例子:
std::list<int> values {17, 11, 40, 36, 22, 54, 48, 70, 61, 82, 78, 89, 99, 92, 43};
values.sort(); // Sort into ascending sequence
int wanted {22}; // What we are looking for
std::cout << "The lower bound for " << wanted
<< " is " << *std::lower_bound(std::begin(values), std::end(values), wanted) << std::endl;
std::cout << "The upper bound for " << wanted
<< " is " << *std::upper_bound(std::begin(values), std::end(values), wanted) << std::endl;
这会产生以下输出:
The lower bound for 22 is 22
The upper bound for 22 is 36
从列表容器中的整数可以看出,算法正在按照描述的方式工作。这两种算法都有其他版本,它们接受 function 对象作为第四个参数,指定用于排序范围的比较。
equal_range()算法
equal_range()
算法在一个排序范围内找到所有与给定元素等价的元素。前两个参数是指定范围的前向迭代器,第三个参数是所需的元素。该算法返回一个带有前向迭代器成员的pair
对象,第一个成员指向不小于第三个参数的元素,第二个成员指向大于第三个参数的元素。因此,您可以在一次调用中得到调用lower_bound()
和upper_bound()
的结果。因此,您可以用以下语句替换前面代码片段中的两个输出语句:
auto pr = std::equal_range(std::begin(values), std::end(values), wanted);
std::cout << "the lower bound for " << wanted << " is " << *pr.first << std::endl;
std::cout << "the upper bound for " << wanted << " is " << *pr.second << std::endl;
输出将与前面的代码完全相同。与以前的二分搜索法算法一样,有一个版本的equal_range()
带有一个额外的参数,该参数提供了使用小于运算符之外的比较进行排序的范围。
我说过,本节中的算法要求对它们所应用的范围内的元素进行排序,但这并不是全部。所有二分搜索法算法也适用于以特定方式划分的范围。对于给定的wanted
值,范围内的元素必须相对于(element < wanted)
进行分区,并且相对于!(wanted < element)
进行分区。我可以用equal_range()
二分搜索法算法证明这是可行的。在对values
容器中的元素执行equal_range()
之前,我们可以这样划分它:
std::list<int> values {17, 11, 40, 36, 22, 54, 48, 70, 61, 82, 78, 89, 99, 92, 43};
// Output the elements in original order
std::copy(std::begin(values), std::end(values), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl;
int wanted {22}; // What we are looking for
std::partition(std::begin(values), std::end(values), // Partition the values wrt value < wanted
wanted { return value < wanted; });
std::partition(std::begin(values), std::end(values), // Partition the values wrt !(wanted < value)
wanted { return !(wanted < value); });
// Output the elements after partitioning
std::copy(std::begin(values), std::end(values), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl;
这样的输出将是:
17 11 40 36 22 54 48 70 61 82 78 89 99 92 43
17 11 22 36 40 54 48 70 61 82 78 89 99 92 43
第一行包含原始序列中的元素,第二行显示分区后的序列。这两个分区操作改变了顺序,但没有改变太多。我们现在可以使用wanted
的值将equal_range()
应用于values
中的元素:
auto pr = std::equal_range(std::begin(values), std::end(values), wanted);
std::cout << "the lower bound for " << wanted << " is " << *pr.first << std::endl;
std::cout << "the upper bound for " << wanted << " is " << *pr.second << std::endl;
该代码的输出将与前面的代码片段相同,其中使用容器对象的sort()
成员对元素进行了完全排序。本节中的所有算法都处理以这种方式划分的范围。显然,如果分区使用>
,那么您必须为搜索算法提供一个与此一致的函数对象。
前面的代码片段将equal_range()
应用于只包含一个wanted
实例的范围。如果范围包含几个实例,pr.first
将指向第一次出现的通缉,因此范围pr.first, pr.second)
将包含所有实例。这里有一个工作程序来说明这一点:
// Ex 6_05.cpp
// Using partition() and equal_range() to find duplicates of a value in a range
#include <iostream> // For standard streams
#include <list> // For list container
#include <algorithm> // For copy(), partition()
#include <iterator> // For ostream_iterator
int main()
{
std::list<int> values {17, 11, 40, 13, 22, 54, 48, 70, 22, 61, 82, 78, 22, 89, 99, 92, 43};
// Output the elements in their original order
std::cout << "The elements in the original sequence are:\n";
std::copy(std::begin(values), std::end(values), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl;
int wanted {22}; // What we are looking for
std::partition(std::begin(values), std::end(values), // Partition the values with (value < wanted)
[wanted { return value < wanted; });
std::partition(std::begin(values), std::end(values), // Partition the values with !(wanted < value)
wanted { return !(wanted < value); });
// Output the elements``after
std::cout << "The elements after partitioning are:\n";
std::copy(std::begin(values), std::end(values), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl;
auto pr = std::equal_range(std::begin(values), std::end(values), wanted);
std::cout << "The lower bound for " << wanted << " is " << *pr.first << std::endl;
std::cout << "The upper bound for " << wanted << " is " << *pr.second << std::endl;
std::cout << "\nThe elements found by equal_range() are:\n";
std::copy(pr.first, pr.second, std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl;
}
输出将是:
The elements in the original sequence are:
17 11 40 13 22 54 48 70 22 61 82 78 22 89 99 92 43
The elements after partitioning are:
17 11 13 22 22 22 48 70 54 61 82 78 40 89 99 92 43
The lower bound for 22 is 22
The upper bound for 22 is 48
The elements found by equal_range() are:
22 22 22
这里的values
容器有几个值为 22 的元素,这是wanted
的值。wanted
的三个实例都在equal_range()
返回的范围内。该范围只进行了分区,没有完全排序,因此当该范围完全排序时,这显然是可行的。
那么,当范围只是像 Ex6_05 中那样划分,而不是完全排序时,equal_range()
为什么返回所有出现的wanted
?要理解这一点,你需要理解两个partition()
呼叫的影响:
- 第一次分区操作确保所有严格小于
wanted
的元素都在左分区中;这些元素不一定按顺序排列。这个操作还确保了所有不小于wanted
的元素——也就是大于或等于wanted
的元素——都在正确的分区中,所以它们按顺序跟随它,也不一定是按顺序。所有出现的wanted
将在正确的分区中,但是与大于wanted
的元素混合在一起。在前一个片段中的第一个partition()
调用之后,values
中的元素是:
17 11 13 40 22 54 48 70 22 61 82 78 22 89 99 92 43
17
、11
和13
是唯一小于wanted
的值,并且这些值明显在左分区中。分区不以任何特定的方式定位对应于wanted
的值。22
的所有实例都位于右分区中元素的任意位置。
- 第二个分区操作应用于第一个分区操作的结果。表达式
!(wanted < value)
等价于(value <= wanted)
。因此,所有小于或等于wanted
的元素将位于左分区,所有严格大于wanted
的元素将位于右分区。这样做的效果是将wanted
的所有实例移动到左分区中,这样它们就像左分区中的最后一个元素一样作为一个序列在一起。第二次partition()
调用后,values
包含:
17 11 13 22 22 22 48 70 54 61 82 78 40 89 99 92 43
因此,equal_range()
找到的下限指向第一次出现的22
,上限指向最后一次出现的22
之后的元素,即值为48
的元素。
摘要
如果你使用 STL 容器,你会希望熟悉我在本章中讨论的算法。排序是非常常见的需求,尤其是在处理事务的应用程序中。当您需要对数据进行排序时,通常也需要合并数据。确保事务与它们所应用的记录顺序相同通常会使更新过程更快。当然,默认情况下,有序的set
和map
容器以及priority_queue
容器适配器提供了它们所包含的元素的排序,这消除了显式排序操作的需要。但是,当您需要在不同的时间以不同的顺序处理相同的数据时,您需要排序操作来在需要时重新排列对象。STL sort()
算法的强大之处一方面在于它们的灵活性——你可以对任何你可以比较的东西进行排序,另一方面在于它们的效率——这个实现几乎肯定比你自己实现的排序算法要好。这并不是说 STL 算法总是最好的选择。C++ 标准库中没有实现许多专门的数据排序方法,但是当您使用 STL 容器来管理数据,并且只是想要一个通用的排序功能时,STL 提供了一个即时的解决方案。
STL 查找算法比排序和合并算法应用得更广泛。除了能够比较元素之外,它们对要搜索的范围没有任何要求。同样,定义要搜索的范围的迭代器所需的功能也很少。对范围进行排序时,实现二分搜索法的算法是对查找算法的补充。最后,分区算法在无序和有序范围之间提供了一个中间站。正如您所看到的,您可以将二分搜索法算法应用于分区范围,并允许在不应用完全排序的情况下找到一系列相同的元素。
ExercisesDefine a Card
class to represent playing cards in a standard deck. Create a vector
of Card
objects that represents a complete deck of fifty-two cards. Deal the cards randomly into four vector
containers so that each represents a hand of thirteen cards in a game. Output the cards in the four vector containers under the headings “North,” “South,” “East,” and “West” after sorting each hand using the sort( ) algorithm. The cards should be sorted into the usual suit and value order - so in card value order from 2 through to 10, then Jack, Queen, King, Ace within the suit sequence Clubs, Diamonds, Hearts, Spades. Add code to your solution to Exercise 1 to merge the four hands and output the result. Define a Person
class that identifies a person by at least their name and hair color. The class should implement operator<<()
for output streams and function members to compare hair color. Create a vector
that contains Person
objects in no particular order, including some that have blond, gray, brown, and black hair. Use the partition()
algorithm to arrange the objects in the container so that they are ordered by hair color with those that have black hair first, then those that are gray, followed by brown, and blond last. Output the Person
objects in hair color groups using the copy()
algorithm. Add function members for comparing names to the Person
class type in Exercise 3, and extend your solution to this exercise to use the sort()
algorithm to order the Person
objects with a given hair color in ascending sequence of their names before outputting them.
公职选举通常随机排列候选人名字在选票上出现的顺序,以避免对名字出现在字母顺序后面的候选人产生偏见,如 Joe Yodel 和 Bob Zippo。定义一个Name
类,使用以下字母顺序对名称进行排序:
英语作文网-英语作文网-英语作文网
所以以R
开头的名字排在前面,以L
开头的名字排在最后。在一个矢量容器中创建各种Name
对象,并使用stable_sort()
算法根据上面的字母顺序对它们进行升序排序。
七、更多算法
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-0004-9_7) contains supplementary material, which is available to authorized users.
本章描述了 STL 提供的更多算法。算法通常分为两组:改变应用范围的变异算法和非变异算法。我将在这一章中讨论算法,这些算法根据你能用它们做什么来分组,而不是根据它们是否改变事情。如果你知道一个算法是做什么的,那么它是否改变了它所应用的数据就显而易见了。在本章中,您将了解:
- 测试某个范围内元素属性的算法
- 计算给定属性范围内元素数量的算法
- 比较两个元素范围的算法
- 复制或移动范围的算法
- 设置或更改某个范围内的元素的算法
测试元素属性
在algorithm
头中定义了三种算法,用于测试给定谓词在应用于一系列元素时何时返回true
。这些算法的前两个参数是输入迭代器,它们定义了谓词应用的范围;第三个参数指定谓词。测试元素以查看谓词是否返回true
可能看起来过于简单,但它仍然是一个强大的工具。例如,您可以测试是否有任何或所有学生通过了所有考试,或者是否所有学生都去上课了,或者是否没有绿色眼睛的Person
物体,甚至是否每个Dog
物体都有过辉煌的一天。谓词可以很简单,也可以很复杂。测试元素属性的三种算法是:
- 如果谓词为范围内的所有元素返回
true
,则all_of()
算法返回true
。 - 如果谓词为范围中的任何元素返回
true
,则any_of()
算法返回true
。 - 如果谓词对于范围内的所有元素都不返回
true
,则none_of()
算法返回true
。
不难想象这些是如何运作的。这里有一些代码来说明如何使用none_of()
算法:
std::vector<int> ages {22, 19, 46, 75, 54, 19, 27, 66, 61, 33, 22, 19};
int min_age{18};
std::cout << "There are "
<< (std::none_of(std::begin(ages), std::end(ages),
min_age { return age < min_age; }) ? "no": "some")
<< " people under " << min_age << std::endl;
谓词是一个 lambda 表达式,它将作为参数传递的ages
中的元素与min_age
的值进行比较。由none_of()
返回的bool
值用于选择包含在输出消息中的"no"
或"some"
。当ages
中没有元素小于 min_age 时,none_of()
算法返回true
,因此在这种情况下选择"no"
。当然,您可以使用any_of()
来产生相同的结果:
std::cout << "There are "
<< (std::any_of(std::begin(ages), std::end(ages),
min_age { return age < min_age; }) ? "some": "no")
<< " people under " << min_age << std::endl;
any_of()
算法仅在一个或多个元素小于min_age
时返回true
。没有少于min_age
的元素,所以这里也选择了"no"
。
下面的代码片段展示了如何使用all_of()
来测试ages
容器中的元素:
int good_age{100};
std::cout << (std::all_of(std::begin(ages), std::end(ages),
good_age { return age < good_age; }) ? "None": "Some")
<< " of the people are centenarians." << std::endl;
lambda 表达式将ages
中的一个元素与good_age
的值进行比较,后者是100
。所有元素都小于100
,因此all_of()
将返回true
,输出消息将正确报告没有记录任何百岁老人。
count()
和count_if()
算法告诉您在前两个参数指定的范围内有多少元素满足您用第三个参数指定的条件。count()
算法返回等于第三个参数的元素个数。count_if()
算法返回第三个参数谓词返回true
的元素数量。下面的代码展示了应用于ages
容器的这些功能:
std::vector<int> ages {22, 19, 46, 75, 54, 19, 27, 66, 61, 33, 22, 19};
int the_age{19};
std::cout << "There are "
<< std::count(std::begin(ages), std::end(ages), the_age)
<< " people aged " << the_age << std::endl;
int max_age{60};
std::cout << "There are "
<< std::count_if(std::begin(ages), std::end(ages),
max_age { return age > max_age; })
<< " people aged over " << max_age << std::endl;
第一条输出语句使用count()
算法来确定ages
中等于the_age
的元素数量。第二个输出语句使用count_if()
来报告超过max_age
值的元素数量。
当您想要了解某个元素范围的一般特征时——当您只想知道某个特征是否适用,或者有多少符合某个标准时,可以使用本节中的所有算法。当你想知道细节——范围中的哪些元素匹配——你可以使用在第六章中遇到的查找算法。
比较范围
可以用类似于比较字符串的方式来比较两个范围。如果两个范围长度相同,并且对应的元素对相等,则equal()
算法返回true
。equal()
算法有四个版本,其中两个使用==
操作符比较元素,另两个使用您作为参数提供的函数对象比较元素。所有指定范围的迭代器必须至少是输入迭代器。
使用==
操作符比较两个范围的一个版本需要三个输入迭代器参数。前两个参数是第一个范围的开始和结束迭代器。第三个参数是第二个范围的 begin 迭代器。如果第二个范围包含的元素比第一个范围少,则结果是未定义的。使用==
操作符的第二个版本需要四个参数:第一个范围的开始和结束迭代器,第二个范围的开始和结束迭代器。如果两个范围的长度不同,那么结果总是false
。我将演示这两个版本,但是我建议您总是使用接受四个参数的equal()
版本,因为它不会导致未定义的行为。下面是一个工作示例,展示了如何应用这些功能:
// Ex7_01.cpp
// Using the equal() algorithm
#include <iostream> // For standard streams
#include <vector> // For vector container
#include <algorithm> // For equal() algorithm
#include <iterator> // For stream iterators
#include <string> // For string class
using std::string;
int main()
{
std::vector<string> words1 {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};
std::vector<string> words2 {"two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"};
auto iter1 = std::begin(words1);
auto end_iter1 = std::end(words1);
auto iter2 = std::begin(words2);
auto end_iter2 = std::end(words2);
std::cout << "Container - words1: ";
std::copy(iter1, end_iter1, std::ostream_iterator<string>{std::cout, " "});
std::cout << "\nContainer - words2: ";
std::copy(iter2, end_iter2, std::ostream_iterator<string>{std::cout, " "});
std::cout << std::endl;
std::cout << "\n1\. Compare from words1[1] to end with words2: ";
std::cout << std::boolalpha << std::equal(iter1 + 1, end_iter1, iter2) << std::endl;
std::cout << "2\. Compare from words2[0] to second-to-last with words1: ";
std::cout << std::boolalpha << std::equal(iter2, end_iter2 - 1, iter1) << std::endl;
std::cout << "3\. Compare from words1[1] to words1[5] with words2: ";
std::cout << std::boolalpha << std::equal(iter1 + 1, iter1 + 6, iter2) << std::endl;
std::cout << "4\. Compare first 6 from words1 with first 6 in words2: ";
std::cout << std::boolalpha << std::equal(iter1, iter1 + 6, iter2, iter2 + 6) << std::endl;
std::cout << "5\. Compare all words1 with words2: ";
std::cout << std::boolalpha << std::equal(iter1, end_iter1, iter2) << std::endl;
std::cout << "6\. Compare all of words1 with all of words2: ";
std::cout << std::boolalpha << std::equal(iter1, end_iter1, iter2, end_iter2) << std::endl;
std::cout << "7\. Compare from words1[1] to end with words2 from first to second-to-last: ";
std::cout << std::boolalpha
<< std::equal(iter1 + 1, end_iter1, iter2, end_iter2 - 1) << std::endl;
}
输出将是:
Container - words1: one two three four five six seven eight nine
Container - words2: two three four five six seven eight nine ten
1\. Compare from words1[1] to end``with
2\. Compare from words2[0] to second-to-last with words1: false
3\. Compare from words1[1] to words1[5] with words2: true
4\. Compare first 6 from words1 with first 6 in words2: false
5\. Compare all words1 with words2: false
6\. Compare all of words1 with all of words2: false
7\. Compare from words1[1] to end with words2 from first to second-to-last: true
该示例比较了来自words1
和words2
容器的各种元素序列。equal()
调用产生输出的原因是:
- 第一个输出产生
true
,因为从第二个到最后的words1
元素匹配从第一个开始的words2
元素。第二个范围中的元素数量比第一个范围中的数量多一个,但是第一个范围中的元素数量决定了要比较多少个对应的元素。 - 第二个输出产生
false
,因为有一个直接的不匹配;words2
和words1
中的第一个元素是不同的。 - 第三个语句显示
true
,因为从第二个开始的来自words1
的五个元素与来自words2
的前五个元素相同。 - 在第四条语句中,来自
words2
的元素范围由 begin 和 end 迭代器指定。范围长度相同,但第一个元素不同,因此结果是false
。 - 在第五个语句中,两个范围中的第一个元素是直接不匹配的,所以结果是
false
。 - 第六个语句产生
false
,因为范围不同。该语句不同于前面的equal()
调用,因为为第二个范围指定了结束迭代器。 - 第七个语句从第二个开始比较来自
words1
的元素,从第一个开始比较来自words2
的相同数量的元素,所以输出是true
。
当第二个范围被 begin 迭代器标识为equal()
时,第二个范围中与第一个范围相比的元素数量由第一个范围的长度决定。第二个范围可以比第一个范围有更多的元素,并且equal()
仍然可以返回true
。当您为两个范围提供 begin 和 end 迭代器时,范围必须是相同的长度才能得到一个true
结果。
虽然您可以使用equal()
来比较两个相同类型容器的全部内容,但是最好使用容器的operator==()
成员来完成这项工作。示例中的第六条输出语句可以写成:
std::cout << std::boolalpha << (words1 == words2) << " "; // false
接受谓词作为附加参数的两个版本的equal()
以相同的方式工作。谓词定义了元素之间相等的比较。下面的代码片段说明了它们的用法:
std::vector<string> r1 {"three", "two", "ten"};
std::vector<string> r2 {"twelve", "ten", "twenty"};
std::cout << std::boolalpha
<< std::equal(std::begin(r1), std::end(r1), std::begin(r2),
[](const string& s1, const string& s2) { return s1[0] == s2[0]; })
<< std::endl; // true
std::cout << std::boolalpha
<< std::equal(std::begin(r1), std::end(r1), std::begin(r2), std::end(r2),
[](const string& s1, const string& s2) { return s1[0] == s2[0]; })
<< std::endl; // true
第一次使用equal()
仅通过 begin 迭代器指定第二个范围。谓词是一个 lambda 表达式,当string
参数中的第一个字符相等时,它返回true
。最后一条语句显示了完全指定两个范围并使用相同谓词的equal()
算法。
你不应该使用equal()
来比较无序的map
或set
容器中的元素范围。一个无序容器中给定元素集的顺序可能与存储在另一个无序容器中的相同元素集的顺序不同,因为元素在桶中的分配可能因容器而异。
查找范围不同的地方
equal()
算法告诉你两个范围是否匹配。mismatch()
算法告诉你两个范围是否匹配,如果不匹配,它们在哪里不同。四个版本的mismatch()
和四个版本的equal()
有相同的参数——有和没有第二个范围的结束迭代器,每个版本都有和没有一个函数对象的额外参数来定义比较。mismatch()
算法返回一个包含两个迭代器的pair
对象。first
成员是来自前两个参数指定范围的迭代器,第二个成员是来自第二个范围的迭代器。当范围不匹配时,pair
包含指向第一对不匹配元素的迭代器;因此,对象将是pair<iter1 + n, iter2 + n>
,其中范围中索引n
处的元素是第一个不匹配的元素。
当范围匹配时,pair
成员取决于您使用的mismatch()
版本和环境。iter1
和end_iter1
代表定义第一个范围的迭代器,iter2
和end_iter2
代表第二个范围的开始和结束迭代器,为匹配范围返回的pair
的内容如下:
对于mismatch(iter1, end_iter1, iter2)
:
- 返回
pair<end_iter1, (iter2 + (end_iter1 - iter1))>
,所以第二个成员是iter2
加上第一个范围的长度。如果第二个范围比第一个范围短,则行为未定义。
For mismatch(iter1, end_iter1, iter2, end_iter2)
:
- 当第一个范围比第二个范围长时,返回
pair<end_iter1, (iter2 + (end_iter1 - iter1))>
,所以second
成员是iter2
加上第一个范围的长度。 - 当第二个范围比第一个范围长时
pair<(iter1 + (end_iter2 - iter2)), end_iter2>
被返回,所以第一个成员是iter1
加上第二个范围的长度。 - 当范围相同时,返回长度
pair<end_iter1, end_iter2>
。
这同样适用于您是否添加了为比较定义函数对象的参数。
下面是一个工作示例,展示了使用默认比较来比较是否相等的mismatch()
:
// Ex7_02.cpp
// Using the mismatch() algorithm
#include <iostream> // For standard streams
#include <vector> // For vector container
#include <algorithm> // For equal() algorithm
#include <string> // For string class
#include <iterator> // For stream iterators
using std::string;
using word_iter = std::vector<string>::iterator;
int main()
{
std::vector<string> words1 {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};
std::vector<string> words2 {"two", "three", "four", "five", "six", "eleven", "eight", "nine", "ten"};
auto iter1 = std::begin(words1);
auto end_iter1 = std::end(words1);
auto iter2 = std::begin(words2);
auto end_iter2 = std::end(words2);
// Lambda expression to output mismatch() result
auto print_match = [](const std::pair<word_iter, word_iter>& pr, const word_iter& end_iter)
{
if(pr.first != end_iter)
std::cout << "\nFirst pair of words that differ are "
<< *pr.first << " and " << *pr.second << std::endl;
else
std::cout << "\nRanges are identical." << std::endl;
};
std::cout << "Container - words1: ";
std::copy(iter1, end_iter1, std::ostream_iterator<string>{std::cout, " "});
std::cout << "\nContainer - words2: ";
std::copy(iter2, end_iter2, std::ostream_iterator<string>{std::cout, " "});
std::cout << std::endl;
std::cout << "\nCompare from words1[1] to end with words2:";
print_match(std::mismatch(iter1 + 1, end_iter1, iter2), end_iter1);
std::cout << "\nCompare from words2[0] to second-to-last with words1:";
print_match(std::mismatch(iter2, end_iter2 - 1, iter1), end_iter2 - 1);
std::cout << "\nCompare from words1[1] to words1[5] with words2:";
print_match(std::mismatch(iter1 + 1, iter1 + 6, iter2), iter1 + 6);
std::cout << "\nCompare first 6 from words1 with first 6 in words2:";
print_match(std::mismatch(iter1, iter1 + 6, iter2, iter2 + 6), iter1 + 6);
std::cout << "\nCompare all words1 with words2:";
print_match(std::mismatch(iter1, end_iter1, iter2), end_iter1);
std::cout << "\nCompare all``of
print_match(std::mismatch(iter2, end_iter2, iter1, end_iter1), end_iter2);
std::cout << "\nCompare from words1[1] to end with words2[0] to second-to-last:";
print_match(std::mismatch(iter1 + 1, end_iter1, iter2, end_iter2 - 1), end_iter1);
}
注意,words2
的内容与前面的例子略有不同。每次应用mismatch()
的结果都是由定义为print_match
的λ表达式生成的。参数是一对对象和一个vector<string>
容器的迭代器。用于别名word_iter
的using
指令使得 lambda 的定义更加简单。main()
中的代码使用不包含比较函数对象参数的版本对mismatch()
进行了修改。当第二个范围仅由一个 begin 迭代器标识时,只需要它的元素数量至少与第一个匹配范围一样多,但可以更长。当完全指定第二个范围时,最短的范围决定了要比较多少个元素。
以下是输出结果:
Container - words1: one two three four five six seven eight nine
Container - words2: two three four five six eleven eight nine ten
Compare from words1[1] to end with words2:
First pair of words that differ are seven and eleven
Compare from words2[0] to second-to-last with words1:
First pair of words that differ are two and one
Compare from words1[1] to words1[5] with words2:
Ranges are identical.
Compare first 6 from words1 with first 6 in words2:
First pair of words that differ are one and two
Compare all words1 with words2:
First pair of words that differ are one and two
Compare all of words2 with all of words1:
First pair of words that differ are two and one
Compare from words1[1] to end with words2[0] to second-to-last:
First pair of words that differ are seven and eleven
输出显示了每次应用mismatch()
的结果。
当您提供自己的比较对象时,您可以完全灵活地定义等式。例如:
std::vector<string> range1 {"one", "three", "five", "ten"};
std::vector<string> range2 {"nine", "five", "eighteen", "seven"};
auto pr = std::mismatch(std::begin(range1), std::end(range1), std::begin(range2), std::end(range2),
[](const string& s1, const string& s2)
{ return s1.back() == s2.back(); });
if(pr.first == std::end(range1) || pr.second == std::end(range2))
std::cout << "The ranges are identical." << std::endl;
else
std::cout << *pr.first << " is not equal to " << *pr.second << std::endl;
当两个字符串的最后一个字母相等时,比较返回true
,因此执行这段代码的输出将是:
five is not equal to eighteen
当然,这是正确的——而且根据比较函数,"one"
等于"nine"
,"three"
等于"five"
。
词典范围比较
两个字符串的字母顺序是通过比较相应的字符对获得的,从第一个字符开始。第一对不同的对应字符决定了哪个字符串先出现。字符串的顺序将是不同字符的顺序。如果字符串长度相同,并且所有字符都相等,则字符串相等。如果字符串长度不同,并且较短字符串中的字符序列与较长字符串中的初始序列相同,则较短字符串小于较长字符串。因此,“年龄”先于“美丽”,“平静”先于“风暴”同样显而易见的是,“先有鸡”而不是“先有蛋”
词典排序是对任何类型的对象序列的字母排序思想的推广。两个序列中的对应对象从第一个开始连续比较,前两个不同的对象决定序列的顺序。显然,序列中的对象必须具有可比性。lexicographical_compare()
算法比较由 begin 和 end 迭代器定义的两个范围。前两个参数定义第一个范围,第三和第四个参数是第二个范围的开始和结束迭代器。默认情况下,<
操作符用于比较元素,但是您可以在必要时提供一个实现小于比较的函数对象作为可选的第五个参数。如果第一个范围按字典顺序小于第二个范围,算法返回true
,否则返回false
。因此,错误的返回意味着第一个范围大于或等于第二个范围。这些范围是逐元素比较的。不同的第一对对应元素决定了范围的顺序。如果范围具有不同的长度,并且较短的范围匹配较长范围中的元素的初始序列,则较短的范围小于较长的范围。长度相同且对应元素相等的两个范围相等。空范围总是小于非空范围。下面是一个使用lexicographical_compare()
的例子:
std::vector<string> phrase1 {"the", "tigers", "of", "wrath"};
std::vector<string> phrase2 {"the", "horses", "of", "instruction"};
auto less = std::lexicographical_compare(std::begin(phrase1), std::end(phrase1),
std::begin(phrase2), std::end(phrase2));
std::copy(std::begin(phrase1), std::end(phrase1), std::ostream_iterator<string>{std::cout, " "});
std::cout << (less ? "are" : "are not") << " less than ";
std::copy(std::begin(phrase2), std::end(phrase2), std::ostream_iterator<string>{std::cout, " "});
std::cout << std::endl;
因为范围中的第二个元素不同,并且"tigers"
大于"horses
,所以此代码将生成以下输出:
the tigers of wrath are not less than the horses of instruction
您可以向lexicographical_compare()
调用添加一个参数,得到相反的结果:
auto less = std::lexicographical_compare(std::begin(phrase1), std::end(phrase1),
std::begin(phrase2), std::end(phrase2),
[](const string& s1, const string& s2){ return s1.length() < s2.length(); });
该算法使用第三个参数 lambda 表达式来比较元素。这将比较范围内字符串的长度,因为phrase1
中第四个元素的长度小于phrase2
中相应元素的长度,phrase1
小于phrase2
。
范围的排列
如果你对这个术语不熟悉的话——排列就是一系列对象或值的一种排列。例如,"ABC"
中字符的可能排列是:
"ABC", "ACB", "BAC", "BCA", "CAB", and "CBA"
三个不同的字符有六种可能的排列,数字是3 × 2 × 1
的结果。一般来说,n
不同的物体有n!
种可能的排列,其中n!
是n × (n-1) × (n-2) × ... × 2 × 1
。很容易理解为什么会这样。使用n
对象,您可以为序列中的第一个对象选择n
。对于第一个对象的每个选择,序列中的第二个对象还有n-1
个可供选择,因此前两个对象有n × (n-1)
个可能的选择。选择了前两个之后,还剩下n-2
来选择第三个,所以还有n × (n-1) × (n-2)
个前三个的可能序列——以此类推,直到序列中的最后一个是霍布森的选择,因为只剩下一个。
如果一个值域包含相同的元素但顺序不同,那么它就是另一个值域的置换。next_permutation()
算法生成一个范围的重排,它是所有可能排列的字典顺序中的下一个排列。默认情况下,它使用小于运算符来实现这一点。参数是定义范围的迭代器,当新的排列大于先前的元素排列时,函数返回一个bool
值,即true
,如果先前的排列是序列中最大的排列,则返回false
,这样就创建了字典上最小的排列。
下面是如何创建包含四个整数的vector
的排列:
std::vector<int> range {1,2,3,4};
do
{
std::copy(std::begin(range), std::end(range), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl;
} while(std::next_permutation(std::begin(range), std::end(range)));
当next_permutation()
返回false
时,循环结束,表示排列到达最小值。这恰好创建了序列在该范围内的所有排列,但仅仅是因为初始排列1 2 3 4
是可能排列集合中的第一个。确保创建所有排列的一种方法是使用next_permutation()
获得最小值:
std::vector<string> words {"one","two", "three", "four", "five", "six", "seven", "eight"};
while(std::next_permutation(std::begin(words), std::end(words))) // Change to minimum
;
do
{
std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
} while(std::next_permutation(std::begin(words), std::end(words)));
words
中的初始序列不是最小排列序列,但是while
循环继续,直到words
包含最小值。do-while 循环然后输出完整的集合。如果您想执行这个片段,请记住它将产生8!
,这是输出的40,320
行,因此您可以考虑首先减少words
中的元素数量。
元素序列的最小排列是当每个元素小于或等于后面的元素时,因此您可以使用min_element()
算法返回一个指向某个范围内最小元素的迭代器,同时使用iter_swap()
算法交换两个迭代器指向的元素以创建最小排列,如下所示:
std::vector<string> words {"one","two", "three", "four", "five", "six", "seven", "eight"};
for (auto iter = std::begin(words); iter != std::end(words)-1 ;++iter)
std::iter_swap(iter, std::min_element(iter, std::end(words)));
for
循环从容器范围的第一个到倒数第二个遍历迭代器。作为 for 循环主体的语句将iter
指向的元素与min_element()
返回的迭代器指向的元素交换。这将最终产生最小排列,您可以将它用作next_permutation()
生成所有排列的起点。
您可以在开始创建所有排列之前,通过创建原始容器的副本并更改do-while
循环来避免达到最小排列的所有开销:
std::vector<string> words {"one","two", "three", "four", "five", "six", "seven", "eight"};
auto words_copy = words; // Copy the original
do
{
std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
std::next_permutation(std::begin(words), std::end(words));
} while(words != words_copy); // Continue until back to the original
该循环现在继续创建新的排列,直到到达原始排列。
下面是一个工作示例,它查找一个单词中所有字母的排列:
// Ex7_03.cpp
// Finding rearrangements of the letters in a word
#include <iostream> // For standard streams
#include <iterator> // For iterators and begin() and end()
#include <string> // For string class
#include <vector> // For vector container
#include <algorithm> // For next_permutation()
using std::string;
int main()
{
std::vector<string> words;
string word;
while(true)
{
std::cout << "\nEnter a word, or Ctrl+z to end: ";
if((std::cin >> word).eof()) break;
string word_copy {word};
do
{
words.push_back(word);
std::next_permutation(std::begin(word), std::end(word));
} while(word != word_copy);
size_t count{}, max{8};
for(const auto& wrd : words)
std::``cout
std::cout << std::endl;
words.clear(); // Remove previous permutations
}
}
它从标准输入流中读入一个单词到word
,在word_copy
中复制一份,然后将word
中所有字母的排列存储到words
容器中。程序继续处理单词,直到你输入Ctrl+Z
。word 的副本用于决定何时存储所有排列。然后排列被写入标准输出流,8 个一行。我已经说过,排列的数目随着被排列的元素数目迅速增加,所以不要用长词来尝试这个。这个例子并不是很有用,但是我会在第九章的中重新访问这个程序,其中介绍了更多使用 STL 文件的细节。在那里,可以读取一个包含大量英语单词的文件,并搜索这些单词来确定哪些排列是有效的单词。因此,程序找到原始单词的变位词并输出它们。
您可以提供一个 function 对象作为第三个参数给next_permutation()
,它定义了一个比较函数,作为缺省函数的替代。下面是如何使用这个版本通过比较最后几个字母来生成单词序列的排列:
std::vector<string> words {"one", "two", "four", "eight"};
do
{
std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
} while(std::next_permutation(std::begin(words), std::end(words),
[](const string& s1, const string& s2){return s1.back() < s2.back(); }));
这段代码使用作为最后一个参数传递给next_permutation()
的 lambda 表达式来生成words
中元素的所有 24 种排列。
next_permutation()
算法按升序字典顺序生成排列。当你想产生降序排列时,你可以使用prev_permutation()
算法。这与next_permutation()
有相同的两个版本,默认情况下使用<
来比较元素。因为排列是按降序生成的,所以该算法在大多数情况下返回 true,并且当它创建的排列是最大排列时返回false
。例如:
std::vector<double> data {44.5, 22.0, 15.6, 1.5};
do
{
std::copy(std::begin(data), std::end(data), std::ostream_iterator<double> {std::cout, " "});
std::cout << std::endl;
} while(std::prev_permutation(std::begin(data), std::end(data)));
该代码输出data
中四个double
值的所有二十四种排列,因为初始序列是最大值,而prev_permutation()
仅在输入序列是最小值时返回false
。
您可以使用is_permutation()
算法测试一个序列是否是另一个序列的排列,如果是这种情况,该算法将返回true
。下面是一些代码,展示了这个算法在 lambda 表达式中的应用:
std::vector<double> data1 {44.5, 22.0, 15.6, 1.5};
std::vector<double> data2 {22.5, 44.5, 1.5, 15.6};
std::vector<double> data3 {1.5, 44.5, 15.6, 22.0};
auto test = [](const auto& d1, const auto& d2)
{
std::copy(std::begin(d1), std::end(d1), std::ostream_iterator<double> {std::cout, " "});
std::cout << (is_permutation(std::begin(d1), std::end(d1), std::begin(d2), std::end(d2)) ?
"is": "is not")
<< " a permutation of ";
std::copy(std::begin(d2), std::end(d2), std::ostream_iterator<double> {std::cout, " "});
std::cout << std::endl;
};
test(data1, data2);
test(data1, data3);
test(data3, data2);
使用auto
指定test
lambda 的参数类型,这导致编译器将实际类型推断为const std::vector<double>&
。使用auto
来指定参数类型的 Lambda 表达式被称为泛型 lambda。test
λ表达式使用is_permutation()
来评估一个参数是否是另一个参数的排列。该算法的参数是两对迭代器,它们定义了要比较的范围。返回的bool
值的参数用于选择两个可能的字符串之一进行输出。输出将是:
44.5 22 15.6 1.5 is not a permutation of 22.5 44.5 1.5 15.6
44.5 22 15.6 1.5 is a permutation of 1.5 44.5 15.6 22
1.5 44.5 15.6 22 is not a permutation of 22.5 44.5 1.5 15.6
还有另一个版本的is_permutation()
允许第二个范围仅由 begin 迭代器指定。在这种情况下,第二个范围可以包含比第一个范围更多的元素,但是只考虑第一个范围包含的元素数量。但是,我建议您不要使用它,因为如果第二个范围包含的元素比第一个范围少,它会导致未定义的行为。我将展示一些使用它的代码。您可以向data3
添加元素,元素的初始序列仍然表示data1
的排列。例如:
std::vector<double> data1 {44.5, 22.0, 15.6, 1.5};
std::vector<double> data3 {1.5, 44.5, 15.6, 22.0, 88.0, 999.0};
std::copy(std::begin(data1), std::end(data1), std::ostream_iterator<double> {std::cout, " "});
std::cout << (is_permutation(std::begin(data1), std::end(data1), std::begin(data3)) ?
"is": "is not")
<< " a permutation of ";
std::copy(std::begin(data3), std::end(data3), std::ostream_iterator<double> {std::cout, " "});
std::cout << std::endl;
这将确认data1
是data3
的排列,因为只考虑了data3
中的前四个元素。您可以在任一版本的is_permutation()
中添加一个额外的参数来指定要使用的比较。
你可以使用shuffle()
算法来创建一个范围的随机排列,但是我将把这个讨论推迟到第八章详细讨论 STL 提供的随机数生成能力。
复制范围
本节讨论复制区域的算法;但是不要忘记,当你想把一个容器中的全部内容转移到另一个容器时,你还有其他的可能性。容器定义了赋值操作符,该操作符将一个容器的全部内容复制到同类型的另一个容器中。也有一些容器的构造函数接受一个范围作为初始内容的来源。大多数情况下,本节中的算法用于复制容器中元素的子集。
你已经看到了许多copy()
算法的应用,所以你知道它是如何工作的。它将元素从作为输入迭代器的前两个参数定义的源范围复制到从第三个参数(必须是输出迭代器)指定的位置开始的目标范围。你还有三种算法,它们提供的不仅仅是简单的复制过程。
复制一些元素
copy_n()
算法将特定数量的元素从源复制到目的地。第一个参数是指向第一个源元素的输入迭代器,第二个参数是要复制的元素数量,第三个参数是指向目标中第一个位置的输出迭代器。该算法返回一个迭代器,该迭代器指向最后一个复制的元素之后的一个元素,或者如果第二个参数为零,则只返回第三个参数——输出迭代器。下面是一个使用它的例子:
std::vector<string> names {"Al", "Beth", "Carol", "Dan", "Eve",
"Fred", "George", "Harry", "Iain", "Joe"};
std::unordered_set<string> more_names {"Janet", "John"};
std::copy_n(std::begin(names) + 1, 3, std::inserter(more_names, std::begin(more_names)));
copy_n()
操作从第二个名字开始将三个元素从names
容器复制到关联容器more_names
。目的地由inserter()
函数模板创建的unordered_set
容器的insert_iterator
对象指定。insert_iterator
对象通过调用其insert()
成员向容器中添加元素。
当然,copy_n()
操作中的目的地可以是一个流迭代器:
std::copy_n(std::begin(more_names), more_names.size()-1,
std::ostream_iterator<string> {std::cout, " "});
这将输出more_names
中除最后一个以外的所有元素。请注意,如果要复制的元素数量超过了可用的数量,您的程序将会陷入困境。如果元素数为零或负数,copy_n()
算法什么也不做。
条件复制
copy_if()
算法从一个谓词返回true
的源范围中复制元素,因此您可以把它看作是一个过滤器。前两个参数是定义源范围的输入迭代器,第三个参数是指向目标范围中第一个位置的输出迭代器,第四个参数是谓词。返回一个输出迭代器,它指向最后一个被复制的元素之后的一个元素。这里有一个使用copy_if()
的例子:
std::vector<string> names {"Al", "Beth", "Carol", "Dan", "Eve",
"Fred", "George", "Harry", "Iain", "Joe"};
std::unordered_set<string> more_names {"Jean", "John"};
size_t max_length{4};
std::copy_if(std::begin(names), std::end(names), std::inserter(more_names, std::begin(more_names)),
max_length{ return s.length() <= max_length; });
这里的copy_if()
操作只复制来自names
的四个字符或更少的元素,因为这是第四个参数 lambda 表达式强加的条件。目的地是unordered_set
集装箱more_names
,它已经包含了两个四个字母的名字。与上一节一样,insert_iterator
将符合条件的元素添加到关联容器中。如果您想证明它是有效的,您可以使用copy()
算法列出more_names
的内容:
std::copy(std::begin(more_names), std::end(more_names), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
当然,copy_if()
的目的地也可以是一个流迭代器:
std::vector<string> names {"Al", "Beth", "Carol", "Dan", "Eve",
"Fred", "George", "Harry", "Iain", "Joe"};
size_t max_length{4};
std::copy_if(std::begin(names), std::end(names), std::ostream_iterator<string> {std::cout, " "},
max_length { return s.length() > max_length; });
std::cout << std::endl;
这将把具有五个或更多字符的名称从names
容器写入标准输出流。这将输出:
Carol George Harry
您可以使用输入流迭代器作为copy_if()
算法的源代码,就像您可以使用其他需要输入迭代器的算法一样。这里有一个例子:
std::unordered_set<string> names;
size_t max_length {4};
std::cout << "Enter names of less than 5 letters. Enter Ctrl+Z on a separate line to end:\n";
std::copy_if(std::istream_iterator<string>{std::cin}, std::istream_iterator<string>{}, std::inserter(names, std::begin(names)),
max_length { return s.length() <= max_length; });
std::copy(std::begin(names), std::end(names), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
names
容器是一个最初为空的unordered_set
。copy_if()
算法复制从标准输入流中读取的名字,但只限于四个或更少的字符。执行这段代码会产生以下输出:
Enter names of less than 5 letters. Enter Ctrl+Z on a separate line to end:
Jim Bethany Jean Al Algernon Bill Adwina Ella Frederick Don
^Z
Ella Jim Jean Al Bill Don
从cin
中读取超过五个字母的名称,但将其丢弃,因为在这些情况下,第四个参数指定的谓词返回false
。因此,输入的十个名字中只有六个存储在容器中。
逆序复制
不要被copy_backward()
算法的名字误导了。它不会颠倒元素的顺序。它就像copy()
算法一样复制,但是从最后一个元素开始,并返回到第一个元素。copy_backward()
算法复制由前两个迭代器参数指定的范围。第三个参数是目标区域的结束迭代器,通过将源区域的最后一个元素复制到目标区域结束迭代器之前的元素,源区域被复制到目标区域,如图 7-1 所示。copy_backward()
的三个参数都必须是双向迭代器,即可以递增或递减的迭代器。这意味着该算法只能应用于序列容器中的范围。
图 7-1。
How copy_backward()
works
图 7-1 显示了如何将源范围from
中的最后一个元素首先复制到目标范围to
中的最后一个元素。从源位置向后穿过源范围的每个后续元素都被复制到目标位置上前一个元素之前的位置。在执行操作之前,目标中的元素必须存在,因此目标中的元素必须至少与源中的元素一样多,但也可以更多。copy_backward()
算法返回一个迭代器,该迭代器指向最后一个被复制的元素,这将是该区域在新位置的开始迭代器。
您可能想知道copy_backward()
与从第一个元素开始复制元素的常规copy()
算法相比有什么优势。一个答案是当范围重叠时。您可以使用copy()
将元素复制到左边的重叠目标区域——也就是说,复制到源区域中第一个元素之前的位置。如果您试图使用copy()
将相同范围内的元素复制到右边,该操作将不起作用,因为仍要复制的元素将在被复制之前被覆盖。当你想向右复制时,你可以使用copy_backward()
,只要目标区域的末端在源区域末端的右边。图 7-2 说明了在重叠范围之间向右复制时两种算法的区别。
图 7-2。
Copying overlapping ranges to the right
图 7-2 显示了将copy()
和copy_backward()
算法应用于右侧前三个位置的结果。很明显,当复制到右边时,copy()
算法不能做你想要的,因为一些元素在被复制之前就被覆盖了。在这种情况下,copy_backward()
算法确实做了你想做的事情。当在一个范围内向左复制时,情况正好相反- copy()
有效,但copy_backward()
无效。
这里有一些代码来说明copy_backward()
的作用:
std::deque<string> song{"jingle", "bells", "jingle", "all", "the", "way"};
song.resize(song.size()+2); // Add 2 elements
std::copy_backward(std::begin(song), std::begin(song)+6, std::end(song));
std::copy(std::begin(song), std::end(song), std::ostream_iterator<string>{std::cout, " "});
std::cout << std::endl;
通过使用其resize()
成员创建反向序列复制操作所需的额外元素,增加了deque
容器中的元素数量。copy_backward()
算法将原始元素向右复制两个位置,保留前两个元素不变,因此这段代码的输出将是:
jingle bells jingle bells jingle all the way
复制和反转元素的顺序
reverse_copy()
算法将一个源区域复制到一个目标区域,这样目标区域中的元素顺序相反。源范围由前两个迭代器参数定义,必须是双向的。目的地由第三个参数标识,这是一个输出迭代器,是目的地的 begin 迭代器。如果范围重叠,则行为未定义。该算法返回一个输出迭代器,它指向目标范围中最后一个元素之后的一个元素。这里有一个使用reverse_copy()
和copy_if()
的工作示例:
// Ex7_04.cpp
// Testing for palindromes using reverse_copy()
#include <iostream> // For standard streams
#include <iterator> // For stream iterators and begin() and end()
#include <algorithm> // For reverse_copy() and copy_if()
#include <cctype> // For toupper() and isalpha()
#include <string>
using std::string;
int main()
{
while(true)
{
string sentence;
std::cout << "Enter a sentence or Ctrl+Z to end: ";
std::getline(std::cin, sentence);
if(std::cin.eof()) break;
// Copy as long as the characters are alphabetic & convert to upper case
string only_letters;
std::copy_if(std::begin(sentence), std::end(sentence), std::back_inserter(only_letters),
[](char ch) { return std::isalpha(ch); });
std::for_each(std::begin(only_letters), std::end(only_letters), [](char& ch) { ch = toupper(ch); });
// Make a reversed copy
string reversed;
std::reverse_copy(std::begin(only_letters), std::end(only_letters), std::back_inserter(reversed));
std::cout << '"' << sentence << '"'
<< (only_letters == reversed ? " is" : " is not") << " a palindrome." << std::endl;
}
}
这个程序检查一个句子(或者许多句子)是否代表一个回文;回文是这样一个句子,如果你忽略了空格和标点符号这样的小细节,它的前后读起来是一样的。循环允许你检查尽可能多的句子。使用getline()
将一个句子读入sentence
。如果只读取了Ctrl+Z
,则将为输入流设置EOF
标志,这将终止循环。使用copy_if()
将sentence
中的字母复制到only_letters
。lambda 表达式只为字母返回true
,因此任何其他字符都将被忽略。由back_inserter()
创建的back_insert_iterator
对象将字符附加到only_letters
。for_each()
算法将第三个参数指定的函数应用于由前两个参数定义的范围内的元素,因此这里它将only_letters
中的字符转换为大写。使用reverse_copy()
算法在reverse
中创建only_letters
内容的反向副本。比较only_letters
和reversed
确定输入是否是回文。
以下是一些输出示例:
Enter a sentence or Ctrl+Z to end: Lid off a daffodil.
"Lid off a daffodil." is a palindrome.
Enter a sentence or Ctrl+Z to end: Engage le jeu que je le gagne.
"Engage le jeu que je le gagne." is a palindrome.
Enter a sentence or Ctrl+Z to end: Sit on a potato pan Otis!
"Sit on a potato pan Otis!" is a palindrome.
Enter a sentence or Ctrl+Z to end: Madam, I am Adam.
"Madam, I am Adam." is not a palindrome.
Enter a sentence or Ctrl+Z to end: Madam, I’m Adam.
"Madam, I’m Adam." is a palindrome.
Enter a sentence or Ctrl+Z to end: ^Z
回文很难创建,但是一个法国人乔治·佩雷克成功地创建了一个包含一千多个单词的回文。
reverse()
算法将由两个双向迭代器参数指定的范围内的元素就地反转。你可以在Ex7_04.cpp
中使用这个来代替reverse_copy()
——就像这样:
string reversed {only_letters};
std::reverse(std::begin(reversed), std::end(reversed));
这两条语句将取代在Ex7_04.cpp
中对reversed
和reverse_copy()
调用的定义。他们创造了reversed
作为only_letters
的复制品。调用reverse()
然后将reversed
中的字符顺序颠倒过来。
复制一个区域,删除相邻的重复项
unique_copy()
将一个范围复制到另一个范围,同时删除连续的重复元素。默认情况下,它使用==
操作符来决定元素何时相等。前两个参数是指定源的迭代器,第三个参数是指向目标中第一个元素的输出迭代器。可选的第四个参数接受一个函数对象,该对象定义了一个对==
操作符的替代。该算法返回一个输出迭代器,它指向目标中最后一个元素之后的一个元素。
复制一个序列,如1
、1
、2
、2
、3
,将导致目的地包含1
、2
、3
。因为只消除相邻的重复项,所以将复制序列中的所有元素,如1
、2
、1
、2
、3
。当然,如果源区域已经排序,所有重复的区域都将被删除,因此目标区域将包含唯一的元素。
下面是一些显示应用于字符串中字符的unique_copy()
的代码:
string text {"Have you seen how green the trees seem?"};
string result{};
std::unique_copy(std::begin(text), std::end(text), std::back_inserter(result));
std::cout << result << std::endl;
复制操作的源是整个字符串text
,目的地是result
的back_insert_iterator
,所以每个被复制的字符将被附加到result
。这输出了几乎无用的句子:
Have you sen how gren the tres sem?
尽管输出确认了unique_copy()
消除了相邻的重复。
当你提供你自己的比较对象时,你并不局限于一个简单的等式——你可以把它变成你喜欢的。这使得有可能选择不被复制的重复元素。下面的代码展示了如何从字符串中删除重复的空格:
string text {"there’s no air in spaaaaaace!"};
string result {};
std::unique_copy(std::begin(text), std::end(text), std::back_inserter(result),
[](char ch1, char ch2) { return ch1 == ' ' && ch1 == ch2; });
std::cout << result << std::endl;
unique_copy()
的第四个参数是一个 lambda 表达式,仅当两个参数都是空格时才返回true
。执行此代码会产生以下输出:
There’s no air in spaaaaaace!
这表明空格已经被删除,但是spaaaaaace
中的a
没有被删除。
从范围中删除相邻的重复项
您还可以使用unique()
算法来删除序列中的重复项。这需要前向迭代器来指定要处理的范围。它返回一个前向迭代器,该迭代器是删除重复项后新范围的结束迭代器。您可以提供一个 function 对象作为可选的第三个参数,该参数定义了用于比较元素的==的替代。这里有一个例子:
std::vector<string> words {"one", "two", "two", "three", "two", "two", "two"};
auto end_iter = std::unique(std::begin(words), std::end(words));
std::copy(std::begin(words), end_iter, std::ostream_iterator<string>{std::cout, " "});
std::cout << std::endl;
这通过覆盖来消除words
中的连续元素。输出将是:
one two three two
当然,不会从输入范围中删除任何元素;该算法无法移除元素,因为它不知道它们的上下文。整个系列仍将存在。然而,如果我在上面的代码中使用std::end(words)
而不是end_iter
来输出结果,那么在新的 end 之外的元素的状态没有保证,我在我的系统上得到这样的输出:
one two three two two two
同样数量的元素仍然存在,但是新的 end 迭代器指向的元素只是空字符串;最后两个元素和之前一样。在您的系统上,结果可能有所不同。正因为如此,在执行unique()
之后截断原始范围是个好主意,就像这样:
auto end_iter = std::unique(std::begin(words), std::end(words));
words.erase(end_iter, std::end(words));
std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
容器的erase()
成员从新的末端迭代器中移除元素,因此end(words)
将返回end_iter
。
当然,您可以将unique()
应用于字符串中的字符:
string text {"there’s no air in spaaaaaace!"};
text.erase(std::unique(std::begin(text), std::end(text),
[](char ch1, char ch2) { return ch1 == ' ' && ch1 == ch2; }),
std::end(text));
std::cout << text << std::endl; // Outputs: there’s no air in spaaaaaace!
这使用unique()
从text
字符串中删除相邻的重复空格。代码使用迭代器,迭代器由unique()
作为第一个参数返回给text
的erase()
成员,并指向第一个要删除的字符。erase()
的第二个参数是text
的结束迭代器,所以新字符串后面没有重复空格的所有字符都被删除。
旋转范围
rotate()
算法向左旋转一系列元素。其工作原理如图 7-3 所示。为了理解旋转范围的工作原理,您可以将范围中的元素想象成手镯上的珠子。rotate()
操作使得一个新元素成为 begin 迭代器指向的第一个元素。旋转后,最后一个元素是新的第一个元素之前的元素。
图 7-3。
How the rotate() algorithm works
rotate()
的第一个参数是范围的开始迭代器;第二个参数是一个迭代器,指向新的第一个元素应该是什么,它必须在范围内;第三个参数是范围的结束迭代器。图 7-3 中的例子显示了ns
容器上的rotate()
操作使得值为 4 的元素成为新的第一个元素,最后一个元素的值为 3。元素的循环顺序保持不变,因此它实际上只是旋转元素的循环,直到新的第一个元素成为范围的开始。该算法返回一个迭代器,指向新位置的原始第一个元素。这里有一个例子:
std::vector<string> words {"one", "two", "three", "four", "five", "six", "seven", "eight"};
auto iter = std::rotate(std::begin(words), std::begin(words)+3, std::end(words));
std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl << "First element before rotation: " << *iter << std::endl;
这段代码将旋转应用于words
中的所有元素。执行这段代码将产生以下输出:
four five six seven eight one two three
First element before rotation: one
输出表明"four"
是新的第一个元素,并且rotate()
返回的迭代器确实指向了之前的第一个元素"one".
当然,您旋转的范围不必是容器中的所有元素。例如:
std::vector<string> words {"one", "two", "three", "four", "five",
"six", "seven", "eight", "nine", "ten"};
auto start = std::find(std::begin(words), std::end(words), "two");
auto end_iter = std::find(std::begin(words), std::end(words), "eight");
auto iter = std::rotate(start, std::find(std::begin(words), std::end(words), "five"), end_iter);
std::copy(std::begin(words), std::end(words), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl << "First element before rotation: " << *iter << std::endl;
它使用find()
算法获得指向words
中匹配“二”和“八”的元素的迭代器。这些定义了要旋转的范围,它是容器中元素的子集。这个范围被旋转以使"five"
成为第一个元素,输出显示它按预期工作:
one five six seven two three four eight nine ten
First element before rotation: two
rotate_copy()
算法在一个新的范围内生成一个范围的旋转副本,而不影响原始副本。rotate_copy()
的前三个参数与rotate()
的相同;第四个参数是一个输出迭代器,指向目标范围的第一个元素。该算法返回目的地的输出迭代器,该迭代器指向复制的最后一个元素之后的一个元素。这里有一个例子:
std::vector<string> words {"one", "two", "three", "four", "five",
"six", "seven", "eight", "nine", "ten"};
auto start = std::find(std::begin(words), std::end(words), "two");
auto end_iter = std::find(std::begin(words), std::end(words), "eight");
std::vector<string> words_copy;
std::rotate_copy(start, std::find(std::begin(words), std::end(words), "five"), end_iter,
std::back_inserter(words_copy));
std::copy(std::begin(words_copy), std::end(words_copy), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
这产生了从words
开始从"two"
到"seven"
的元素的旋转副本。使用一个back_insert_iterator
将复制的元素添加到words_copy
容器中,该容器将调用words_copy
的push_back()
成员来插入每个元素。这段代码产生的输出是:
five six seven two three four
rotate_copy()
在这里返回的迭代器是words_copy
中元素的结束迭代器。这段代码中没有记录或使用它,但它可能很有用。例如:
std::vector<string> words {"one", "two", "three", "four", "five",
"six", "seven", "eight", "nine", "ten"};
auto start = std::find(std::begin(words), std::end(words), "two");
auto end_iter = std::find(std::begin(words), std::end(words), "eight");
std::vector<string> words_copy {20}; // vector with 20 default elements
auto end_copy_iter = std::rotate_copy(start, std::find(std::begin(words), std::end(words), "five"), end_iter, std::begin(words_copy));
std::copy(std::begin(words_copy), end_copy_iter, std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
words_copy
容器是用二十个默认元素创建的。rotate_copy()
算法现在将旋转后的范围存储在words_copy
的现有元素中,从开始处开始。算法返回的迭代器用于标识输出的words_copy
中范围的结束;如果没有它,我们将不得不根据源范围中的元素数量来计算。
移动范围
move()
算法将由前两个输入迭代器参数指定的范围移动到目的地,从第三个参数定义的位置开始,第三个参数必须是输出迭代器。该算法返回一个迭代器,该迭代器的值超过了移动到目的地的最后一个元素。这是一个移动操作,所以不能保证操作后元素的输入范围保持不变;源元素仍将存在,但可能不具有相同的值,因此在移动后不应使用它们。如果源范围要被替换,或者要被破坏,你可以使用move()
算法。如果你需要源范围不受干扰,使用copy()
算法。下面是一个展示如何使用它的示例:
std::vector<int> srce {1, 2, 3, 4};
std::deque<int> dest {5, 6, 7, 8};
std::move(std::begin(srce), std::end(srce), std::back_inserter(dest));
这将把所有来自vector
容器srce
的元素追加到deque
容器dest
中。要替换dest
中的现有元素,您可以使用std::begin(dest)
作为move()
的第三个参数。只要目标中的第一个元素在源范围之外,就可以使用move()
将元素移动到与源范围重叠的目标中;这意味着在该范围内向左移动。这里有一个例子:
std::vector<int> data {1, 2, 3, 4, 5, 6, 7, 8};
std::move(std::begin(data) + 2, std::end(data), std::begin(data));
data.erase(std::end(data) - 2, std::end(data)); // Erase moved elements
std::copy(std::begin(data), std::end(data), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl; // 3, 4, 5, 6, 7, 8
这将把data
的最后六个元素移回到容器的开头。这是因为目标在源范围之外。移动后不能保证最后两个元素的值。这里的元素被删除了,但是你同样可以将它们重置为一个已知的值——比如零。结果显示在最后一行的注释中。当然,您可以使用rotate()
算法而不是move()
算法来移动元素,在这种情况下,您肯定会知道最后两个元素的值。
如果移动操作的目的地在源范围内,move()
将不能正常工作;这意味着在范围内向右移动。原因是有些元素在移动之前会被覆盖。尽管如此,move_backward()
算法仍然有效。前两个参数指定要移动的范围,第三个参数是目标的结束迭代器。这里有一个例子:
std::deque<int> data {1, 2, 3, 4, 5, 6, 7, 8};
std::move_backward(std::begin(data), std::end(data) - 2, std::end(data));
data[0] = data[1] = 0; // Reset moved elements
std::copy(std::begin(data), std::end(data), std::ostream_iterator<int> {std::cout, " "});
std::cout << std::endl; // 0, 0, 1, 2, 3, 4, 5, 6
我在这里使用了一个deque
容器,只是为了改变一下。这会将前六个元素向右移动两个位置。操作后其值不确定的元素被重置为 0。最后一行显示了操作的结果。
您可以使用swap_ranges()
算法交换两个范围。该算法需要三个前向迭代器参数。前两个参数是一个区域的开始和结束迭代器,第三个参数是第二个区域的开始迭代器。显然,范围必须是相同的长度。该算法为第二个范围返回一个迭代器,该迭代器指向经过最后一个交换的元素。这里有一个例子:
using Name = std::pair<string, string>; // First and second name
std::vector<Name> people {Name{"Al", "Bedo"}, Name{"Ann", "Ounce"}, Name{"Jo", "King"}};
std::list<Name> folks {Name{"Stan", "Down"}, Name{"Dan", "Druff"}, Name{"Bea", "Gone"}};
std::swap_ranges(std::begin(people),
std::begin(people) + 2, ++std::begin(folks));
std::for_each(std::begin(people), std::end(people),
[](const xName& name){std::cout << '"' << name.first << " " << name.second << "\" ";});
std::cout << std::endl; // "Dan Druff" "Bea Gone" "Jo King"
std::for_each(std::begin(folks), std::end(folks),
[](const Name& name){std::cout << '"' << name.first << " " << name.second << "\" "; });
std::cout << std::endl; // "Stan Down" "Al Bedo" "Ann Ounce"
vector
和list
容器存储代表名字的pair<string,string>
类型的元素。swap_ranges()
算法用于将people
中的前两个元素与folks
中的后两个元素进行交换。没有用于将pair
对象写入流的operator<<()
函数重载,因此copy()
不能与列出容器的输出流迭代器一起使用。我选择使用for_each()
算法,通过对容器中的每个元素应用 lambda 表达式来产生输出。lambda 表达式只是将传递给它的Name
元素的成员写入标准输出流。注释显示了执行这段代码的输出。
有一个函数模板重载了在原型的utility
头中定义的swap()
算法:
template<typename T1, typename T2> void swap(std::pair<T1,T2> left, std::pair<T1,T2> right);
这交换了pair<T1,T2>
对象,并被swap_ranges()
用来交换前面代码片段中的元素。
交换两个相同类型的对象T
的swap()
模板也在utility
头中定义。除了对pair
对象的重载之外,在utility
头中还有模板重载,它将交换任意给定类型的两个容器对象。也就是说,它将交换两个list<T>
容器或两个set<T>
容器,而不是一个list<T>
与一个vector<T>
或一个list<T1>
与一个list<T2>
。另一个swap()
模板重载可以交换两个相同类型的数组。还有几个其他的swap()
重载来交换其他类型的对象,包括tuple
和智能指针类型。正如你在本章前面看到的,iter_swap()
算法有点不同;它交换两个前向迭代器指向的元素。
从范围中删除元素
在不知道元素的上下文(存储元素的容器)的情况下,从一个范围中删除元素是不可能的。因此,“移除”元素的算法不会,它们只是覆盖选定的元素或忽略复制元素。移除操作不会改变“移除”的元素范围内的元素数量。有四种删除算法:
remove()
从前两个前向迭代器参数指定的范围中删除元素,这两个参数等于作为第三个参数的对象。实际上,每个匹配元素都是通过被后面的元素覆盖而被删除的。该算法返回一个迭代器,该迭代器指向范围中新的最后一个元素之后的一个元素。remove_copy()
将元素从前两个前向迭代器参数指定的范围复制到第三个参数标识的目标范围,忽略等于第四个参数的元素。该算法返回一个迭代器,它指向复制到目标范围的最后一个元素之后的一个元素。范围不得重叠。remove_if()
删除由前两个前向迭代器参数指定的范围内的元素,其中作为第三个参数的谓词返回true
。remove_copy_if()
将元素从前两个前向迭代器参数指定的范围复制到第三个参数标识的目标范围,第四个参数的谓词返回true
。该算法返回一个迭代器,它指向复制到目标的最后一个元素之后的一个元素。范围不得重叠。
下面是你如何使用remove()
:
std::deque<double> samples {1.5, 2.6, 0.0, 3.1, 0.0, 0.0, 4.1, 0.0, 6.7, 0.0};
samples.erase(std::remove(std::begin(samples), std::end(samples), 0.0), std::end(samples));
std::copy(std::begin(samples), std::end(samples), std::ostream_iterator<double> {std::cout, " "});
std::cout << std::endl; // 1.5 2.6 3.1 4.1 6.7
samples
包含不应为零的物理测量值。remove()
算法通过向左移动其他元素来覆盖它们,从而消除虚假的零值。remove()
返回的迭代器是操作产生的元素范围的新结束,因此它被用作要通过调用samples
的erase()
成员来删除的范围的开始迭代器。注释显示了剩余的元素。
当您需要保留原始范围并创建一个新范围时,您可以使用remove_copy()
,该新范围是删除了所选元素的副本。例如:
std::deque<double> samples {1.5, 2.6, 0.0, 3.1, 0.0, 0.0, 4.1, 0.0, 6.7, 0.0};
std::vector<double> edited_samples;
std::remove_copy(std::begin(samples), std::end(samples), std::back_inserter(edited_samples), 0.0);
非零元素从samples
容器复制到edited_samples
容器,这恰好是不同的——这是一个vector
。元素由一个back_insert_iterator
对象添加到edited_samples
,所以容器将只包含从samples
复制的元素。
remove_if()
算法提供了一个更强大的功能,可以从一个范围中删除只匹配一个值的元素。谓词决定是否删除元素;只要它接受范围中的一个元素作为参数并返回一个bool
值,任何事情都可以。这里有一个例子:
using Name = std::pair<string, string>; // First and second name
std::set<Name> blacklist {Name {"Al", "Bedo"}, Name {"Ann", "Ounce"}, Name {"Jo", "King"}};
std::deque<Name> candidates {Name {"Stan", "Down"}, Name {"Al", "Bedo"}, Name {"Dan", "Druff"},
Name {"Di", "Gress"}, Name {"Ann", "Ounce"}, Name {"Bea", "Gone"}};
candidates.erase(std::remove_if(std::begin(candidates), std::end(candidates),
&blacklist { return blacklist.count(name); }),
std::end(candidates));
std::for_each(std::begin(candidates), std::end(candidates), [](const Name& name)
{std::cout << '"' << name.first << " " << name.second << "\" "; });
std::cout << std::endl; // "Stan Down" "Dan Druff" "Di Gress" "Bea Gone"
这个代码模型处理不接受公众投票的俱乐部成员的申请。已知的麻烦制造者的名字存储在blacklist
容器中,这是一个set
。当前的会员申请存储在candidates
容器中,这是一个deque
。使用remove_if()
算法来确保没有来自blacklist
容器的名字通过选择过程。谓词是一个 lambda 表达式,它通过引用捕获blacklist
容器。当参数存在时,set
容器的count()
成员将返回1
。谓词返回的值被隐式转换为bool
,因此谓词实际上为出现在blacklist
中的candidates
中的每个元素返回true
,因此这些元素将从candidates
中移除。通过选择过程的候选人在评论中显示。
remove_copy_if()
之于remove_copy()
如同remove_if()
之于remove()
。以下是它的工作原理:
std::set<Name> blacklist {Name {"Al", "Bedo"}, Name {"Ann", "Ounce"}, Name {"Jo", "King"}};
std::deque<Name> candidates {Name {"Stan", "Down"}, Name {"Al", "Bedo"}, Name {"Dan", "Druff"},
Name {"Di", "Gress"}, Name {"Ann", "Ounce"}, Name {"Bea", "Gone"}};
std::deque<Name> validated;
std::remove_copy_if(std::begin(candidates), std::end(candidates), std::back_inserter(validated),
&blacklist { return blacklist.count(name); });
除了结果存储在validated
容器中并且不修改candidates
容器之外,这段代码与前面的片段完成了相同的任务。
设置和修改范围内的元素
fill()
和fill_n()
算法提供了一种用给定值填充一系列元素的简单方法。fill()
填充整个范围;fill_n()
从给定迭代器指向的元素开始,为指定的元素数量设置一个值。下面是fill()
的用法:
std::vector<string> data {12}; // Container has 12 elements
std::fill(std::begin(data), std::end(data), "none"); // Set all elements to "none"
fill()
的前两个参数是定义范围的前向迭代器。第三个参数是分配给每个元素的值。当然,该范围不必代表容器中的所有元素。例如:
std::deque<int> values(13); // Container has 13 elements
int n{2}; // Initial element value
const int step {7}; // Element value increment
const size_t count{3}; // Number of elements with given value
auto iter = std::begin(values);
while(true)
{
auto to_end = std::distance(iter, std::end(values));// Number of elements remaining
if(to_end < count) // In case no. of elements not a multiple of count
{
std::fill(iter, iter + to_end, n); // Just fill remaining elements...
break; // ...and end the loop
}
else
{
std::fill(iter, std::end(values), n); // Fill next count elements
}
iter = std::next(iter, count); // Increment iter
n += step;
}
用13
元素创建了values
容器。在这种情况下,必须使用圆括号将值传递给构造函数;使用大括号将创建一个包含值为 13 的单个元素的容器。在循环中,fill()
算法被用来给count
元素的序列赋值。iter
从容器的 begin 迭代器开始,如果还有足够多的元素,它会在每次循环迭代中增加count
,从而指向下一个序列中的第一个元素。执行此操作会将values
中的元素设置为:
2 2 2 9 9 9 16 16 16 23 23 23 30
fill_n()
的参数是一个前向迭代器,指向要修改的范围中的第一个元素、要修改的元素数量以及要设置的值。distance()
和next()
功能在iterator
标题中定义。前者使用输入迭代器,但后者需要前向迭代器。
用函数生成元素值
您已经看到,您可以使用for_each()
算法将一个函数对象应用于一个范围内的每个元素。function 对象有一个参数,该参数引用由算法的前两个参数定义的范围内的元素,因此它可以直接更改存储的值。generate()
算法略有不同。前两个参数是指定范围的前向迭代器,第三个参数是定义函数形式的函数对象:
T fun(); // T is a type that can be assigned to an element in the range
从函数内部无法访问范围内元素的值。generate()
算法只是为范围内的每个元素存储函数返回的值;并且generate()
没有返回任何东西。要使算法有用,您需要能够生成不同的值,并将其分配给不带参数的函数中的不同元素。一种可能是将generate()
的第三个参数定义为一个函数对象,它捕获一个或多个外部变量。这里有一个例子:
string chars (30, ' '); // 30 space characters
char ch {'a'};
int incr {};
std::generate(std::begin(chars), std::end(chars), [ch, &incr]
{
incr += 3;
return ch + (incr % 26);
});
std::cout << chars << std::endl; // chars is: dgjmpsvybehknqtwzcfiloruxadgjm
chars
变量用一串 30 个空格字符初始化。作为generate()
的第三个参数的 lambda 表达式返回的值将存储在chars
的连续字符中。lambda 通过值捕获ch
,通过引用捕获incr
,因此后者可以在 lambda 的主体中修改。lambda 返回将incr
与ch
相加得到的字符,增量值以26
为模,因此返回值总是在'a'
到'z'
的范围内,假定起始值为'a'
。该操作的结果显示在注释中。有可能设计出一个 lambda,它适用于任何大写或小写字母,并且只生成存储在ch,
中的那种类型的字母,但是我将把它留给你作为练习。
generate_n()
算法的工作方式与generate()
相似。不同之处在于,第一个参数仍然是范围的 begin 迭代器,而第二个参数是由第三个参数设置的元素数量的计数。该范围必须至少包含第二个参数定义的元素数,以避免程序崩溃。这里有一个例子:
string chars (30, ' '); // 30 space characters
char ch {'a'};
int incr {};
std::generate_n(std::begin(chars), chars.size()/2,[ch, &incr]
{
incr += 3;
return ch + (incr % 26);
});
这里,chars
中只有一半的字符会有算法设置的新值。后半部分将保留为空格字符。
转换范围
transform()
算法将一个函数应用于一个范围内的元素,并将该函数返回的值存储在另一个范围内。它返回一个迭代器,指向输出范围中存储的最后一个元素之后的一个元素。该算法的一个版本与for_each()
的相似之处在于,您将一元函数应用于一系列可以修改其值的元素,但也有显著的不同。使用for_each()
应用的函数必须有一个 void 返回类型,您可以通过函数的引用参数改变输入范围中的值。使用transform()
一元函数必须返回一个值,并且您有可能将应用该函数的结果存储在另一个范围中,并且输出范围中的元素可以是与输入范围不同的类型。还有另一个区别:使用for_each()
,函数总是按顺序应用于元素,但是使用transform()
不能保证。
transform()
的第二个版本允许将二元函数应用于两个范围内的相应元素,但是让我们先来看看将一元函数应用于一个范围。在这个版本的算法中,前两个参数是定义输入范围的输入迭代器,第三个参数是目标中第一个元素的输出迭代器,第四个参数是一元函数。该函数必须接受输入范围中的一个元素作为参数,并且必须返回一个可以存储在输出范围中的值。这里有一个例子:
std::vector<double> deg_C {21.0, 30.5, 0.0, 3.2, 100.0};
std::vector<double> deg_F(deg_C.size());
std::transform(std::begin(deg_C), std::end(deg_C), std::begin(deg_F),
[](double temp){ return 32.0 + 9.0* temp/5.0; }); // Result 69.8 86.9 32 37.76 212
transform()
算法调用将deg_C
容器中的摄氏温度值转换为华氏温度,并将结果存储在deg_F
容器中。用存储所有结果所需的元素数量创建了deg_F
容器,因此第三个参数是deg_F
的 begin 迭代器。您可以使用一个back_insert_iterator
作为transform()
的第三个参数,将结果存储在一个空容器中:
std::vector<double> deg_F; // Empty container
std::transform(std::begin(deg_C), std::end(deg_C), std::back_inserter(deg_F),
[](double temp){ return 32.0 + 9.0* temp/5.0; }); // Result 69.8 86.9 32 37.76 212
存储操作结果的元素由back_insert_iterator
在deg_F
中创建;结果是一样的。
第三个参数可以是指向输入容器中元素的迭代器。例如:
std::vector<double> temps {21.0, 30.5, 0.0, 3.2, 100.0}; // In Centigrade
std::transform(std::begin(temps), std::end(temps), std::begin(temps),
[](double temp) { return 32.0 + 9.0* temp / 5.0; }); // Result 69.8 86.9 32 37.76 212
这将把temps
容器中的数值从摄氏温度转换为华氏温度。第三个参数是输入范围的 begin 迭代器,因此应用由第四个参数指定的函数的结果被存储回它所应用的元素中。
下面的代码说明了目标区域与输入区域的类型不同的情况:
std::vector<string> words {"one", "two", "three", "four", "five"};
std::vector<size_t> hash_values;
std::transform(std::begin(words), std::end(words), std::back_inserter(hash_values),
std::hash<string>()); // string hashing function
std::copy(std::begin(hash_values), std::end(hash_values),
std::ostream_iterator<size_t> {std::cout, " "});
std::cout << std::endl;
输入范围包含string
对象,应用于元素的函数是在string
头中定义的标准散列函数对象。哈希函数返回类型为size_t,
的哈希值,这些值使用back_inserter()
助手函数从iterator
头返回的back_insert_iterator
对象存储在hash_values
容器中。在我的系统上,这段代码会产生以下输出:
3123124719 3190065193 2290484163 795473317 2931049365
您的系统可能会产生不同的输出。注意,因为目标范围被指定为一个back_insert_iterator
对象,这里的transform()
算法将返回一个back_insert_iterator<vector<size_T>>
类型的迭代器,所以您不能将它用作copy()
算法的输入范围的结束迭代器。为了利用transform()
返回的迭代器,代码应该是:
std::vector<string> words {"one", "two", "three", "four", "five"};
std::vector<size_t> hash_values(words.size());
auto end_iter = std::transform(std::begin(words), std::end(words), std::begin(hash_values),
std::hash<string>()); // string hashing function
std::copy(std::begin(hash_values), end_iter, std::ostream_iterator<size_t> {std::cout, " "});
std::cout << std::endl;
现在transform()
返回hash_values
容器中元素范围的结束迭代器。
没有什么可以阻止您从函数内部调用算法,该函数由transform()
函数应用于一系列元素。这里有一个可能性的例子:
std::deque<string> names {"Stan Laurel", "Oliver Hardy", "Harold Lloyd"};
std::transform(std::begin(names), std::end(names), std::begin(names),
[](string& s) { std::transform(std::begin(s), std::end(s),std::begin(s), ::toupper);
return s;
});
std::copy(std::begin(names), std::end(names), std::ostream_iterator<string> {std::cout, " "});
std::cout << std::endl;
transform()
算法将 lambda 表达式定义的函数应用于names
容器中的元素。lambda 表达式调用transform()
将cctype
头中定义的toupper()
函数应用于传递给它的字符串中的每个字符。最终结果是将names
中的每个元素转换成大写,因此输出将是:
STAN LAUREL OLIVER HARDY HAROLD LLOYD
当然,还有其他可能更简单的方法来达到同样的效果。
应用二元函数的版本transform()
需要五个参数:
- 前两个参数是第一个输入范围的输入迭代器。
- 第三个参数是第二个输入范围的 begin 迭代器,显然,这个范围必须包含至少与第一个输入范围一样多的元素。
- 第四个参数是输出迭代器,它是存储函数应用结果的范围的开始迭代器。
- 第五个参数是一个 function 对象,它定义了一个具有两个参数的函数,该函数将接受来自两个输入范围的元素参数,并返回一个可存储在输出范围中的值。
让我们考虑一些简单的几何计算的例子。折线是点之间的一系列直线。折线可以由一个Point
对象的vector
来表示,折线中的线段是连接连续点的线。如果最后一个点与第一个点相同,折线将是闭合的,即多边形。图 7-4 显示了一个将Point
定义为类型别名的例子,如下所示:
图 7-4。
A polyline that represents a hexagon
using Point = std::pair<double, double>; // pair<x,y> defines a point
有七个点,所以图 7-4 中的hexagon
物体有六条线段。由于第一个点和最后一个点是相同的,这六条线段确实形成了一个多边形——一个六边形。我们可以使用transform()
算法计算线段的长度:
std::vector<Point> hexagon {{1,2}, {2,1}, {3,1}, {4,2}, {3,3}, {2,3}, {1,2}};
std::vector<double> segments; // Stores lengths of segments
std::transform(std::begin(hexagon), std::end(hexagon) - 1, std::begin(hexagon) + 1, std::back_inserter(segments), [](const Point& p1, const Point& p2)
{ return std::sqrt(
(p1.first-p2.first)*(p1.first - p2.first) + (p1.second - p2.second)*(p1.second - p2.second)); });
transform()
的第一个输入范围包含从第一个到倒数第二个的hexagon
中的Point
个对象。第二个输入范围从第二个Point
对象开始,因此二元函数的连续调用的参数将是点 1 和 2、点 2 和 3、点 3 和 4 等等,直到输入范围的最后两个点 6 和 7。图 7-4 显示了两点间距离的公式,x 1 ,y 1 和 x 2 ,y 2,以及作为transform()
最后一个参数的 lambda 表达式实现了这一点。由 lambda 表达式计算的每个片段长度都存储在segments
容器中。我们可以使用另外两种算法输出六边形的线段长度和总周长,如下所示:
std::cout << "Segment lengths: ";
std::copy(std::begin(segments), std::end(segments), std::ostream_iterator<double> {std::cout, " "});
std::cout << std::endl;
std::cout << "Hexagon perimeter: " << std::accumulate(std::begin(segments), std::end(segments), 0.0) << std::endl;
使用copy()
算法输出线段长度。accumulate()
函数将segments
中元素的值相加,得出周长的总和。
替换范围中的元素
replace()
算法用新值替换与给定值匹配的元素。前两个参数是要处理的范围的前向迭代器,第三个参数是要替换的值,第四个参数是新值。以下是它的工作原理:
std::deque<int> data {10, -5, 12, -6, 10, 8, -7, 10, 11};
std::replace(std::begin(data), std::end(data), 10, 99); // Result: 99 -5 12 -6 99 8 -7 99 11
这里,data
容器中所有匹配10
的元素都被替换为99
。
当谓词返回true
时,replace_if()
算法用新值替换元素。第三个参数是谓词,第四个参数是新值。参数类型通常是对元素类型的const
引用;const
不是强制性的,但是谓词不应该修改参数。这里有一个使用replace_if()
的例子:
string password {"This is a good choice!"};
std::replace_if(std::begin(password), std::end(password),
[](char ch){return std::isspace(ch);}, '_'); // Result: This_is_a_good_choice!
谓词为任何是空格字符的元素返回true
,因此这些将被下划线替换。
replace_copy()
算法做replace()
做的事情,但是结果存储在另一个范围内,原始结果保持不变。前两个参数是输入范围的前向迭代器,第三个是输出范围的 begin 迭代器,最后两个是要替换的值和替换。这里有一个例子:
std::vector<string> words {"one", "none", "two", "three", "none", "four"};
std::vector<string> new_words;
std::replace_copy(std::begin(words), std::end(words), std::back_inserter(new_words),
string{"none"}, string{"0"}); // Result: "one", "0", "two", "three", "0", "four"
执行这段代码后,new_words
将包含注释中所示的string
元素。
有选择地替换一个范围内元素的最后一个算法是replace_copy_if()
,它和replace_if()
做的一样,但是结果存储在另一个范围内。前两个参数是输入范围的迭代器,第三个参数是输出的 begin 迭代器,最后两个分别是谓词和替换值。这里有一个例子:
std::deque<int> data {10, -5, 12, -6, 10, 8, -7, 10, 11};
std::vector<int> data_copy;
std::replace_copy_if(std::begin(data), std::end(data),
std::back_inserter(data_copy),
[](int value) {return value == 10;}, 99); // Result: 99 -5 12 -6 99 8 -7 99 11
data_copy
容器是一个vector
,只是为了说明输出容器可以不同于输入容器。作为执行这段代码的结果,它将包含注释中显示的元素。
应用算法
我将在本章中创建最后一个工作示例,它将一些算法应用于在标准输出流上绘制曲线。这样会更现实一点。曲线将由代表x,y
个点的pair<double,double>
个对象的范围来定义。我们可以首先定义一个plot()
函数模板,它将在标准输出流上绘制一条曲线。模板类型参数将是定义范围的迭代器的类型,所以点可以来自任何序列容器,或者可能是一个数组。每个点将被标为星号,其中x
轴穿过页面,y
轴向下。因为这个输出是一个字符流,字体的纵横比会影响图形的纵横比。理想情况下,字体应该有相同的宽度和高度,我在我的系统上选择了 8×8 的字体。
plot()
函数的参数将是定义曲线上的点的范围的迭代器、指定输出曲线名称的字符串以及图形宽度中的字符数。最后两个参数将有默认值,允许它们被省略。点中的x
值的范围必须符合为绘图宽度指定的字符数。这将确定 x 在一个字符和下一个字符之间的步长。为了保持图形的纵横比,各行之间的y
值之间的步长(沿页面向下)将与x
值之间的步长相同。下面是plot()
函数模板的代码:
template<typename Iterator>
void plot(Iterator begin_iter, Iterator end_iter, string name = "Curve", size_t n_x = 100)
{ // n_x is plot width in characters, so it’s the number of characters along the x axis
// Comparison functions for x and for y
auto x_comp = [](const Point& p1, const Point& p2) {return p1.first < p2.first; };
auto y_comp = [](const Point& p1, const Point& p2) {return p1.second < p2.second; };
// Minimum and maximum x values
auto min_x = std::min_element(begin_iter, end_iter, x_comp)->first;
auto max_x = std::max_element(begin_iter, end_iter, x_comp)->first;
// Step length for output - same step applies to x and y
double step {(max_x - min_x) / (n_x + 1)};
// Minimum and maximum y values
auto min_y = std::min_element(begin_iter, end_iter, y_comp)->second;
auto max_y = std::max_element(begin_iter, end_iter, y_comp)->second;
size_t nrows {1 + static_cast<size_t>(1 + (max_y - min_y)/step)};
std::vector<string> rows(nrows, string(n_x + 1, ' '));
// Create x-axis at y=0 if this is within range of points
if(max_y > 0.0 && min_y <= 0.0)
rows[static_cast<size_t>(max_y/step)] = string(n_x + 1, '-');
// Create y-axis at x=0 if this is within range of points
if(max_x > 0.0 && min_x <= 0.0)
{
size_t x_axis {static_cast<size_t>(-min_x/step)};
std::for_each(std::begin(rows), std::end(rows),
x_axis { row[x_axis] = row[x_axis] == '-' ? '+' : '|'; });
}
std::cout << "\n\n " << name << ":\n\n";
// Generate the rows for output
auto y {max_y}; // Upper y for current output row
for(auto& row : rows)
{
// Find points to be included in an output row
std::vector<Point> row_pts; // Stores points for this row
std::copy_if(begin_iter, end_iter, std::back_inserter(row_pts),
&y, &step { return p.second < y + step && p.second >= y; });
std::for_each(std::begin(row_pts), std::end(row_pts), // Set * for pts in the row
&row, min_x, step {row[static_cast<size_t>((p.first - min_x) / step)] = '*'; });
y -= step;
}
// Output the plot - which is all the rows.
std::copy(std::begin(rows), std::end(rows), std::ostream_iterator<string>{std::cout, "\n"});
std::cout << std::endl;
}
定义了两个λ表达式x_comp
和y_comp
,用于比较x
和 y 值。这些在max_element()
和min_element()
算法调用中使用,找到x
值和y
值的上限和下限。x 的限制用于确定在输出行中的字符之间水平应用的步长,以及在一个输出行和下一个输出行之间垂直应用的步长。输出中的行数由y
值的范围和步长决定。输出中的每一行都将是一个string
对象,因此完整的绘图将在rows
容器中创建,该容器是string
对象的vector
。
为了在rows
中创建图,需要找到属于每一行的具有y
值的点。这些点的y
值是从某个当前y
值到y+step
值。copy_if()
算法将输入范围中满足该条件的点复制到每行的row_pts
容器中。row_pts
中点的x
值随后被用于传递给for_each()
的函数中。对于每个点,该函数确定当前行中对应于该点的x
值的字符的索引,并将其设置为星号。
该示例包括为特定类型的曲线创建点的两个函数。一种是在正弦曲线上创建点,计算起来相对简单;另一个是心形,稍微复杂一点,但却是一条有趣的曲线。正弦曲线很有趣,因为它们出现在很多场合。例如,音频信号可以被视为不同频率和振幅的正弦波的组合。示例中的函数将只计算由公式 y = sin(x)定义的曲线的点,但是您可以轻松地扩展它,以允许不同的频率和幅度。下面是该函数的代码:
// Generate x,y points on curve y = sin(x) for x values 0 to 4π
std::vector<Point> sine_curve(size_t n_pts = 100)
{ // n_pts is number of data points for the curve
std::vector<double> x_values(n_pts);
double value {};
double step {4 * pi / (n_pts - 1)};
std::generate(std::begin(x_values), std::end(x_values),
[&value, &step]() { double v {value};
value += step;
return v; });
std::vector<Point> curve_pts;
std::transform(std::begin(x_values), std::end(x_values), std::back_inserter(curve_pts),
[](double x) { return Point {x, sin(x)}; });
return curve_pts;
}
这些点作为vector
容器中的Point
元素返回,其中Point
是类型pair<double,double>
的别名。代码使用generate()
算法产生从 0 到4π
的 x 值。然后,transform()
算法在返回的curve_pts
容器中创建Point
对象。
基于半径为r
的圆的心形的笛卡尔方程为(x2+y2–r2)2= 4r2((x–r2)2+y2),这对于确定曲线上的点不是很有用。一个更有用的表示是参数形式,其中x
和y
值是根据独立参数 t 定义的:
x = r(cos(t)-cos(2t))y = r(sin(t)-sin(2t))
通过将 t 从 0 变到 2π,我们可以获得心形上的点,对应于将一个半径为r
的圆绕另一个半径相同的圆滚动。我们可以很容易地使用这些等式定义一个函数来生成点:
std::vector<Point> cardioid_curve(double r = 1.0, size_t n_pts = 100)
{ // n_pts is number of data points
double step = 2 * pi / (n_pts - 1); // Step length for x and y
double t_value {}; // Curve parameter
// Create parameter values that define the curve
std::vector<double> t_values(n_pts);
std::generate(std::begin(t_values), std::end(t_values),
[&t_value, step]() { auto value = t_value;
t_value += step;
return value; });
// Function to define an x,y point on the cardioid for a given t
auto cardioid = r
{ return Point {r*(2*cos(t) + cos(2*t)), r*(2*sin(t) + sin(2*t))}; };
// Create the points for the cardioid
std::vector<Point> curve_pts;
std::transform(std::begin(t_values), std::end(t_values), std::back_inserter(curve_pts),
cardioid);
return curve_pts;
}
这与正弦曲线的逻辑基本相同。generate()
算法为自变量创建一系列值,在本例中,自变量是方程参数t
。cardioid
lambda 表达式是独立定义的,因为这样更容易理解。它根据参数方程为给定的t
创建一个Point
对象。transform()
算法将心形应用于输入范围的t
值的vector
,以在曲线上创建Point
对象的vector
。
绘制正弦曲线和心形曲线的main()
程序现在非常简单:
// Ex7_05.cpp
// Apply some algorithms to plotting curves
// To get the output with the correct aspect ratio, set the characters
// in the standard output stream destination to a square font, such as 8 x 8 pixels
#include <iostream> // For standard streams
#include <iterator> // For iterators and begin() and end()
#include <string> // For string class
#include <vector> // For vector container
#include <algorithm> // For algorithms
#include <cmath> // For sin(), cos()
using std::string;
using Point = std::pair<double, double>;
static const double pi {3.1415926};
// Definition of plot() function template here...
// Definition of sine_curve() function here...
// Definition of cardioid_curve() function here...
int main()
{
auto curve1 = sine_curve(50);
plot(std::begin(curve1), std::end(curve1), "Sine curve", 90);
auto curve2 = cardioid_curve(1.5, 120);
plot(std::begin(curve2), std::end(curve2), "Cardioid", 60);
}
静态变量pi
在全局范围内定义,使其对程序中的所有代码都可用;生成曲线的两个函数都使用它。定义曲线的点数、x 值的范围和绘图宽度之间存在相互作用。字符图中隐含的离散化意味着曲线的某些部分可能看起来有点平坦或者起伏不平。我得到的输出如图 7-5 所示。
图 7-5。
Output for Ex 7_05.cpp - plotting curves
摘要
这一章已经展示了 STL 提供的算法是多么丰富。除了自己编写一个循环之外,一个算法通常有不止一个选项来执行给定的操作。任何情况下的最终选择往往归结为个人偏好。通常,算法通常比显式编程循环更快,但是使用循环的代码有时更容易理解。然而,编写自己的循环更容易出错,所以最好尽可能使用算法。
作为参考,以下是您在本章中看到的算法的总结:
查找具有给定属性的范围中元素的数量
- 如果
p
为[beg,end)
中的所有元素返回true
,则all_of(Input_Iter beg, Input_Iter end, Unary_Predicate p)
返回true
。 - 对于
[beg,end)
中的任意元素,如果p
返回true
,则any_of(Input_Iter beg, Input_Iter end, Unary_Predicate p)
返回true
。 - 如果
p
为[beg,end)
中的所有元素返回false
,则none_of(Input_Iter beg, Input_Iter end, Unary_Predicate p)
返回true
。 count(Input_Iter beg, Input_Iter end, const T& obj)
返回[beg,end)
中等于obj
的元素个数。count_if(Input_Iter beg, Input_Iter end, Unary_Predicate p)
返回[beg,end)
中p
返回true
的元素个数。
比较范围
- 如果范围
[beg1,end1)
中的元素等于范围开始beg2
中的相应元素,则equal(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2)
返回true
。 - 如果范围
[beg1,end1)
中的元素等于范围[beg2,end2)
中的相应元素,则equal(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Input_Iter2 end2)
返回true
。 - 对于范围
[beg1,end1)
和范围开始beg2
的对应元素,如果p
返回true
,则equal(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Binary_Predicate p)
返回true
。 - 对于范围
[beg1,end1)
和[beg2,end2)
中的对应元素,如果p
返回true
,则equal(Input_Iter1 beg1, Input_Iter1 end1,
Input_Iter2 beg2, Input_Iter2 end2, Binary_Predicate p)
返回true
。 mismatch(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2)
返回一个pair<Input_Iter1, Input_Iter2
>对象,包含指向第一对不相等元素的迭代器。mismatch(Input_Iter1 beg1,Input_Iter1 end1,Input_Iter2 beg2,Input_Iter2 end2)
返回与上一版本相同的结果。mismatch(Input_Iter1 beg1, Input_Iter1 end1,
Input_Iter2 beg2, Binary_Predicate p)
返回一个pair<Input_Iter1,Input_Iter2
>对象,该对象包含指向第一对元素的迭代器,对于这些元素p
返回false
。mismatch(Input_Iter1 beg1, Input_Iter1 end1,
Input_Iter2 beg2, Input_Iter2 end2, Binary_Predicate p)
返回与上一版本相同的结果。- 如果范围包含相同数量的元素并且对应的元素相等,则
lexicographical_compare(Input_Iter1 beg1, Input_Iter1 end1,
Input_Iter2 beg2, Input_Iter2 end2)
返回true
;否则返回false
。 - 如果范围包含相同数量的元素,并且 p 为所有对应的元素对返回 true,则
lexicographical_compare(Input_Iter1 beg1, Input_Iter1 end1,
Input_Iter2 beg2, Input_Iter2 end2, Binary_Predicate p)
返回true
;否则返回false
。
置换一系列元素
next_permutation(Bi_Iter beg, Bi_Iter end)
如果有下一个排列,将元素重新排列成升序字典序的下一个排列,并返回true
。否则,元素被重新排列成序列中的第一个排列,算法返回false
。next_permutation(Bi_Iter beg, Bi_Iter end, Compare compare)
根据元素比较函数compare
将元素重新排列成字典序中的下一个排列,并返回true
。如果没有下一个排列,元素将根据compare
重新排列成序列中的第一个排列,算法返回false
。prev_permutation(Bi_Iter beg, Bi_Iter end)
如果有前一个排列,将元素重新排列到前一个排列中,并返回true
。否则,元素被重新排列成序列中的最后一个排列,算法返回false
。next_permutation(Bi_Iter beg, Bi_Iter end, Compare compare)
根据元素比较函数compare
将元素重新排列成字典顺序中的前一个排列,并返回true
。如果没有下一个排列,元素将根据compare
重新排列成序列中的最后一个排列,算法返回false
。- 如果从
beg2
开始的范围中的(end1-beg1)
元素是范围[beg1,end1)
的排列,则is_permutation(Fwd_Iter1 beg1, Fwd_Iter1 end1, Fwd_Iter2 beg2)
返回true
,否则返回false
。使用==
比较元素。 is_permutation(Fwd_Iter1 beg1, Fwd_Iter1 end1, Fwd_Iter2 beg2, Binary_Predicate p)
与前一版本相同,除了使用p
比较元素是否相等。- 如果范围
[beg2,end2)
是范围[beg1,end1)
的置换,则is_permutation(Fwd_Iter1 beg1, Fwd_Iter1 end1, Fwd_Iter2 beg2, Fwd_Iter2 end2)
返回true
,否则返回false
。使用==
比较元素。 is_permutation(Fwd_Iter1 beg1, Fwd_Iter1 end1,
Fwd_Iter2 beg2, Fwd_Iter2 end2,
Binary_Predicate p)
除了使用p
比较元素是否相等之外,与前一版本相同。
从范围中复制元素
copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2)
将范围[beg1, end1)
复制到从beg2
开始的范围。它返回一个迭代器,指向目标中复制的最后一个元素之后的一个元素。copy_n(Input_Iter beg1, Int_Type n, Output_Iter beg2)
将n
元素从beg1
开始的范围复制到beg2
开始的范围。它返回一个迭代器,指向目标中复制的最后一个元素之后的一个元素。copy_if(Input_Iter beg1, Input_Iter end1, Output_Iter beg2, Unary_Predicate p)
将p
返回true
的范围[beg1, end1)
中的元素复制到从beg2
开始的范围。它返回一个迭代器,指向目标中复制的最后一个元素之后的一个元素。copy_backward(Bi_Iter1 beg1, Bi_Iter1 end1, Bi_Iter2 end2)
将范围[beg1, end1)
复制到结束于end2
的范围。该操作以相反的顺序复制元素,从end1-1
指向的元素开始。该算法返回一个迭代器iter
,它指向复制到目的地的最后一个元素,因此在操作之后,目的地范围将是[iter, end2)
。reverse_copy(Bi_Iter beg1, Bi_Iter end1, Output_Iter beg2)
将范围[beg1, end1)
反向复制到从beg2
开始的范围,并返回一个迭代器iter
,该迭代器指向在目的地复制的最后一个元素之后的元素。因此[beg2, iter)
将以相反的顺序包含来自[beg1, end1)
的元素。reverse(Bi_Iter beg, Bi_Iter end)
反转范围[beg, end)
中元素的顺序。unique_copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2)
将范围[beg1, end1)
复制到从beg2
开始的范围,忽略连续的重复。使用==
对元素进行比较,算法返回一个迭代器,该迭代器指向目标中复制的最后一个元素之后的一个元素。- 除了使用
p
比较元素之外,unique_copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2, Binary_Predicate p)
与前面的算法相同。 unique(Fwd_Iter beg, Fwd_Iter end)
通过向左复制覆盖,从范围[beg, end)
中删除连续的重复项。使用==
比较元素,算法返回运算结果范围的结束迭代器。- 除了使用
p
比较元素之外,unique(Fwd_Iter beg, Fwd_Iter end, Binary_Predicate p)
与前面的算法相同。
移动范围
move(Input_Iter beg1, Input_Iter end1, Output_Iter beg2)
将范围[beg1, end1)
移动到从beg2
开始的范围。该算法返回一个迭代器,该迭代器指向目标中移动的最后一个元素之后的一个元素。beg2
一定不在[beg1, end1)
内。move_backward(Bi_Iter1 beg1, Bi_Iter1 end1, Bi_Iter
2end2)
将范围[beg1, end1)
移动到结束于end2
的范围,元素以相反的顺序移动。该算法返回一个迭代器,指向移动到目标的最后一个元素。end2
不得在[beg1, end1)
内。
旋转一系列元素
rotate(Fwd_Iter beg, Fwd_Iter new_beg, Fwd_Iter end)
逆时针旋转[beg, end)
中的元素,使new_beg
成为范围中的第一个元素。该算法返回一个迭代器,该迭代器指向范围中最初的第一个元素。rotate_copy(Fwd_Iter beg1, Fwd_Iter new_beg1, Fwd_Iter end1, Output_Iter beg2)
将[beg1, end1)
中的所有元素复制到从beg2
开始的范围中,这样new_beg1
指向的元素就是目的地中的第一个元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。
从范围中删除元素
remove(Fwd_Iter beg, Fwd_Iter end, const T& obj)
从[beg, end)
中删除等于obj
的元素,并返回一个迭代器,指向结果范围中最后一个元素之后的一个元素。remove_if(Fwd_Iter beg, Fwd_Iter end, Unary_Predicate p)
从[beg, end)
中删除p
返回 true 的元素。该算法返回一个迭代器,指向结果范围中最后一个元素之后的一个元素。remove_copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2, const T& obj)
将元素从[beg1, end1)
复制到从beg2
开始的范围,忽略等于obj
的元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。remove_copy_if(Input_Iter beg1, Input_Iter end1, Output_Iter beg2, Unary_Predicate p)
将元素从[beg1, end1)
复制到从beg2
开始的范围,忽略p
返回true
的元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。
替换范围中的元素
replace(Fwd_Iter beg, Fwd_Iter end, const T& obj, const T& new_obj)
用new_obj
替换[beg, end)
中等于obj
的元素。replace_if(Fwd_Iter beg, Fwd_Iter end, Unary_Predicate p, const T& new_obj)
用new_obj
替换[beg, end)
中p
返回true
的元素。replace_copy(Input_Iter beg1, Input_Iter end1, Output_Iter beg2, const T& obj, const T& new_obj)
将元素从[beg1, end1)
复制到从beg2
开始的范围,用new_obj
替换等于obj
的元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。范围不得重叠。replace_copy_if(Input_Iter beg1, Input_Iter end1, Output_Iter beg2, Unary_Predicate p, const T& new_obj)
将[beg1, end1)
中的元素复制到从beg2
开始的范围,用new_obj
替换p
返回true
的元素。该算法返回一个迭代器,它指向目标中最后一个元素之后的一个元素。范围不得重叠。
修改范围内的元素
fill(Fwd_Iter beg, Fwd_Iter end, const T& obj)
将obj
存储在[beg, end)
范围内的每个元素中。fill_n(Output_Iter beg, Int_Type n, const T& obj)
将obj
存储在从beg
开始的范围的前 n 个元素中。generate(Fwd_Iter beg, Fwd_Iter end, Fun_Object gen_fun)
存储gen_fun
返回的值在[beg, end). gen_fun
中的每个元素必须没有参数,并返回一个可以存储在范围内的值。generate_n(Output_Iter beg, Int_Type n, Fun_Object gen_fun)
将gen_fun
返回的n
值存储在从beg
开始的范围的第一个n
元素中。该算法返回一个迭代器,该迭代器指向存储的最后一个值之后的值。transform(Input_Iter beg1, Input_Iter end1, Output_Iter beg2, Unary_Op op)
将op
应用于范围[beg1, end1)
中的每个元素,并将返回值存储在从beg2
开始的范围的相应元素中。transform(Input_Iter1 beg1, Input_Iter1 end1, Input_Iter2 beg2, Output_Iter beg3, Binary_Op op)
将op
应用于范围[beg1, end1)
和从beg2
开始的范围中的相应元素对,并将返回值存储在从beg3
开始的范围的相应元素中。
交换算法
swap(T& obj1, T& obj2)
交换值obj1
和obj2
。该算法的第二个版本交换两个相同类型的数组,这意味着它们的长度相同。iter_swap(Fwd_Iter iter1, Fwd_Iter iter2)
交换iter1
和iter2
指向的值。swap_ranges(Fwd_Iter1 beg1, Fwd_Iter1 end1, Fwd_Iter2 beg2)
在范围[beg1, end1)
和从beg2
开始的范围之间交换相应的元素。该算法返回一个迭代器,指向从beg2
开始的范围中的最后一个元素。
恐怕你还不能高枕无忧——即使你对目前看到的算法感到满意。在接下来的章节中会有更多的内容。特别是,第十章将介绍专门针对数值计算的算法
Exercises
在所有这些练习中尽可能使用算法。
Write a program to read dates from the standard input stream as: month(string) day(integer) year(4-digit integer)
Store the dates as tuple
objects in a deque
container. Find the number of different months that occur in the container and output it. Find the different month names and output them. Copy dates for each different month into separate containers. Output the dates for each month in descending order of days within years. Read an arbitrary number of heights from the standard input stream as: feet (integer) inches(floating point)
and store each height in a container as a pair
object. Use the transform()
algorithm to convert the heights to metric values stored as tuple
objects (meters, centimetres, millimeters - all integer) in another container. Use the transform()
algorithm to create pair objects in a third container that combine both corresponding height representations. Use an algorithm to output the elements from this container so that feet, inch and metric representations of each height are displayed. Write a program that will:
- 从标准输入流中读取包含标点符号的段落,并将单词作为元素存储在序列容器中。
- 确定输入中重复单词的数量。
- 将所有少于五个字符的单词集合在一个容器中并输出。
Write a program to read a phrase of an arbitrary number of words and use algorithms to assemble all different permutations in a container and output them. Test the program with phrases containing up to five words. (You can try more than five if you want but keep in mind that there are n!
permutations of n
words.) Read an arbitrary number of names consisting of a first and a second name from the standard input stream. Store the names in a container of pair<string,string>
elements. Extract the names that have a common initial letter for the second name into separate containers and output their contents. Reproduce the original set of names in another container of pair<char, string>
objects where the first
member of the pair
is the initial, and the second
member is the second name. Output the names in the new form in order of second name lengths.