第十章 泛型算法
泛型算法 称为“算法”,是因为它们实现了一些经典算法的公共接口,如排序和搜索。泛型算法 称为“泛型”,是因为它们可以用于不同类型的元素和多种容器类型。
10.1 概述
大多数算法都定义在头文件algorithm 中。 标准库还在头文件numeric 中定义了一组数值泛型算法。 这些算法通常遍历两个迭代器指定的一个元素范围来进行操作。 最好的例子是标准库算法find:传递给find的前两个参数是表示元素范围的迭代器,第三个参数是一个值。 由于find操作的是迭代器,因此我们可以用同样的find函数在任何容器中查找值。
int val = 42 ;
auto result = find ( vec. cbegin ( ) , vec. cend ( ) , val) ;
string val = : "a value" ;
auto result = find ( lst. cbegin ( ) , lst. cend ( ) , val) ;
int ia[ ] = { 27 , 210 , 12 , 47 , 109 , 83 ) ;
int val= 83 ;
int * result= find ( begin ( ia) , end ( ia) , val) ;
算法工作步骤(以find为例):
访问序列中的首元素。 比较此元素与我们要查找的值。 如果此元素与我们要查找的值匹配,find返回标识此元素的值。 否则,find前进到下一个元素,重复执行步骤2和3。 如果到达序列尾,find应停止。 如果find到达序列末尾,它应该返回一个指出元素未找到的值。此值和步骤3返回的值必须具有相容的类型。 迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。
10.2 初识泛型算法
标准库提供了超过100个算法,与容器类似,这些算法有一致的结构。
10.2.1 只读算法
一些算法只会读取其输入范围内的元素,而从不改变元素,被称为只读算法 。 除find算法,在头文件numeric中,accumulate函数 也是一个只读算法。
int sum = accumualte ( vec. cbegin ( ) , vec. cend ( ) , 0 ) ;
accumulate将第三个参数作为求和起点,这蕴含若一个编程假定:将元素类型加到和的类型上的操作必须是可行的。
string sum = accumulate ( v. cbegin ( ) , v. cend ( ) , string ( "" ) ) ;
string sum = accumulate ( v. cbegin ( ) , v. cend ( ) , "" ) ;
只读算法equal ,用于确定两个序列是否保存相同的值。
equal ( roster1. cbegin ( ) , roster1. cend ( ) , roster2. cbegin ( ) ) ;
那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
10.2.2 写容器元素的算法
一些算法将新值赋予序列中的元素,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。 算法不会执行容器操作,因此它们自身不可能改变容器的大小。
fill ( vec. begin ( ) , vec. end ( ) , 0 ) ;
fill ( vec. begin ( ) , vec. begin ( ) + vec. size ( ) / 2 , 10 ) ;
fill_n算法 ,不能在空容器上调用fill_n。
vector< int > vec;
fill_n ( vec. begin ( ) , vec. size ( ) , 0 ) ;
一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器 。 back_inserter 定义在头文件iterator中的一个函数,它接受一个指向容器的引用,返回一个与容器绑定的插入迭代器。
vector< int > vec;
auto it = back_inserter ( vec) ;
* it = 42 ;
常常使用back_inserter 来创建一个迭代器,作为算法的目的位置来使用。
vector< int > vec;
fill_n ( back_inserter ( vec) , 10 , 0 ) ;
copy 算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。
int a1[ ] = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ;
int a2[ sizeof ( a1) / sizeof ( * a1) ] ;
auto ret = copy ( begin ( a1) , end ( a1) , a2) ;
replace 算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。
replace ( ilst. begin ( ) , ilst. end ( ) , 0 , 42 ) ;
replace_copy 算法调用后,ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中值为0的元素在ivec中都变为42。
replace_copy ( ilst. cbegin ( ) , ilst. cend ( ) , back_inserter ( ivec) , 0 , 42 ) ;
10.2.3 重排容器元素的算法
某些算法会重排容器中元素的顺序,一个明显的例子是sort 。
void elimDups ( vector< string> & words)
{
sort ( words. begin ( ) , words. end ( ) ) ;
auto end_unique = unique ( words. begin ( ) , words. end ( ) ) ;
words. erase ( end_unique, words. end ( ) ) ;
}
the quick red fox jumps over the slow red turtle
fox, jumps, over, quick, red, red, slow, the, the, turtle
fox, jumps, over, quick, red, slow, the, turtle, ? ? ? , ? ? ?
fox, jumps, over, quick, red, slow, the, turtle
10.3 定制操作
很多算法都会比较输入序列中的元素,标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。
10.3.1 向算法传递函数
此外希望单词按其长度排序,大小相同的再按字典序排列。将使用sort的第二个版本,此版本是重载过的,它接受第三个参数,此参数是个谓词 。 谓词 是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(只接受一个参数)和二元谓词(接受两个参数)。
bool isShorter ( const string & s1, const string & s2)
{
return s1. size ( ) < s2. size ( ) ;
}
sort ( words. begin ( ) , words. end ( ) , isShorter) ;
stable_sort 算法可以保持等长元素间的字典序。
10.3.2 lambda表达式
有时希望进行的操作需要更多参数,超出了算法对谓词的限制。 可以向一个算法传递任何类别的可调用对象 ,对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。 一个lambda表达式 表示一个可调用的代码单元,可以理解为一个未命名的内联函数。 一个lambda表达式 具有如下形式:
[ capture list] ( parameter list) -> return type { function body }
capture list (捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空)。return type 、parameter list 和function body 与任何普通函数一样,分别表示返回类型、参数列表和函数体。与普通函数不同,lambda必须使用尾置返回来指定返回类型。 可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。
auto f = [ ] { return 42 ; }
忽略括号和参数列表等价于指定一个空参数列表。 如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。
cout << f ( ) << endl;
编写一个与isShorter函数完成相同功能的lambda。
[ ] ( const string & a, const string & b) { return a. size ( ) < b. size ( ) ; }
sort ( words. begin ( ) , words. end ( ) ,
[ ] ( const string & a, const string & b)
{ return a. size ( ) < b. size ( ) ; } ) ;
关于求大于等于一个给定长度的单词有多少,还会修改输出,使程序只打印大于等于给定长度的单词。
void biggies ( vector< string> & words, vector< string> :: size_type sz)
{
elimDups ( woreds) ;
stable_sort ( words. begin ( ) , words. end ( ) , isShorter) ;
}
标准库find_if算法来查找第一个具有特定大小的元素。 find_if接受一元谓词,但是没有任何办法能传递给它第二个参数来表示长度。 所以使用捕获列表来捕获局部变量。
[ sz] ( const string & a)
{ return a. size ( ) >= sz; } ;
void biggies ( vector< string> & words, vector< string> :: size_type sz)
{
elimDups ( woreds) ;
stable_sort ( words. begin ( ) , words. end ( ) , [ ] ( const string & a, const string & b)
{ return a. size ( ) < b. size ( ) ; } ) ;
auto wc = find_if ( words. begin ( ) , word. end ( ) , [ sz] ( const string & a)
{ return a. size ( ) >= sz; } ) ;
auto count = words. end ( ) - wc;
cout << count << " " << make_plural ( count, "word" , "s" ) << " of length " << sz << " or longer " << endl;
for_each ( wc, words. end ( ) , [ ] ( const string & s)
{ cout << s << " " ; } ) ;
cout << endl;
}
10.3.3 lambda捕获和返回
当定义一个lambda时,编译器生成一个与lambda对应的新的未命名的类类型。 类似参数传递,变量的捕获方式也可以是值或引用。 采用值捕获的方式,与传值参数类似,采用值捕获的前提是变量可以拷贝。 与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。 值捕获 。
void fcn1 ( )
{
size_t v1 = 42 ;
auto f = [ v1]
{ return v1; } ;
v1 = 0 ;
auto j = f ( ) ;
}
引用捕获 。当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。
void fcn2 ( )
{
size_t v1 = 42 ;
auto f2 = [ & v1]
{ return v1; } ;
v1 = 0 ;
auto j = f2 ( ) ;
}
引用捕获有时是必要的,如不能拷贝ostream对象,但是希望biggies函数接受一个ostream。
void biggies ( vector< string> & words, vector< string> :: size_type sz, ostream & os = cout, char c = ' ' )
{
for_each ( words. begin ( ) , word. end ( ) , [ & os, c] ( const string & s)
{ os << s << c; } ) ;
}
隐式捕获 。可以让编译器根据lambda体中的代码来推断我们要使用哪些变量&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。
wc = find_if ( words. begin ( ) , word. end ( ) , [ = ] ( const string & s)
{ return s. size ( ) >= sz; } ) ;
混合使用隐式捕获和显式捕获 ,对一部分变量采用值捕获,对其他变量采用引用捕获。
void biggies ( vector< string> & words, vector< string> :: size_type sz, ostream & os = cout, char c = ' ' )
{
for_each ( words. begin ( ) , word. end ( ) , [ & , c] ( const string & s)
{ os << s << c; } ) ;
for_each ( words. begin ( ) , word. end ( ) , [ = , & oc] ( const string & s)
{ os << s << c; } ) ;
}
lambda捕获列表 描述 [] 空捕获列表 [names] names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。 [&] 隐式捕获列表,采用引用捕获方式。 [=] 隐式捕获列表,采用值捕获方式。 [ & , identifer_list] identifer_list是一个逗号分隔的列表, 包含0个或多个来着所在函数的变量,这些变量采用值捕获,而任何隐式捕获的变量都采用引用捕获。 [ = , identifer_list] identifer_list中的值都是采用引用捕获,而任何隐式捕获的变量都采用值捕获
对于一个值被拷贝的变量,lambda不会改变其值。 希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。 可变lambda 能省略参数列表。
void fcn3 ( )
{
size_t v1 = 42 ;
auto f = [ v1] ( ) mutable
{ return ++ v1; } ;
v1 = 0 ;
auto j = f ( ) ;
}
void fcn4 ( )
{
size_t v1 = 42 ;
auto f2 = [ & v1]
{ return ++ v1; } ;
v1 = 0 ;
auto j = f2 ( ) ;
}
如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。
transform ( vi. begin ( ) , vi. end ( ) , vi. begin ( ) , [ ] ( int i)
{ return i < 0 ? - i : i } ) ;
以下if版本,编译器推断这个版本的lambda返回类型为void。
transform ( vi. begin ( ) , vi. end ( ) , vi. begin ( ) , [ ] ( int i)
{ if ( i< 0 ) return i < 0 ? - i : i } ) ;
当我们需要为一个lambda定义返回类型时,必须尾置返回类型。
transform ( vi. begin ( ) , vi. end ( ) , vi. begin ( ) , [ ] ( int i) -> int
{ if ( i< 0 ) return i < 0 ? - i : i } ) ;
10.3.4 参数绑定
对于捕获局部变量的lambda,用函数来替换它不是那么容易。 如用在find_if调用中的lambda比较一个string和一个给定大小。
bool check_size ( const string & s, string:: size_type sz)
{
return s. size ( ) >= sz;
}
但是不能用这个函数作为find_if的一个参数。 为了解决check_size传递一个长度参数的问题,是使用bind 的标准库函数,定义在头文件functional中。 可以将bind函数 看作一个通用的函数适配器,接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。 bind的一般形式为:当调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
newCallable本身是一个可调用对象。 arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。 arg_list中的参数可能包含形如_n的名字,这些参数是”占位符",
auto newCallable = bind ( callable, arg_list) ;
bool check_size ( const string & s, string:: size_type sz)
{
return s. size ( ) >= sz;
}
auto check6 = bind ( check_size, _1, 6 ) ;
string s = "hello" ;
bool b1 = check6 ( s) ;
auto wc = find_if ( words. begin ( ) , word. end ( ) , [ sz] ( const string & a)
{ return a. size ( ) >= sz; } ) ;
auto wc = find_if ( words. begin ( ) , word. end ( ) , bind ( check_size, _1, sz) ) ;
sort ( words. begin ( ) , words. end ( ) , isShorter) ;
sort ( words. begin ( ) , words. end ( ) , bind ( isShorter, _2, _1) ) ;
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中,对有些绑定的参数希望以引用方式传递,或是要绑定参数的类型无法拷贝。
for_each ( words. begin ( ) , words. end ( ) , [ & os, c] ( const string & s)
{ os << s << c; } ) ;
ostream & print ( ostream & os, cost string & s, char c)
{
reutrn os << s << c;
}
for_each ( words. begin ( ) , words. end ( ) , bind ( print, os, _l, ' ' ) ) ;
for_each ( words. begin ( ) , words. end ( ) , bind ( print, ref ( os) , _l, ' ' ) ) ;
10.4 再探迭代器
除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器。 以下几种:
插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素。 流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所有关联的IO流。 反向迭代器:这些迭代器向后而不是向前移动,除了forward_list之外的标准库容器都有反向迭代器。 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。
10.4.1 插入迭代器
back_inserter 创建一个使用push_back的迭代器。front_inserter 创建一个使用push_front的迭代器。inserter 创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。
10.4.2 iostream迭代器
istream_iterator操作 操作含义 istream_iterator in(is); in从输入流is读取类型为T的值 istream_iterator end; 读取类型为T的值的istream_iterator迭代器,表示尾后位置 in1 == in2 in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同的输入,则两者相等 in1 != in2 同上不相等 *in 返回从流中读取的值 in->mem 与(*in).mem的含义相同 ++in, in++ 读取下一个值,前置版本返回后值,后置版本返回旧值
ostream_iterator 向一个输出流写数据。
ostream_iterator操作 操作含义 ostream_iterator out(os); out将类型为T的值写到输出流os中 ostream_iterator out(os, d); out将类型为T的值写到输出流os中,每个值后而都输出一个d,d指向一个空字符结尾的字符数组 out = val 用<<运算符将val写入到out所绑定的ostream中。val的类型必须与out可写的类型兼容 *out, ++out, out++ 这些运算符是存在的,但不对out做任何事情。每个运算符都返回out
istream_iterator< int > int_it ( cin) ;
istream_itreator< int > int_eof;
ifstream in ( "afile" ) ;
istream_iterator< string> str_it ( in) ;
istream_iterator< int > int_iter ( cin) ;
istream_iterator< int > eof;
while ( in_iter != eof)
{
vec. push_back ( * in_iter++ ) ;
}
istream_iterator< int > in_iter ( cin) , eof;
vector< int > vec ( in_iter, eof) ;
用ostream_iterator来输出值的序列。 因为*out++不对out做任何事情,每个运算符都返回out,所以可以省略。但是推荐第一种形式,循环的行为更为清晰。
ostream_iterator< int > out_iter ( cout, " " ) ;
for ( auto e : vec)
{
* out_iter++ = e;
}
cout << endl;
istream_iterator< Sales_item> item_iter ( cin) , eof;
ostream_iterator< Sales_item> out_iter ( cout, "\n" ) ;
Sales_item sum = * item_iter++ ;
while ( item_iter != eof)
{
if ( item_iter-> isbn ( ) == sum. isbn ( ) )
{
sum += * item_iter++ ;
}
else
{
out_iter = sum;
sum = * item_iter++ ;
}
}
out_iter = sum;
10.4.3 反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。 可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器。
vector< int > vec = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ;
for ( auto r_iter = vec. crbegin ( ) ; r_iter != vec. crend ( ) ; ++ r_iter)
{
cout << * r_iter << endl;
}
sort ( vec. begin ( ) , vec. end ( ) ) ;
sort ( vec. rbegin ( ) , vec. rend ( ) ) ;
反向迭代器有递减运算符,但流迭代器不支持。 如果要打印line中的第一个单词。
auto comma = find ( line. cbegin ( ) , line. cend ( ) , ',' ) ;
cout << string ( line. cbegin ( ) , comma) << endl;
如果要打印line中最后第一个单词,反转反向迭代器。
auto rcomma = find ( line. crbegin ( ) , line. crend ( ) , ',' ) ;
cout << string ( line. crbegin ( ) , rcomma) << endl;
cout << string ( rcomma. base ( ) , line. cend ( ) ) << endl;
10.5 泛型算法结构
任何算法的最基本的特性是它要求其迭代器提供哪些操作。 算法所要求的迭代器操作可以分为5个迭代器类别 。
迭代器类别 操作含义 输入迭代器 只读,不写,单遍扫描,只能递增 输出迭代器 只写,不读,单遍扫描,只能递增 前向迭代器 可读写,多遍扫描,只能递增 双向迭代器 可读写,多遍扫描,可递增递减 随机访问迭代器 可读写,多遍扫描,支待全部迭代器运算
10.5.1 5类迭代器
迭代器也定义了一组公共操作。一些操作所有迭代器都支持,另外一些只有特定类别的迭代器才支持。 对于向一个算法传递错误类别的迭代器的问题,很多编译器不会给出任何警告或提示。 输入迭代器 :可以读取序列中的元素。
用于比较两个迭代器的相等和不相等运算符(=、!=)。 用于推进迭代器的前置和后置递增运算(++)。 用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧。 箭头运算符(->),等价于(*t).member,即,解引用迭代器,并提取对象的成员。 算法find和accumulate要求输入迭代器,而istream_iterator是一种输入迭代器。 输出迭代器 :可以看作输入迭代器功能上的补集,只写而不读元素。
用于推进迭代器的前置和后置递增运算(++)。 解引用运算符(*),只出现在赋值运算符的左侧。 copy函数的第三个参数就是输出迭代器。ostream_iterator类型也是输出迭代器。 前向迭代器 :可以读写元素。
这类迭代器只能在序列中沿一个方向移动。 前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。 算法replace要求前向迭代器,forward_list上的迭代器是前向迭代器。 双向迭代器 :可以正向/反向读写序列中的元素。
除了支持所有前向迭代器的操作之外双向迭代器还支持前置和后置递减运算符(–)。 算法reverse要求双向迭代器,除了forward_list之外,其他标准库都提供符合双向迭代器要求的迭代器。 随机访问迭代器 :提供在常量时间内访问序列中任意元素的能力。
此类迭代器支持双向迭代器的所有功能 用于比较两个迭代器相对位置的关系运算符(<、<=、>和>=)。 迭代器和一个整数值的加减运算(+、+=、-和-=),计算结果是迭代器在序列中前进(或后退)给定整数个元素后的位置。 用于两个迭代器上的减法运算符(-),得到两个迭代器的距离。 下标运算符(iter[n]), 与*(iter[n])等价。 算法sort要求随机访问迭代器。array、deque、string和vector的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是 ,原理如此 。
10.5.2 算法形参模式
在任何其他算法分类之上,还有一组参数规范。 大多数算法具有如下4种形式之一:
alg ( beg, end, other args) ;
alg ( beg, end, dest, other args) ;
alg ( beg, end, beg2, other args) ;
alg ( beg, end, beg2, end2, other args) ;
其中alg是算法的名字。 beg, end表示算法所操作的输入范围。 dest指定目的位置。 beg2, end2表示第二个范围。 除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。 向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。
10.5.3 算法命名规范
除了参数规范,算法还遵循一套命名和重载规范。 一些算法使用重载形式传递一个谓词。
unique ( beg, end) ;
unique ( beg, end, comp) ;
find ( beg, end, val) ;
find_if ( beg, end, pred) ;
reverse ( beg, end) ;
reverse_copy ( beg, end, dest) ;
remove_if ( vl. begin ( ) , vl. end ( ) ,
[ ] ( int i)
{ return i % 2 ; } ) ;
remove_copy_if ( v1. begin ( ) , v1. end ( ) , back_inserter ( v2) , [ ] ( int i)
{ return i % 2 ; } ) ;
10.6 特定容器算法
与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法。
list和forward_list成员函数版本的算法 算法含义 lst.merge(lst2) 将来自lst2的元素合并入lst,lst1和lst2都必须是有序的,元素将从lst2中删除,在合并之后,lst2变为空。使用<运算符操作 lst.merge(lst2,comp) 同上,使用给定比较操作 lst.remove(val) 调用erase删除掉与给定值相等(==) lst.remove_if(pred) 调用erase删除掉令一元谓词为真的每个元素 lst.reverse() 反转lst中元素的顺序 lst.sort() 使用<操作排序元素 lst.sort(comp) 使用给定比较操作排序元素 lst.unique() 调用erase删除同一个值的连续拷贝,使用== lst.unique(pred) 调用erase删除同一个值的连续拷贝,使用给定的二元谓词
链表类型还定义了splice算法,此算法是链表数据结构所特有的,因此不需要通用版本。
list和forward_list的splice成员函数的参数 算法含义 lst.splice(args)或flst.splice_after(args) (p, lst2) p是一个指向lst中元素的迭代器,或一个指向flst首前位置的迭代器。函数将lst2的所有元素移动到lst中p之前的位置或是flst中p之后的位置。将元素从lst2中删除。lst2的类型必须与1st或flst相同,且不能是同一个链表 (p, lst2, p2) p2是一个指向lst2中元素的迭代器,将p2指向的元素移动到lst中,或将p2之后的元素移动到flst中。lst2可以是与lst或flst相同的链表。 (p, lst2, b, e) b和e必须表示lst2中的合法范围。将给定范围中的元素从lst2移动到lst或flst。lst2与lst(或flst)可以是相同的链表,但p不能指向给定范围中元素。
链表特有的操作会改变容器。
remove的链表版本会删除指定的元素。 unique的链表版本会删除第二个和后继的重复元素。 merge和splice会销毁其参数。 链表版本的merge函数会销毁给定的链表。