本科教育忽略的黄金C++<3> 泛型算法

来源:互联网 发布:高性能网络编程 陶辉 编辑:程序博客网 时间:2024/04/30 10:19

标准库容器定义的操作合集非常小。标准库并未给每个容器添加大量功能,而是提供一组算法。这些算法的大多数都独立于任何特定的容器。这些算法是通用的。(generic,泛型):他们可以用于不同类型的容器和不同类型的元素。

泛型算法结构
每个算法都会对它的每个迭代器参数指明必须提供哪类迭代器。迭代器是按照提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持低层次类别的迭代器的所有操作。C++标准指明了泛型和数值算法的每个迭代器参数的最小类别。

迭代器分为五种:
输入迭代器:可以读取序列中的元素。一个输出迭代器必须支持:
用于比较两个迭代器的相等和不相等运算符(==、!=)
用于推进迭代器的前置和后置递增运算(++)
用于读取元素的解引用运算符(*)。解引用只会出现在赋值运算符的右侧。
箭头运算符(->)等价于(*it).member,即,解引用迭代器,并且提供对象的成员。

输入迭代器只用于顺序访问。对于一个输入迭代器,*it++保证是有效的,但是递增它可能导致所有其他指向流的迭代器失效。其结果就是不能保证输入迭代器的状态可以保存下来并且用来访问元素。因此,输入迭代器只能用于单遍扫描算法。

输出迭代器:可以看做输入迭代器功能上的补集—–只写而不读元素。一个输出迭代器必须支持:
用于推进迭代器前置和后置递增运算++。
解引用运算符(*),只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素。)

我们只能向一个输出迭代器赋值一次,类似输入迭代器,输出迭代器只能用于单遍扫描算法。用作目的位置的迭代器一般都是输出迭代器。

前向迭代器:可以读写元素。这类迭代器只能在序列中沿着一个方向移动。前向迭代器支持所有输入和输出迭代器的操作,并且可以多次读写同一个元素。因此我们可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多次扫描。比如replace算法。

双向迭代器:可以正向反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符–。算法reverse要求的就是双向迭代器。

随机迭代器:提供在常量时间内访问序列中任意元素的能力。此类迭代器支持双向迭代器当中的所有功能。支持的功能:
用于比较两个迭代器相对位置的关系运算符(<、<=、>、>=)
迭代器和一个整数值的加减运算(+、+=、-和-=)计算结果是迭代器在序列中前进或者后退,给定整数个元素后的位置。
用于两个迭代器上的减法运算符-来得到两个迭代器的距离。
下标运算符(iter[n])和*(iter[n])等价。

!!算法形参模式!!
在任何其他算法分类之上,还有一组参数规范。

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:第二个范围。接受单独beg2的算法假定从beg2开始的序列与beg和end所表示的范围至少一样大。

!!算法命名规范 !!
一些算法使用重载形式传递一个谓词:
接受谓词参数来代替 >或者==运算符的算法。

unique(beg,end);//使用==比较元素unique(beg,end,comp);//使用comp来比较元素

_if版本的算法:
接受一个元素值的算法通常有另一个不同名的版本,该版本接受一个谓词代替元素值。接受谓词参数的算法都有附加的_if的前缀。

find(beg,end,val);//查找输入范围中val第一次出现的位置find_if(beg,end,pred);//查找第一个令pred为真的元素

这两个算法都在输入范围中查找特定元素第一次出现的位置。find查找一个特定值。而find_if是查找使得pred返回非零值的元素。

区分拷贝元素的版本和不拷贝的版本
默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中。这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。写到额外目的空间的算法都在名字后面附加_copy。

reverse(beg,end);reverse_copy(beg,end,dest);

一些算法同时提供_if和 _copy版本。

1、迭代器深入
标准库在头文件iterator中定义了额外几种迭代器。这些迭代器:
(1)插入迭代器
是一种迭代器适配器。接受一个容器,生成一个迭代器,能够实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值的时候。该迭代器调用容器操作来向给定容器添加元素。

it=t;//在it指定的当前位置插入值t。假定c是it绑定的容器,依赖于插入迭代器的不同种类。此赋值会分别调用 c.push_back(t)、c.push_front(t)或c.insert(t,p)。p为传递给inserter的迭代器位置。*it;++itit++;//不会对it做任何事情,每个操作都返回it。

插入器有三种类型:差异在于元素插入的位置。
I.back_inserter:创建一个使用 push _back的迭代器。
II.front_inserter:创建一个使用push _front的迭代器。
III.inserter:创建一个使用insert的迭代器。此函数接受第二个参数。这个参数必须是一个给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

(2)iostream迭代器
标准库定义了可以用于IO类对象的迭代器。istream_iterator读取数据流,ostream _iterator向一个输出流写数据。这些迭代器将他们对应的流当成一个特定类型的 元素序列来处理。。

istream_iterator
创建一个流迭代器,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取数据流。因此,istream _iterator要读取的类型必须定义了输入运算符。

istream_iterator<int> int_it(cin);//从cin读取int。可以把它绑定到一个流。istream_iterator<int> int_eof;//默认初始化迭代器,创建一个可以当做尾后值使用的迭代器。ifstream in("afile");istream_iterator<string> str_it(in);//从文件中读取字符串。

示例程序:

    vector<int> vec;    istream_iterator<int> in_iter(cin);    istream_iterator<int> eof;    while (in_iter!=eof)//每一步循环都检查输入值是否等于eof。    {        vec.push_back(*in_iter++);//迭代器的旧值包含了从流中读取数据获得的。    }

关于istream_iterator相关操作

*in//返回从流中读取的值。in->mem//与(*in).mem的功能相同++inin++//使用元素类型所定义的>>运算符从输入流中读取下一个值。

注意:istream_iterator允许使用懒惰求值,具体实现可以推迟从流中读取数据,知道我们使用迭代器的时候才真正读取。

ostream_iterator操作
我们可以提供第二参数,他是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个c风格字符串(一个字符串字面常量或者一个指向空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到一个指定的流。不允许空的或者表示尾后位置的ostream _iterator。

ostream_iterator<T> out(os);//out将类型为T的值写入到输出流os中。ostream_iterator<T> out(os,d);//out将类型为T的值写入到输出流os中 ,每个值后面都输出一个d。d指向一个空字符结尾的字符数组。out=val; 用<<运算符将val写入到out所绑定的ostream中。val的类型必须与out可写的类型兼容。*outout++++out

反向迭代器
反向迭代器就是在容器中从为元素反向移动的迭代器。对于反向迭代器,递增递减操作的含义会颠倒。
反向迭代器需要递减运算符。但是流迭代器不支持递减运算。因为不可能在一个流中反向移动。
反向迭代器的目的是表示元素范围,而这些范围是不对称的,这导致一个重要的结果:当我们从一个普通迭代器初始化一个反向迭代器,或者给一个反向迭代器赋值的时候,迭代器和原迭代器指向的并不是相同的元素。

.base() //该操作可以使一个反向迭代器返回一个正向的迭代器,但是其指向的元素不同。

2、泛型算法概述
大部分算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。这些算法并不直接操作容器,而是遍历两个迭代器指定的一个元素范围进行操作。

示例算法:find
原理:传递给find的前两个参数是表示元素范围的迭代器。第三个参数是一个值。find将范围中每个元素和给定元素进行比较。他返回指向第一个等于给定值的元素的迭代器。如果范围中没有匹配元素,那么久返回第二个参数来表示搜索失败。
代码:

int val=42;auto resault=find(vec.cbegin(),vec.cend(),val);

还可以在序列的子范围中查找,只需要将指向子范围首元素和为元素之后位置的迭代器(指针)传递给find。

auto resault=find(ia+1,ia+4,val);

迭代器令算法不依赖于容器,但是算法依赖于元素类型的操作。
利用迭代器的解引用运算符可以实现元素访问。find可以返回尾后迭代器来表示未找到给定元素。
算法永远不会执行容器的操作。
泛型算法本身不会执行容器的操作,他们只会运行于迭代器之上,执行迭代器的操作。
算法永远不会改变底层容器的大小。算法可能改变容器中保存元素的值,也可能在容器内部移动元素,但是永远不会直接添加和删除元素。

3、初级泛型算法

输入范围:标准库算法都对一个范围内的元素进行操作。我们将此元素范围成为输入范围。由要处理的第一个元素和尾元素的迭代器来决定这个输入范围。

(1)只读算法
一些算法只会读取输入范围内的元素而不改变元素。

<1>示例算法:accumulate
头文件:numeric
原理:接受三个参数,前两个参数指定需要求和的元素的范围,第三个参数是和的初值。
代码:

int sum= accumulate(vec.cbegin(),vec.cend(),0);string sum=accumulate(v.cbegin(),v.cend(),string(" "));//将v中所有的元素和string连接起来。第三个参数显式的创造一个string。

翻译:这条语句将sum设置为 vec中元素的和,和的初值设置为0。
注意:第三个参数作为求和起点,存在一个编程假设:序列中元素的类型必须与第三个参数匹配。或者能够转换为第三个参数的类型。

<2>示例算法:equal
原理:确定两个序列是否保存相同的值。将第一个序列中的每个元素都和第二个序列中的对应元素进行比较。如果所有元素都相等,那么返回true,否则返回false。
代码:

equal(roster1.cbegin(),roster1.cend(),roster2.cbegin());

翻译:第三个参数表示第二个序列的首元素。
注意:假定第二个序列至少和第一个序列一样长。

(2)写容器算法
一些算法将新值赋予序列中的元素。·必须确保序列原大小至少不小于我们要求算法写入的元素数目。

<3>示例算法:fill
原理:接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素。

fill(v.begin(),v.end(),0);//将每个元素重置为0。//将容器的一个子序列设置为10.fill(v.begin(),v.begin()+v.size()/2,10);//传递了一个有效的输入序列,写入操作就是安全的。

关键概念:一个算法从两个序列中读取元素。构成这两个序列的元素可以来自于不同类型的容器。两个序列中的元素类型也不要求严格匹配。算法要求的是能够比较两个序列中的元素。操作两个序列的算法之间的区别在于我们如何传递第二个序列。一些算法:equal元素类型不要求相同,但是我们必须能使用==来比较来自两个序列中的元素。

<4>示例算法:fill_n
原理:接受一个单迭代器、一个计数值和一个值,它将给定值赋予迭代器指向的元素开始的指定个元素。
代码:

vector<int> vec;fill_n(vec.begin(),vec.end(),0);//将所有元素都重置为0。

注意:假定dest指向一个元素,而且从dest开始的序列至少包含n个元素。一个初学者非常容易犯的错误是在一个空容器上调用fill_n。

<5>示例算法:back_inserter
头文件:iterator
原理:插入迭代器是一种向容器中添加元素的迭代器。通常情况下,当我们通过一个迭代器向容器元素赋值的时候,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值的时候,一个和赋值号右侧值相等的元素将会被添加到容器中。
代码:

vector<int> vec;//空向量auto it=back_inserter(vec);//创建一个插入迭代器,可以用来向vec添加元素。*it=42;//vec现在有一个元素42

可以利用back_inserter和fill _n算法的结合,避免fill _n不能实现对空vector插入元素的缺点。

<6>示例算法:copy
原理:copy算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器。前两个表示输入范围,第三个表示目的序列的初始位置。
代码:

int a1[]={0,1,2,3,4,5}int a2[sizeof(a1)/sizeof(*a1)];auto ret=copy(begin(a1),end(a1),a2);//ret指向拷贝到a2的尾元素之后的位置。

注意:传递给copy的目的序列至少要包含于输入序列一样多的元素。copy返回的是其目的位置迭代器的值。

<7>示例算法:replace
原理:读入一个序列,并且将其中所有等于给定值的元素都改为另一个值。算法接受四个参数,前两个是迭代器表示输入序列。后两个一个是要搜索的值,另一个是新值。
代码:

replace(list.begin(),list.end(),0,42);

<8>示例算法:replace_copy
原理:接受额外的第三个迭代器参数,指出调整后序列的保存位置。保留原序列保持不变。
代码:

replace_copy(list.begin(),list.end(),back_inserter(ivec),0,42);

注意:调整之后,list并没有变化,ivec是变化之后的序列。

(3)重排容器元素的算法

<9>示例算法:sort
原理:排序序列,按照字典顺序。接受两个迭代器,表示输入范围。

<10>示例算法:unique
原理:消除重复。接受两个迭代器,重新排列输入序列。将相邻的重复项消除。并且返回一个指向不重复值范围末尾的迭代器。unique实际上并不是真的删除任何元素,只是覆盖相邻的重复元素。使得不重复元素出现在序列最开始的部分。
为了真正删除容器中的多余元素,需要调用erase容器操作。

0 0
原创粉丝点击