泛型算法----再探迭代器,泛型算法结构,特定容器算法

一、再探迭代器

  除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器。这些迭代器包括以下几种:

a、插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素。

b、流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。

c、反向迭代器:这些迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器。

d、移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。

1、插入迭代器

  插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。

  插入迭代器操作:

操作 说明
it = t 在it指定的当前位置插入值t。假定c是it绑定的元素,依赖于插入迭代器的不同种类,此赋值会分别调用c.push_back(t)、c.push_front(t)、c.insert(t, p),其中p为传递给insert的迭代器位置
*it, ++it, it++ 这些操作虽然存在,但不会对it做任何事情。每个操作都返回it

  插入迭代器有三种类型,差异在于元素插入的位置:

a、back_inserter创建一个使用push_back的迭代器。

b、front_inserter创建一个使用push_front的迭代器。

c、inserter创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将插入到给定迭代器所表示的元素之前。

  注意:只有在容器支持push_front的情况下,我们才可以使用front_inserter。类似的,只有在容器支持push_back的情况下,我们才能使用back_inserter。

1)inserter

  当调用inserter(c, iter)时,我们得到一个迭代器,接下来使用它时,会将元素插入到iter原来所指向的元素之前的位置。

  如果it是由inserter生成的迭代器,则下面的赋值语句:

  *it = val;

其效果与下面代码一样:

  it = c.insert(it, val); // it指向新加入的元素

  ++it; // 递增it使它指向原来的元素

  一个例子:

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <list>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::list<int> lst;
13     auto it = std::inserter(lst, lst.begin());
14     *it = 1;
15     *it = 2;
16     *it = 3;
17     for (auto iter = lst.begin(); iter != lst.end(); ++iter)
18     {
19         std::cout << *iter << " ";
20     }
21     std::cout << std::endl;
22     return 0;
23 }
View Code

2)front_inserter

  front_inserter生成的迭代器的行为与inserter生成的迭代器完全不一样。当我们使用front_inserter时,元素总是插入到容器第一个元素之前。即使我们传递给inserter的位置原来指向的第一个元素,只要我们在此元素之前插入一个新元素,此元素就不是容器的首元素了。

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <list>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::list<int> lst = { 1, 2, 3, 4, 5 };
13     std::list<int> lst2, lst3, lst4;
14     
15     copy(lst.begin(), lst.end(), front_inserter(lst2));
16     copy(lst.begin(), lst.end(), inserter(lst3, lst3.begin()));
17     copy(lst.begin(), lst.end(), back_inserter(lst4));
18 
19     std::cout << "front_inserter lst2: ";
20     for (auto iter = lst2.begin(); iter != lst2.end(); ++iter)
21     {
22         std::cout << *iter << " ";
23     }
24     std::cout << std::endl;
25 
26     std::cout << "inserter lst3: ";
27     for (auto iter = lst3.begin(); iter != lst3.end(); ++iter)
28     {
29         std::cout << *iter << " ";
30     }
31     std::cout << std::endl;
32 
33     std::cout << "back_inserter lst4: ";
34     for (auto iter = lst4.begin(); iter != lst4.end(); ++iter)
35     {
36         std::cout << *iter << " ";
37     }
38     std::cout << std::endl;
39     return 0;
40 }
View Code

  当调用front_inserter(c)时,我们得到一个插入迭代器,接下来会调用push_front。当每个元素插入到容器c中时,它变为c的新的首元素。因此,front_inserter生成的迭代器会将插入的元素的序列的顺序颠倒过来,而inserter和back_inserter则不会。

2、iostream迭代器

  虽然iostream类型不是容器,但标准库类型定义了可以用于这些IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator向一个输出流写数据。这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。

1)istream_iterator操作

  当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符。当创建一个istream_iterator时,我们可以将它绑定到一个流。当然,我们还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。

  对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <list>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::istream_iterator<int> in_iter(std::cin); // 从cin读取int
13     std::istream_iterator<int> eof; // 尾后迭代器
14     while (in_iter != eof) // 当有数据可供读取时
15     {
16         std::cout << *in_iter++ << " ";
17     }
18     std::cout << std::endl;
19     return 0;
20 }
View Code

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::istream_iterator<int> in_iter(std::cin); // 从cin读取int
13     std::istream_iterator<int> eof; // 尾后迭代器
14     std::vector<int> vec(in_iter, eof);
15     for (auto iter = vec.begin(); iter != vec.end(); ++iter)
16     {
17         std::cout << *iter << " ";
18     }
19     std::cout << std::endl;
20     return 0;
21 }
View Code

 

  istream_iterator操作:

操作 说明
std::istream_iterator<T> in(is) in从输入流is读取类型为T的值
std::istream_iterator<T> end 读取类型为T的值的istream_iterator迭代器,表示尾后位置
in1 == in2 in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同的输入,则两者相等
in1 != in2  
*in 返回从流中读取的值
in->mem 与(*in).mem的含义相同
++in, in++ 使用元素类型所定义的>>运算符从输入流中读取下一个值。与以往一样,前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值

2)使用算法操作流迭代器

  由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某些迭代器操作,因此我们至少可以用某些算法来操作流迭代器。

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::istream_iterator<int> in_iter(std::cin); // 从cin读取int
13     std::istream_iterator<int> eof; // 尾后迭代器
14     std::cout << accumulate(in_iter, eof, 0) << std::endl;
15     return 0;
16 }
View Code

3)istream_iterator允许使用懒惰求值

  当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现所保证的是。在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了。对于大多数程序来说,立即读取还是推迟读取没什么差别。但是,如果我们创建了一个istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读取同一个流,那么何时读取可能就很重要了。

4)ostream_iterator操作

  我们可以对任何具有输出运算符(<<运算符)的类型定义ostream_iterator。当创建一个ostream_iterator时,我们可以提供(可选的)第二个参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个C风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的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可写的类型兼容
*out, ++out, out++ 这些运算是存在的,但不对out做任何事情。每个运算符都返回out
 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::vector<int> vec = { 1, 2, 3, 4, 5 };
13     std::ostream_iterator<int> out_iter(std::cout, "-*-");
14     for (auto e: vec)
15     {
16         *out_iter++ = e; // 赋值语句实际将元素写到cout
17     }
18     std::cout << std::endl;
19     return 0;
20 }
View Code

5)使用流迭代器处理类类型

  我们可以为任何定义了输入运算符(>>)的类型创建istream_iterator对象。类似的,只要类型由输出运算符(<<),我们就可以为其定义ostream_iterator。

3、反向迭代器

   反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++it)会移动到前一个元素;递减一个迭代器(--it)会移动到下一个元素。

  除了forward_list之外,其他容器都支持反向迭代器。我们可以调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有const和非const版本。

1)反向迭代器需要递减运算符

  我们只能从既支持++也支持--的迭代器来定义反向迭代器。毕竟反向迭代器的目的是在序列中反向移动。除了forward_list之外,标准容器上的其他迭代器都既支持递增运算又支持递减运算。但是,流迭代器不支持递减运算,因为不可能在一个流中反向移动。因此,不可能从一个forward_list或一个流迭代器创建反向迭代器

2)反向迭代器和其他迭代器之间的关系

  我们通过调用反向迭代器的base成员函数完成到普通迭代器的转换,此成员函数会返回其对应的普通迭代器。

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::string line = "first,middle,last";
13     auto rcomma = find(line.crbegin(), line.crend(), ','); // 查找最后一个逗号的位置
14     std::cout << std::string(line.crbegin(), rcomma) << std::endl; // 打印最后一个单词
15     return 0;
16 }
View Code

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::string line = "first,middle,last";
13     auto rcomma = find(line.crbegin(), line.crend(), ','); // 查找最后一个逗号的位置
14     std::cout << std::string(rcomma.base(), line.cend()) << std::endl; // 打印最后一个单词
15     return 0;
16 }
View Code

  注意:反向迭代器的目的是表示元素范围,而这些范围是不对称的,这导致一个重要的结果:当我们从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原迭代器指向的并不是相同的元素

二、泛型算法结构

   任何算法的最基本的特性是它要求其迭代器提供哪些操作。算法所要求的迭代器操作可以分为5个迭代器类别:

迭代器类别 说明
输入迭代器 只读,不写;单遍扫描,只能递增
输出迭代器 只写,不读;单遍扫描,只能递增
前向迭代器 可读写;多遍扫描,只能递增
双向迭代器 可读写;多遍扫描,可递增递减
随机访问迭代器 可读写,多遍扫描,支持全部迭代器运算

1、5类迭代器

  类似容器,迭代器也定义了一组公共操作。一些操作所有迭代器都支持,另外一些只有特定类别的迭代器才支持。迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持低层类别迭代器的所有操作。

  C++标准指明了泛型和数值算法的每个迭代器参数的最小类别。向算法传递一个能力更差的迭代器会产生错误。

1)输入迭代器

  可以读取序列中的元素。一个输入迭代器必须支持:

a、用于比较两个迭代器的相等和不相等运算符(==、!=)。

b、用于推进迭代器的前置和后置递增运算(++)。

c、用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧。

d、箭头运算符(->),等价于(*it).member,即,解引用迭代器,并提取对象的成员。

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

2)输出迭代器

  可以看作输入迭代器功能上的补集----只写而不读元素。输出迭代器必须支持:

a、用于推进迭代器的前置和后置递增运算符(++)。

b、解引用运算符(*),只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)。

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

3)前向迭代器

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

4)双向迭代器

  可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符(--)。除了forward_list之外,其他标准库都提供符合双向迭代器要求的迭代器。

5)随机访问迭代器

  提供在常量时间内访问序列中任意元素的能力。此类迭代器支持双向迭代器的所有功能,还支持:

a、用于比较两个迭代器相对位置的关系运算符(<、<=、>、>=)。

b、迭代器和一个整数值的加减运算(+、+=、-、-=),计算结果是迭代器在序列中前进(或后退)给定整数个元素后的位置。

c、用于两个迭代器上的减法运算(--),得到两个迭代器的距离。

d、下标运算(iter[n]),与*(iter[n])等价。

  算法sort要求随机访问迭代器。array、deque、vector、string的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是。

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,都是迭代器参数。顾名思义,如果用到了这些迭代器参数,它们分别承担指定目的位置和第二个范围的角色。除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。

1)接受单个目标迭代器的算法

  dest参数是一个表示算法可以写入的目的位置的迭代器。算法假定:按其需要写入数据,不管写入多少个元素都是安全的。

  注意:向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。

  如果dest是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内。更常见的情况是,dest被绑定到一个插入迭代器或是一个ostream_iterator。插入迭代器会将新元素添加到容器中,因而保证空间是足够的。ostream_iterator会将数据写入到一个输出流,同样不管要写入多少个元素都没有问题。

2)接受第二个输入序列的算法

  接受单独的beg2或是接受beg2和end2的算法用这些迭代器表示第二个输入范围。这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算。

  如果一个算法接受beg2和end2,这两个迭代器表示第二个范围。这类算法接受两个完整指定的范围:[beg, end)表示的范围和[beg2, end2)表示的第二个范围。

  只接受单独的beg2(不接受end2)的算法将beg2作为第二个输入范围中的首元素。此范围的结束位置未指定,这些算法假定从beg2开始的范围与beg和end所表示的范围至少一样大。

3、算法命名规范

  除了参数规范,算法还遵循一套命名和重载规范。这些规范处理诸如:如何提供一个操作代替默认的<或==运算符以及算法是将输出数据写入输入序列还是一个分离的目的位置等问题。

1)一些算法使用重载形式传递一个谓词

  接受谓词参数来代替<或==运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数。函数的一个版本用元素类型的运算符来比较元素;另一个版本接受一个额外谓词参数,来代替<或==:

  unique(beg, end); // 使用==运算符比较元素

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

2)_if版本的算法

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

  find(beg, end, val); // 查找输入范围中val第一次出现的位置

  find_if(beg, end, pred); // 查找第一个令pred为真的元素

3)区分拷贝元素的版本和不拷贝的版本

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

  reverse(beg, end); // 反转输入范围中元素的顺序

  reverse_copy(beg, end, dest); // 将元素逆序拷贝到dest

  一些算法同时提供_copy和_if版本。这些版本接受一个目的位置迭代器和一个谓词:

  remove_if(v1.begin(), v1.end(), [](int i) { return i%2; }); // 从v1中删除奇数元素

  remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i%2; }); // v1不变,将v1的偶数元素拷贝到v2中

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::vector<int> v1 = { 1, 2, 3, 4, 5 };
13     auto end_it = remove_if(v1.begin(), v1.end(), [](int i) { return i % 2; });
14     for (auto iter = v1.begin(); iter != end_it; ++iter)
15     {
16         std::cout << *iter << " ";
17     }
18     std::cout << std::endl;
19     return 0;
20 }
View Code

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::vector<int> v1 = { 1, 2, 3, 4, 5 };
13     std::vector<int> v2;
14     remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i % 2; });
15     for (auto iter = v1.begin(); iter != v1.end(); ++iter)
16     {
17         std::cout << *iter << " ";
18     }
19     std::cout << std::endl;
20     for (auto iter = v2.begin(); iter != v2.end(); ++iter)
21     {
22         std::cout << *iter << " ";
23     }
24     std::cout << std::endl;
25     return 0;
26 }
View Code

三、特定容器算法

  与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法。特别是,它们定义了独有的sort、merge、remove、reverse和unique。通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。

  链表类型定义的其他算法的通用版本可以用于链表,但代价太高。这些算法需要交换序列中的元素。一个链表可以通过改变元素减的链接而不是真正的交换它们的值来快速“交换”元素。因此,链表版本的算法的性能比对应的通用版本好得多

  list和forward_list成员函数版本的算法:

  这些操作都返回void。

操作 说明
lst.merge(lst2) 将来自lst2的元素合并入lst。lst和lst2都必须是有序的
lst.merge(lst2, comp) 元素将从lst2删除。在合并之后,lst2变为空。第一个版本使用<运算符;第二个版本使用给定的比较操作
lst.remove(val) 调用erase删除掉与给定值相等(==)或令一元谓词为真的每个元素
lst.remove_if(pred)  
lst.reverse() 反转lst中元素的顺序
lst.sort() 使用sort或给定比较操作排序元素
lst.sort(comp)  
lst.unique() 调用erase删除同一个值的连续拷贝。第一个版本使用==;第二个版本使用给定的二元谓词
lst.unique(pred)  

  链表类型还定义了splice算法,此算法是链表数据结构所特有的。

  list和forward_list的splice成员函数的参数:

参数 说明
lst.splice(args) 或 flst.splice_after(args)
(p, lst2)

p是一个指向lst中元素的迭代器,或一个指向flst首前位置的迭代器。函数将lst2的所有位置移动到lst中p之前的位置或是flst中p之后的位置。

将元素从lst2中删除。lst2的类型必须与lst或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不能指向给定范围中元素

  多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器

原文地址:https://www.cnblogs.com/ACGame/p/10241088.html