Loading

欢乐C++ —— 8. 泛型算法概览

简介

顺序容器实际上只定义了一小部分操作:添加元素,删除元素,获取迭代器等等。而像一些查找特定元素,或删除特定值的元素等行为都是通过一组泛型算法实现。

像标准库sort 函数就是泛型算法,其不但可以对数组排序,也可以对不同类型的容器排序。大多数算法都定义在 少数跟数值相关的操作定义在 辅助这些算法的迭代器相关在

迭代器令泛型算法不依赖于特定的容器类型,进一步提升了泛型算法的适用性,但算法仍依赖于元素类型的操作(例:find 中,元素必须支持 == 操作)。并且标准库算法是对迭代器进行操作,而不对容器直接操作。

只读算法初识

一些例子:

  • find(begin, end, val) : 在[begin, end) 中查找 val , 成功返回特定位置的迭代器,失败返回尾后迭代器
  • count(begin, end, val) : 返回 [begin, end) 中 val 出现的次数
  • accumulate(begin, end, val) : 返回在[begin, end) 元素的和,和的初值是 val,类型是val 类型
  • equal(begin_1, end_1, begin_2) : 比较两个容器的对应元素是否相同。并且假定第二个容器元素个数不小于第一个容器。返回bool 值
  • begin() end() 获得数组的迭代器,对动态分配的内存无效。
#include <algorithm>
#include <numeric>
#include <iterator>
int main( ) {
	int arr[ ] = { 12,12,23,12,34,45,34,67,78,12 };
	int n = end(arr) - begin(arr);
	int *brr = new int[n - 2];
	
	copy(begin(arr), end(arr)-2, brr);

	cout << "find: " << ( end(arr) == find(begin(arr), end(arr), 1) ) << endl;
	cout << "count: " << count(begin(arr), end(arr), 12) << endl;
	cout << "accumulate: " << accumulate(begin(arr), end(arr), 0) << endl;
	vector<string> vec { "abcd","efg" };
	cout << "accumulate: " << accumulate(begin(vec), end(vec),string("")) << endl;
	cout << "equal: " << equal(brr, brr+n-2 ,begin(arr)) << endl;
}
/*结果:
find: 1
count: 4
accumulate: 329
accumulate: abcdefg
equal: 1
*/

写算法初识

写的算法本身不会改变容器大小,不过可以借助插入迭代器而间接改变容器大小。

  • fill(begin, end, val) fill_n(begin, n, val) 给容器的元素赋新值。 要注意,fill 操作不会使容器扩容,所以要假定容器大小 >= 赋值的元素个数。

    fill 会构造一个临时的val ,然后用这个val 调用赋值运算符,结束后析构。

    back_inserter() 创建一个尾后插入迭代器的函数。这个东西可以结合一些算法使用,举个例子像下面给一个空容器元素赋值,因为容器中没有元素,而使用插入迭代器,它的功能就是为了给容器中添加元素,所以可以正常执行。

    使用 back_inserter() 添加元素相当于push_back。

  • copy(begin_1, end_1, begin_2) 拷贝容器1中元素到容器2 中,同样假定容器2有足够的大小,也同样可以使用插入迭代器。返回容器2因拷贝产生的尾后元素的迭代器。

  • replace(begin, end, old_v, new_v) 将容器中和old_v 相等的元素替换为new_v, 也可以不改变原容器,指定存放。

  • unique(begin, end) 通过将元素向前赋值从而消除相邻重复元素,返回最后一个不重复元素的尾后迭代器。这块并没有真正消除容器中的元素,所以 unique 之后,容器元素个数不变。如果真正要消除容器元素,则可以再调用erase(返回值, end),这块再次验证了算法只操作迭代器,而不直接操作容器本身。另外,sort , unique , erase 这样消除容器中所有重复值。

//后面所需的头文件和自定义类都在这里,篇幅原因就不赘述
#include <iostream>
#include <algorithm>
#include <iterator>
#include <numeric>
#include <vector>

using namespace std;
//使用自定义类可以更清楚看见这些算法执行了那些操作

class Test {
public:
	Test( ):x(0) { cout << "Test()" << endl; }
	Test(int i ) :x(i){ cout << "Test(int)" << endl; }
	Test(const Test&other ):x(other.x) { cout << "Test(const Test&)" << endl; }
	Test(Test&&other )noexcept :x(other.x) { cout << "Test(Test&&)" << endl; }
	Test & operator =(const Test&other) { cout << "operator =(const Test&other)" << endl; return *this; }
	Test & operator =(Test&&other) noexcept { cout << "operator =(Test&&other)" << endl; return *this; }
	~Test( ) { cout << "~Test()" << endl; }
	bool operator == (const Test&other) { return this->x == other.x;}
	int x;
};
int main(){
    int arr[10];
	fill(begin(arr), end(arr), -1);
	fill_n(begin(arr), 5, 0);
    
	vector<Test> vec(5);
	fill_n(vec.begin( ), 2, Test(-1 ));
	fill_n(vec.rbegin( ), 2, Test( 1));
	fill_n(back_inserter(vec), 1, Test(666));
    /*经操作后vec的Test对象中x的值
		-1 -1 0 1 1 666
	*/
    vector<Test> vec2;
	copy(begin(vec), end(vec), back_inserter(vec2));
    
    replace(vec.begin( ), vec.end( ), Test(-1), Test(1));
	replace_copy(vec.begin( ), vec.end( ),vec2.begin(), Test(-1), Test(1));
    
    vector<int >vec3 { 1,2,4,2,1,-1,3 };
	sort(begin(vec3), end(vec3));
	auto it = unique(begin(vec3), end(vec3));
	vec3.erase(it, end(vec3));
   	
    return 0;
}

定制操作

我们可以向泛型算法传递一些特殊操作,从而提升进一步泛型算法的适用性。标准库定义了一些常见的函数对象,比较大小,生成常见类型的哈希值等等。我们应该掌握如何自定义操作。

bool mless(int a, int b) {
	return a < b;
}
bool mgreater(int a, int b) {
	return a > b;
}
int main(){
    //函数对象
    sort(begin(arr), end(arr), less<int>( )); // 从小到大
	sort(begin(arr), end(arr), greater<int>( )); //从大到小
    
    //自定义函数
    sort(begin(arr), end(arr), mless); //从小到大
	sort(begin(arr), end(arr), mgreater); //从大到小
    
}

自定义函数虽然更香,但是对我们来说如果只是完成简单的操作就需要手动定义一个函数有一点点繁琐,为了简化操作,C++提供了lambda表达式。

我们可以向一个算法传递任何形式的可调用对象,对于一个对象或表达式,如果可以对其使用调用运算符,则称其是可调用的

截至 C++11 可调用对象包括函数,函数指针,重载了函数调用运算符的类,以及lambda 表达式。在这块,我们重点看看lambda 表达式。

lambda 表达式

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

参数列表和普通函数一样,但返回类型使用的是尾置返回类型。捕获列表使得 lambda表达式 可以使用所在函数内的局部变量。

虽然lambda表达式可以根据函数体推断返回类型,但为了清晰起见,还是写上为好。

lambda 表达式最简形式为 [capture list]{function body}

int main(){
...
    //找到第一个满足谓词为真的元素的迭代器。
    find_if(begin(arr), end(arr), [ ] (int a)->bool {return a >= 0; });
    //按照从大到小排序
    sort(begin(arr), end(arr), [ ] (int a, int b)->bool {return a > b; });
    //对每个元素执行调用谓词
    for_each(begin(arr), end(arr), [ ] (int a) {cout << a << ends; });
    //将容器元素划分为两部分,前部分谓词结果为真,返回谓词结果为真的尾后迭代器
    partition(begin(arr), end(arr), [ ] (int a) {return a < 0; });
...
}

lambda 实质

lambda 表达式实际上是函数对象。当定义一个 lambda 时,编译期会生成一个对应的匿名的类类型。

可以这样理解,当向函数传递一个lambda 时,实际传递的是这个匿名类的匿名对象。而所捕获的变量实际上就是该对象的数据成员。

//表达式后面的() 是调用一次
int main{
...
    int n=666;
    
    //值传递n给lambda 表达式,C++ 11 允许修改,但C++14不允许
    [n]{n+=5;}();
    //引用传递n ,
    [&n]{n+=5;}();
    
    //以上都为显式捕获(显式指定了要捕获的变量),下面是隐式捕获。
    
    [=]{n+=5;}(); // 以值的形式
    [&]{n+=5;}(); // 以引用的形式
    
...
}

lambda 表达式是很香,但是不能滥用,通常来说,一两次简单操作用lambda 表达式,如果复杂点或多点还是写成函数或函数对象形式。

bind 参数绑定

lambda 表达式因为有个 capture list 所以能很方便的使用函数的变量。如果想将现有的函数传给泛型算法但是又参数个数不对。这个时候我们可以使用bind 函数,它定义在 头文件中,其本质是一个函数适配器。

调用bind 的通常形式为 auto newCall = bind(call,args); bind 会生成一个给原来的可调用对象 call 传递 args 参数列表 的新的可调用对象。

这个可调用对象实际上是一个类对象,该对象重载了函数调用运算符

#include <functional>
using namespace std;
using namespace std::placeholders;	//占位符 _1 _2 定义的命名空间
bool mless(int a,int b){
    return a<b;
}
int main(){
    auto fun = bind(mless, _2, _1);   
    //mless 后面的都是给mless 传递的参数,_1,_2..._n定义在std::placeholders 中,表示的是传入新的可调用对象的参数。
	cout << fun(7, 19) << endl;
    //这个代码等于调用 mless(19,7);
    
    auto fun2 = bind(fun, _1, ref(_2));
    //想以引用形式传递,就使用ref函数
}
当bind 类的成员函数时

当绑定非静态成员函数的指针或指向非静态数据成员的指针时,第一个参数必须是指向一个对象的引用或指针。也就是说其返回的可调用对象中重载了函数调用运算符,可以接受引用,也可以接受指针。

迭代器再论

在标准库头文件 中定义了一些迭代器,进一步提升泛型算法的适用性。

插入迭代器

泛型算法都不会直接向容器中插入元素,假如我们想使用一个泛型算法向空容器拷贝元素,因为容器空间为0,所以直接拷贝会失败。此时就需要插入迭代器,当给一个插入迭代器赋值,插入迭代器会自动调用容器添加元素操作。所以使用此迭代器容器必须支持相应的操作。

image-20200415171209600

根据插入位置不同分为三种:

  • back_inserter() , 创建一个使用容器 push_back() 的迭代器。
  • front_inserter() , 创建一个使用容器 push_front() 的迭代器。
  • inserter() , 创建一个向特定位置添加元素的迭代器。

要留意下inserter 迭代器 ,此迭代器会一直保持第一次调用的相对位置。而上面两个迭代器都是绝对位置 假如要头插入 1, 2, 3, 4 到空容器,使用front_inserter(vec.begin()) 会产生 4, 3, 2, 1 而使用inserter(vec, vec.begin()) 会产生 1, 2, 3, 4 ,因为初始的位置就是空,所以当它添加一个元素后,会++ 继续指向空。

	list<Test> mlist;

	auto it_b = back_inserter(mlist);
	it_b = Test(22);
	auto it_f = front_inserter(mlist);
	it_f = Test(11);
	auto it_i = inserter(mlist, mlist.begin( ));
	it_i = Test(3);

	for_each(mlist.cbegin( ), mlist.cend( ), 
             [ ] (const Test &a) {cout << a.x << ends; }); 
	cout << endl;
流迭代器

虽然流不是类类型,但是为了在流上方便使用一些泛型算法,标准库提供了流迭代器,通过它就可以将流视为元素序列。

以下的演示我都只绑定到了标准IO cin cout 。当然也可以绑定到别的流。

istream_iterator<int> in(cin),eof;
cout << accumulate(in, eof, 0) << endl;

ostream_iterator<int> out(cout," "); //第二个参数是每个元素之间的间隔。必须是c风格字符串
copy(mlist.begin( ), mlist.end( ), out); cout << endl;

空的流迭代器可以当作结尾标记使用。如果类型为类类型,则类需要重载相应的输入输出运算符

反向迭代器

即提供正向又提供反向的好处之一,就是我们可以使用同样的操作造成颠倒的效果。

vector<int> vec{1,2,3};
sort(vec.begin(),vec.end()); //递增
sort(vec.rbegin(),vec.rend()); //递减


vec.end()-vec.begin();
vec.rend()-vec.rbegin();
//忽略底层。两个得到的结果相同。

在计算容器大小时正反向用法相同, 忽略底层。

移动迭代器

和普通迭代器不同的是,移动迭代器解引用产生右值。这样做是为了避免某些不必要的拷贝。

make_move_iterator() 将一个迭代器转化为移动迭代器。

假如一个vector 中保存类类型,现在这个vector 不需要了,要将它拷贝到另一个vector 中,可以使用std:: move(vec1,vec2); 它将会从底层交换两个vector 。如果只需要部分元素,那么可以使用带条件的copy_if,并结合移动迭代器。

函数对象

函数对象,就是重载了函数调用运算符的类所创建的对象。由于该对象的调用行为看起来很像函数,所以称为函数对象。

标准库定义的函数对象

STL 在头文件 里定义了一些函数对象,和泛型算法结合使用。

image-20200511172703660 image-20200511212201630

由于泛型算法一般只有一个或两个运算对象,所以表中定义了两个实参类型。

在泛型算法中,可以使用可调用对象,但在标准库定义的模板类的模板参数,却只能使用函数对象类型,不过它将接受所有符合的的可调用对象。

此外,标准库不能识别重载的函数名,即使其它重载版本的参数不匹配。正确的做法是使用函数指针,或者lambda 表达式中调用特定版本。

image-20200512083849703

特殊容器的算法

容器应该优先使用成员函数的算法:像通用版本的sort 因为其要求的是随机迭代器,所以不适用于list 和forward_list 。而且一些成员算法和通用算法的行为不同。例如merge ,通用版本会将合并后的链表写到序列中,原序列都在。而链表版本则会将两个链表的节点挪动合并成一个链表。

链表特有的splice 成员函数,挪动节点。

标准库算法概览

有的算法可以传入定制操作,我只写出部分,有个大体印象即可。

查找
  • 简单查找

    • find(beg, end, val); find_if(beg, end, unaryPred); //查找 == val 或 满足条件,返回迭代器
    • count(beg, end,val); count_if; //进行计数
    • all_of(beg, end, unaryPred); any_of ; none_of; //对所有元素检测是否 全/有一个/全不 满足条件
  • 查找重复值

    • adjacent_find(beg, end) //查找相等的相邻两元素
    • search_n(beg, end, count, val); //寻找相邻count 个重复val
  • 查找子序列

    • search(beg1, end1, beg2, end2); //在第一个序列中顺序查找第二个序列第一次出现
    • find_end(beg1, end1, beg2, end2); //反序查找
    • find_first_of(beg1, end1, beg2, end2); //在第一个序列中第二个序列中任意元素
其它只读算法
  • for_each(beg, end, unaryPred) //对所有元素执行unaryPred
  • mismatch(beg1, end1, beg2, binaryPred) //返回第一个不满足条件的元素的迭代器。
  • equal() //相等判断
二分搜索

要求元素是有序的

  • lower_bound(beg, end, val); upper_bound(beg, end, val); //返回第一个<= 、 > 迭代器。equal_bound //返回由前两个迭代器组成的pair,表示一个范围
  • binary_search(beg, end, val); //返回bool 值,用于判断是否有等于val 的元素。
写算法
  • fill(beg, end, val); fill_n // 都是给容器中的元素赋新值 val
  • generate(beg, end, gen); generate_n //和fill 相似,不过不同的是fill 赋的值都是val,而这个每次赋的值都是由可调用对象gen 生成。

给一个容器元素赋同一个值,可以使用for_each + lambda ,generate + lambda 也是可以,但最简单的还是fill。如果赋不同的值,可以for_each + lambda ,但最好还是generate 。毕竟要见名知意。

  • copy(beg, end, dest); copy_if; copy_n; //将元素拷贝到dest 的位置。copy_backward(beg, end, dest); // 和copy 类似,不过这块dest 是尾后迭代器,并且拷贝时从后往前拷贝,结果都是一样。
  • move(beg, end, dest); //对所有元素调用std::move() 将其移动到dest 位置。move_backward(beg, end, dest); //相似,不过是从后往前移动,并且dest 是尾后迭代器。
  • transform(beg, end, dest, unaryPred); transform(beg, end, beg2, dest, binaryPred); //对元素执行unaryPred 操作后,并将其返回值拷贝到dest 。第二个是对两个输入序列执行操作。
  • replace; replace_if ;replace_copy(beg, end, dest, oldval, newval); replace_copy_if(beg, end, dest, unaryPred,newval); //将满足条件的值置换为新值
  • inplace_merge() //归并排序那块的,将同一序列中两个有序子序列合并为同一个有序序列。
  • iter_swap(it1, it2) //交换两个迭代器所指的元素 swap_range(beg1, end1, beg2) //交换一个范围的元素
划分与排序算法

排序一直是亘古不变的话题,为了适应不同场景的排序,标准库提供了多个策略。

划分算法

划分算法将一个序列划分为两个部分。

  • partition(beg, end, unaryPred); //将序列划分,满足条件的在前面,返回的迭代器指向划分后第一个不满足条件的元素。
  • stable_partition(beg, end, ) ; //和上面一样,不过是稳定版
  • partition_copy(); //划分的copy 版本
  • is_partitioned(beg, end, unaryPred); //检查是否满足条件的划分都在不满足之前,返回bool
  • partition_point() ; //返回第一个不满足条件的元素迭代器。
排序
  • sort(beg, end); sort(beg, end, binaryPred); stable_sort(); //排序
  • is_sorted(beg, end, binaryPred); //检测是否整个数组有序,返回bool
  • is_sorted_until(beg, end, binaryPred); //返回从初始位置开始最长有序子序列的尾后迭代器。
  • partial_sort(beg, mid, end); //只将部分序列有序排列。考察的元素是整个数组。也就是说如果mid-beg 为1的话,那么第一个位置的元素就是整个数组最小的元素。
  • nth_element(beg, nth, end); //将nth 元素排序到正确位置。
重排序列
  • remove(beg, end, val); remove_if() ; remove_copy(); //采取有效值覆盖删除值的形式删除元素。指向尾后迭代器,稍后需要调用erase 。 对裸指针序列调用这个操作会引起内存泄漏,
  • unique(beg, end); //将相邻重复元素采取覆盖的方式进行删除。返回尾后迭代器,稍后需要调用erase
  • rotate(beg, mid, end); //将元素逐个向左移动mid -beg 位。
  • reverse() ;// 反转序列
  • shuffle(); random_shuffle; //将序列打乱
排列算法
  • next_permutation(); prev_permutation();
  • is_permutation()
集合算法
  • includes(); //检测一个序列中是否包含另一个序列
  • set_union(); //用两个序列中元素创建不重复有序序列,
  • set_intersection(); set_difference(); //等等都是集合相交元素
最小值最大值
  • min(init_list); max(init_list); //返回一个序列的最小值和最大值。
  • minmax(init_list); //返回pair ,first 元素是最小值,second 元素是最大值。
  • min_element(beg, end); //返回容器中最大元素或最小元素的迭代器。
数值算法
  • accumulate(beg, end, init); //返回序列中所有元素的和。
  • inner_product(beg1, end1, beg2, init); //返回两个序列的内积
  • partital_sum(beg, end, dest); //将不同阶段的序列和分别写入dest 中
  • adjacent_difference(); //相邻元素之差写入 dest 中
  • iota(beg, end, val); //将val 赋值并递增;

总结

image-20200415170050916
posted @ 2020-03-15 17:49  沉云  阅读(171)  评论(0编辑  收藏  举报