泛型算法:可用于不同类型的容器和不同类型的元素的通用算法。
概述
大多数算法都定义在头文件algorithm 中。标准库在头文件 numeric 中定义了一组数值泛型算法。
一般情况下,泛型算法不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。
迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。
算法不会改变容器的大小,只能改变其内的元素值以及移动元素。
初识泛型算法
标准库提供了超过 100 个算法。除了少数例外,标准库算法都对一个范围内的元素进行操作,此元素范围称为 “输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。
只读算法
只读取其输入范围内的元素,而从不改变元素。通常最好使用 cbegin()
和 cend()
。
常见的三种只读算法:
int sum = accumulate(vec.cbegin(),vec.cend(),val);//对 vec 中的元素在 val 的基础上求和。accumulate 的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。
bool F = equal(vec1.cbegin(),vec1,end(),vec2.cbegin());//比较两个序列的元素是否全部相等(第二个序列至少与第一个序列一样长)
auto iter = find_if(vec.begin(),vec.end(),'一元谓词');//查找符合某条件的元素,返回迭代器,如果没有找到就返回尾迭代器
写容器元素的算法
使用赋值序列中的元素的算法时,必须确保序列原大小至少不小于算法要写入的元素数目。
//算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数
fill(vec.begin(), vec.end(),0);//将每个元素重置为0
fill(vec.begin(), vec.begin() + vec.size()/2,10);//将容器的一个子序列设置为10
fill_n(vec.begin(), vec.size(),0);//将所有元素重置为0
fill_n(dest,n,val);//将 val 赋予从 dest 开始的连续 n 个元素。假定dest开始的序列有至少 n 个元素
插入迭代器
插入迭代器是一种向容器中添加元素的迭代器。
通常情况下,通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。
back_inserter:定义在头文件 iterator 中。
back_inserter 接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:
vector<int> vec; //空向量
auto it = back_inserter(vec);//通过它赋值会将元素添加到vec中
*it =42;// vec 中现在有一个元素,值为42
使用back inserter来创建一个迭代器,作为算法的目的位置来使用:
vector<int> vec; //空向量
//正确:back inserter创建一个插入迭代器,可用来向vec添加元素
fill_n(back inserter(vec),10,0);//添加10个元素到vec
在每步迭代中,fill_n
向给定序列的一个元素赋值。由于传递的参数是 back_inserter
返回的迭代器,因此每次赋值都会在 vec
上调用 push_back
。最终,这条 fill_n
调用语句向 vec
的末尾添加了10个元素,每个元素的值都是0。
拷贝算法
拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。
此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素。
int a1[] = {0,1,2,3,4,5,6,7,8,91;
int a2[sizeof(a1)/sizeof(*a1)];//a2与al大小一样
//ret指向拷贝到a2的尾元素之后的位置
auto ret = copy(begin(al),end (al),a2);//把a1的内容拷贝给a2
replace 算法读入一个序列,并将其所有等于给定值的元素都改为另一个值。
replace(ilst.begin(),ilst.end(),0,42);//把输入范围内所有值为 0 的元素改为 42
replace_copy(ilst.begin(),ilst.end(),dest,0,42);//把输入范围内所有值为 0 的元素改为 42 并拷给 dest 开头的序列。
重排容器元素的算法
调用sort()
函数会重新输入序列中的元素,使之变得有序。
sort(words.begin(),words.end());//默认按升序从小到大排列输入范围内的元素
调用unique()
来使得重复的元素放在序列的后端,并且返回指向序列后端重复元素中的第一个重复元素的迭代器。
auto end_unique = unique(words.begin(),words.end());
调用erase()
删除元素。
words.erase(end_unique,words.end());//erase()函数可以接受两个相等的迭代器作为参数。删除一个空范围没有什么后果。
定制操作
允许使用自定义操作符来替代默认运算符。
向算法传递函数
谓词:是一个可调用的表达式,其结果是作为一个能用作条件的值。
标准库使用的谓词分为两类:一元谓词(只接受单一参数)和二元谓词(有两个参数)。
接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须转换为谓词的参数类型。
lambda 表达式
一个lambda表达式表示一个可调用的代码单元。可以将其理解为一个为命名的内联函数。
Lambda 表达式如下:
[capture list](parameter list) -> return type {function body}
//lambda:捕获列表、参数列表、返回类型、函数体
与函数唯一的不同就是:1.lambda 可能定义在函数内部;2.lambda 必须使用尾置返回来指定返回类型。
可以忽略参数列表和返回类型,但必须包含捕获列表和函数体
auto f = [] {return 42}//定义了一个可调用对象 f,返回 42
如果 lambda 的函数体包含任何单一 return 语句之外的内容,且未指定返回类型,则返回void。
lambda表达式的调用方式也是使用调用运算符,内含实参。一个 Lambda 调用的实参数目永远与形参数目相等。因为,Lambda 不能有默认参数。
一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
//错误:sz未捕获
[](const string &a){return a.size()>=sz;
};[sz](const string &a){return a.size()>=Sz;
};
捕获列表只用于局部非 static 变量,lambda 可以直接使用局部 static 变量和在它所在函数之外声明的名字。
lambda 捕获和返回
当定义一个 lambda时,编译器生成一个与 lambda对应的新的(未命名的)类类型。
当向一个函数传递一个 lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。
当使用 auto 定义一个用 lambda 初始化的变量时,定义了一个从 lambda 生成的类型的对象。
默认情况下,从 lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。
变量的捕获方式也可以是值或引用。
lambda 捕获列表
值捕获
采用值捕获的前提是变量可以拷贝。
void fcnl(){size_t vl=42;//局部变量//将v1拷贝到名为f的可调用对象auto f= [v1] { return vl; };v1 =0;auto j=f();//j为42;f保存了我们创建它时v1的拷贝
}
被值捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝。因此,在 lambda 创建后改变被捕获的变量不会影响 lambda 中对应的值。
引用捕获
采用引用捕获,要确保被引用的对象在 lambda 执行的时候是存在的。
void fcn2(){size_t v1=42;//局部变量//对象f2包含v1的引用auto f2=[&v1] { return vl; };//v1之前的&指出v1应该以引用方式捕获。v1=0;autoj=f2();//j为0;f2保存v1的引用,而非拷贝
}
在 lambda 函数体内使用以引用方式捕获的变量时,实际上使用的是引用所绑定的对象。
lambda 捕获的都是局部变量。
隐式捕获
可以使编译器根据 lambda 体中的代码来推断我们要使用哪些变量:&
告诉编译器采用捕获引用方式,=
则表示采用值捕获方式。
//sz为隐式捕获,值捕获方式
wC = find_if(words.begin() , words.end(),[=](const string &s){return s.size()>=sz;});
希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获。
void biggies(vector<string> &words,vector<string>::size_type sz,ostream &os = cout, char c=' '){//其他处理与前例一样//os 隐式捕获,引用捕获方式,c显式捕获,值捕获方式for_each(words.begin(),words.end(),[&,c](const string &s){os << s << c;});//os 显式捕获,引用捕获方式,c阴式捕获,值捕获方式for_each(words.begin(),words.end(),[=,&os](const string &s){os << s << c;});
可以通过混合使用隐式捕获和显示捕获来实现对一部分变量使用值传递,而另一部分使用引用传递。
混合捕获时捕获列表中的第一个元素必须是一个 &
或 =
:
- 如果是
&
,表示采用隐式的引用捕获,此时后面显式捕获的变量必须都采用值捕获。 - 如果是
=
,表示采用隐式的值捕获,此时后面显式捕获的变量必须都采用引用捕获。
可变 lambda
默认情况下,通过值捕获拷贝的变量,lambda 不会改变其值。如果改变一个被捕获的变量的值,必须在参数列表首加上关键字 mutable
。
void fcn3(){size_t v1=42;//局部变量//f可以改变它所捕获的变量的值auto f = [v1]() mutable { return ++v1; };v1=0;auto j= f();//j为43
}
一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个const类型还是一个非const类型。
指定 lambda 返回类型
默认情况下,如果一个 lambda 体包含 return
之外的任何语句,则编译器假定此 lambda 返回 void
。与其他返回 void
的函数类似,被推断返回 void
的 lambda不能返回值。
如果 lambda 中只包含一个单一的 return 语句,可以省略返回类型。
如果 lambda 中除了 return 还有其他语句,此时应该指明返回类型。
需要为一个lambda定义返回类型时,必须使用尾置返回类型。
参数绑定
lambda 表达式适合那种只在一两个地方使用的简单操作。如果操作需要很多语句或需要在很多地方使用,通常应该定义一个函数。
对于捕获列表为空的 lambda,通常可以用函数来代替。
标准库 bind 函数
调用 bind
的一般形式为:
//newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。
auto newCallable bind(callable,arg _list);
bind
定义在头文件 functional
中。可以将bind
函数看作一个通月的函数适配器,它接受一个可调用对象,生成一个新的可调用象来"适应"原对象的参数列表。
bind 占位符
arg_list 中的参数可能包含形如 n 的名字,其中 n 是一个整数。这些参数是 “占位符”,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的“位置”。数值 n 表示生成的可调用对象中参数的位置:_1
为 newCallable 的第一个参数,_2
为第二个参数,以此类推。
名字_n
都定义在一个名为 placeholders
的命名空间中,而这个命名空间本身定义在std
命名空间中。
声明语句为:using std::placeholders::_n;
bind 的参数
1.可以用 bind
修正参数的值,固定某些参数。
//假定 f 是一个可调用对象,有 5 个参数
// g 是一个有两个参数的可调用对象
auto g = bind(f,a,b,_2,c,_1)
//生成一个新的可调用对象,它有两个参数,分别用占位符_2和_1表示。
2.可以用 bind
绑定给定可调用对象中的参数或重新安排其顺序。
// 用 bind 颠倒 isShroter 的含义
//按单词长度由短至长排序
sort(words.begin(),words.end(), isShorter);
//按单词长度由长至短排序
sort(words.begin(),words.end(),bind (isShorter,_2,_1));
绑定引用参数
默认情况下,bind
的那些不是占位符的参数是值传递,被拷贝到 bind
返回的可调用对象中。
如果要传递引用或该参数类型不能拷贝(ostream
),可以使用 ref 函数:
for_each (words.begin(), words.end(),bind(print, ref(os), 1, ' '));
函数ref
返回一个对象,包含给定的引用,此对象是可以拷贝的。
cref
可以生成保存 const 引用的类。ref
和 cref
也定义在头文件 functional
中。
再探迭代器
- 插入迭代器(insert iterator):迭代器被绑定到一个容器上,可用来向容器插入元素。
- 流迭代器(stream iterator):迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。
- 反向迭代器(reverse iterator):迭代器向后而不是向前移动。除了forward list之外的标准库容器都有反向迭代器。
- 移动迭代器(move iterator):这些专用的迭代器不是拷贝其中的元素,而是移动它们。
插入迭代器
插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。
通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
插入迭代器支持的操作:
it = t //在it指定的当前位置插入值t。假定c是it绑定的容器,依赖于插入迭代器的不同种类,此赋值会分别调用c.push_back(t)、c.push_front(t) 或 c.insert(t,p),其中p为传递给 inserter 的迭代器位置
*it,++it,it++ //这些操作虽然存在,但不会对it做任何事情。每个操作都返回it
插入器有三种类型,差异在于元素插入的位置:
1.back_inserter
创建一个使用 push_back
的迭代器。
2.front_inserter
创建一个使用 push_front
的迭代器。
3.inserter
创建一个使用 insert
的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。
iostream 迭代器
IO 类型对象的迭代器:istream_iterator
读取输入流,ostream_iterator
向一个输出流写数据。这些迭代器将对应的流当作一个特定类型的元素序列来处理。
创建一个流迭代器时,必须指定迭代器将要读写的对象类型。
istream_iterator 操作
一个 istream_iterator
使用 >>
来读取流。istream_iterator
要读取的类型必须定义了输入运算符。
ostream_iterator 操作
ostream_iterator
可以对任何具有输出运算符(<<运算符)的类型定义。其赋值操作等价于输出流的输出操作。每赋一次值,输出一次。
反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。 对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++it
)会移动到前一个元素;递减一个迭代器(--it
)会移动到下一个元素。
除了 forward_list 外,其他容器都支持反向迭代器。
可以通过调用 rbegin
、rend
、crbegin
和 crend
成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前的一个位置。
反向迭代器支持递增和递减操作。注意流迭代器不支持递减操作,因为不可能在一个流中反向移动。
反向迭代颠倒:reverse_iterator
的 base
成员函数会返回相应的正向迭代器,正向迭代器指向靠后一个元素。
//正确:得到一个正向迭代器,从逗号开始读取字符直到line末尾
// rcomma 是一个反向迭代器
cout<<string(rcomma.base(), line.cend())<<endl;
泛型算法结构
迭代器类别
输入迭代器
1.用于比较两个迭代器的相等和不相等运算符(=
、!=
)
2.用于推进迭代器的前置和后置递增运算(++
)
3.用于读取元素的解引用运算符(*
);解引用只会出现在赋值运算符的右侧
4.箭头运算符(->
),等价于(*it).member
,即,解引用迭代器,并提取对象的成员
输出迭代器
1.用于推进迭代器的前置和后置递增运算(++
)
2.解引用运算符(*
),只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)
前向迭代器
1.只能在序列中沿一个方向移动。
2.支持所有输入和输出迭代器的操作,可以多次读写同一个元素。
3.算法 replace 要求前向迭代器,forward_list 上的迭代器是前向迭代器。
双向迭代器
1.可以正向/反向读写序列中的元素。
2.支持所有迭代器操作,还支持前置和后置递减运算符(--
)
3.算法 reverse 要求双向迭代器。
随机访问迭代器
1.用于比较两个迭代器相对位置的关系运算符(<
、<=
、>
和>=
)
2.迭代器和一个整数值的加减运算(+
、=
、-
和-=
),计算结果是迭代器在序列中前进(或后退)给定整数个元素后的位置
3.用于两个迭代器上的减法运算符(-
)
4.得到两个迭代器的距离下标运算符(iter[n])
,与*(iter[n])
等价
算法形参模式
算法的形参一般是以下 4 种模式之一
- alg(beg, end, other args);
- alg(beg, end, dest, other args); dest 经常是一个插入迭代器或一个 ostream_iterator,他们都能确保不管写多少元素都肯定是安全的;
- alg(beg, end, beg2, other args);
- alg(beg, end, beg2, end2, other args);
alg 是算法的名字,beg 和 end 表示算法所操作的输入范围。
算法命名规范
除了参数规范,算法还遵循一套命名和重载规范。
重载谓词的算法
一些算法使用重载形式传递一个谓词,来代替 <
或 ==
。
unique(beg, end); // 使用 == 比较元素
unique(beg, end, comp); // 使用 comp 比较元素
_if 版本的算法
接受谓词参数的算法都有附加的 if 前缀
find(beg, end, val); // 查找范围内 val 第一次出现的版本。
find_if(beg, end, pred); // 查找第一个令 pred 为真的元素。
_if 版本的算法不是重载算法。
拷贝版本的算法
拷贝版本将元素写到一个给定的位置。
reverse (beg, end);//反转输入范围中元素的顺序
reverse_copy (beg, end, dest);//将元素按逆序考贝到dest
特定容器算法
链表类型 list
和 forward_list
定义了几个成员函数形式的算法。定义了独有的 sort, merge, remove, reverse , unique
。
通用版本的 sort 要求随机访问迭代器,因此不能用于list
和 forward_list
其他链表类型定义的算法的通用版本可以用于链表,但是性能差很多,因为这些算法需要交换输入序列中的元素 。
优先使用成员函数版本算法
list
和 forward_list
成员函数版本的算法
lst.merge(lst2);// 将 lst2 的元素合并入 lst。两者必须都是有序的,元素将从 lst2 删除,合并后。lest2 变为空。使用 < 比较元素或给定操作。
lst.merge(lst2,comp);//给定操作lst.remove(val); // 调用 erase 删除掉每个元素。
lst.remove_if(pred); // 删除掉 lst 中使一元谓词 pred 为真的每个元素。lst.reverse();// 反转 lst 中元素的顺序。lst.sort();// 使用 < 排序元素
lst.sort(comp); //给定操作排序元素lst.unique(); // 调用 erase 删除同一个值的连续拷贝。
lst.unique(pred);// pred 是个二元谓词,删除使 pred 为真的连续拷贝。
splice 成员
重要术语
泛型算法(generic algorithm) 类型无关的算法。
谓词(predicate) 返回可以转换为 bool 类型的值的函数。泛型算法通常用来检测元素。标准库使用的谓词是一元(接受个参数)或二元(接受两个参数)的。