STL泛型算法

泛型算法概述

STL定义了一组泛型算法(generic algorithm),大多数算法都定义在头文件algorithm中,还有一组数值泛型算法定义在头文件numeric

泛型算法可以用于不同类型的元素和多种容器类型,所以称它们是"泛型的"

迭代器令泛型算法不依赖于容器,但依赖于元素类型的操作

※关键概念:泛型算法永远不会执行容器的操作

泛型算法本身不会执行容器的操作,它们只会运行与迭代器之上,甚至无需理会保存元素的是不是容器

泛型算法可能改变容器中保存元素的值,也可能在容器内移动元素,但永远不会改变底层容器的大小,添加或删除元素是容器/迭代器的工作,泛型算法自身永远不会做这样的操作

练习10.1:编写程序,读取int序列存入vector中,打印有多少个元素的值等于给定值

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
	vector<int>vec = { 27,210,12,47,109,83 };
	int val = 83;
	cout << count(vec.cbegin(), vec.cend(), val) << endl;
	system("pause");
	return 0;
}

练习10.2:重做上一题,但读取string序列存入list中

#include<iostream>
#include<string>
#include<list>
#include<algorithm>
using namespace std;
int main() {
	list<string>lis = { "27","210","12","47","109","83" };
	string val = "83";
	cout << count(lis.cbegin(), lis.cend(), val) << endl;
	system("pause");
	return 0;
}

泛型算法的结构

只读算法

对于只读算法,通常最好使用cbegin()和cend()
如果计划使用算法返回的迭代器来改变元素的值,就需要使用begin()和end()

※泛型算法中的"泛型操作"必须对元素类型是可行的

例:对vec中的元素求和,和的初值是init_sum

vector<_Ty>vec;
accumulate(vec.cbegin(), vec.cend(), init_sum);

vec中的元素可以是int,或者是double、long long,或者其他任何可以加到_Ty上的类型

操作两个序列的算法一般只接受三个迭代器
例:equal用于确定两个序列是否保存相同的值

equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

※确保泛型算法不会访问不存在的元素

那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长
确保算法不会试图访问第二个序列中不存在的元素是程序员的责任

练习10.3:用accumulate求一个vector中的元素之和

#include<iostream>
#include<vector>
#include<numeric>
using namespace std;
int main() {
	vector<int>vec = { 27,210,12,47,109,83 };
	cout << accumulate(vec.cbegin(), vec.cend(), 0) << endl;
	system("pause");
	return 0;
}

练习10.4:假定v是一个vector< double >,那么调用accumulate(v.cbegin(), v.cend(), 0)有何错误?
调用accumulate(v.cbegin(), v.cend(), 0)等价于执行以下代码:

int sum = 0;
for (const int&val : v)
	sum += val;
return sum;

0是int型,sum += val;语句会发生强制类型转换,导致可能得不到预期结果

练习10.5:调用accumulate(v.cbegin(), v.cend(), string(""))会发生什么?(v = vector< string >)
调用结果为v中所有string元素首尾连接而成的string

写容器元素的算法

泛型算法不改变容器大小,所以使用这类算法时,必须确保序列原大小不小于要求算法写入的元素数目

例:将迭代器表示范围内的每个元素置为0

fill(vec.begin(), vec.end(), 0);

※泛型算法不检查写操作

泛型算法不检查写入位置是否合法,也不检查写操作本身是否合法

vector<int>vec;
fill_n(vec.begin(), 10, 0);

错误,vec是空的,写入位置不合法

vector<string>vec(10);
fill(vec.begin(), vec.end(), 3.14);

错误,string未定义double的转换构造函数,写操作不合法

back_insert_iterator和back_inserter

back_insert_iterator是插入迭代器(insert iterator)
通常通过迭代器想容器元素赋值时,值会被赋予迭代器指向的元素
通过插入迭代器赋值时,值会被添加到容器中

back_inserter是定义在头文件iterator中的一个函数
back_inserter接受一个容器引用,返回一个与该容器绑定的插入迭代器

vector<int>vec;					//空的vector
auto iter = back_inserter(vec);	//通过back_inserter获取与vec绑定的插入迭代器
*it = 42;						//vec中现在有一个元素,值为42
*it = 0;						//vec中现在有两个元素,分别是{42, 0}

通过上例可以发现,通过back_insert_iterator赋值不仅会向容器中添加元素,还会引起迭代器向后移动

通过插入迭代器使用泛型算法会改变容器的大小

vector<int>vec;					//空的vector
auto iter = back_inserter(vec);	//通过back_inserter获取与vec绑定的插入迭代器
fill_n(iter, 10, 0);			//添加10个元素到vec

上例是否违背了"泛型算法不改变容器大小"?
不违背,改变容器大小的不是泛型算法本身,而是插入迭代器,泛型算法的操作是基于迭代器的

拷贝算法(copy)

copy接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置
copy将输入范围中的元素拷贝到目的序列中

可以用copy实现内置数组的拷贝,代码如下:

int a1[] = { 0,1,2,3,4,5,6,7,8,9 };
int a2[sizeof(a1) / sizeof(*a1)];
auto ret = copy(begin(a1), end(a1), a2);

练习10.6:编写程序,使用fill_n将一个序列中的int值都设置为0

int arr[10];
fill_n(begin(arr), 10, 0);

练习10.7:下面的程序是否有错误?如果有,请改正
(a)

vector<int>vec;
list<int>lst;
int i;
while (cin >> i)
	lst.push_back(i);
copy(lst.cbegin(), lst.cend(), vec.begin());

错误,vec是空的,写操作不合法,改正如下:

list<int>lst;
int i;
while (cin >> i)
	lst.push_back(i);
vector<int>vec(lst.size());
copy(lst.cbegin(), lst.cend(), vec.begin());

(b)

vector<int>vec;
vec.reserve(10);	//reserve为容器预留空间
fill_n(vec.begin(), 10, 0);

错误,reserve方法为容器预留空间,但容器size外的预留空间同样是out_of_range的,改正如下:

vector<int>vec;
vec.resize(10);	//resize调整容器的大小
fill_n(vec.begin(), 10, 0);

练习10.8:“泛型算法不改变容器大小”,这句话在使用back_inserter时依然成立吗?
成立,改变容器大小的不是泛型算法本身,而是插入迭代器,泛型算法的操作是基于迭代器的

重排容器元素的算法

排序算法(sort)

sort算法可以依靠<运算符实现,凡是重载了<运算符的数据类型都可以利用sort排序
sort算法也可以依靠仿函数实现,详情戳sort仿函数

"删除"相邻重复元素(unique)

unique算法可以"删除"相邻重复元素,并返回一个指向不重复值范围末尾的迭代器,如

vector<int>vec = { 1,1,1,1,1,3,3,3,3,1,1, };
auto end = unique(vec.begin(), vec.end());
for (auto iter = vec.begin(); iter != end; iter++)
	cout << *iter << ' ';

输出结果:1 3 1

unique并不是真的删除元素,只是覆盖了相邻的重复元素,这符合"泛型算法不改变容器大小"
要真的删除元素,必须使用容器操作,如erase方法
*实验发现unique返回的迭代器以后的元素没有发生改变,猜测unique的实现方式类似于双指针

练习10.9:设计程序,消除vector< string >中重复的单词

void elimDups(vector<string>&words) {
	sort(words.begin(), words.end());
	auto end = unique(words.begin(), words.end());
	words.erase(end, words.end());
}

练习10.10:泛型算法为什么设计成不能直接改变容器大小?
1.泛型算法的所有操作被迭代器隔离的,这使泛型算法不依赖于容器,使泛型算法具有普适性
2.如果泛型算法可以直接改变容器大小,这会导致算法对容器类型有依赖性,这就破坏了泛型算法的普适性

定制操作

向算法传递谓词函数

谓词(predicate)

谓词是一个可调用的表达式,其返回结果是一个能用作条件的值
一元谓词(unary predicate)只接受单一参数
二元谓词(binary predicate)有两个参数

例:定义谓词函数,按长度排序字符串

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

部分泛型算法接受一个额外的参数,用于用户自定义操作,此参数是一个谓词

例:按长度由短至长排序字符串向量words

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

练习10.11:编写程序,按长度由短至长排序字符串向量

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

int main() {
	vector<string>words = { "red","yellow","apple","banana" ,"is","football" };
	stable_sort(words.begin(), words.end(), isShorter);
}

练习10.12:排序自定义数据类型

struct myClass {
	string name;
	int num;
};

bool cmp(myClass const&x, myClass const&y) {
	return x.name < y.name || x.name == y.name&&x.num < y.num;
}

int main() {
	vector<myClass>vec = { {"apple",12},{"air",2},{"foot",23},{"banana",0} };
	sort(vec.begin(), vec.end(), cmp);
}

练习10.13:标准库定义了名为partition的算法,它接受一个谓词,对容器内容进行划分,使得谓词为true的值会排在容器的前半部分,而使谓词为false的值会排在后半部分
partition算法返回一个迭代器,指向最后一个使谓词为true的元素之后的位置
编写函数,接受一个string,返回一个bool值,指出string是否有5个或更多字符,使用此函数划分words

bool filter(string const&s) {
	return s.size() >= 5;
}

int main() {
	vector<string>vec = { "apple","is","banana","big","love","vector" };
	partition(vec.begin(), vec.end(), filter);
}

lambda表达式(匿名函数)

lambda表达式是一个可调用的代码单元,可以理解为一个未命名的内联函数
lambda表达式可以定义在函数内部
和函数类似,lambda表达式有返回类型、参数列表和函数体,但和函数不同的是,lambda表达式还有一个捕获列表

lambda表达式的形式

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

capture list是捕获列表
parameter list是参数列表
return type是返回类型

例:lambda表达式的形式

int a = 0, b = 0, c = 0;
[a,b,c] (int x, int y) -> int {
return a + b + c + x + y;
}

capture list是捕获列表,lambda表达式虽然可以定义在函数内部,但是不能随意使用函数内部的局部变量,需要使用的局部变量必须声明在捕获列表中

例:在lambda表达式中使用局部变量

int main() {
	int a = 42;
	auto f = [a]() -> int { return a; };
	cout << f() << endl;
}

捕获参数的传递方式是值传递,如果需要采用址传递方式,要在捕获变量名前加’&’

例:在lambda表达式中引用局部变量

int main() {
	vector<int>vec = { 1,2,3,4 };
	auto f = [&vec]() {
		for (auto num : vec)
			cout << num << ' ';
	};
	f();
}

实际上,lambda除了不能随意使用函数内部的局部变量外,还有一些其他限制,关于lambda捕获的详细信息会在下文展开

parameter list是参数列表lambda表达式不能有默认参数,lambda调用的实参数目必须和形参数目相等

参数列表和返回类型可以被省略,但捕获列表和函数体不能被省略

例:省略参数列表和返回类型的lambda表达式

auto f = [] { return 42; };

lambda表达式的应用

(1) 调用find_if方法,查找vector< string >words中第一个长度大于等于int sz的元素

vector<string>words;
int sz;
auto id = find_if(words.begin(), words.end(),
	[sz](string const&s)->bool
{return s.size() >= sz; });

(2) 调用for_each方法,依次打印容器内所有元素

vector<int>vec;
for_each(vec.begin(), vec.end(), [](int const&val) {cout << val << ' '; });
cout.put(10);

练习10.14:编写一个lambda,接受两个int,返回它们的和

int a = 2, b = 3;
auto f = [](int a, int b)->int {return a + b; };
cout << f(a, b) << endl;

练习10.15:编写一个lambda,捕获它所在函数的int,并接受一个int参数,返回捕获的int和int参数的和

int a = 2, b = 3;
auto f = [a](int b)->int {return a + b; };
cout << f(b) << endl;

练习10.16:编写lambda作为sort方法的谓词,自定义排序算法

vector<string>words;
sort(words.begin(), words.end(),
	[](string const&s1, string const&s2)->bool
{return s1.size() < s2.size(); });

练习10.17:打印vector< string >words中所有长度大于等于int sz的字符串

vector<string>words = { "apple","banana","dog","cat" };
int sz = 4;
stable_sort(words.begin(), words.end(),
	[](string const&s1, string const&s2)->bool
{return s1.size() < s2.size(); });
auto beg = find_if(words.begin(), words.end(),
	[sz](string const&s)->bool
{return s.size() >= sz; });
for_each(beg, words.end(),
	[](string const&s)
{cout << s << ' '; });
cout.put(10);

练习10.18:编写lambda作为partition方法的谓词,重写练习10.17

vector<string>words = { "apple","banana","dog","cat" };
int sz = 4;
auto end = partition(words.begin(), words.end(),
	[sz](string const&s)->bool
{return s.size() >= sz; });
for_each(words.begin(), end,
	[](string const&s)
{cout << s << ' '; });
cout.put(10);

练习10.19:用stable_partition方法重写前一题的程序,与stable_sort类似,在划分后的序列中维持原有元素的顺序

vector<string>words = { "apple","banana","dog","cat" };
int sz = 4;
auto end = stable_partition(words.begin(), words.end(),
	[sz](string const&s)->bool
{return s.size() >= sz; });
for_each(words.begin(), end,
	[](string const&s)
{cout << s << ' '; });
cout.put(10);

lambda捕获方式

为什么lambda又被叫做匿名函数
1.当定义一个lambda时,编译器会生成一个未命名的类
2.当lambda作为参数传递给函数时,传递的是未命名类的对象
由以上两点,不难想到lambda捕获的变量会作为未命名类的成员,随着lambda的创建被初始化

前文简单介绍了lambda表达式的捕获列表,下面介绍lambda表达式的捕获方式

值捕获

值捕获:被捕获变量是原变量的一个拷贝,值捕获方式传递的被捕获变量类似于函数的形式参数

例:修改原变量不会影响被捕获变量

size_t sz = 42;
auto f = [sz] { return sz; }
sz = 0;
auto _sz = f();	//sz == 0, _sz = 42

与函数的形式参数不同,被捕获的变量的值在lambda创建时拷贝,而不是调用时拷贝

※注意:虽然值捕获变量是原变量的拷贝,但是却不能修改其值

例:试图修改值捕获变量的值

int a = 0;
auto f = [a](){a++; };
//编译不通过,a不是一个可修改的左值

要想修改值捕获变量的值,需要使用后文会介绍的mutable关键字

引用捕获

引用捕获:被捕获变量是原变量的引用

例:上文出现过的引用捕获

int main() {
	vector<int>vec = { 1,2,3,4 };
	auto f = [&vec]() {
		for (auto num : vec)
			cout << num << ' ';
	};
	f();
}

※当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的
当lambda作为参数传入函数时,lambda可以含有引用捕获
当lambda作为函数返回值时,lambda不能含有引用捕获,因为lambda引用的局部变量会随着函数调用结束而消亡

※当lambda捕获的变量是一个指针或迭代器时,必须保证与指针或迭代器绑定的对象存在,来避免潜在的问题

隐式捕获

隐式捕获:不显式列出捕获列表,让编译器判断lambda需要捕获哪些变量

隐式捕获的格式:在捕获列表中写一个’&‘或’=’,’&‘告诉编译器采用引用捕获方式,’='告诉编译器采用值捕获方式

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

例:寻找数组中是否存在数字3

vector<int>vec = { 1,2,3,4,5 };
auto it = find_if(vec.begin(), vec.end(),
	[=](int val) {return val == 3; });
if (it != vec.end())
	cout << *it << endl;

如果希望默认采用值捕获,但对个别变量采用引用捕获,可以混合使用隐式捕获和引用捕获

例:混合使用隐式捕获和引用捕获

int a, b, c, d;
auto f = [=, &c, &d]() {return a + b + c + d; };

如果希望默认采用引用捕获,但对个别变量采用值捕获捕获,可以混合使用隐式捕获和值捕获

例:混合使用隐式捕获和值捕获

int a, b, c, d;
auto f = [&, c, d]() {return a + b + c + d; };
可变lambda

上文提到,lambda不会改变值拷贝变量的值,如果我们希望能改变,就必须在参数列表后加上关键字mutable

例:mutable定义的可变参数lambda

int a = 0;
auto f = [a]()mutable {return a++; };
cout << f() << ' ';		//f() == 0, a == 1
cout << f() << ' ';		//f() == 1, a == 2
cout << f() << endl;	//f() == 2, a == 3

可以看出,可变lambda的参数类似于函数的静态变量,在每次调用后值可能改变

※注意:可变lambda只会改变lambda中的捕获变量的值,不会改变原变量的值,要改变原变量的值,必须使用引用捕获

lambda的返回类型

lambda的返回类型可以被省略,前提是返回类型可以被编译器推断出来
当编译器无法推测lambda的返回类型时,默认的返回类型是void,此时可能需要显式地指定返回类型

例:存在尾置返回语句时,可以省略返回类型

auto f = [](int a,int b) {
	if (a == 0)
		return 0;
	a *= a;
	b *= b;
	return a + b;
};

例:编译器不能推测返回类型时,不能省略返回类型

auto f = [](int a, int b) {
	if (a == 0)
		return a;
	else return a + b;
};
cout << f(1, 2) << endl;

编译错误,不能推断lambda的返回类型

正确示例:

auto f = [](int a, int b) -> int {
	if (a == 0)
		return a;
	else return a + b;
};
cout << f(1, 2) << endl;

*注:C++17标准下,上例可以通过编译

练习10.20:STL定义了一个名为count_if的算法,类似于find_if,此函数接受一对迭代器,表示一个范围,还接受一个谓词,会对输入范围中每个元素执行,count_if返回一个计数值,表示谓词有多少次为真
使用count_if,统计vector< string >中有多少个长度超过4的部分

vector<string>words = { "cat","apple","banana","dog","pig" };
auto f = [](string const&str) {
	return str.size() > 4;
};
cout << count_if(words.begin(), words.end(), f) << endl;

练习10.21:编写一个lambda,捕获一个局部int变量,每次调用时递减变量值,直至它变为0,一旦变量变为0,再调用lambda应该不再递减变量,lambda返回一个bool值,指出捕获的变量是否为0

int time = 3;
auto f = [time]()mutable {
	return time > 0 ? time-- : false;
};
cout << f() << ' ';
cout << f() << ' ';
cout << f() << ' ';
cout << f() << ' ';
cout << f() << endl;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值