1、泛型算法思想
-
一个算法可用于多种数据类型,算法与所操作的数据结构分离 -
泛型使用迭代器作为 算法 与 数据结构 的桥梁
1.1 引 例:find 函数
find 函数能找到序列中目标值的位置,他接受三个参数,分别是序列的起止位置,以及要找的值。
为了充分理解泛型算法【思想】,我们来看find 函数如何工作:
- 1:访问序列中的首元素
- 2:比较此元素与我们要查找的值
- 3:如果此元素与我们要查找的值匹配,返回【标识】此元素的值。
- 4:否则,前进到下一个元素,重复步骤2和3。
- 5:如果到达序列尾,算法停止。
- 6:如果到达序列末尾,应该返回一个能表示未找到的值。此值和步骤3返回的值必须具有相容的类型。
可以看到,上述步骤并没有对查找的序列做任何限制,所以传入的既可以是个容器,也可以是个数组等。
1.2 借back_inserter 进一步理解“标准库算法不改变容器大小”
前面学习到,标准库算法不会改变他们所操作的容器的大小,但杀出了back_inserter 这么个东西,使得标准库能够向容器中添加新元素。这不是矛盾了吗???
严格来说,标准库算法根本不知道有【容器】这个东西。它们只接受迭代器参数,通过迭代器访问元素。 因此,当传递给算法普通迭代器时,这些迭代器只能访问元素,造成的效果就是算法只能读取、移动、改变元素值,但无法添加或删除元素。 但当我们传递给算法插入器,例如back_inserter 时,由于这类迭代器能调用下层容器的操作来向容器插入元素,造成的算法执行的效果就是向容器中添加了元素。 因此,关键要理解:标准库算法【从来不直接操作容器】,它们【只操作迭代器】,从而【间接访问】容器。能不能插入和删除元素,【不在于算法,而在于传递给它们的迭代器是否具有这样的能力】。
算法是不应该知道容器的存在的!!!
算法你只管机械化的算,完成任务,不用知道对象是谁。 算法:“只要我有迭代器,我就能完成任务,容器??容器是啥???有啥用?”
2、lambda 表达式
2.1 lambda 的作用
lambda 表达式可以让我们把函数当作【数据】一样传递,使我们从繁琐的语法中解放出来,更加关注于 “算法” 本身。 lambda 表达式完全可以取代谓词。
2.2 使用lambda 表达式有如下几点限制:
- 捕获列表只用于局部非静态变量。
lambda 可以直接使用定义在当前函数之外的名字,所以可以直接使用cout
2.3 捕获列表:
[] 不捕获任何变量,这种情况下lambda表达式内部不能访问外部的变量。 [&] 以引用方式捕获所有变量 [=] 用值的方式捕获所有变量(可能被编译器优化为const & [=, &foo] 以引用捕获变量foo, 但其余变量都靠值捕获 [&, foo] 以值捕获foo, 但其余变量都靠引用捕获 [bar] 以值方式捕获bar; 不捕获其它变量 [this] 捕获所在类的this指针 (Qt中使用很多,如此lambda可以通过this访问界面控件的数据)
如果想改变以值的方式捕获的变量的话,要用mutable 关键字,如下图所示:
int x = 1;
auto func = [x] () mutable { return ++x; }; 此时,lambda的参数列表的一对括号()就不能省略了
cout << func() << endl;
3、bind
3.1 bind 重排参数的妙用
按单词长度【由短至长】排序
sort(words.begin(), words.end(), isShorter);
按单词长度【由长至短】排序
sort(words.begin(). words.end(), bind(isShorter, _2, _1));
3.2 bind 函数需注意:非占位符参数是直接拷贝的
所以,当必须传引用时(如cout ),需要用ref 来传。如下面的例子所示:
ostream& print(ostream& os, const string& s);
for_each(s.begin(), s.end(), bind(print, cout, _1); 错误,不能拷贝一个ostream
for_each(s.begin(), s.end(), bind(print, ref(cout), _1); 正确
在bind 里要想传递一个对象而又不拷贝他,只能使用标准库函数ref 。
目前bind 有什么用呢?
答:为替代lambda 提供一个办法。当lambda 不捕获局部变量时,用函数替代它很容易,但当lambda 捕获局部变量时就不行了,这时,bind 就能做到了。
bind 接受几个参数???
bind 是可变参数的。它接受的第一个参数是一个【可调用对象】,即实际工作函数A ,返回【供算法使用】的【新的可调用对象B 】。若A 接受x 个参数,则bind 的参数个数应该是x+1 ,即除了A 外,其他参数应【一一对应】A 所接受的参数。这些参数中有【一部分来自于B(_n) 】,另外一些来自于【所处函数的局部变量】。
不要把调用bind 所返回的可调用对象 和 bind 搞混了啊!Kora-a–a—a!!! find 函数返回的可调用对象就是一个函数适配器,它是通过底层存在的原函数修改参数(修改、增加等)变成的新的函数。
4、再探迭代器
4.1 插入迭代器
back_inserter 、front_inserter 和 inserter
使用方法:使用这三个成员返回的迭代器【解引用直接赋值】,即可完成在相应位置插入元素。
list<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto beg = v.begin();
auto iter = inserter(v, beg);
*iter = 100000;
auto fiter = front_inserter(v);
*fiter = 2133214;
auto biter = back_inserter(v);
*biter = 0;
for (auto i : v) cout << i << ' ';
注意:
- 赋完值完成一次插入后可以接着用,仍然是在【原来的位置】前面插入。
- 这些迭代器【只能】用于直接赋值插入元素,连解引用访问元素都不行。
4.2 iostream 迭代器
istream_iterator 操作:
注意:
- 当
T 具有>> 运算符时,才能创建istream_iterator 对象。
用istream_iterator 从标准输入读取int 类型,存入vector :
istream_iterator<int> is_iter(cin);
istream_iterator<int> eof; 尾后迭代器
while (is_iter != eof)
vec.push_back( *is_iter++ );
循环里的代码:【先返回】迭代器的【旧值】,然后【解引用迭代器】,获得从流中读取的前一个值
ostream_iterator 操作:
注意:
- 当
T 具有<< 运算符时,才能创建ostream_iterator 对象。 ostream_iterator 必须绑定到一个流,不允许空的或表示尾后位置的ostream_iterator
用ostream_iterator 输出序列:
vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
ostream_iterator<int> out_iter(cout, " ");
for (auto i : v) *out_iter++ = i; 这里out_iter加不加*和++都行,但这样写更加清晰
每次向ous_iter 赋值,写操作就会被提交。 上面的代码也可以写成如下这种形式,更简单一些:
copy(vec.begin(), vec.end(), out_iter); 把`vec`【复制到输出流】中,也就是输出 出去了
4.3 反向迭代器
forward_list 和流迭代器没有反向迭代器。
reverse_iterator::base() 返回调用此成员的反向迭代器的普通迭代器。
vector<int>::reverse_iterator riter(iter) 这样可以把正向迭代器强转成反向迭代器。
注:反向迭代器与普通迭代器的转换是闭合区间的转换,他们左右的开闭是反着的。
4.4 迭代器类型
- 输入迭代器:只读,不写,单遍扫描,只能递增,支持
== 、!= ,解引用运算符* (且只能出现在赋值运算符右侧,因为只能读)和箭头运算符-> - 输出迭代器:只写,不读,单遍扫描,只能递增,支持解引用运算符
* (且只出现在赋值运算符左侧,因为只能写)。 - 前向迭代器:可读写,多遍扫描,只能递增,支持所有输入、输出迭代器的操作。
- 双向迭代器:可读写,多遍扫描,可递增递减,支持所有前向迭代器操作。
- 随机访问迭代器:全能,支持【所有】操作。(比上面的多了:
> < >= <= + - += -= ,以及两个迭代器相减)
4.5 链表容器【特有的算法】
splice 成员,这些成员进行链表插入链表的操作: 由于并非通过迭代器实现,所以这些操作基本都会修改链表大小。
|