C++ Primer 5th学习笔记9 泛型算法

泛型算法

1 初识泛型算法

  大多数算法都定义在algorithm,其中头文件numeric中定义了一组数值泛型算法。
算法永远不会执行容器的操作,即永远不会改变底层容器的大小。

只读算法
  只读算法:只读取其输入范围内的元素,而不改变元素。例如:find,accumulate。accumulate定义在头文件中,接受三个参数。调用示例:

//对vec中的元素求和,和的初始值是0
int num = accumulate(vec,cbegin(), vec.cend(), 0);

写容器元素的算法
  会将新值赋予序列中的元素。例如fill算法。fill接受一对迭代器表示一个范围,接受一个值作为第三个参数,fill将给定的这个值赋予输入序列中的每个元素。调用示例:

//每个元素重置为0
fill(vec.begin(), vec.end(), 0);
//将容器的一个子序列设置为10
fill(vec.begin(), vec.begin() + vec.size()/2, 10);

算法不检查写操作
  算法接受一个迭代器来指出一个单独的目的位置。例如函数fill_n。该函数将给定值赋予迭代器指向的元素开始的指定个元素,调用示例:

vector<int> vec;        //空vector
//使用vec,赋予它不同的值
fill_n(vec.begin(), vec.size(), 0);        //将所有元素重置为0

注意vec是空向量时,fill_n的第二个参数不能是常数

back_inserter
  back_inserter定义在头文件iterator中,该函数接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。调用示例:

vector<int> vec;        //空vector
auto it = back_inserter(vec);        //通过it赋值会将元素添加到vec中
*it = 42;       //vec中选择有一个元素,值为42

vector<int> v;        //空vector
//使用vec,赋予它不同的值
fill_n(back_inserter(v),10, 0);        //向v的末尾添加10个值为0的元素

拷贝算法
  拷贝算法是一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。传递给copy的目的序列至少要包含于输入序列一样多的元素。调用示例如下:

int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)];        //a2与a1大小一样
//ret指向拷贝到a2的尾元素之后的位置
auto ret = copy(begin(a1), end(a1), a2);        //把a1的内容拷贝给a2
//利用replace算法来将ilst中所有值为0的元素改为42
replace(ilst.begin(), ilst.end(), 0, 42);
//如果希望保留原序列不变,可以调用replace_copy
//使用back_inserter按需要增长目标序列
replace_copy(ilst.begin(), ilst.end(), back_inserter(ivec), 0, 42)

此调用后,ilst并未改变,ivec包含ilst的一份拷贝,在ilst中值为0的元素,在ivec中都变为42。

重排容器元素的算法
 重排容器中元素的顺序,例如sort。sort算法利用元素类型的<运算符来实现排序的。示例如下:
the qucik red fox jumps over the slow red turtle
排序后生成如下vector:
fox jumps over quick red slow the turtle

消除重复单词

void elimDups(vector<string> &words)
{
    //按字典排序words,以便查找重复单词
    sort(words.begin(), words.end());
    //unique重排输入范围,使得每个单词只出现一次
    //排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    //使用向量操作erase删除重复单词
    words.erase(end_unique, words.end());
}

重排后的序列如下:
fox jumps over quick red slow the turtle ??? ???
此时words的大小并未改变,unique只是覆盖相邻的重复元素,使得不重复元素出现在序列开始部分。unique返回的迭代器指向最后一个不重复元素之后的位置,即上述序列中第一个???的位置。

2 定制操作

2.1 向算法传递函数

 谓词,标准库算法所使用的谓词分为两类:一元谓词(只接受单一参数)和二元谓词(有两个参数)。例如接受二元谓词参数的sort版本,示例如下:

bool isShorter(const string &s1, const string &s2)
{
    return s1.size() < s2.size();
}

//按长度由短至长排序words
sort(words.begin(), words.end(), isShorter);

排序算法
 将words按大小重排时,还希望具有相同长度的元素按字典序排列。为了保持相同长度的单词按字典序排序,可使用stable_sort算法,可维持相等元素的原有顺序。调用如下:

//按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);

2.2 lambda表达式

 lambda表达式表示可调用的代码单元,其具有一个返回类型、一个参数列表和一个函数体。形式如下:
[ccapture list](paramenter list) -> return type { function body }
其中,ccapture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type、paramenter list、function body和普通函数一样,分别为返回类型、参数列表和函数体。与普通函数不同,lambda必须使用尾置返回来指定返回类型。

向lambda传递参数

//与isShorter函数完成相同功能的lambda表达式、
[](const string &a, const string &b)
{ return z.size() < b.size();}

//按长度排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), 
            [](const string &a, const string &b)
            { return a.size() < b.size();});

使用捕获列表
 使用lambda捕获sz,并只有单一的string参数,该函数将string的大小与捕获的sz的值进行比较,示例如下:

[sz](const string &a)
    { return a.size() >= sz;};

//查找第一个长度大于等于sz的元素 返回一个迭代器
auto wc = find_if(words.begin(), words.end(),
                 [sz](const string &a)
                 { return a.size() >= sz; });
//计算满足size >= sz的元素的数目
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s")
     << " of length " << sz << " or longer" << endl;

一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。

for_each算法
  使用for_each算法打印words中长度大于等于sz的元素。其示例如下:

//打印长度大于等于给定值的单词,每个单词后面接一个空格
for_each(wc, words.end(),
         [](const string &s){ cout << s << " "});
         cout << endl;

捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字

2.3 lambda捕获和返回

lambda捕获列表
捕获类型 描述
[] 空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有在捕获变量后才能使用他们
[names] names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量
[&] 隐式捕获列表,采用引用捕获。lambda体中所使用的来着所在函数的实体都采用引用引用方式使用
[=] 隐式捕获列表,采用值捕获。lambda体中所使用的来着所在函数的实体的值
[&, identifier_list] identifier_list是一个逗号分隔列表,这些变量采用值捕获方式。identifier_list中的名字前面不能使用&
[=, identifier_list] identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且这些名字之前必须使用&

值捕获
  采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建是拷贝,而不是调用时拷贝,示例如下:

void fcn1()
{
    size_t v1 = 42;    //局部变量
    //将v1拷贝到名为f的可调用对象
    auto f = [v1] { return v1; };
    v1 = 0;
    auto j = f();    //j为42;f保存了我们创建它时v1的拷贝
}

由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值

引用捕获
  采用引用方式捕获变量,示例如下:

void fcn2()
{
    size_t v1 = 42;    //局部变量
    //对象f2包含v1的引用
    auto f2 = [&v1] { return v1; };
    v1 = 0;
    auto j = f2();    //j为0;f2保存v1的引用,而非拷贝
}

  v1之前的&指出v1应该是引用方式捕获,在lambda函数体内使用此变量时,实际上所有的是引用所绑定的对象,在上例中,lambda返回的是v1指向的对象的值。

隐式捕获
  在捕获列表中写一个&或=,&表示采用捕获引用方式,=则表示采用值捕获方式。例如,重写find_if的lambda:

//sz为隐式捕获,值捕获方式
wc = find_if(words.begin(), words.end(),
            [=](const string &s)
            { return s.size() >= sz; });

当混合使用隐式捕获和显示捕获时,捕获列表中的第一个元素必须是一个&或=。若隐式捕获是引用方式(使用了&),则显示捕获命名变量必须采用值方式,因此在名字前不能使用&;若隐式捕获是值方式(使用了=),则显示捕获命名变量必须采用引用方式,即,在名字前使用&;

可变lambda
  默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。若希望改变一个呗捕获的变量的值,就必须在参数列表首加上关键字mutable,因此,可变lambda能省略参数列表,示例如下:

void fcn3()
{
    size_t v1 = 42;    //局部变量
    //f可以改变它所捕获的变量的值
    auto f = [v1] () mutable {return ++v1};
     v1 = 0;
     auto j = f();    //j为43
}

指定lambda返回类型
  当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型,示例如下:

transform(vi.begin(), vi.end(),vi.begin(),
          [](int i) -> int
          { if (i < 0) return -i; else return i; });

2.4 参数绑定

标准库bind函数
  bind标准库函数,定义在头文件functional中,可以将其看做一个通用的函数适配器,其一般调用形式如下:

auto newCallable = bind(callable, arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表。使用示例如下:

//使用bind生成一个调用check_size的对象
auto check6 = bind(check_size, _1, 6);
string s = "hello";
bool b1 = check6(s);    //check6(s)会调用check_size(s, 6)

使用placeholder名字
  名字_n表示占位符,都定义在名为placeholders的命名空间中。例如,_1对应的using声明为:
using std::placeholders::_1;
与bind函数一样,placeholders命名空间也定义在functional头文件中。

绑定引用参数
  如果我们希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数,例如:

for_each(words.begin(), words.end(),
         bind(print, ref(os), _1, ''));

3 再探迭代器

3.1 插入迭代器

  插入器是一种迭代器适配器。能够向给定容器添加元素

插入迭代器操作
操作 描述
it = t 在it指定的当前位置插入值t
*it,++it,it++ 每个操作都返回it

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

  • back_inserter创建一个使用push_back的迭代器

  • front_inserter创建一个使用push_front的迭代器

  • inserter创建一个使用inserter的迭代器

3.2 iostream迭代器

  iostream类型不是容器,但istream_iterator可用来读取输入流,ostream_iterator可用来向一个输出流写数据。

istream_iterator操作

istream_iterator操作
操作 描述
istream_iterator<T> in(is) in从输入流is读取类型为T的值
istream_iterator<T> end 读取类型T的值的istream_iterator 迭代器,表示尾后位置
in1 = in2 in1 ! = in2 比较两个迭代器。in1in2必须读取相同类型
*in 返回从流中读取的值
in->mem (*in).mem的含义相同
++in, in++ 使用元素类型所定义的>>运算符从输入流中读取下一个值

使用istream_iterator从标准输入读取数据,存入一个vector,示例如下:

istream_iterator<int> in_iter(cin);    //从cin读取int
istream_iterator<int> eof;    //istream尾后迭代器
while (in_iter != eof)        //当有数据可供读取时
    vec.push_back(*in_iter++);

//将程序重写如下:
istream_iterator<int> in_iter(cin)。eof;    //从cin读取int
vector<int> vec(in_iter, eof);    //从迭代器范围构造vec

ostream_iterator操作

ostream_iterator操作
操作 描述
ostream_iterator<T> out(os) out将类型为T的值写到输出流os
ostream_iterator<T> out(os, d) out将类型为T的值写到输出流os中,每个值后面都输出一个d
out = val 用<<运算符将val写入到out所绑定的ostream中,val的类型必须与out可写的类型兼容
*out,++out,out++ 每个运算符都返回out

  可以用ostream_iterator来输出值的序列:

ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec)
    *out_iter++ = e;    //赋值语句实际上将元素写到cout 每次赋值,写操作就会被提交。
cout << endl;

//也可以重写为如下:
for (auto e : vec)
    out_iter = e;    //赋值语句实际上将元素写到cout 每次赋值,写操作就会被提交。
cout << endl;

3.3 反向迭代器

  反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器,除forward_list外,其他容器都支持反向迭代器。可以通过调用rbegin()、rend()、crbegin()、crend()成员函数来获得反向迭代器

4 泛型算法结构

  算法所要求的迭代器操作可以分为以下5个迭代器类别:

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

4.1 5类迭代器说明

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

  • 用于比较两个迭代器的相等和不相等运算符(==、!=)
  • 用于推进迭代器的前置和后置递增运算(++)
  • 用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧
  • 箭头运算符(->),等价于(*it*).member,即,解引用迭代器,并提取对象的成员

输出迭代器:可以看做是输入迭代器功能上的补集——只写而不读元素,一个输出迭代器必须支持以下几点:

  • 用于推进迭代器的前置和后置递增运算(++)
  • 解引用运算符(*),只出现在赋值运算的左侧

前向迭代器:可以读写元素。只能在序列中沿一个方向移动,支持所有的输入和输出迭代器操作,可以多次读写同一个元素。算法replace和forward_list的迭代器用到前向迭代器

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

随机访问迭代器:通过在常量时间内访问序列中任意元素的能力。支持双向迭代器的所有功能,还支持以下操作:

  • 用于比较两个迭代器相对位置的关系运算符(<、<=、>、>=)
  • 迭代器和一个整数值的加减运算(+、+=、-、-=)。计算结果是迭代器在序列中前进(或后退)给定整数个元素后的位置
  • 用于两个迭代器上减法运算符(-),得到两个迭代器的距离
  • 下标运算符(iter[n]),与*(iter[n])等价。

5 特定容器算法

 与其他容器不同,链表类型list和forward_list定义成员函数形式的算法,如下图所示:

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

splice成员
 链表还定义了splice算法,lst.splice(args)flst.splice_after(args),其参数描述如下:

list和forward_list成员函数的参数
操作 描述
(p, lst2) p是一迭代器,函数将lst2的所有元素移动到lst中p之前的位置或是flst中p之后的位置。两个链表类型必须相同。操作后,lst2变为空
(p, lst2, p2) p2是一个指向lst2中位置有效的迭代器,将p2指向的元素移动到lst
(p, lst2, b, e) b和e必须表示lst2中的合法范围。将给定范围中的元素从lst2移动到lstflst

猜你喜欢

转载自blog.csdn.net/qq_18150255/article/details/89032424