《C++Primer》第十一章-泛型算法-学习笔记(1)

《C++Primer》第十一章-泛型算法-学习笔记(1)

日志:
1,2020-03-15 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
《C++Primer》第十二章-类-学习笔记(1)

摘要

标准库容器定义的操作非常少。标准库没有给容器添加大量的功能函数,而是选择提供一组算法,这些算法大都不依赖特定的容器类型,是“泛型”的,可作用在不同类型的容器和不同类型的元素上。泛型算法以及对迭代器更详尽的描述,组成了本章的主题。
标准容器(the standard container)定义了很少的操作。大部分容器都支持添加和删除元素;访问第一个和最后一个元素;获取容器的大小,并在某些情况下重设容器的大小;以及获取指向第一个元素和最后一个元素的下一位位置的迭代器。
可以想像,用户可能还希望对容器元素进行更多其他有用的操作也许需要给顺序容器排序,或者查找某个特定的元素,或者查找最大或最小的元素,等等
标准库并没有为每种容器类型都定义实现这些操作的成员函数,而是定义了一组泛型算法(generic algorithm):因为它们实现共同的操作,所以称之为“算法”;而“泛型”指的是它们可以操作在多种容器类型上——不但可作用于 vector 或 list 这些标准库类型,还可用在内置数组类型、甚至其他类型的序列上,这些我们将在本章的后续内容中了解。自定义的容器类型只要与标准库兼容,同样可以使用这些泛型算法。
大多数算法是通过遍历由两个迭代器标记的一段元素来实现其功能。典型情况下,算法在遍历一段元素范围时,操纵其中的每一个元素。算法通过迭代器访问元素,这些迭代器标记了要遍历的元素范围。

概述

vector 上的find 运算
假设有一个 int 的 vector 对象,名为 vec,我们想知道其中包含某个特定值。解决这个问题最简单的方法是使用标准库提供的 find 运算:

// value we'll look for
int search_value = 42;
// call find to see if that value is present
vector<int>::const_iterator result =find(vec.begin(), vec.end(), search_value);
// report the result  使用了find函数在迭代器标记的范围内查找,没有则返回指向end的迭代器
cout << "The value " << search_value<< (result == vec.end()? " is not present" : " is present")<< endl;

使用两个迭代器和一个值调用 find 函数,检查两个迭代器实参标记范围内的每一个元素。只要找到与给定值相等的元素,find 就会返回指向该元素的迭代器。如果没有匹配的元素,find 就返回它的第二个迭代器实参,表示查找失败。于是,只要检查该函数的返回值是否与它的第二个实参相等,就可得知元素是否找到了。我们在输出语句中使用条件操作符(第 5.7 节)实现这个检查并报告是否找到了给定值。
由于 find 运算是基于迭代器的,因此可在任意容器中使用相同的 find 函数查找值。
list上的find 运算
例如,可在一个名为 lst 的 int 型 list 对象上,使用 find 函数查找一个值:

// call find to look through elements in a list
list<int>::const_iterator result =find(lst.begin(), lst.end(), search_value);
cout << "The value " << search_value<< (result == lst.end()? " is not present" : " is present")<< endl;

除了 result 的类型和传递给 find 的迭代器类型之外,这段代码与使用 find 在 vector 对象中查找元素的程序完全相同。
内置数组上的find运算
类似地,由于指针的行为与作用在内置数组上的迭代器一样,因此也可以使用 find 来搜索数组:

int ia[6] = {27, 210, 12, 47, 109, 83};
int search_value = 83;
int *result = find(ia, ia + 6, search_value);
cout << "The value " << search_value<< (result == ia + 6? " is not present" : " is present")<< endl;

这里给find 函数传递了两个指针:指向 ia 数组中第一个元素的指针,以及指向 ia 数组起始位置之后第 6 个元素的指针(即 ia 的最后一个元素的下一位置)。如果返回的指针等于 ia + 6,那么搜索不成功;否则,返回的指针指向找到值。
如果需要传递一个子区间,则传递指向这个子区间的第一个元素以及最后一个元素的下一位置的迭代器(或指针)。例如,在下面对 find 函数的调用中,只搜索了 ia[1] 和 ia[2]:

// only search elements ia[1] and ia[2]
int *result = find(ia + 1, ia + 3, search_value);

算法如何工作

每个泛型算法的实现都独立于单独的容器。这些算法还是大而不全的,并且不依赖于容器存储的元素类型。为了知道算法如何工作,让我们深入了解find 操作。该操作的任务是在一个未排序的元素集合中查找特定的元素。从概念上看,find 必须包含以下步骤:

  1. 顺序检查每个元素。
  2. 如果当前元素等于要查找的值,那么返回指向该元素的迭代器。
  3. 否则,检查下一个元素,重复步骤 2,直到找到这个值,或者检查完所有的元素为止。
  4. 如果已经到达集合末尾,而且还未找到该值,则返回某个值,指明要查找的值在这个集合中不存在。

标准算法固有地独立于类型

这种算法,正如我们所指出的,与容器的类型无关:在前面的描述中,没有任何内容依赖于容器类型。这种算法只在一点上隐式地依赖元素类型:必须能够对元素做比较运算。该算法的明确要求如下:

  1. 需要某种遍历集合的方式:能够从一个元素向前移到下一个元素。
  2. 必须能够知道是否到达了集合的末尾。
  3. 必须能够对容器中的每一个元素与被查找的元素进行比较。
  4. 需要一个类型指出元素在容器中的位置,或者表示找不到该元素。

迭代器将算法和容器绑定起来

泛型算法用迭代器来解决第一个要求:遍历容器。所有迭代器都支持自增操作符,从一个元素定位下一个元素,并提供解引用操作符访问元素的值。 除了 第11.3.5 节将介绍的一个例外情况之外,迭代器还支持相等和不等操作符,用于判断两个迭代是否相等。

大多数情况下,每个算法都需要使用(至少)两个迭代器指出该算法操纵的元素范围。第一个迭代器指向第一个元素,而第二个迭代器则指向最后一个元素的下一位置。第二个迭代器所指向的元素[有时被称为超出末端迭代器]本身不是要操作的元素,而被用作终止遍历的哨兵(sentinel)
使用超出末端迭代器还可以很方便地处理第四个要求,只要以此迭代器为返回值,即可表示没有找到要查找的元素。如果要查找的值未找到,则返回超出末端迭代器;否则,返回的迭代器指向匹配的元素。
第三个要求——元素值的比较,有两种解决方法。默认情况下,find 操作要元素类型定义了相等==操作符,算法使用这个操作符比较元素。如果元素类型不支持相等==操作符,或者打算用不同的测试方法来比较元素,则可使用第二个版本的 find 函数。这个版本需要一个额外的参数:实现元素比较的函数名字。
这些算法从不使用容器操作,因而其实现与类型无关,元素的所有访问和遍历都通过迭代器实现。 实际的容器类型未知(甚至所处理的元素是否存储在容器中也是未知的)。
标准库提供了超过 100 种算法。与容器一样,算法有着一致的结构。比起死记全部一百多种算法,了解算法的设计可使我们更容易学习和使用它们。本章除了举例说明这些算法的使用之外,还将描述标准库算法的统一原理。附录 A根据操作分类列出了所有的算法。

算法永不执行容器提供的操作

泛型算法本身从不执行容器操作,只是单独依赖迭代器和迭代器操作实现。 算法基于迭代器及其操作实现,而并非基于容器操作。这个事实也许比较意外,但本质上暗示了:使用“普通”的迭代器时,算法从不修改基础容器的大小。正如我们所看到的,算法也许会改变存储在容器中的元素的值,也许会在容器内移动元素,但是,算法从不直接添加或删除元素。
第 11.3.1 节将介绍标准库提供的另一种特殊的迭代器类:插入器(inserter),除了用于遍历其所绑定的序列之外,还可实现更多的功能。在给这类迭代器赋值时,在基础容器上将执行插入运算。如果算法操纵这类迭代器,迭代器将可能导致在容器中添加元素。但是,算法本身从不这么做。

初窥算法

在研究算法标准库的结构之前,先看一些例子。上一节已经介绍了 find 函数的用法;本节将要使用其他的一些算法。使用泛型算法必须包含 algorithm 头文件

#include <algorithm>

标准库还定义了一组泛化的算术算法(generalized numeric algorithm),其命名习惯与泛型算法相同。使用这些算法则必须包含 numeric 头文件:

#include <numeric>

除了少数例外情况,所有算法都在一段范围内的元素上操作,我们将这段范围称为“输出范围(input range)”带有输入范围参数的算法总是使用头两个形参标记该范围。这两个形参是分别指向要处理的第一个元素和最后一个元素的下一位置的迭代器。
尽管大多数算法对算法对输入范围的操作是类似的,但在该范围内如何操纵元素却有所不同。理解算法的最基本方法是了解该算法是否读元素、写元素或者对元素进行重新排序。在本节的余下内容中,将会观察到每种算法的例子。

只读算法

(只读算法)许多算法只会读取其输入范围内的元素,而不会写这些元素。 find就是一个这样的算法。另一个简单的只读算法是 accumulate,该算法在 numeric 头文件中定义。假设 vec 是一个 int 型的 vector 对象,下面的代码:

// sum the elements in vec starting the summation with the value 42
int sum = accumulate(vec.begin(), vec.end(), 42);

将 sum 设置为 vec 的元素之和再加上 42。accumulate 带有三个形参。头两个形参指定要累加的元素范围。第三个形参则是累加的初值。accumulate 函数将它的一个内部变量设置为指定的初值,然后在此初值上累加输入范围 accumulate用于指定累加起始值的第三个实参是必要的,因为 accumulate 对将要累加的元素类型一无所知,因此,除此之外,没有别的办法创建合适的起始值或者关联的类型。
accumulate 对要累加的元素类型一无所知,这个事实有两层含义。首先,调用该函数时必须传递一个起始值,否则,accumulate 将不知道使用什么起始值。
其次,容器内的元素类型必须与第三个实参的类型匹配,或者可转换为第三个实参的类型。在 accumulate 内部,第三个实参用作累加的起点;容器内的元素按顺序连续累加到总和之中。因此,必须能够将元素类型加到总和类型上。
考虑下面的例子,可以使用 accumulate 把 string 型的 vector 容器中的元素连接起来:

// concatenate elements from v and store in sum
string sum = accumulate(v.begin(), v.end(), string(""));

这个函数调用的效果是:从空字符串开始,把 vec 里的每个元素连接成一个字符串。注意:程序显式地创建了一个 string 对象,用该函数调用的第三个实参。传递一个字符串字面值,将会导致编译时错误。因为此时,累加和的类型将是 const char*,而 string 的加法操作符(第 3.2.3 节)所使用的操作数则分别是 string 和 const char* 类型,加法的结果将产生一个 string 对象,而不是 const char* 指针。

find_first_of 的使用

除了 find 之外,标准库还定义了其他一些更复杂的查找算法。当中的一部分类似 string 类的 find 操作(第 9.6.4 节),其中一个是find_first_of 函数这个算法带有两对迭代器参数来标记两段元素范围,在第一段范围内查找与第二段范围中任意元素匹配的元素,然后返回一个迭代器,指向第一个匹配的元素。如果找不到元素,则返回第一个范围的 end 迭代器。假设 roster1 和 roster2 是两个存放名字的 list 对象,可使用 find_first_of 统计有多少个名字同时出现在这两个列表中:

// program for illustration purposes only:
// there are much faster ways to solve this problem
size_t cnt = 0;
list<string>::iterator it = roster1.begin(); // roster1 和 roster2 是两个存放名字的 list 对象
// look in roster1 for any name also in roster2
while ((it = find_first_of(it, roster1.end(),roster2.begin(), roster2.end()))!= roster1.end()) {
	++cnt;
// we got a match, increment it to look in the rest of roster1
	++it;
}
cout << "Found " << cnt << " names on both rosters" << endl;

调用 find_first_of 查找 roster2 中的每个元素是否与第一个范围内的元素匹配,也就是在 it 到 roster1.end() 范围内查找一个元素。该函数返回此范围内第一个同时存在于第二个范围中的元素。在 while 的第一次循环中,遍历整个 roster1 范围。第二次以及后续的循环迭代则只考虑 roster1 中尚未匹配的部分。
循环条件检查 find_first_of 的返回值,判断是否找到匹配的名字。如果找到一个匹配,则使计数器加 1,同时给 it 加 1,使它指向 roster1 中的下一个元素。很明显可知,当不再有任何匹配时,find_first_of 返回 roster1.end(),完成统计。

迭代器实参类型

通常,泛型算法都是在标记容器(或其他序列)内的元素范围的迭代器上操作的。标记范围的两个实参类型必须精确匹配,而迭代器本身必须标记一个范围:它们必须指向同一个容器中的元素(或者超出容器末端的下一位置),并且如果两者不相等,则第一个迭代器通过不断地自增,必须可以到达第二个迭代器。有些算法,例如 find_first_of,带有两对迭代器参数。每对迭代器中,两个实参的类型必须精确匹配,但不要求两对之间的类型匹配。特别是,元素可存储在不同类型序列中,只要这两序列的元素可以比较即可。
上述程序中,roster1 和 roster2 的类型不必精确匹配: roster1 可以是 list 对象,而 roster2 则可以是 vector 对象、 deque 对象或者是其他后面要学到的序列。只要这两个序列的元素可使用相等( == )操作符进行比较即可。如果 roster1 是 list 对象,则 roster2 可以是 vector<char*> 对象,因为 string 标准库为 string 对象与 char* 对象定义了相等(==)操作符。

写容器元素的算法

(写容器元素的算法)一些算法写入元素值。

在使用这些算法写元素时要当心,必须确保算法所写的序列至少足以存储要写入的元素。

  • 有些算法直接将数据写入到输入序列
  • 另外一些则将指定数目的元素写入某个序列。
  • 还有第三种算法带有一个额外的迭代器参数指定写入目标。这类算法将目标迭代器用作输出的位置。

写入输入序列的元素

写入到输入序列的算法本质上是安全的——只会写入与指定输入范围数量相同的元素。
写入到输入序列的一个简单算法是fill 函数,考虑如下例子:

fill(vec.begin(), vec.end(), 0); // reset each element to 0
// set subsequence of the range to 10
fill(vec.begin(), vec.begin() + vec.size()/2, 10);

fill 带有一对迭代器形参,用于指定要写入的范围,而所写的值是它的第三个形参的副本。执行时,将该范围内的每个元素都设为给定的值。如果输入范围有效,则可安全写入。这个算法只会对输入范围内已存在的元素进行写入操作。

不检查写入操作的算法

fill_n 函数带有的参数包括:一个迭代器、一个计数器以及一个值。该函数从迭代器指向的元素开始,将指定数量的元素设置为给定的值。 fill_n 函数假定对指定数量的元素做写操作是安全的。 初学者常犯的错误的是:在没有元素的空容器上调用 fill_n 函数(或者类似的写元素算法)。

vector<int> vec; // empty vector
// disaster: attempts to write to 10 (nonexistent) elements in vec
fill_n(vec.begin(), 10, 0);

这个 fill_n 函数的调用将带来灾难性的后果。我们指定要写入 10 个元素,但这些元素却不存在——vec 是空的。其结果未定义,很可能导致严重的运行时错误。

对指定数目的元素做写入运算,或者写到目标迭代器的算法,都不检查目标的大小是否足以存储要写入的元素。

引入 back_inserter

确保算法有足够的元素存储输出数据的一种方法是使用插入迭代器。插入迭代器是可以给基础容器添加元素的迭代器。通常,用迭代器给容器元素赋值时,被赋值的是迭代器所指向的元素。而使用插入迭代器赋值时,则会在容器中添加一个新元素,其值等于赋值运算的右操作数的值。
第 11.3.1 节将会讨论更多关于插入迭代器的内容。然而,为了说明如何安全使用写容器的算法,下面将使用 back_inserter. 使用 back_inserter 的程序必须包含 iterator 头文件。
back_inserter 函数迭代器适配器。与容器适配器(第 9.7 节)一样,迭代器适配器使用一个对象作为实参,并生成一个适应其实参行为的新对象。在本例中,传递给 **back_inserter 的实参是一个容器的引用。back_inserter 生成一个绑定在该容器上的插入迭代器。**在试图通过这个迭代器给元素赋值时,赋值运算将调用 push_back 在容器中添加一个具有指定值的元素。使用 back_inserter 可以生成一个指向 fill_n 写入目标的迭代器:

vector<int> vec; // empty vector
// ok: back_inserter creates an insert iterator that adds elements to vec
fill_n (back_inserter(vec), 10, 0); // appends 10 elements to vec

现在,fill_n 函数每写入一个值,都会通过 back_inserter 生成的插入迭代器实现。 效果相当于在 vec 上调用 push_back,在 vec 末尾添加 10 个元素,每个元素的值都是 0。

写入到目标迭代器的算法

第三类算法向目标迭代器写入未知个数的元素。正如 fill_n 函数一样,目标迭代器指向存放输出数据的序列中第一个元素。这类算法中最简单的是copy 函数copy 带有三个迭代器参数头两个指定输入范围,第三个则指向目标序列的一个元素。传递给 copy 的目标序列必须至少要与输入范围一样大。
假设 ilst 是一个存放 int 型数据的 list 对象,可如下将它 copy 给一个 vector 对象:

vector<int> ivec; // empty vector
// copy elements from ilst into ivec
copy (ilst.begin(), ilst.end(), back_inserter(ivec)); 
//copy 从输入范围中读取元素,然后将它们复制给目标 ivec。

当然,这个例子的效率比较差:通常,如果要以一个已存在的容器为副本创建新容器,更好的方法是直接用输入范围作为新构造容器的初始化式:

// better way to copy elements from ilst
vector<int> ivec(ilst.begin(), ilst.end());

算法的 _copy 版本

有些算法提供所谓的“复制(copying)”版本这些算法对输入序列的元素做出处理,但不修改原来的元素,而是创建一个新序列存储元素的处理结果。(也就是对于一个序列中的数据,算法对其进行复制后下,进行一些计算,存储到另外一个序列中)replace 与replace_copy的对比就是一个很好的例子。
replace 算法对输入序列做读写操作,将序列中特定的值替换为新的值。该算法带有四个形参:一对指定输入范围的迭代器和两个值。每一个等于第一值的元素替换成第二个值。

// replace any element with value of 0 by 42
replace(ilst.begin(), ilst.end(), 0, 42);

这个调用将所有值为 0 的实例替换成 42。如果不想改变原来的序列,则调用 replace_copy。这个算法接受第三个迭代器实参,指定保存调整后序列的目标位置。

// create empty vector to hold the replacement
vector<int> ivec;
// use back_inserter to grow destination as needed
replace_copy (ilst.begin(), ilst.end(),back_inserter(ivec), 0, 42);

调用该函数后,ilst 没有改变,ivec 存储 ilst 一份副本,而 ilst 内所有的 0 在 ivec 中都变成了 42。

对容器元素重新排序的算法

假设我们要分析一组儿童故事中所使用的单词。例如,可能想知道它们使用了多少个由六个或以上字母组成的单词。每个单词只统计一次,不考虑它出现的次数,也不考虑它是否在多个故事中出现。要求以长度的大小输出这些单词,对于同样长的单词,则以字典顺序输出。
假定每本书的文本已经读入并保存在一个 string 类型的 vector 对象中,它的名字是 words。现在,应该怎么解决包括统计单词出现次数这个问题呢?为了解此问题,要做下面几项操作:

  1. 去掉所有重复的单词。
  2. 按单词的长度排序。
  3. 统计长度等于或超过 6 个字符的单词个数。

上述每一步都可使用泛型算法实现。为了说清楚,使用下面这个简单的故事作为我们的输入:

the quick red fox jumps over the slow red turtle

对于这个输入,我们的程序应该产生如下输出:

1 word 6 characters or longer

去除重复

假设我们的输入存储在一个名为 words 的 vector 对象中,第一个子问题是将 words 中重复出现的单词去除掉:

// sort words alphabetically so we can find the duplicates
//vector 对象包含每个故事中使用的所有单词。首先对此 vector 对象排序。
sort(words.begin(), words.end());//sort 算法带有两个迭代器实参,指出要排序的元素范围。
/* eliminate duplicate words:
* unique reorders words so that each word appears once in the
* front portion of words and returns an iterator one past the
unique range;
* erase uses a vector operation to remove the nonunique elements
*/
vector<string>::iterator end_unique =unique(words.begin(), words.end());
//unique 实际上并没有删除任何元素,而是将无重复的元素复制到序列的前端,从而覆盖相邻的重复元素
words.erase(end_unique, words.end()); //返回的迭代器尾部多余的元素

这个算法使用小于(<)操作符比较元素。在本次调用中,要求对整个 vector 对象排序。
调用 sort算法 后,此 vector 对象的元素按次序排列:

fox jumps over quick red red slow the the turtle  //10个元素

注意,单词 red 和 the 重复出现了。

unique 的使用

单词按次序排列后,现在的问题是:让故事中所用到的每个单词都只保留一个副本。unique 算法很适合用于解决这个问题,它带有两个指定元素范围的迭代器参数。该算法删除相邻的重复元素,然后重新排列输入范围内的元素,并且返回一个迭代器,表示无重复的值范围的结束。
调用 unique 后,vector 中存储内容是:
在这里插入图片描述
注意,words 的大小并没有改变,依然保存着 10 个元素;只是这些元素的顺序改变了。调用 unique“删除”了相邻的重复值。给“删除”加上引号是因为 unique 实际上并没有删除任何元素,而是将无重复的元素复制到序列的前端,从而覆盖相邻的重复元素。 unique 返回的迭代器指向超出无重复的元素范围末端的下一位置。

使用容器操作删除元素

如果要删除重复的项,必须使用容器操作,在本例中调用 erase 实现该功能。这个函数调用从 end_unique 指向的元素开始删除,直到 words 的最后一个元素也删除掉为止。调用之后,words 存储输入的 8 个不相同的元素。
算法不直接修改容器的大小。如果需要添加或删除元素,则必须使用容器操作。
值得注意的是,对没有重复元素的 vector 对象,调用 erase 也是安全的。
如果不存在重复的元素,unique 就会返回 words.end(),此时,调用 erase 的两个实参值相同,都是 words.end()。两个迭代器相等这个事实意味着 erase 函数要删除的范围是空的。删除一段空的范围没有任何作用,所以即使输入中没有重复的元素,我们的程序仍然正确。

定义需要的实用函数

下一个子问题统计长度不小于 6 的单词个数。为了解决这个问题,需要用到另外两个泛型算法:stable_sortcount_if。使用这些算法,还需要一个配套的实用函数,称为谓词(predicate)谓词(predicate)是做某些检测的函数,返回用于条件判断的类型,指出条件是否成立。
我们需要的第一个谓词将用在基于大小的元素排序中。为了实现排序,必须定义一个谓词函数来实现两个 string 对象的比较,并返回一个 bool 值,指出第一个字符串是否比第二个短:

// comparison function to be used to sort by word length
bool isShorter(const string &s1, const string &s2)   //这里的排序是根据字符的大小的
{
	return s1.size() < s2.size();
}

另一个所需的谓词函数将判断给出的 string 对象的长度是否不小于 6:

// determine whether a length of a given word is 6 or more
bool GT6(const string &s)
{
	return s.size() >= 6;
}

尽管这个函数能解决问题,但存在不必要限制——函数内部硬性规定了对长度大小的要求。如果要统计其他长度的单词个数,则必须编写另一个函数。其实很容易写出更通用的比较函数,使它带有两个形参,分别是 string 对象和一个长度大小值即可。但是,传递给 count_if 算法的函数只能带有一个实参,因此本程序不能使用上述更通用的方法。第 14.8.1 节将为这个问题提供更好的解决方案。

排序算法

标准库定义了四种不同的排序算法,上面只使用了最简单的sort 算,使 words 按字典次序排列(不是按长度的)。除了 sort 之外,标准库还定义了stable_sort 算法stable_sort 保留相等元素的原始相对位置。通常,对于已排序的序列,我们并不关心其相等元素的相对位置,毕竟,这些元素是相等的。但是,在这个应用中,我们将“相等”定义为“相同的长度”,有着相同长度的元素还能以字典次序的不同而区分。调用 stable_sort 后,对于长度相同的元素,将保留其字典顺序。
sort 和 stable_sort 都是重载函数。其中一个版本使用元素类型提供的小于(<)操作符实现比较。在查找重复元素之前,我们就是用这个 sort 版本对元素排序。第二个重载版本带有第三个形参:比较元素所使用的谓词函数的名字。
这个谓词函数必须接受两个实参,实参的类型必须与元素类型相同,并返回一个可用作条件检测的值。下面将比较元素的 isShorter 函数作为实参,调用第二个版本的排序函数:

// sort words by size, but maintain alphabetic order for words of the same size
stable_sort(words.begin(), words.end(), isShorter);

调用后,words 中的元素按长度大小排序,而长度相同的单词则仍然保持字典顺序:
在这里插入图片描述

统计长度不小于 6 的单词

现在此 vector 对象已经按单词长度排序,剩下的问题就是统计长度不小于 6 的单词个数。使用 count_if 算法处理这个问题:

vector<string>::size_type wc = count_if(words.begin(), words.end(), GT6);

执行 count_if 时,首先读取它的头两个实参所标记的范围内的元素。每读出一个元素,就将它传递给第三个实参表示的谓词函数。此谓词函数需要单个元素类型的实参,并返回一个可用作条件检测的值。count_if 算法返回使谓词函数返回条件成立的元素个数。在这个程序中,count_if 将每个单词传递给 GT6,而 GT6 返回一个 bool 值,如果单词长度不小于 6,则该 bool 值为 true。

将全部程序段放在一起了解程序的细节之后,下面是完整的程序:

// return plural version of word if ctr isn't 1
//一个计数器、一个单词 word 和单词结束字符串ending,当计数器的值大于 1 时,返回该单词的复数版本:
string make_plural(size_t ctr, const string &word,const string &ending)
{
return (ctr == 1) ? word : word + ending;
}
// comparison function to be used to sort by word length
bool isShorter(const string &s1, const string &s2)  //比较单词长度的谓词函数
{
	return s1.size() < s2.size();
}
// determine whether a length of a given word is 6 or more
bool GT6(const string &s)  //比较单词是否大于6
{
	return s.size() >= 6;
}
int main()
{
	vector<string> words;  //存储输入的容器
// copy contents of each book into a single vector
	string next_word;
	while (cin >> next_word) {
// insert next book's contents at end of words
		words.push_back(next_word);
	}
// sort words alphabetically so we can find the duplicates
	sort (words.begin(), words.end());  //按字典顺序排个序,因为unique只能删除相邻的重复元素
/* eliminate duplicate words:
* unique reorders words so that each word appears once in the
* front portion of words and returns an iterator one past
the unique range;
* erase uses a vector operation to remove the nonunique elements
*/
	vector<string>::iterator end_unique =unique(words.begin(), words.end()); //重复的键给变成一个。
	words.erase(end_unique, words.end()); //删除unique之后结尾多余的元素
// sort words by size, but maintain alphabetic order for wordsof the same size
	stable_sort(words.begin(), words.end(), isShorter);  //按单词长度排序
	vector<string>::size_type wc =count_if (words.begin(), words.end(), GT6); //输出长度大于6的个数
	cout << wc << " " << make_plural(wc, "word", "s")<< " 6 characters or longer" << endl;
	return 0;
}

最后,我们留下按长度顺序输出单词这个问题作为习题。

参考资料

【1】C++ Primer 中文版(第四版·特别版)

注解

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104788182