C++ 泛型算法(一)初识泛型算法,lambda表达式

文章目录

    • 概述
      • 算法工作原理
    • 泛型算法
      • 只读算法
      • 写容器算法
      • 拷贝算法
      • 重排容器元素算法
    • 指定操作及算法传参
      • 谓词
    • lambda表达式
      • lambda介绍
      • lambda传参
      • 捕获列表
    • lambda捕获和返回
      • 值捕获
      • 引用捕获
      • 隐式捕获
      • 可变lambda
      • 指定lambda返回类型

概述

对于顺序容器的操作,我们前面介绍了添加,删除,大小操作,但是我们还有比如查找,排序等操作,这些操作是基于算法的,C++给我们提供了很好的算法库,这些算法大部分在algorithm头文件中,我们使用这些算法的时候需要我们包含进头文件。

算法工作原理

一般情况下,算法是不直接操作容器本身的,而是依赖于迭代器,通过迭代器进行容器的遍历,所以通过算法不能添加,删除元素等能够改变容器大小的操作。
在algorithm中,我们有一个find查找算法,基本算法是
f i n d ( i t e r 1 , i t e r 2 , v a l ) find(iter1,iter2,val) find(iter1,iter2,val)
意思是传入两个迭代器,表示查找的范围,最后一个val表示需要查找的值,如果找到,返回找到该元素的迭代器,如果没找到,返回find第二个参数。

vector<string> s{ "a","b","c","d","e","f","g","h" };
auto l = find(s.cbegin(), s.cend(), "d");
cout << *l << endl;

find查找算法是通过遍历容器,逐一跟val值进行匹配,如果匹配到了就返回指向该元素的迭代器,如果直到范围尾部都没有匹配,则返回范围尾部迭代器,当然这些操作都是通过迭代器进行操作的。

注意
迭代器让算法不依赖于容器,但是算法依赖于元素的类型
如同我们的find算法,它的底层使用的是 ==的运算符,所以我们的比较的类型需要能够使用该运算符才能够使用该算法。

[注意]:算法永远都不会操作于容器

泛型算法

泛型算法有超过100多个,此篇只举几个。泛型算法的基本结构大部分是相同的,如果对其他算法感兴趣可以百度查找或者阅读C++11文档

只读算法

对于只读算法,find算法也是一种只读算法。
其他的只读算法还有accumulate,他是定义在numeric头文件中的。accumulate它能够让容器中的元素进行相加,它接收3个参数,前两个是元素的范围跟find一样,后面一个是相加的初始值。

accumulate的返回结果是根据最后一个参数类型而定的

vector<int> i{ 1,2,3,4,5,6 };
auto sum = accumulate(i.cbegin(), i.cend(), 0); // 把i容器相加,初始值为0
cout << sum << endl;
// 输出结果21

如果我们的初始值任然为0,但是把容器的类型改成double,并且将1改写成1.5,输出结果与预期的不符。

vector<double> i{1.5,2,3,4,5,6 };
auto sum = accumulate(i.cbegin(), i.cend(), 0); // 把i容器相加,初始值为0
cout << sum << endl;
// 原本想要的输出结果 21.5
// 实际输出结果 21

如果我们把初值改写成0.0,输出结果就为21.5.这就证明了accumulate的返回结果是根据初值来决定的。
我们可以拼接一个容器中的字符串,使用“” 的方式来拼接

vector<string> i{"hello","word","c++" };
auto sum = accumulate(i.cbegin(), i.cend(), string("")); // 把i容器相加,初始值为0
cout << sum << endl;
// 输出结果:hellowordc++

切记一点是,我们要把初始值用strin连接,如果不用string就变成了const char*类型,该类型没有+运算符。

对于只读的操作,建议选定元素范围的时候使用cbegin和cend,这样可以增加执行的效率

还有一种只读的比较两个容器大小的算法,equal
equal接收三个参数,和前面一样,前两个参数是第一个容器的输入范围,第三个是第二个容器比较序列的首元素
如果两个容器的对应的元素都一致,则返回true,如果不相等返回false。
由于算法是建立在迭代器而不是容器的基础上的,所以equal允许比较两个数据类型不同的容器,但是这两个容器的数据类型可以使用==运算符即可

还有一点就是,euqal的第二个序列必须是大于等于第一个序列的,否则报错。

写容器算法

写容器算法依然不是对容器进行操作,在执行写容器操作的时候要求写入元素的大小小于等于容器的大小。
算法并不检查写操作,只对迭代器操作,所以如果我们传进去的元素超出了容器大小,那这是违规的。因为迭代器访问了一个未定义的空间

写操作-- fill算法
fill算法接收三个参数,前两个参数传入元素的范围,第三个参数传入写入的值。

vector<int> i{ 1,2,3,4,5,6,7,8,9,10 };
fill(i.begin(), i.end(), 0);  // 将i容器中的代码全部置0
for (auto a : i)
{
	cout << a << " ";
}
// 输出结果:0 0 0 0 0 0 0 0 0 0

还有另外一个写入算法:fill_n
fill_n同样接收3个参数,第一个参数是写入是首元素的位置,第二个参数是写入元素的个数,第三个是写入的值

vector<int> i{ 1,2,3,4,5,6,7,8,9,10 };
fill_n(i.begin(), 5, 0);  // 将前五个置0
for (auto a : i)
{
	cout << a << " ";
}
// 输出结果:0 0 0 0 0 6 7 8 9 10

前面两个算法都是不能扩大容器的大小,因为受到迭代器的缘故,该迭代器只能在begin和end之间修改元素。所以下面要介绍另外一种算法能够在容器中的尾部添加元素-- back_inserter算法。
当然该算法依然不直接操作容器,而是迭代器的类型与前面的不一样,他操作的是一种插入迭代器,关于迭代器的知识将在另外一篇文章中讲到,这里不多扩展。
back_inserter是定义在iterator头文件中的函数。接收一个容器的引用,并且返回一个尾后迭代器。

vector<int> i;
auto s = back_inserter(i); // 返回一个尾后迭代器
*s = 12; // 向容器中添加元素
for (auto a : i)
{
	cout << a << endl;
}
// 输出结果:12

我们常常使用back_inserter来创建一个迭代器来作为插入的初始位置。

vector<int> i;
fill_n(back_inserter(i), 10, 5); // 在i的末尾插入10个5
for (auto s : i)
	cout << s << endl;

拷贝算法

拷贝算法是将选中的容器范围拷贝到另外一个容器中,使用的是copy进行拷贝。
copy拷贝接收三个参数,前面两个接收的是输入的元素的范围,第三个是写入的第一个位置。copy返回的是拷贝尾元素之后位置的迭代器。

vector<int> i{1,2,3,4,5,6,7,9,};
vector<int>	u;
copy(i.begin(), i.end(), back_inserter(u)); // 将i容器拷贝到u容器中
for (auto a : u)
	cout << a << endl;

重排容器元素算法

重排容器的元素比较经典的算法是sort排序算法,这个算法使用<运算符实现的,排序规则也是按照容器的排序规则实现。

vector<string> s{ "hello","word", "C++","slow","word" };
sort(s.begin(),s.end());
for (auto a : s)
	cout << a << " ";
// 输出结果:C++ hello slow word word

可以发现容器中出现了相同的元素,我们可以消除重复元素。
消除重复元素使用的是unique函数,该函数接收两个参数,就是元素范围,虽然是消除,但也不是真正意义上的消除,他返回不重复元素的最后一个位置的迭代器,其余的重复元素依然在容器中,在该迭代器之后。所以还需要我们使用erase来进行删除。

C++ 泛型算法(一)初识泛型算法,lambda表达式_第1张图片

vector<string> s{ "hello","word", "C++","slow","word" };
auto t = unique(s.begin(), s.end());
for (; t != s.end(); t++)
	s.erase(t);
for (auto a : s)
	cout << a << " ";
// 输出结果:hello word C++ slow word

指定操作及算法传参

对于sort操作,默认情况下我们只能够使用从小到大的顺序排列,当然我们也可以自定义大小。

谓词

谓词是一个可调用的表达式,返回结果是一个能够调用的值。标准库算法使用谓词分类是两类:一元谓词(只接收一个参数和二元谓词(只接收两个参数)。接收谓词的算法会调用谓词,并且传入相应的参数。

bool isbig(const int &i,const int &s)
{
	return i > s;
}

int main()
{
	vector<int> s{ 1,4,5,3,7,6,8,2,9,10 };
	sort(s.begin(), s.end(),isbig); // sort传入一个谓词
	for (auto a : s)
		cout << a << " ";
	// 输出结果:10 9 8 7 6 5 4 3 2 1
}

注意:传入的是isbig函数名不是函数,如果传入的是函数,则报错

lambda表达式

我们使用如果要查找一个字符串中大于6的长度,我们可以使用find_if进行查找,find_if接收3个参数前两个是元素的范围,后面一个是查找的条件,如果我们传入一个谓词,那么这个谓词只能是一元谓词

bool elimdups(const string &i)
{
	return i.size() > 6; // 查找关于大于6的字符串
}


int main()
{
	vector<string> s{ "word","hello","aaaaaaa"};
	auto d = find_if(s.begin(), s.end(), elimdups);
	cout <<*d<< endl;
	//输出结果:aaaaaaa
}

这段代码的缺陷在于,我们没办法指定需要传入的比较长度,大大降低了程序的灵活性。但是我们在isbig参数中不能传入两个参数。这个时候想要实现这个功能就要使用lambda表达式

lambda介绍

我们可以向参数中传入可调用对象,我们所知道的可调用对象是函数和指针,接下来要讲解的lambda表达式也是可调用对象
lambda格式:[引用变量] (传入的参数)->返回类型 { 功能 }

auto f = [](string s)->string // 传入一个string参数,并且返回string类型
{
	cout << s;
	return s;
};

f("hello word");
// 输出结果:hello word

lambda传参

lambda表达式与普通的函数不相同,不能有自己的默认参数,也就是说实参的数量与形参的数量是相同的

auto f = [](int s,int i)->int // 传入两个int参数,并且返回int类型
{
	cout << s+i; // 传入两个参数,让他们相加
	return s + i;
}; 
f(1, 2);

捕获列表

在前面的代码中我们一直都没有用到捕获列表。捕获列表在很多情况下是空的,但是有的时候我们也是需要传入的。
上例我们使用find_if算法的时候,由于find_if只能够接收一个一元谓词,所以我们没办法传入一个比较的字符大小。但是我们可以使用lambda表达式进行比较。

int main()
{
	string::size_type t = 6;
	vector<string> s{ "word","hello","aaaaaaa" };
	auto d = find_if(s.begin(), s.end(), 
		[t](const string &s)
	{
		return s.size() > t;
	});
	cout << *d << endl;
	//输出结果:aaaaaaa
}

lambda表达式只有在捕获列表中捕获局部变量才能够调用该变量
捕获列表只能捕获非static的变量,其他的static变量和函数外部变量可以直接在lambda表达式中调用

lambda捕获和返回

与传参一样,lambda表达式的传参也分为值和引用。

值捕获

我们之前使用的一直都是值传参,也就是说松门传入的变量要是能够进行拷贝的。

int s = 10;
auto f = 
	[s] 
{
	return s;
};
s = 1;
auto t = f();
cout << t << endl;
// 输出结果:10

因为f在初始化的时候传入的是s的拷贝,所以以后不管s怎么变f返回的都是创建时s的拷贝

引用捕获

使用引用传值则不一样,他存放的是捕获变量的地址,如果变量改变,那么值也随着改变。

int s = 10;
auto f = 
	[&s] 
{
	return s;
};
s = 1;
auto t = f();
cout << t << endl;
// 输出结果:1

在使用引用的时候必须确保引用变量的存在,当然,就像我们函数不能够返回函数的引用一样,lambda表达式也不能返回变量的引用。

隐式捕获

除了显示捕获以外,我们也可以使用隐式捕获。在捕获列表中加上&或=来告诉编译器是使用引用不捕获还是值捕获

int s = 10;
auto f = 
	[=] 
{
	return s;
};
s = 1;
auto t = f();
cout << t << endl;
// 输出结果:10

在该例中,捕获列表中传入=表示值捕获,s通过隐式捕获拷贝进lambda。
当然,我们也可以使用&和=一起使用表示一部分是引用捕获,一部分是值捕获。

int s = 10;
int i = 100;
auto f = 
	[=,&i]  // s是值捕获,i是引用捕获
{
	return s;
};

注意:不能使用值捕获后在添加值捕获变量
以下错误释放
int s = 10; int i = 100; auto f = [=,i] { return s; };
在使用的=值捕获后面不能在添加值捕获变量i,可以添加&i
注意点二: 如果使用值和引用捕获的混合,捕获列表的开头必须是=或&,这样子就指定了默认捕获方式

前面引用后面值捕获也是一样的原理。

可变lambda

对于值捕获我们不能够修改捕获后拷贝的值的,如果想要修改那么可以使用mutable来修饰

int s = 10;
auto f = 
	[s]() mutable 
{
	return ++s;
};
s = 1;
auto t = f();
cout << t << endl;
// 输出结果:11

指定lambda返回类型

目前为止,我们写的lambda都是单一的return语句,在lambda表达式中,除了return语句以外的其他语句编译器都默认返回void类型。再有的情况下我们需要指定返回类型。

auto s = [](int i,int t )
		{
			return i >t?i:t;
		};

如果我们将上面代码改写为以下代码则发生错误

auto s = [](int i,int t )
		{
			if(i>t)
				return i;
			else return t;
		};

因为此时系统默认返回void但是我们返回了一个整数型,这个时候我们就需要指定返回类型
指定返回类型是在形参后面使用-> 返回类型 的方式进行返回。

auto s = [](int i,int t ) ->int
		{
			if(i>t)
				return i;
			else return t;
		};

你可能感兴趣的:(C++基础)