【C++笔记】10. 泛型算法

10. 泛型算法

标准库并未给每个容器都定义成员函数来实现一些公共操作,标准库因此定义了一组泛型算法,可以用于不同类型的元素和多种容器。

10.1 标准库

  1. 大多数算法都定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法。

  2. find函数的两种用法(迭代器和数组)
    类似的,还有count算法(计数)

    auto result = find(vec.cbegin(), vec.cend(),val);
    auto result = find(ia + 2, ia + 5, ia);
    
  3. 迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。
    举例:find操作使用==对元素进行比较,而用户可以重载==的功能。

  4. 算法不执行容器的操作,它可能改变容器中保存的元素的值,也可能移动元素,但不会添加或删除元素。

10.2 初识泛型算法

10.2.1 只读算法

  1. 举例:find、count。

  2. accumulate算法,定义在头文件numeric中。
    前两个参数指定的序列必须与第三个参数匹配。

    int sum = accumulate(vec.begin(), vec.cend(), 0);
    string sum = accumulate(v.cbegin(), v.cend(), "");
    string sum = accumulate(v.cbegin(), v.cend(), string(""));
    
  3. equal算法:用于确定两个序列是否保存相同的值。
    所对比的容器类型允许不一样,只要对应元素可以==运算即可。
    三个参数中,后者容器长度必须大于或等于前者,否则函数将试图访问后者容器中不存在的元素,引发错误。

    equal(vec.cnegin(), vec.cend(), list.cbegin());
    
  4. 三个参数的算法,后者容器长度必须大于或等于前者,否则函数将试图访问后者容器中不存在的元素,引发错误。

10.2.2 写容器的算法

  1. 要求我们算法写入的元素数目大于或等于容器原容量。

  2. fill算法、fill_n算法:对容器原有的元素进行修改。

    fill(vec.begin(), vec.begin() + vec.size(), 0);
    fill_n(vec.begin(), 5, 0);
    
  3. back_inserter插入迭代器:定义在头文件iterator中。
    back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。
    当使用此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。

    vector<int> vec;
    auto it = back_inserter(vec);
    *it = 42;
    // 先执行back_inserter增加空间,再执行fill_n改值
    fill_n(back_inserter(vec), 10, 0);
    
  4. copy算法:前两个参数接收输入,第三个参数为目的序列的起始位置。
    copy算法可用于内置数组。
    copy算法返回的是其目的位置的尾后迭代器(或指针)。

    int a1[] = {0,1,2,3,4,5,6,7,8,9};
    int a2[sizeof(a1) / sizeof(*a1)];
    // 此时返回值是end(a2),即a2尾元素之后的位置
    auto ret = copy(begin(a1), end(a1), a2);
    
  5. replace算法:把第三个参数的值替换为第四个参数。
    replace_copy算法:中间加一个参数,用于指出调整后序列的保存位置。
    即,原来的ilist并未改变,

    replace(ilist.begin(), ilist.end(), 0, 42);
    replace_copy(ilist.begin(), ilist.end(), back_inerter(ivec), 0, 42)
    

10.2.3 重排容器的算法

  1. sort算法:利用<运算符来实现排序,形参为两个迭代器

  2. unique算法:重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。

  3. 标准库算法对迭代器而不是容器进行操作。因此,算法不能直接添加或删除迭代器。真正的删除元素,必须使用容器操作。

10.3 定制操作

  1. sort算法默认使用元素类型的<运算符,标准库还为这些算法定义了额外版本,允许用户提供自己定义的操作来代替默认运算符。

10.3.1 向算法传递参数

  1. 谓词:一个可调用的表达式,返回结果是一个能用做条件的值(即表达式运算结果是0和非0)。
    一元谓词:只接受单一参数;二元谓词:有两个参数。

  2. 举例:使用isShorter替代sort函数原有操作(<)。

    bool isShorter(const string &s1, const string &s2);
    sort(words.begin(), words.end(), isShorter);
    
  3. stable_sort算法:稳定排序算法维持相等元素的原有顺序,
    以上面的例子继续,调用stable_sort,可以保持等长元素间的字典序。

    elimDumps(words);  // 字典序重排元素
    stable_sort(words.begin(), words.end(), isShorter);
    
  4. partition算法:接受一个谓词,对容器内容进行划分,使得谓词为true的值排在前半部分。返回一个迭代器,指向最后一个true元素之后的位置。

  5. stable_partition:划分后的序列中维持原有的顺序。

10.3.2 lambda表达式

  1. find_if算法:接受一对迭代器表示范围,接受谓词,返回第一个谓词运算非0的元素的迭代器。

  2. 可调用对象:函数、函数指针、lambda表达式

  3. lambda表达式:根据函数体内唯一的return语句返回类型。
    如果函数体内不只有return语句,且不指定返回类型,默认返回类型为void。
    如果指定函数类型,必须采用至尾返回方式。
    定义:[捕获列表](参数列表) -> 类型 {函数体}

  4. 向lambda传递参数:不能有默认参数,即调用时,实参与形参一一匹配。

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

  6. for_each算法:接受一对范围用来遍历,执行谓词的操作。

    for_each(iter, words.end(),
    [](const string &str) { cout << str << endl; } );
    
  7. 上一个例子中的cout为什么可以直接用?答:cout的定义在biggies函数外。
    lambda所在函数的局部变量,需要捕获列表来进行“捕获”;
    lambda所在函数的局部static变量、所在函数之外的名字,不需要“捕获”。

10.3.3 lambda捕获和返回

  1. 当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。因此,lambda的数据成员也在lambda对象创建时被初始化。

  2. 值捕获:创建时拷贝值(类似于值传递)

  3. 引用捕获:创建时绑定变量(类似于引用传递)
    使用引用捕获时应避免,所引用的变量在调用lambda之前被销毁。
    引用捕获一般应用于iostream这种不能拷贝的对象。

  4. 显式捕获、隐式捕获:
    [&, a, b]:除了a、b是值捕获,其余全是引用捕获。
    [=, &a, &b]:除了a、b是引用捕获,其余全是值捕获。

  5. 可变lambda:在表达式中加上关键字mutable,使之可以改变捕获来的值。
    注:如果是引用捕获,则还要看原变量本身是否是const。

  6. count_if算法:接受一对迭代器,接受一个谓词并执行,返回true的计数值。

10.3.4 参数绑定

  1. 如果lambda表达式的捕获列表为空,通常可以用比较小的函数替代。

  2. 如果lambda表达式的捕获列表非空,可以使用bind函数来替代,bind函数定义在头文件functional中。

  3. bind函数接受一个可调用对象,生成一个新的可调用对象。
    新生成的可调用对象的参数个数<=bind里面的调用对象的参数个数。

    auto check = bind(check_size, _1, 6);
    
  4. 新生成的可调用对象的参数 在 bind里面的调用对象的参数列表 里使用占位符。
    占位符:_1表示 新生成的可调用对象 的第一个参数。
    占位符定义在命名空间placeholders中,placeholders定义在std中:

    using namespace std::placeholders;
    
  5. bind函数作用:修正参数的值,参数列表重新排序等。

  6. 绑定引用参数:ref、cref,定义在头文件functional中
    默认情况下,bind的那些不是占位符的参数是被拷贝到bind返回的可调用对象中的。
    对于像cout这种无法拷贝的对象,可以使用标准库ref函数。
    标准库还有一个cref函数,生成一个保存const引用的类。

    for_each(words.begin(), words.end(),
            bind(print, ref(cout), _1, ' ') );
    
  7. 向后兼容:参数绑定
    旧版本使用bind1st、bind2nd函数实现bind类似功能,新版不再支持。

10.4 再探迭代器

10.4.1 插入迭代器

  1. 迭代器支持的操作:

    • it = t:在it指定的位置插入值t。
    • *it++itit++:不会对it做任何事情,返回it。
  2. 插入器有三种类型:

    • back_inserter:创建一个使用push_back的迭代器
    • front_inserter:创建一个使用push_front的迭代器
    • inserter:创建一个使用insert的迭代器,此函数接受第二个参数,这个参数必须指向给定容器的迭代器。元素插入到指定元素之前。
  3. 只有当迭代器支持push_back时,才能使用back_inserter。
    只有当迭代器支持front_back时,才能使用front_inserter。

  4. 迭代器工作:

    1. inserter:在it之前插入元素,it再向后移动,使其仍然指向原来的元素。

      *it = cal;
      // 与下面的写法等价
      it = c.insert(it, val);
      ++it;
      
    2. front_inserter:插入到第一个元素之前,首元素会更新。

      list<int> lst = {1,2,3,4};
      list<int> lst2, lst3;
      // 拷贝完成后,lst2包含4 3 2 1
      copy(lst.cbegin(), lst.cend(), front_inserter(lst2);
      // 拷贝完成后,lst3包含1 2 3 4
      copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));
      
    3. unique_copy函数:接受第三个迭代器,表示拷贝不重复元素的目的位置。

10.4.2 iostream迭代器

  1. istream_iterator读取输入流
    一个istream_iterator使用>>来读取流。
    当创建一个istream_iterator时,我们可以将它绑定到一个流。

    istream_iterator<int> int_t(cin);  // 从cin读取int
    istream_iterator<int> int_eof;     // 尾后迭代器
    ifstream in("afile");
    istream_iterator<string> str_in(in);// 从afile中读取字符串
    
  2. 下面是一个例子:

    istream_iterator<int> in_iter(cin);
    istream_iterator<int> eof;
    while ( in_iter != eof )
        vec.push_bck(*in_iter++);
    // 以上代码可以简化为:
    istream_iterator<int> in_iter(cin), eof;
    vector<int> vec(in_iter, eof);
    
  3. 输入迭代器的运算:

    操作说明
    istream_iterator<int> in (is)in从输入流is读取int值。
    istream_iterator<int> end读取int值的istream_iterator迭代器,表示尾后位置。
    in1 == in2in1和in2必须读取相同的类型才能进行==!=的比较。若均为尾后迭代器或绑定到相同的输入,则两者相等。
    *in返回从流中读取的值。
    in->mem(*in).mem含义相同。
    ++inin++使用元素类型所定义的>>运算符从输入流中读取下一个值。
  4. 下面是一个例子:

    istream_iterator<int> in(cin), eof;
    cout << accumulate(in, eof, 0) << endl;
    // 输入为1 2 3 4 5 6 7 8 9
    // 输出为45
    
  5. istream_iterator允许使用懒惰求值:
    istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。
    另外,当两个不同对象同步读取一个流时,一定要注意度的顺序。

  6. ostream_iterator向一个输出流写数据。
    可以对任何具有输出运算符(<<)的类型定义ostream_iterator。

    操作说明
    ostream_iterator<int> out(os);out将int值写到输出流os中
    ostream_iterator<int> out(os, d);out将int值写到输出流os中,每个值后面都输出一个d,d指向一个空字符结尾的字符数组
    out = val;<<运算符将val写入到out所绑定的ostream中(但不推荐使用)
    *out++outout++运算符是存在的,但不对out做任何事情。每个运算符都返回out
  7. 下面是一个例子:

    ostream_iterator<int> out_iter(cout, " ");
    for (auto e : vec)
        // 等价于out_iter = e; 但不推荐
        *out_iter++ = e;
    cout << endl;
    // 代码中的for语句可以简化为:
    copy(vec.cbegin(), vec.cend(), out_iter);
    cout << endl;
    
  8. istream_iterator支持定义了>>运算的类的对象;
    同样,ostream_iterator支持定义了<<运算的类的对象。

10.4.3 反向迭代器

  1. 反向迭代器就是容器中向首元素反向移动的迭代器。
    除了forward_list外,其他容器都支持反向迭代器。
    递增一个反向迭代器(++it)会移动到前一个元素;递减会移动到下一个元素。
    特别注意:crbegin为尾元素迭代器,crend为首前迭代器。

  2. 使用反向迭代器的一个例子:

    // 此例将打印9 8 7 6 5 4 3 2 1 0
    vector<int> vec = {0,1,2,3,4,5,6,7,8,9};
    for (auto r_iter = vec.crbegin();  // 将r_iter绑定到尾元素
     r_iter != vec.crend(); ++r_iter);  // crend指向首元素之前的位置
        cout << *r_iter << " " << endl;
    
  3. 除了forward_list外,标准容器上的其他迭代器都既支持递增运算,又支持递减运算。
    但流迭代器不支持递减运算,因为不可能从一个流中反向移动。

  4. 使用反向迭代器,会反向处理string中的字符。
    如需正向移动,应使用reverse_iterator的base成员函数。

    // 此时rcomma类型是reverse_iterator,而不是iterator
    auto rcomma = find(line.crbegin(), line.crend(), ',');
    // 此语句会将最后一个单词,倒序输出
    cout << string(line.crbegin(), rcomma) << endl;
    // 实际上,base成员是rcomma的后一个位置的“正向迭代器”
    // reverse_iterator无法和iterator进行比较和参与运算
    // 即,rcomma + 1 != rcomma.base();
    // 即,rcomma - 1 != rcomma.base();
    cout << string(rcomma.base(), line.cend()) << endl;
    

10.5 泛型算法结构

  1. 迭代器按操作进行分类,形成了一种层次。除了输出迭代器之外,一个高层次类别的迭代器支持低层次类别迭代器的所有操作。
  2. C++指明了泛型和数值算法的每个迭代器参数的最小类别。例如:
    • find算法在一个序列上进行一边扫描,对元素进行只读操作,因此至少需要输入迭代器。
    • replace函数需要一对迭代器,至少是前向迭代器。
    • replace_copy的前两个迭代器参数也要求至少是前向迭代器,第三个迭代器表示目的位置,必须至少是输出迭代器。
  3. 对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。向算法传递一个能力更差的迭代器会产生错误。

10.5.1 五类迭代器

  1. 输入迭代器(input iterator):只读,不写,单遍扫描,只能递增

    • 相等或不等:==、!=
    • 前置或后置的递增或递减:++、–
    • 解引用:*
    • 箭头运算符:->
      对于一个输入迭代器,*it++保证是有效的,但递增它可能导致所有其他指向流的元素失效。
      其结果就是,输入迭代器只能用于单遍扫描算法。
      算法find和accumulate要求输入迭代器。
      istream_iterator是一种输入迭代器。
  2. 输出迭代器(output iterator):只写,不读,单遍扫描,只能递增

    • 前置或后置的递增或递减:++、–
    • 解引用:*
      我们只能向一个输出迭代器赋值一次。
      输出迭代器只能用于单遍扫描算法。
      用作目的位置的迭代器通常都是输出迭代器。
      copy函数的第三个参数就是输出迭代器。
      ostream_iterator类型也是输出迭代器。
  3. 前向迭代器(forward iterator):可读写,多遍扫描,只能递增
    只能在序列中沿一个方向移动,支持所有输入和输出操作。
    可以多次读写同一个元素,保存状态并多次扫描。
    replace要求前向迭代器。
    forward_list上的迭代器是前向迭代器。

  4. 双向迭代器(bidirectional iterator):可读写,多遍扫描,可递增递减
    可以正向/反向读写序列中的元素,支持所有输入和输出操作。
    同时,还支持前置和后置递减运算符。
    算法reverse要求双向迭代器。
    除了forward_list之外,其他标准库都提供符合双向迭代器要求的迭代器。

  5. 随机访问迭代器(random-access iterator):可读写,多遍扫描,支持全部迭代器运算

    • 提供在常量时间内访问序列中任意元素的能力。
    • 此类迭代器支持双向迭代器的所有功能。
    • 比较操作:<、<=、>、>=
    • 加减、复合运算:+、+=、-、-=
    • 两个迭代器相减运算
    • 下标运算。

10.5.2 算法形参形式

  1. 在算法分类基础上,还有参数规范,可以帮助学习新算法。

    • alg(beg, end, other args);
    • alg(beg, end, dest, other args);
    • alg(beg, end, beg2, other args);
    • alg(beg, end, beg2, end2, other args);
  2. 接受单个目标迭代器的算法
    dest经常是插入迭代器,或ostream_iterator。
    dest需保证空间是足够的。

  3. 接受第二个序列的算法
    beg2或beg2, ned2,表示第二个输入范围。。
    需保证 beg2或beg2, ned2 的范围比 beg, end 的范围一样或更大。

10.5.3 算法命名规范

  1. 使用重载形式传递一个谓词
    接受谓词参数来代替原有算法,如:

    unique(beg, end);
    unique(beg, end, comp);
    
  2. _if版本算法
    接受谓词参数来代替原比较值。

    find(beg, end, val);
    find_if(beg, end, pred);
    
  3. _copy版本算法
    比如重排算法,普通版本会把元素重排后写回原容器。
    而_copy版本会写到另一个位置,原容器元素位置不变。

    reverse(beg, end);
    reverse_copy(beg, end, dest);
    
  4. _if和_copy同时作用:
    比如remove算法:

    // 第一个,在输入序列中将奇数元素删除
    remove_if(v1.begin(), v1.end(),
            [](int i){return i % 2;});
    // 第二个,将非奇数从v1拷贝到v2中
    remove_copy_if(v1.begin(), v1.end(), back_inserter(v2),
            [](int i){return i % 2;});
    

10.6 特定容器算法

  1. 与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法。
    原因:list采用双向迭代器,forward_list采用前向迭代器。

    操作说明
    lst.merge(lst2)将来自lst2的元素并入lst,在合并之后,lst2变为空。此版本默认使用<运算符。
    lst.merge(lst2, comp)此版本使用谓词comp
    lst.remove(val)调用erase,删除与给定值相等(==)的元素
    lst.remove_if(val, pred)调用erase,删除使给定谓词为真的元素
    lst.reverse()反转lst中的元素顺序
    lst.sort()使用<比较排序元素
    lst.sort(comp)使用一元谓词comp排序元素
    lst.unique()调用erase删除同一个值的连续调用,此版本使用==
    lst.unique(pred)调用erase删除同一个值的连续调用,此版本使用二元谓词pred
  2. splice成员:lst.splice(args);flst.splice_after(args);

    args类型说明
    (p, lst2)p是一个指向lst中元素的迭代器,或一个指向flst首前位置的迭代器。函数将lst2的所有元素移动到lst中p之前的位置或flst中p之后的位置。将元素从lst2中删除。lst2的类型必须与lst或flst相同,且不能是同一个链表
    (p, lst2, p2)p2是一个指向lst2中位置的有效的迭代器。将p2指向的元素移动到lst中,或将p2之后的元素移动到flat中。lst2可以是lst或flst相同的链表
    (p, lst2, b, e)b和e必须表示lst2中合法的范围。将给定范围中的元素从lst2移动到lst或flst。lst与lst(或flst)可以是相同的链表,但p不能指向给定范围中的元素
  3. 链表特有的操作会真的改变容器:
    链表特有的操作remove、uniuqe都会删除元素。

    通用的remove、unique算法,实际上仅对迭代器进行操作。
    如果想真的删除元素,需要使用容器操作,而非泛型算法。

  4. 类似的,链表特有的操作merge和splice会销毁其参数,元素仍然存在,但已在同一个链表中。

    通用的merge和splice算法,将合并的序列写到一个给定的目的迭代器,两个输入序列是不变的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值