【C++详解】STL-list使用(三大特性之一封装详解、cpu高速缓存命中率)
文章目录
- 一、list简介
- 二、构造函数
- 三、迭代器详解
- 迭代器的特点
- 封装详解
- 复用
- 不同类型迭代器
- 四、operations相关接口
- sort
- cpu高速缓存命中率
- reverse
- merge
- unique
- remove
- splice
一、list简介
list结构上等同于在数据结构初阶介绍的带头双向循环链表。
它是一个顺序容器,允许在任意位置以O(1)时间复杂度实现插入删除操作,它的迭代器是一个双向迭代器。
二、构造函数
//默认构造
list<int> lt1;
//n个val构造
list<int> lt2(10, 1);//迭代器区间构造
vector<int> v1 = { 1,2,3,4,5,6 };
list<int> lt3(v1.begin(), v1.end());
int arr[] = { 1,2,3,4,5 };
vector<int> v2(arr, arr + 5); //范围也应该左闭右开
list<int> lt4(arr, arr + 5);//结构化绑定
list<int> lt5 = { 8,7,6,5,4,3 };
迭代器区间构造除了传当前容器或其他容器的迭代器区间还可以传数组指针,因为指针也是一种特殊的迭代器,前提是这个指针是指向数组的指针。(迭代器的行为本质模拟的就是指向数组的指针)
迭代器区间构造本质就是通过模板参数来接受不同类型的迭代器。
三、迭代器详解
介绍了上面有关迭代器的内容后,小编补充讲解一下有关迭代器的一些内容。
迭代器的特点
迭代器的两个特点:封装与复用。
封装详解
在面试过程中一个常问的面试题:C++三大特性之一封装是什么? 分两个层次作答+举样例:
1、相比C语言,C语言实现时它的数据和方法是分离的,C++把数据和方法放到一起,把不想被别人看见的数据和方法封装为私有,不让外界访问。
2、STL迭代器的设计是一种更高维度的封装,它并没有被封装为私有,它是一种通用的遍历容器的方式,所有容器的迭代器都封装为一个统一的iterator,它上层的使用是一样的,底层被迭代器封装起来了,屏蔽掉了容器结构的差异,和底层实现细节。
样例:比如以前微信支付宝还没普及时线上支付要用银行卡,但是不同银行公司有不同的系统,所以银行卡差异也很大,有各种验证方式:密保、U盾、短信验证码…
这就类似于不同容器的底层差异。但是现在有了微信和支付宝,它把各家银行不同的底层系统都封装起来了,上层都统一用一种支付验证方式,这就类似迭代器,使得遍历容器更加方便快捷。
复用
迭代器还有一个特点:复用,实现算法时用迭代器函数模板方式实现,不同的容器只要传迭代器就可以实现不同容器的算法,所以算法就是一个通用的算法,可以复用于不同的容器,用这种复用的方式,就可以和容器的底层结构解耦。
不同类型迭代器
我们用算法库里面的各种算法会发现不同算法里的迭代器参数名称都不同,原因是各种算法对容器迭代器的功能是有要求的,(算法迭代器参数的名字就是要求)所以对于一个算法,不是所有容器都可以使用。
容器的迭代器按功能分为三类,具体看下面表格:
其实还有两个迭代器,只是几乎没有对应的功能:
只写迭代器:input
只读迭代器:output
迭代器之间的包含关系如下:
不同类型容器的迭代器的区别在于它们底层结构的差异,容器的迭代器支不支持相应的行为。
四、operations相关接口
其他ist接口和之前介绍的容器接口差不多,小编就不再过多介绍了,这里重点介绍区别于其他容器的一些接口。
sort
因为我们知道算法库的sort是不支持list使用的,所以ist自己实现了一个排序算法。
我们来看一下下面这段代码的运行结果:
srand(time(0));const int N = 1000000;vector<int> v1;list<int> l1;for (int i = 0; i < N; i++){int e = rand() + i;v1.push_back(e);l1.push_back(e);}int begin1 = clock();sort(v1.begin(), v1.end());int end1 = clock();int begin2 = clock();l1.sort();int end2 = clock();printf("vector sort: %d\n", end1 - begin1);printf("list sort: %d\n", end2 - begin2);
我们可以看到debug版和release版两个算法的效率天差地别,原因就在于vector是用的算法库的sort,底层是快排,而list的sort底层是归并,快排是用递归实现的,因为递归要建立栈帧,在debug模式下会打很多调试信息,所以递归程序在debug下是不占优势的。在release下会将优化全开,所以要比较程序的效率最好在release下比较。
这里我们已经感受到list的sort效率是比较低的,我们再看下面这段代码和运行结果:
srand(time(0));const int N = 1000000;list<int> l1;list<int> l2;for (int i = 0; i < N; i++){int e = rand() + i;l1.push_back(e);l2.push_back(e);}int begin1 = clock();vector<int> v(l1.begin(), l1.end());sort(v.begin(), v.end());l1.assign(v.begin(), v.end());int end1 = clock();int begin2 = clock();l2.sort();int end2 = clock();printf("list copy vector sort: %d\n", end1 - begin1);printf("list sort: %d\n", end2 - begin2);
list把值拷贝给vector排序然后再拷贝回list的效率都比ist直接sort的效率高,所以以后数据量大时最好不要用list的sort,因为效率实在太低。
那为什么两个排序算法的时间复杂度都为n*logn,差别会这么大呢?除了算法本身的效率差别,还有一个影响因素:cpu高速缓存命中率,与vector和ist的底层结构有关,下面来详细介绍。
cpu高速缓存命中率
我们执行排序算法时会大量访问内存,访问内存操作需要经过cpu执行指令,这些指令再去访问内存,但是cpu速度非常快,内存跟不上,所以现在的硬件设计在内存之上还会配备三级缓存和寄存器,小块内存的访问就经过寄存器,大段数据就会加载进缓存。
当cpu执行指令访问内存的数据时,会先看数据在不在缓存,如果在缓存就命中,不在就不命中,cpu只会访问在缓存的数据。当数据不在缓存时,会先将内存的数据加载进缓存,再访问。由于硬件设计原因,加载一次数据可以理解成缓存开着公交车去拉数据,一次不会只拉一个数据,因为拉一次的花销是固定的,所以拉一次都会尽可能拉满。
访问数据过程中还有一个局部性原理,访问一个数据还会把它后面的一段数据一起访问(一般是cpu的字长,具体多少字节和硬件的设计有关),所以在访问顺序结构例如数组时效率就很高,因为数组是连续存储的,所以访问一次数据就可以加载一段连续的有效数据进缓存,但是链式结构效率就比较低了,链式结构的数据一般都不会连续存储,所以访问一次数据只会加载一个有效数据和在它后面的无效数据,这就比顺序结构访问数据的效率低多了。
reverse
逆置
merge
合并两个有序ist
unique
去重,比如链表有三个1,会去掉两个重复的1,去重之前需要先将list排序为有序list。
remove
删除掉list中的所有参数值的结点。
splice
它的作用的粘接、剪切,它可以将一整个list、list的其中一个结点、list的一段迭代器区间剪切到另一个list的position之前。前提是两个list的模板参数要一样。
它还可以在同一个list里进行剪切转移。
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~