C++ Primer10.generic

泛型算法

generic algorithm

查找特定元素,替换或删除一个特定值,重排元素顺序等操作,标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法,他们实现了一些经典算法的公共借口,如排序和搜索。因为他们可以用于不同类型的元素和多种容器类型,所以他们是泛型的。

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

概述

标准库 find

int val = 42;
auto result = find(vec.cbegin(), vec.cend(), val);
cout << "The value " << val << (result == vec.cend()
    ? " is not present" : " is present") << endl;

find 的前两个参数指定了迭代器范围,返回查找到的元素的指针,如果没找到则返回第二个参数。result 的类型是元素指针类型。
查找范围不包含第二个参数。

int ia[] = {27, 210, 12, 47, 109, 83};
int val = 83;
int* result = find(begin(ia), end(ia), val);

初识泛型算法

只读算法

count : 计数

accumulate : 累加

// accumulate 函数执行元素的 + 操作;最后参数表示累加的初始值
int isum = accumulate(iv.cbegin(), iv.cend(), 0);
string ssum1 = accumulate(sv.cbegin(), sv.cend(), "");           // 错误
string ssum2 = accumulate(sv.cbegin(), sv.cend(), string(""));   // 正确

const char* 没有 + 操作

equal

第二个序列只接受单一迭代器,这种算法都假定第二个序列与第一个序列一样长。

equal(v1.cbegin(), v1.cend(), v2.cbegin());

操作两个序列的算法,不要求两个序列类型一样,比如 list 和 deque,但要求其中元素能用 == 进行比较。
equal迭代器接受三个迭代器,而其他算法接受4个迭代器

写容器元素的算法

fill

fill(v.begin(), v.end(), 0);                    // 全部重置为0
fill(v.begin(), v.begin() + v.size()/2, 10);    // 前一半元素置为10

back_inserter

插入迭代器,通过它可以向容器插入元素

vector<int> vec;
auto it = back_inserter(vec);           // 对it赋值会向 vec 插入元素
* it = 42;

vector<int> vec1;
fill_n(back_inserter(vec1), 10, 0);     // 添加10个元素到 vec1,值为0

拷贝算法

int a1[] = {0, 1, 2, 3, 4, 5, 6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)];             // a2 与 a1 大小相同

auto ret = copy(begin(a1), end(a1), a2);

copy 返回的是目的位置迭代器的值,即,ret 指向拷贝到 a2 的尾元素之后的位置

// 将所有值为0的元素改成42
replace(list.begin(), list.end(), 0, 42);

// 保留原序列不变
replace_copy(list.cbegin(), list.cend(), back_inserter(ivec), 0, 42);

重排

// 排序然后去重
string words[] = {"the", "red", "fox", "jumps", "over", "the", "red", "slow", "turtle", "quick"};

sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());

unique 将不同元素排在容器的前面,重复元素放在后面,返回一个迭代器,指向最后一个不重复元素之后的位置,该位置和之后位置的元素内容可能被清除。
unique 不能操作容器,所以它不能改变容器大小,需要用 erase 删除元素。

定制操作

改变默认的比较运算符 <

向算法传递函数

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

sort(words.begin(), words.end(), isShorter);

https://www.cnblogs.com/wildricky/p/15532952.html
stable_sort()的比较函数的参数不能使用引用(可以用常引用或者常量或者值传递)。

sort()在数据小于16的时候采用插入排序,大于16且划分均匀(靠一个划分元素数目的定值判断)时采用快速排序,在划分不均匀的时候采用堆排序。

stable_sort()为了保证数据有序性采用归并排序,归并排序利用inplace_merge实现(一个序列的前一部分和后一部分分别有序,然后将其整个排序)而inplace_merge又分两种情况,有缓冲区和无缓冲区。无缓冲区的排序比较复杂,STL源码剖析也没写。所以只考虑有缓冲区。

lambda 表达式 (for_each)

lambda 表达式规则

[capture list](parameter list) -> return type { function body }

capture list (捕获列表)表示 lambda 所在函数中定义的局部变量的列表(通常为空);
return type 表示返回类型,lambda 必须使用尾置返回来指定返回类型

可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:

auto f = [] { return 42; };
// f 不接受参数
cout << f() << endl;

使用捕获列表:

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

比较大小改为 lambda 表达式:

stable_sort(words.begin(), words.end(),
            [](const string &a, const string &b)
                { return a.size() < b.size(); });

// 获取一个迭代器,指向第一个满足 size() >= sz 的元素
auto wc = find_if(words.begin(), words.end(), [sz](const string &a)
                    { return a.size() >= sz; });

// 打印长度大于给定值的单词,用空格隔开
for_each(wc, words.end(), [](const string &s){ cout << s << " "; });

lambda 捕获和返回

当用 auto 定义一个用 lambda 初始化的变量时,定义了一个从 lambda 生成的类型的对象。
从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员,在 lambda 对象创建时被初始化。
捕获方式分为值捕获引用捕获,值捕获的变量在 lambda 创建时拷贝,而不是调用时拷贝。

引用捕获

void func2(){
    size_t v1 = 42;
    auto f2 = [&v1] { return v1; };
    v1 = 0;
    auto j = f2();
}

引用捕获的方式是在变量名前加 &
当lambda 返回 v1 时,返回的是 v1 指向的对象的值。
cin,cout 无法进行值传递,此时需要用到引用传递。

ostream& os = cout;
char c = ' ';
for_each(words.begin(), words.end()
        [&os, c](const string &s) { os << s << c; });

对于引用捕获,要注意 lambda 创建和执行的时机,确保在执行时捕获的变量没有发生变化

size_t v1 = 42;
auto f1 = [v1] { return v1; };
auto f2 = [&v1] { return v1; };
v1 = 50;
auto i = f1();                      // i: 42
auto j = f2();                      // j: 50

隐式捕获

让编译器根据 lambda 表达式中的代码来推断我们要使用那些变量。在放括号中写一个 &=.
& 表示引用捕获;= 表示值捕获。

wc = find_if(words.begin(), words.end(),
            [=](const string& s)
                { return s.size() >= sz; });

混合使用隐式捕获和显式捕获:

ostream& os = cout;
char c = ' ';
// os使用引用捕获,其他使用值捕获
for_each(words.begin(), words.end()
        [=, &os](const string &s) { os << s << c; });
// c 使用值捕获,其他使用引用捕获
for_each(words.begin(), words.end()
        [&, c](const string &s) { os << s << c; });

当使用混合捕获时,显式捕获必须使用与隐式捕获不同的方式

可变 lambda (mutable)

改变被捕获的变量的值:

size_t v1 = 42;
auto f = [v1]() mutable { return ++v1; };
v1 = 0;
auto j = f();       // j为 43

指定 lambda 返回类型(transform)

当 lambda 函数体中包含除 return 外其他语句时,lambda 函数判定为返回 void,此时需要指定返回类型:

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

参数绑定

find_if 函数中的 lambda 函数只能传递一个参数,若想使用两个参数的函数,可用 bind 进行参数绑定:

auto wc = find_if(words.begin(), words.end(),
                [sz](const string& s)
                    { return s.size() >= sz; });

// 等价于以下
#include <functional>
using namespace std::placeholders;
bool check_size(const string& s, string::size_type sz){
    return s.size() >= sz;
}

auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));

// 最后一句等价于以下
auto callable = bind(check_size, _1, sz);
auto wc = find_if(words.begin(), words.end(), callable);

bind 声明在 functional 头文件中。
占位符 _1find_if 传递给 check_size 的第一个参数。_2 是第二个,以此类推。(调用 callable 时的参数)

名字 _n 都定义在 placeholders 的命名空间中,这个命名空间又定义在 std 命名空间中,想要使用它们,应先用 using 声明:

using std::placeholders::_1;

绑定引用参数

ostream& os = cout;
char c = ' ';

for_each(words.begin(), words.end(),
        [&os, c](const string& s)
            { return os << s << c; });

// 等价于:
ostream& print(ostream& os, const string& s, char c){
    return os << s << c;
}
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));

除了 ref 函数,还定义了一个 cref 函数,生成一个保存 const 引用的类。他们都在头文件 functional 中。

迭代器

标准库 iterator

插入迭代器

接受一个容器,生成一个迭代器

  • back_inserter
  • front_inserter
  • inserter
auto it = back_inserter(c);

// 在指定的当前位置插入值t
it = t;
// it 为 back_inserter时,等价于:
c.push_back(t);
// it 为 front_inserter时,等价于:
c.push_front(t);
// it 为 inserter时,等价于:(p 为传递给 inserter 的迭代器位置)
c.insert(t, p);

*it = val;
++it;

拷贝例:

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()));

流迭代器 (iostream 迭代器)

遍历关联的 IO 流

istream_iterator

istream_iterator<int> int_it(cin);      // 从 cin 读取 int
istream_iterator<int> eof;              // istream 尾后迭代器

// 从文件 a.txt 读取字符串
ifstream in("a.txt");
istream_iterator<string> str_it(in);

// 从标准输入读取整数,存入 vec 中:
vector<int> vec;
while(int_it != eof){
    vec.push_back(*int_it++);
}

// 从迭代器范围构造 vec:
vector<int> vec(int_it, eof);

// 叠加算法:
cout << accumulate(int_it, eof, 0) << endl;

ostream_iterator

ostream_iterator<int> out1(cout);
// out2 将整数写入到 cout 中,每个值后面输出一个 d
// d 指向一个空字符结尾的字符数组
ostream_iterator<int> out2(cout, " ");

for (auto e: vec){
    *out2++ = e;        // 写入到 cout,每个元素后加一个空格
    // 等价于
    out2 = e;
}
cout << endl;

上例中写入到 cout 时的 *++ 操作对程序没有任何影响,这样写只是为了与其他迭代器的使用保持一致,方便移植和修改。

利用 copy 打印 vec 中的元素:

copy(vec.begin(), vec.end(), out_it);
cout << endl;

类类型

istream_iterator<Sales_item> item_it(cin), eof;
ostream_iterator<Sales_item> out_it(cout, "\n");
// 从 cin 中读第一条记录
Sales_item* sum = *item_it++;
while (item_it != eof){
    // 相同isbn号的书合到一个对象中
    if (item_it->isbn() == sum.isbn()){
        sum += *item_it++;
    } else {
        // 打印到 cout
        out_it = sum;
        sum = *item_it++;
    }
}
out_it = sum;

反向迭代器

除 forward_list 外的标准容器库都有反向迭代器

vector<int> vec = {0,1,2,3,4,5,6};
for (auto r_it = vec.crbegin(); r_it != vec.crend(); ++r_it){
    cout << *r_it << endl;
}

++-- 的含义与正向迭代器含义完全相反,++ 是移动到前一个元素,-- 移动到后一个元素。

逆序排列:

sort(vec.rbegin(), vec.rend());

查找字符串中最後有一個单词:

string s = "first,second,third";
auto rcomma = find(s.crbegin(), s.crend(), ',');
string last = string(rcomma.base(), s.cend());      // third

上例的 find 的返回的是反向迭代器,使用时需要转换成普通迭代器,使用 base 进行转换。
rcomma 与 rcomma.base() 并不是相同位置,rcomma.base() 是 rcomma 的后一个位置。(crbegin() 和 cend() 也是一样)

forward_list 和流迭代器都不支持反向迭代器

移动迭代器

泛型算法结构

按操作分类:

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

5类迭代器

输入迭代器:

  • 比较相等和不相等(==,!=)
  • 前置和后置递增(++)
  • 解引用(*
  • 箭头运算符(->),等价于解引用后取成员((*it).member

输出迭代器:

  • 前置和后置递增(++)
  • 解引用(*
    • 对已经解引用的输出迭代器赋值,相当于将值写入它指向的元素
    • 只能向一个输出迭代器赋值一次(单遍扫描特性)

随机访问迭代器

  • 提供在常量时间内访问序列中任意元素的能力,支持双向迭代器所有功能
  • 支持比较(<,<=,>,>=)
  • 和一个整数的加减运算(+,-,+=,-=)
  • 两个迭代器相减,得到两个迭代器的距离
  • 下标运算符 iter[n]*(iter[n]) 等价

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

算法形参模式

alg(beg, end, args);
alg(beg, end, dest, args);
alg(beg, end, beg2, args);
alg(beg, end, beg2, end2, args);

dest 需要一个插入迭代器或者输出迭代器

算法命名规范

使用重载传递一个谓词

unique(beg, end);           // 等价于 ==
unique(beg, end, comp);     // 用 comp 代替默认的 ==

comp 可以是函数或 lambda 表达式

_if 版本

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

pred 可以是函数或 lambda 表达式

区分拷贝和不拷贝的版本

重排算法是一种本地操作,想要写到指定的输出位置,在算法名后加 _copy

reverse(beg, end);
reverse_copy(beg, end, dest);   // 逆序拷贝到 dest

有些算法同时提供 _if_copy:

// 删除奇数元素
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; });

特定容器算法

list 和 forward_list 与其他容器不同,他们分别提供双向迭代器和前向迭代器,因此不能使用通用版的 sort,通用版 sort 要求随机访问迭代器。所以 list 和 forward_list 定义了自己独有的 sort, merge, remove, reverse 和 unique.

其他算法的通用版可以用于链表,但代价太高。这些算法需要交换输入序列中的元素,而链表可以通过改变元素的链接而不必交换值来快速“交换”元素。

链表算法 说明
lst.merge(lst2)
lst.merge(lst2,comp)
将 lst2 的元素合并入 lst.两个链表都必须是有序的。
元素将从 lst2 中删除。合并后 lst2 变为空。
第一个版本使用 <,第二个版本使用谓词
lst.remove(val)
lst.remove_if(pred)
调用 erase 删除与给定值相等或令谓词返回值为真的每一个元素
lst.reverse() 反转
lst.sort()
lst.sort(comp)
排序,默认使用 <
lst.unique()
lst.unique(pred)
调用 erase 删除同一個值的连续拷贝,默认用 ==

上述操作都返回 void

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 不能指向给定范围中元素

链表特有的操作会改变容器

与通用版的区别是链表版会改变底层的容器。
比如 remove 的链表版本会删除指定的元素。
unique 的链表版会删除第二个和后继的重复元素。
merge 和 splice 会销毁其参数。

posted @ 2022-11-27 11:11  keep-minding  阅读(23)  评论(0)    收藏  举报