19.6 C++STL标准模板库大局观-算法简介、内部处理与使用范例

文章介绍了C++STL中的算法概览,包括它们作为全局函数模板的特点、与容器成员函数的区别,以及如何通过迭代器与容器元素区间配合工作。重点讲解了for_each、find、find_if和sort等典型算法的使用,并强调了算法的效率考虑和对不同迭代器类型的处理。此外,还提醒读者注意不同容器与算法的兼容性问题,以及如何在不适用全局算法时利用容器自身的成员函数。
摘要由CSDN通过智能技术生成

19.1 C++STL标准模板库大局观-STL总述、发展史、组成与数据结构谈
19.2 C++STL标准模板库大局观-容器分类与array、vector容器精解
19.3 C++STL标准模板库大局观-容器的说明和简单应用例续
19.4 C++STL标准模板库大局观-分配器简介、使用与工作原理说
19.5 C++STL标准模板库大局观-迭代器的概念和分类
19.6 C++STL标准模板库大局观-算法简介、内部处理与使用范例
19.7 C++STL标准模板库大局观-函数对象回顾、系统函数对象与范例
19.8 C++STL标准模板库大局观-适配器概念、分类、范例与总结

6.算法简介、内部处理与使用范例

  6.1 算法简介

    算法可以理解成函数。更准确的说法是:算法理解为函数模板。
STL中提供了很多算法,如查找、排序等,有数十上百个之多,而且数量还在不断增加中。

在这里插入图片描述
    在学习容器的时候,每个容器都带着许多适合该容器自身进行各种操作的成员函数,而算法不同于这些成员函数,读者可以把算法理解成全局函数或者全局函数模板。
    既然算法是函数模板,它就有参数,也就是形参,那么传递进来的这些形参的类型一般前两个形参都是迭代器类型,用来表示某个容器中元素的一个区间,这个区间还得注意一下。看如下代码:
    假如要把iterbg和itered这个区间传递给算法作为算法的形参,对于和元素有关的算法(和元素无关的算法,如计算迭代器之间距离这种,distance是属于和元素无关的算法)来说,有效的元素只有100、200、300,并不包括end指向的那个位置。也就是说,实际上传递过去的区间是一个前闭后开区间[begin(),end()),也就是包含开头的元素,但不包含最末尾的内容,如图。

在这里插入图片描述
    这种前闭后开区间的好处一般认为有两条:
· 算法只要判断迭代器等于后面这个开区间,那就表示迭代结束。
· 如果第一个形参等于第二个形参,也就是iterbg==itered,那这就表示是一个空区间。
    所以可以认为,算法是一种搭配迭代器来使用的全局函数(或全局函数模板)。读者看到了,==这些算法和具体容器没有什么关系,只跟迭代器有关,大部分容器都有迭代器。==也就是说,这些算法对于大部分容器都是适合的,不需要针对某种容器专门定制。算法的这种编码方式非常好,很大程度上增加了代码编写的灵活性。
    当然,算法作为单独的一个函数(函数模板)来实现,也是违背了面向对象程序设计所讲究的封装性(把成员变量、成员函数包装到一起)的特点,这一点是比较遗憾的。曾经提到过“泛型编程”的概念,也了解了STL(标准模板库)采用的是泛型编程的编码方式来编写,在泛型编程思维的指导下,写出了这么多单独的函数来实现各种算法:灵活性增强,但直观性缺失,而且某些容器和算法之间的兼容性可能也不是那么好。
    STL中算法到底有哪些呢?笔者推荐的《C++标准库》一书中有很长、很清晰的列表,建议读者参考该书籍以对这些算法有个大概了解。手中暂时没有该书的读者,也可以通过搜索引擎来搜索和整理一下STL中的算法,一来是以备将来不时之需,二来是避免自己去写STL中已经提供的算法(这属于重复造轮子)。

  6.2 算法内部一些处理

    一般在使用一个算法的时候直接传递迭代器进去作为实参就可以了,因为算法是函数模板,可以接收各种各样类型的迭代器作为形参。
    很多算法的内部会根据传递进来的迭代器,拿到该迭代器所属的分类,不同种类的迭代器可能会有不同的处理,要编写不同的代码。这种编写不同的代码来处理不同种类迭代器的做法,主要是从算法执行的效率方面来考虑。因为对于算法来讲,效率是很重要的一个指标。例如,算法判断出传递进来的形参是一个随机访问迭代器,那么当需要进行迭代器跳转时,可能就会直接加一个数字来跳转,跳转速度就非常快。如果算法判断出是一个前向迭代器,就不能执行向回读取的操作等等,诸如此类。这也是STL内部为什么要给这些迭代器做一个分类的核心目的之一。

  6.3 一些典型算法使用范例

    因为算法比较多,所以笔者挑几个相对常用和典型的算法来作为范例进行一下演示。一般来说,当使用到STL中的算法时,在.cpp源程序开头位置包含如下头文件:在这里插入图片描述

(1)for_each

void myfunc(int i)
{
	cout << i << endl;
}
{
	vector<int> myvector = { 10,20,30,40,50 };
	for_each(myvector.begin(), myvector.end(), myfunc);
}

在这里插入图片描述

    上面的代码比较清晰,for_each的第一个和第二个形参都是迭代器,表示一段范围(或者说表示某个容器中的一段元素),for_each的第三个参数实际上是一个可调用对象。这里的myfunc是一个函数,属于可调用对象的一种。myfunc有一个形参,是int类型,实际上for_each算法里面就是不断地迭代给进来的两个迭代器之间的元素,拿到这个元素后,以这个元素作为实参来调用myfunc函数。这就是for_each的工作原理。
    找来了一段for_each的实现源码辅助读者理解:

template <class InputIterator, class Function>
Function for_each_sourcecode(InputIterator first, InputIterator last, Function f)
{
	for (; first != last; ++first)
		f(*first); //所有可调用对象,只要这样写代码,就可以被调用,非常统一
	return f;
}
int main()
{
	vector<int> myvector = { 10,20,30,40,50 };
	for_each_sourcecode(myvector.begin(), myvector.end(), myfunc);
}

    上面代码特别值得一提的是代码行“f(*first);”,这是在调用一个可调用对象。可调用对象在写代码的时候偶尔就会用到。可调用对象的一个共同特点是,可以像调用函数一样来调用,而且调用格式非常统一,用“可调用对象名(实参1,实参2,…);”就可以。所以,只要是一个可调用对象(函数、重载了operator()的类、lambda表达式等),用f(*first);就能够直接调用,从而实现了调用代码书写上的统一。

(2)find

    find用于寻找某个特定值,通过范例来理解。
    在main主函数中,继续增加下面的代码:

{
	vector<int>::iterator finditer = find(myvector.begin(), myvector.end(), 400);
	if (finditer != myvector.end())
	{
		cout << "myvector容器中包含内容为400的元素" << endl;
	}
	else
	{
		cout << "myvector容器中不包含内容为400的元素" << endl;
	}
}

在这里插入图片描述

    说到这里笔者要提一下:有些容器自己有同名的成员函数(包括但不限于这里讲到的find函数),优先使用同名的成员函数(但同名的成员函数不像算法,一般不需要传递进去迭代器作为前两个参数),如果没有同名的成员函数,才考虑用全局的算法。例如,map容器有自己的find成员函数,就优先使用该成员函数。在main主函数中加入如下代码:

{
	map<int, string> mymap; //键值对
	mymap.insert(std::make_pair(1, "老王"));
	mymap.insert(std::make_pair(2, "老李"));
	auto iter = mymap.find(2); //查找key为2的元素,有类自己的成员函数,优先用类自己的成员函数
	if (iter != mymap.end())
	{
		//找到
		printf("编号为%d,名字为%s\n", iter->first, iter->second.c_str());
	}
}

(3)find_if

    通过范例来理解find_if更容易一些。
    在main主函数中,加入下面的代码:

{
	vector<int> myvector2 = { 10,20,30,40,50 };
	auto result = find_if(myvector2.begin(), myvector2.end(), [](int val) { //这用lambda表达式,也是一种可调用对象
		if (val > 15)
			return true; //返回true就停止遍历
		return false;
		});
	if (result == myvector2.end())
	{
		cout << "没找到" << endl;
	}
	else
	{
		cout << "找到了,结果为:" << *result << endl;
	}
}

在这里插入图片描述

    注意,find_if的调用返回一个迭代器(上面范例返回类型其实为vector::iterator),指向第一个满足条件的元素,如果这样的元素不存在,则这个迭代器会指向myvector2.end()。
    这个算法与find类似,只是上面演示find时第三个参数是一个数字,而这里的find_if的第三个参数是一个可调用对象(lambda表达式),这个可调用对象里有一个规则——找第一个满足该规则的元素。

(4)sort

    sort用于排序的目的,通过范例来理解。
    在main主函数中,加入下面的代码:

{
	vector<int> myvector3 = { 50,15,80,30,46 };
	//sort(myvector3.begin(), myvector3.end()); //缺省就按照从小到大顺序排列15,30,46,50,80
	sort(myvector3.begin(), myvector3.begin() + 3); //这myvector.begin() + 3应该 是跳到30这里,但因为前闭后开区间,所以参与排序的元素是50,15,80 ,结果是  15,50,80,    30,46
	
	//自定义排序
	//sort(myvector3.begin(), myvector3.end(), myfuncsort);
	A mya;
	sort(myvector3.begin(), myvector3.end(), mya); 	
}

    如果不想按默认的从小到大排序,而是要从大到小排序呢?可以使用一个函数来参与排序,这个函数一般被叫作自定义比较函数,这个函数的返回值是一个bool类型。读者可以比较一下如果要从小到大排序,代码应该怎样写,如果要从大到小排序,代码又应该怎样写(尤其注意代码中的注释部分内容)。
    将main主函数中的sort修改为下面的样子,注意其第三个参数:

bool myfuncsort(int i, int j)
{
	return i>j;
}
sort(myvector3.begin(), myvector3.end(), myfuncsort); 

    读者可以设置断点并跟踪调试,确认myvector3中的元素在执行完sort算法后已经进行了从大到小的排序。结果应该是80,50,46,30,15。
    如果这里不使用myfuncsort函数,而是改用另外一个可调用对象来排序,也是可以的。在MyProject.cpp的上面位置,增加一个类A的定义,注意重载operator():

class A
{
public:
	bool operator()(int i, int j)
	{
		return i > j;
	}
}
A mya;
sort(myvector3.begin(), myvector3.end(), mya);

    上面这种写法就是把可调用对象当作第三个参数传递到sort中去,后续sort内部可以使用形如mya(i,j)的类似函数调用的形式来调用类A的operator()达到同样的排序效果。
    前面的演示都是基于vector容器,下面再试一试list容器。经过测试发现sort算法应用于list容器时会报错,说明这个算法对list容器不适用。其实究其主要原因,是因为sort算法适用于随机访问迭代器而不适用于双向迭代器。其实很多算法都只适用于某些容器而不适用于另外一些容器,这个不用感到奇怪,当算法不适合某个容器时,尝试在该容器中寻找与算法功能相同的成员函数。
    但list容器有自己的sort成员函数,当然就使用list容器自身提供的sort成员函数了。在main主函数中,加入如下代码:

	list<int> mylist = { 50,15,80,30,46 };
	//sort(mylist.begin(), mylist.end()); //编译报错,sort算法不适用于双向迭代器只适用于随机访问迭代器
	mylist.sort(myfuncsort);	

    这里要注意一下,不是所有容器都适合排序的。前面也说过,有些容器中元素的位置不是由程序员决定的,而是由容器内部的算法决定的,所以顺序容器可以排序,但关联容器(包括无序容器)都不适合排序。
    例如,下面这段代码读者可以跟踪调试:

{
	map<int, string> mymap; //键值对
	mymap.insert(std::make_pair(50, "老王"));
	mymap.insert(std::make_pair(15, "老李"));
	mymap.insert(pair<int, string>(80, "老赵"));
	//sort(mymap.begin(), mymap.end());//不让排序,编译报错		
}

    上面这段代码虽然插入时的键的顺序是50、15、80,但插入完成后设置断点跟踪调试可以发现,mymap里的内容中的键顺序看上去是15、50、80。

{
	unordered_set<int> myset = { 50,15,80,30,46 };
	//sort(myset.begin(), myset.end()); 不让排序,编译报错
	cout << "断点掐在这里" << endl;
}

    读者可以设置断点,看一看myset容器中的元素顺序。不同版本的编译器可能看到的顺序不一样,笔者看到的顺序是50、15、80、46、30,如图所示。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值