第十章(泛型算法)
1).容器定义的操作集合很小。因为标准库不是给每一个容器添加大量的功能。思想是提供一组通用的的算法,也就是泛型算法。 2).这些算法是不指针特定类型容器或者特定类型的元素的。
- 不仅仅可以用于标准库类型
vector 和list - 还可以用于内置的数组类型
- 甚至是其他类型的序列
3).顺序容器只定义了少量的操作。更多的操作例如查找特定的元素,替换或者删除一些特定值,或者重排元素等。标准库定义了一组泛型算法,是经典算法的公共接口。
/1.概述
1).算法所在头文件。
- 大多数算法,
algorithm - 一组泛型算法,
numeric 。
2).几点说明。
- 指针就像是内置数组的迭代器一样。
- 使用
begin(array) 或者end(array) 得到数组的首元素和尾后元素的指针。返回的还是指针。int *
3).一个例子,find ,接受三个参数,前两个表示范围迭代器,第三个表示要查找的值。
- 如果有,返回还第一个等于给定值的迭代器。
- 如果没有,返回第二个参数。
4).注意。
- 迭代的使用使得,容器可以忽略容器的类型,甚至不是容器都可以。
- 但是元素类型的操作是需要注意的。有的需要支持
< 等操作。但是大多数的算法提供了一种方法。允许我们是使用自定义的操作来代替默认的运算符。 - 泛型算法本身不会执行容器的操作。它们只会运行于迭代器之上,执行迭代器的操作。也就是说算法本身不会改变底层容器的大小。算法本身可能会修改容器中的值,可能会在容器内移动元素。但是永远不会改变容器中元素的数目。
- 但是标准库还定义了一种特殊的迭代器,叫做插入器。它不仅仅可以遍历所绑定的容器。**给这一类容器迭代器赋值时,它会在底层容器上执行插入操作。**所以当算法执行的是一个这样的迭代器时。迭代器可以完成修改容器中元素数目的效果(添加)。但是算法本身不会做这样的操作。
/2.初始泛型算法
1).除了少数的例子之外,标准库的算法都是对一个范围内的元素进行操作。这个范围称为,输入范围。接受输入范围的算法总是使用前两个参数来表示范围的。两个参数就是范围迭代器,左闭右开。 2).大多数算法遍历输入范围的方式是相似的。但是它们使用范围中的元素的方式是不一样的。了解一个算法重要的就是了解它是否读取元素,改变元素,重排元素。
//1.只读算法
1).算法类型。
算法名称 | 相关描述 |
---|
find | 返回第一次出现的位置。 | count | 返回出现的次数。 | accumulate | 计算和 | equal | 判断两个序列是否相等,序列中的每一个元素都是相等的就返回ture 。 |
2).几点说明。
find 和count 都是前面接受迭代器范围,后面接受val accumulate ,第三个参数表示开始计算的初始值。就是sum 的初始值。不仅仅如此,他还决定了使用哪一个加法运算符号,以及返回值的类型。
- 待求和序列的每一个元素都应该可以和计算起点相匹配,也就是可以相互转换。例如,0,那么序列里面的元素可以是int,double,float等。注意一个特殊,
"",//表示的是一个空字符串,它的类型是const char*。请注意在const char *里面是没有定义+的。 。当我们实现,容器中所有字符序列的求和时,应该显式地进行转换。string(""); 以这个作为计算起点。因为string 是定义了+ 的运算。 - 对于只读的算法,最好使用
cbegin,cend ,除非要修改值。 - 那些只接受单一个迭代器用来表示第二个序列的算法,都假定第二个序列的长度至少和第一个序列是等长的。
equal 算法接受的是三个迭代器,前两个表示一个迭代器的范围,第三个表示第二个序列的 首元素 。- 注意,由于以上使用的都是迭代器,所以我们不需要保证容器类型是一样的,而且对于元素的类型我们也只是要求可以进行相应的运算。
{
equal(a.begin(),a.end(),b.begin());
string sum = accumulate(a.begin(),a.end(),string(""));
int a = accumulate(c.begin(),c.end(),0);
}
练习,
- 10.4,注意
accumulate 的第三个参数表示了计算的返回值,要想计算返回的是double 应该使用的是0.0 。 - 10.5,注意,c风格字符串的本质就是
char* ,所以,两个c风格的字符串比较==,只会比较指针的值是否相等,而不会关注字符是否相等。当两个指针指向一样的地址时返回的时true ,反之返回的时false 。 - 而
string 对象重载了==,它会比较两个字符串的长度以及它们的每一个字符是否相等。
{
char *p[] = {"a","b","c"};
char *q[] = {p[0],p[1],p[2]};
char *r[] = {string(p[0]),string(p[1]),string(p[2])};
equal(begin(p),end(p),q);
equal(begin(p),end(p),r);
}
//2.写容器元素的算法
1).写元素的算法操作。
算法名称 | 相关描述 |
---|
fill(b,e,val) | 将迭代器范围的元素替换为val | fill_n(b,n,val) | 将迭代器所指向的位置开始的n个元素替换为val | back_insert(vec) | 传入的参数是一个容器,(形参是一个容器的引用)返回的是与该容器绑定的插入迭代器 | copy(b,e,b) | 前面两个参数表示的是一个一个迭代器的范围,第三个参数表示的是目的序列的额起始位置。 | replace(b,e,old_val,new_val) | 将迭代器范围中的指定元素换为新的元素。 | replace_copy(b,e,back_insert?,old_val,new_val) | 不会改变原序列,而是将修改过序列,放在一个新创建的序列中。 |
2).注意事项。
- 执行写的操作时,**一定要注意,目的的序列大小不能小于我们要写入的数据的大小。**原因在于,这些算法不会修改容器的大小。
{
vector<int> vec;
fill_n(vec.begin(),10,0);
}
- 有的算法接受四个迭代器,来表示两个迭代器范围。
- 插入迭代器的使用可以保证我们的容器由足够大小的空间,不会出现未定义的结果。
- 插入迭代器是将值添加到容器中,而我们通过迭代器向容器中赋值,只是修改。
back_insert 是一个定义在头文件iterator 中的函数。back_insert 的机理其实就是调用了push_back
{
vector<int> vec;
auto it = back_insert(vec);
*it = 42;
fill_n(back_insert(vec),10,0);
}
- 传递给
copy 的目的序列至少要和输入范围的大小是一样的。
{
int a1[] = {1,2,3,4,5};
int a2[sizeof(al)/sizeof(int)];
auto ret = copy(begin(a1),end(a1),a2);
}
- 它的返回值
ret 是指向目的迭代器递增之后的值。上面例子中,它指向的就是a2 的尾后迭代器。 - 很多算法都提供了一个
copy 的版本,这些算法不在原序列上直接修改,而是创建一个新的序列来保存修改之后的序列。
练习,
- 10.7,注意我们要求的是,容器中有元素,而不是由足够的空间。因为空间不是问题,容器都有自己扩张的能力。
{
vector<int> vec;
vec.reserve(10);
copy(vec.begin(),vec.end(),0);
}
- 10.8,严格地说,算法根本不知道容器是什么,它们只知道一个范围,(迭代器范围)。**它们只是操作迭代器,不是直接操作容器。**能实现什么功能,取决于我们给它什么迭代器。我们给它们传递的迭代器,决定了它们是否可以间接修改容器。
//3.重排容器中元素的算法
1).重排算法。
算法操作 | 相关描述 |
---|
sort(b,e) | 将给定的范围迭代器进行排序,注意这里的排序是使用元素之间的<运算实现的。 | unique(b,e) | 将范围迭代器中不重复的元素排列在原来容器的前部,但是没有删除元素,也就是说,元素的个数没有变化。返回的是指向不重复序列的后一个迭代器。 |
2).几点说明。
- 由于
unique 返回的是不重复序列的“尾后迭代器”,所以当我们需要删除元素时,传入的参数也是两个合法的迭代器。不会犯错。 - 由于泛型算法不会对容器进行操作。它不会修改容器的大小,所以如果我们需要删除元素,需要使用,
erase 函数。
/3.定制操作
1).很多的排序算法在默认情况下是使用元素类型的<或者== 运算符号进行操作的。注意它是一个默认的实参。 2).我们可以用自定义的操作来替换默认的运算符号。
- 例如,
sort 默认情况下是使用< 运算符的。我们可能需要别的排序方式 - 一些元素不支持默认的运算符。
//1.向算法传递函数
1).谓词,其实就是函数。
- 一元谓词,接受一个参数的函数
- 二元谓词,接受两个参数的函数
2).注意事项。
- 要求元素类型可以转换为谓词的形参。
- 因为接受谓词的算法实际上就是对元素调用谓词。
- 谓词要求,首先需要在序列中所有可能的元素中确定一个一致的序。
3).stable_sort 算法。可以实现在保持谓词的条件下,进行字典顺序的排列。 4).例子。
{
sort(v.begin(),v.end()).isShorter);
stable_sort(v.begin(),v.end(),isShorter);
}
练习,
- 10.13,
partition 算法,接受一个谓词,对容器里卖弄的元素进行划分,使得谓词为true 的会排在容器的前半部分,使得谓词为false 的会排列在容器的后半部分。返回的是一个迭代器。指向的是使得谓词为true 的最后一个元素的后一个位置。 - 注意到上述是一个一元谓词。并且也是接受一个范围迭代器。
partition(v.begin(),v.end(),five_or_more);
//2.lambda表达式
1).[捕获列表,以,分隔开](形参列表)->尾后返回类型{函数体}
- 捕获列表,可以为空,但是不能省略,里面包含的是其所在函数的局部变量。对于函数内
static 变量或者定义在函数体之外的变量,可以在lambda 的函数体里面直接使用。 - 形参列表,可以为空,甚至可以省略,不可以是默认实参。传递给
lambda 的实参个数必须和形参个数完全一致。 - 返回类型,可以省略,但是如果省略,除非函数体里面只有一条
return 语句,此时将会根据返回值进行判断返回的类型;而若有其他语句,将会返回void 。只能使用尾置返回类型。注意如果编译器推断一个lambda 的返回值是void 的时候,如果返回了一个int 或者其他的类型。那么将会产生编译错误。 - 函数体,可以为空,但是不可以省略。
{
auto f = []{return 42;};
cout << f() << endl;
[](const string &a,const string &b){return a.size() < return b.size();}
[a]{};
stable(v.begin(),v.end(),[](const string &a,const string &b){return a.size() > b.size();});
for_each(v.begin(),v.end(),[](const string &){cout << s << " ";});
}
2).lambda 用来解决需要多个参数的谓词的情况。 3).介绍两个新的算法。
算法名称 | 相关描述 |
---|
find_if(b,e,f) | 前面两个参数接受迭代器范围,第三个参数是一个谓词。对每一个元素调用该谓词,返回的是第一个使得该谓词返回非0的迭代器。如果没有,那么返回的是一个尾后迭代器的拷贝。 | for_each(b,e,f) | 对范围迭代器中的每一个元素调用f, |
4).几点说明。
- 可调用对象,我们可以向任何一个算法传递一个可调用对象。可以用调用运算符号进行调用的对象或者表达式,就是一个可调用对象。分为
- 函数,
- 函数指针,
- 重载了函数调用运算符的类,
lambda 表达式
- 对于可调用对象,我们可以这样调用它。
e(args) 。其中,args 是用逗号分隔开的一个或者多个参数列表。 - 一个
lambda 一个表达式就是一个可以调用的代码单元。可以理解为未命名的内联函数。 - 与其他函数不一样的是,一个
lambda 表达式可以定义在函数的内部。
//3.lambda捕获和返回
1).我们定义lambda 的时候,编译器是生成了一个与lambda 相互对应的(没有名字的)类类型。当我们向一个函数传递一个lambda 时,同时定义了一个新的类和它的一个对象。而且传递的参数就是这个类的一个对象。
- 当我们使用
auto 定义一个lambda 的变量时,其实就是创建一个lambda 生成类型的一个对象。 - 默认情况下,从
lambda 生成的类,包含lambda 所捕获的数据成员。并且在创建一个对象时,就初始化该数据成员。而不是调用时才初始化。这一点是与类一样的。
2).两种捕获方式。
- 值捕获,等价于函数的传值调用。**在捕获时就创建了该数据成员的拷贝。**我们只需要关心创建时,该捕获是什么状态就可以。后续两个就没有任何关系。互不影响。
- 引用捕获,等价于引用调用。
- 与我们不能返回局部变量的引用或者指针,是一样的。当我们使用一个
lambda 表达式时,需要关注它的引用是否还在作用域,是否还有意义。 - 不可以返回一个捕获局部变量的引用的
lambda 。
- **值捕获,我们只关心创建时的状态。引用捕获时刻关心。**尽量减少引用捕获,尽量减少捕获的数据量。
{
void fc()
{
size_t v1 = 42;
auto f = [v1]{return v1;};
v1 = 1;
cout << f() << endl;
}
}
3).混合两种捕获方式。
[&,a,b,c] 。隐式使用引用捕获,后面显式指出值捕获。[=,&a,&b,&c] 。隐式使用值捕获,后面显式指出引用捕获。- 如果显式指出的捕获与隐式一样,那是错误的。
- 捕获列表第一个必须是
& 或者= 。
4).关于修改数据成员和返回类型的指定。
- 如果我们希望,可以修改数据成员。对于值捕获,需要在形参列表的后面加上
mutable 关键字,auto f = [a]()mutable{return ++a;}; ;对于引用捕获,能否修改,取决于应用的是否是const 。 - 当我们有除了
return 之外的语句时,显式地指出返回类型是必须的。
5).介绍一个算法。transform(v.begin(),v.end(),v.begin(),[](int i){return i < 0? -i : i;};) 函数的作用就是,将范围迭代器中的每一个元素调用lambda 表达式。并将结果写第三个迭代器中去。注意目的迭代器可以和输入迭代器一样。本例中就是将序列中的每一个值变为它的绝对值。
- 注意第三个迭代器是会自己增加的,他只是一个目的起始的作用。
练习,
- 10.20
count_if(b,e,f); 对范围迭代器中的每一个元素调用f ,计算f 为真的次数。 - 10.21,
[]()mutable->int{} 。注意顺序。
//4.参数绑定
1).使用bind 函数,丢弃旧版的bind1st ,和bind2nd 。形式,bind(f,args) 。
- f为带绑定的函数。args可以是占位符,可以是普通的参数,还可以是引用的参数。对于那些不可以拷贝的绑定参数,我们只能使用引用。
- 使用
ref(cout) 返回的是一个对象。包含了给定的引用。此对象是可以进行拷贝的。 - 标准库还定义了一个
cref() ,生成的是一个const 的引用的类。ref 和cref 以及bind 都定义在头文件functional 中。
- 注意之所以说拷贝,是因为,
bind 函数返回一个新的可调用对象,这个可调用对象接受参数完成后,bind 里面的非占位符,在默认情况下将进行**拷贝操作。**所以对于没有拷贝操作的,只能使用引用。 - **占位符。**是定义在命名空间
placeholders 中的,而这个命名空间又是定义在std 中的。为了使用占位符,两个命名空间都需要写上。using::std::placeholders::_1 。这样做很麻烦,一般我们使用的是using namespace std::placeholders; 使得由placeholders 定义的名字都可以直接使用。placeholders 在头文件functional 中定义。
{
auto g = bind(f,a,b,c,_2,d,_1);
g(x,y);
f(a,b,c,y,d,x);
auto a = find_if(v.begin(),v.end(),bind(f,_1,sz));
}
2).lambda 表达式适合用在语句少,重复率不高的情况下。其他情况下应该使用函数。
- 捕获列表为空,函数就可以直接替换它。
- 捕获列表不为空使用
bind 来进行辅助。
3).几点说明。
bind 可以看出是函数的适配器。接受一个可调用对象并且生成另一个可掉用对象来适应不同的参数列表。- 占位符的个数表示新生成的可调用对象接受的参数的个数。
bind 的占位符可以用来重排参数的顺序。bind(f,_2,_1);
练习,
- 10.23,
bind 的参数是f 的参数加一。并且,bind 的一些参数可以是它所在函数的局部变量。
/4.再探迭代器
在头文件iterator 中,还定义了以下迭代器
- 插入迭代器,绑定一个容器,向绑定的容器中插入元素
- 流迭代器,
- 反向迭代器,向后移动的,而不是向前移动的。除了
forward_list 之外的标准库容器都有这一个迭代器。 - 移动迭代器,不是用来拷贝容器中的元素,而是移动容器中的元素。
//1.插入迭代器
1).插入迭代器支持的操作。
操作名称 | 相关描述 |
---|
it = val; | 依赖于绑定的类型,在不同的位置进行插入 | *it,++it,it++ | 虽然存在,但是不会对it有什么影响,都是返回it |
2).三种类型的插入器。
back_inserter(c); 创建一个调用push_back 的插入迭代器。front_inserter(c); 调用的是push_front 。inserter(c,it); 第二个参数必须是指向给定容器的。元素将插入给定迭代器所表示的元素之前。只获取一次,不会一直更新。
3).几点说明。
- 由于底层的原因,容器支持
push_back 才有back_inserter 的迭代器。以此类推。 - 插入迭代器为算法不操作容器本身,而又需要对容器进行插入提供了一种过渡。从而保证了算法的通用性质。
4).典例示范。
{
auto it = inserter(c,iter);
*it = val;
iter = c.insert(iter,val);
++iter;
list<int> lst = {1,2,3,4};
list<int> lst1,lst2;
copy(lst.cbegin(),lst.cend(),front_inserter(lst1));
copy(lst.cbegin(),lst.cend(),inserter(lst2,lst2.bigin()));
}
练习,
- 10.27,
unique_copy ,接受第三个迭代器参数,表示目标的位置。我们可以传递插入迭代器,避免元素数目不够的问题。
//2.iostream迭代器
1).流不是容器,但是是序列。它具有和容器序列一样的特性。标准库定义了两个迭代器。
istream_iterator ,用来从流中读取数据。ostream_iterator ,用来向流中写数据。- 注意,这些迭代器将流当成是特定元素序列来处理,我们可以借助迭代器,对流使用泛型算法。
2).istream_iterator 。
操作 | 相关描述 |
---|
istream_iterator<T> in(is) | in 从流is 中读取数据,T必须指定,表示流序列中的元素的类型。is 表示输入流,可以是istream ,fstream ,以及istringstream 。in 就是指向流序列的第一个元素。需要进行递增和解引用操作 | istream_iterator<T> in | 没有绑定流,表示的是尾后迭代器。但是类型不可以省略 | in1 == in2 | 满足两个条件,1).读取数据的类型相同,2).都是尾后迭代器,或者绑定在同一个元素上 | in1 != in2 | | *in | 返回从流中读取的数据 | in->mem | *相当于,(in).mem | ++in,in++ | |
3).注意事项。
istream_iterator 通过>> 来读取流。因此istream_iterator 要读取的类型需要定义了输入运算符。只要定义了>> 的对象就可以使用istream_iterator ostream_iterator 同上理。只要定义了<< 即可。- 对于绑定到流的迭代器,一旦它所关联的流到了文件的尾或者遇到了IO错误。那么迭代器的值就和尾后迭代器的值是相等的。
- **懒惰求值,**当我们将一个
istream_iterator 绑定到一个流时,标准库并不保证立即从流总读取数据。具体的实现可以推迟从流中读取数据,直到我们使用迭代器时才真正地生效。也就是说标准库保证在我们解引用流迭代器之前,读取操作一定是完成的。虽然大多数情况下这是无关紧要的,但这一点在以下情况是很重要的。
- 我们创建了
istream_iterator 对象但是没有使用就销毁了 - 我们正在从两个不同的对象同步地读取一个流,何时读取就很重要了。
4).典例示范。
{
istream_iterator<int> in_iter(cin),eof;
vector<int> vec(in_ter,eof);
copy(in_ter,eof,back_inserter(vec));
cout << accumulate(in_iter,eof,0) << endl;
}
5).ostream_iterator 支持的操作。
操作名称 | 相关描述 |
---|
ostream_iterator<T> out(os) | 将类型为T的数据写入流os中,注意流不可以省略,类型也是一样不可以省略的。 | ostream_iterator<T> out(os,d) | 在每一个数据输出之后都会又一个d,d是一个c风格的字符串,(字符串字面量,或者以空字符结尾的字符串数组的指针。) | out = val | 用<< 将val 写入到out所绑定的流ostream 中,val 的类型必须和T相容。 | *out,out++,++out | 不对out 做任何事情,返回的还是out |
6).典例示范。
{
ostream_iterator<int> out_it(cout," ");
for (auto i: vec)
*out_it++ = i;
out_it = i;
cout << endl;
copy(vec.begin(),vec.end(),out_it);
cout << endl;
}
7).对类类型进行处理。只要类类型定义了<< 或者>> 。
{
istream_iterator<Sales_data> item_iter(cin),eof;
ostream_iterator<Sales_data> out_iter(cout,"\n");
Sales_data sum = *item_iter++;
while (item_iter != eof)
{
if(sum.isbn() == item_iter->isbn())
sum += *item_iter++;
else
{
*out_iter++ = sum;
sum = *item_iter++;
}
}
out_iter = sum;
}
//3.反向迭代器
1).反向迭代器是reverse_iterator类的一个对象。我们可以这样定义它
- 它是支持递减元素的迭代器。
- 如何获得,调用
crbegin,crend,rbegin,rend 可以得到,同样又const 的版本和非const 的版本。
2).几点说明。
forward_list ,不支持反向迭代器,其他的容器都支持反向迭代器。- 流迭代器不支持递减运算,不可能在一个流里面进行反向的移动。
- 反向迭代器,的增就是向左边移动,恰好与普通迭代器递增时移动的方向是相反的。
- 反向迭代器,的范围是从尾到头方向的。
{
cout << string(line.crbegin(),rcomma) << endl;
}
- **借助,
reverse_iterator 的成员函数base ,**可以达到将反向的迭代器转换为普通的迭代器的目的。并且还是输出一样内容,只是顺序是相反的。
{
cout << string(rcomma.base(),line.cend()) << endl;
}
- 仔细观察可以知道。
rcomma.base() 所指向的元素和rcomma 所指向的元素是不一样的,恰好是相邻的。rbegin 和end ,以及begin 和rend ,所指向的也是相差一个元素。 - 原因在于,它们都是一个范围迭代器,要求左闭区间。而且表示的范围还是一样的。
/5.泛型算法的结构
1).算法的分类。
2).5类迭代器的说明
迭代器的名称 | 相关描述 |
---|
输入迭代器 | 只读不写,单遍扫描,只能递增 | 输出迭代器 | 只写不读,同上 | 前向迭代器 | 可读写,多遍扫描,只能递增 | 双向迭代器 | 同上,递增递减 | 随机迭代器 | 同上,支持所有的迭代器运算 |
//1.5类迭代器
1).5类迭代器。
迭代器名称 | 相关描述 | 支持的操作 | 算法举例 | 迭代器举例 |
---|
输入迭代器 | 可以读取序列中的元素。 | 1).==,!= ;2).前置或者后置++ ;3).解引用* ,**只能作为右值。**4).-> ,解引用运算符,或者(*it).mem; 可以访问对象的成员。只能用来顺序访问,只能对元素读取一次。只能用于单遍扫描的算法。 | find ,accumulate | istream_iterator | 输出迭代器 | 是输入迭代器的补集。1).前置或者后置++ ;2).解引用运算符号,* ,只能作为左值(像一个解引用的输出迭代器赋值就是将值写入到它所指向的元素中)。只能顺序访问,单遍扫描,只能对输出迭代器赋值一次。 | copy 的第三个参数 | 作为目的位置的迭代器。ostream_iterator | | 前向迭代器 | 只能沿着一个方向移动迭代器。可以多次读写元素。可以对序列进行多次的扫描。(可以保留迭代器的状态。) | replace | forward_list 的迭代器。 | | 双向迭代器 | 可以正向,也可以方向地读写序列中的元素。支持递减运算。 | reverse | 除了forward 标准库容器都提供了。 | | 随机访问迭代器 | 1).支持随机访问,2).<,>,>=,<= ;3).支持和一个整数值进行+=,-=,+,- 运算;4).两个迭代器之间的减法;5).用下表运算,iter[n],*(iter[n]) 两者是等价的。 | sort | array,deque,string,vector 的迭代器以及内置数组的指针也是。 | |
2).几点说明。
- 除了输出迭代器之外,其他的迭代器都是层次的。处于上层的迭代器同时也支持下层迭代器的功能。
- 类似容器,迭代器也支持一组公共的操作,不同的迭代器支持不一样的特殊操作。
- 算法通常都会要求最低层次的迭代器。但是向一个算法提供一个不符合要求的迭代器,编译器不会给出警告的信息。例如
find 算法要求至少是,要求少是输入迭代器。replace 算法要求至少是向前迭代器。replace_copy 算法要求,前两个至少是向前迭代器,第三个至少是输出迭代器。
练习,
- 10.40,
reverse 要求两个参数是双向迭代器。unique ,向前迭代器。需要覆盖,和可以多次读写。
//2.算法的形参模式
1).四种形式。(一个范围,一个范围加一个目标,)
alg(b,e,args); alg(b,e,dest,args); alg(b,e,b1,args); alg(b,e,b1,e1,args);
2).注意事项。
- 几乎所有的算法都需要接受一个输入范围。
- 除了迭代器参数,还有
args 表示非迭代器的参数。 - 算法都是假定传入的
dest 迭代器是足够容纳要写入的元素的。通常会使用以下迭代器保证这一点。(接受一个输入范围和一个目标迭代器)
- 使用插入迭代器,
- 使用
ostream_iterator
- **接受两个输入范围。**只接受
b1 的算法假定从b1 开始的范围至少和输入的范围是一样大的。因为这类算法通常需要将第一个范围和第二个范围进行运算。接受两个完整输入范围的没有特别说明。
//3.算法的命名规范
1).处理的问题。
- 是否用一个操作代替默认的
<,== 运算 - 是否将一个修改的序列输出到
dest
2).几种规范。
- **参数数目不一样的规范。**依靠接受参数的不同进行重载。例如,
- 依靠元素类型的运算符进行比较元素。
unique(b,e); - 依靠谓词进行比较。
unique(b,e,comp); - 以上作用都是将相邻的重复元素进行删除。这里的相同,含义不一样,第一个是使得
== 成立,第二个是使得comp 返回为真的。
- **算法的_if后缀规范。**接受谓词的算法都有
_if 后缀。通过接受谓词代替了值。例如,
find(b,e,val); //找到第一个val 的迭代器。find_if(b,e,pred); //找到第一个使得pred 返回非零的元素。- 由于以上的函数接受一样的参数数目,很可能会产生歧义,标准库不使用重载的形式。
- **算法的_copy规范。**实现将修改序列进行输出到
dest 的功能。即,接受一个目的迭代器。例如,
reverse(b,e); //在原序列中进行修改。reverse(b,e,dest); //对原序列不做修改,而是将修改的结果输出到dest 指定的位置。
- **混合_copy,_if的命名规范。**即,既接受一个目的迭代器,又接受一个谓词,注意到和
_copy,_if 顺序是对应的。。例如。
remove(b,e,[]()int i){return i % 2;}); //将满足lambda 表达式的元素进行删除。remove(b,e,back_inserter(c),[](int i){return i % 2;}); //将原序列中的偶数拷贝一份到dest 中去。
练习,
replace_copy(b,e,dest,old_val,new_val); 注意形参的顺序。replace_copy_if(b,e,dest,pred,new_val); 注意形参的顺序。
/6.特定容器的算法
1).几点说明。
list,forward_list 只提供了,双向迭代器和向前迭代器。不能满组一些通用算法的要求。- 它们定义了了几个成员函数形式的算法来满足特定的或者通用的算法。
- 有一些通用的算法可以用于链表,但是由于链表是修改指针,而算法是真的进行元素的交换,从而使得通用算法的效率不如特定的算法。因此在使用时,优先使用成员函数版本,而不是通用的版本。
- **链表的成员函数是会删除元素的(也就是改变容器)。而通用算法不会。**例如,
merge 通用的版本会将合并的序列写到一个给定的目的迭代器中去,序列是保持不变的。而链表的会删除。remove 链表会直接删除指定的元素。unique,splice 链表版本也会删除元素。
2).list 以及forward_list 的成员函数算法。
- 以下操作均返回void
|操作名称|相关描述| |-|-| |lst.merge(lst2)|| |lst.merge(lst2,comp)|将来自lst2 的元素并入lst ,要求两个序列哦都是有序的。并且lst2 元素将会被删除。第一个版本使用< 第二个版本使用谓词| |lst.remove(val)|| |lst.remove(pred)|调用erase 删除元素。一个版本是== ,一个版本是满足一元谓词的元素。| |lst.reverse()|翻转| |lst.sort()|| |lst.sort(comp)|使用< 或者给定的操作进行比较。| |lst.unique()|| |lst.unique(pred)|使用== 或者使用二元谓词。|
3).splice 成员函数。
lst.splice(args) 或者flst.splice_after(args) p 是一个指向lst 中元素的迭代器,q 指向flst 首前位置的迭代器或者元素。函数会将lst2 中的所有元素移动到p 之前的位置,或者q 之后的位置。- 要求
lst2 必须和lst 的类型相同。 |参数形式|参数说明| |-|-| |(p,lst2)|lst2 中的元素会被全部删除。不可以是同一个链表| |(p,lst2,p2)|只对lst2 中p2 指向的元素进行操作。可以是同一个链表| |(p,lst2,b,e)|将b,e 范围进行操作。可以是同一个链表。但是p 不能再b,e 范围里面。|
/7.总结。
1).对迭代器进行按操作分类是由于不同算法的对迭代器的操作需求是不一样的。
|