龙空技术网

C++ STL 各容器简单介绍

C加加Qt技术开发老杰 680

前言:

而今看官们对“c加加结构体排序”可能比较关怀,兄弟们都想要分析一些“c加加结构体排序”的相关内容。那么小编在网摘上汇集了一些关于“c加加结构体排序””的相关知识,希望同学们能喜欢,小伙伴们一起来学习一下吧!

一、什么是STL?

1、STL(Standard Template Library),即标准模板库,是一个高效的C++程序库,包含了诸多常用的基本数据结构和基本算法。为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。

2、从逻辑层次来看,在STL中体现了泛型化程序设计的思想(generic programming)。在这种思想里,大部分基本算法被抽象,被泛化,独立于与之对应的数据结构,用于以相同或相近的方式处理各种不同情形。

3、从实现层次看,整个STL是以一种类型参数化(type parameterized)的方式实现的,基于模板(template)。

STL有六大组件,但主要包含容器、迭代器和算法三个部分。

容器(Containers):用来管理某类对象的集合。每一种容器都有其优点和缺点,所以为了应付程序中的不同需求,STL 准备了七种基本容器类型。迭代器(Iterators):用来在一个对象集合的元素上进行遍历动作。这个对象集合或许是个容器,或许是容器的一部分。每一种容器都提供了自己的迭代器,而这些迭代器了解该种容器的内部结构。算法(Algorithms):用来处理对象集合中的元素,比如 Sort,Search,Copy,Erase 那些元素。通过迭代器的协助,我们只需撰写一次算法,就可以将它应用于任意容器之上,这是因为所有容器的迭代器都提供一致的接口。

STL 的基本观念就是将数据和操作分离。数据由容器进行管理,操作则由算法进行,而迭代器在两者之间充当粘合剂,使任何算法都可以和任何容器交互运作。

二、容器(Containers)

容器用来管理某类对象。为了应付程序中的不同需求,STL 准备了两类共七种基本容器类型:

序列式容器(Sequence containers),此为可序群集,其中每个元素均有固定位置—取决于插入时机和地点,和元素值无关。如果你以追加方式对一个群集插入六个元素,它们的排列次序将和插入次序一致。STL提供了三个序列式容器:向量(vector)、双端队列(deque)、列表(list),此外你也可以把 string 和 array 当做一种序列式容器。关联式容器(Associative containers),此为已序群集,元素位置取决于特定的排序准则以及元素值,和插入次序无关。如果你将六个元素置入这样的群集中,它们的位置取决于元素值,和插入次序无关。STL提供了四个关联式容器:集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)。

示意图如下图所示:

2.1 vector

vector(向量): 是一种序列式容器,事实上和数组差不多,但它比数组更优越。一般来说数组不能动态拓展,因此在程序运行的时候不是浪费内存,就是造成越界。而 vector 正好弥补了这个缺陷,当内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。

特点

拥有一段连续的内存空间,因此它能非常好的支持随机访问,即 [] 操作符和 .at(),随机访问快。(优点)当向其头部或中间插入或删除元素时,为了保持原本的相对次序,插入或删除点之后的所有元素都必须移动,所以插入或删除的效率比较低。(缺点)在后面插入删除元素最快,此时一般不需要移动内存。(优点)总结:相当于可拓展的数组(动态数组),随机访问快,在头部和中间插入或删除效率低,但在尾部插入或删除效率高。

适用场景

适用于对象简单,变化较小,并且频繁随机访问的场景。

例子

以下例子针对整型定义了一个 vector,插入 6 个元素,然后打印所有元素:

#include <iostream>#include <vector>  C++资料领取进裙:580475042  using namespace std;int main(int argc, char* argv[]){	vector<int> vecTemp;	for (int i = 0; i<6; i++)		vecTemp.push_back(i);	for (int i = 0; i<vecTemp.size(); i++)		cout << vecTemp[i] <<" "; // 输出:0 1 2 3 4 5	return 0;}
2.2 deque

deque(double-ended queue)是双向开口的连续内存空间(动态将多个连续空间通过指针数组接合在一起),随时可以增加一段新的空间。deque 的最大任务就是在这些分段的连续空间上,维护其整体连续的假象,并提供随机存取的接口。

特点

一旦要在 deque 的头部和尾部增加新空间,便配置一段定量连续空间,串在整个 deque 的头部或尾部,因此不论在头部或尾部插入元素都十分迅速。 (优点)在中间部分安插元素则比较费时,因为必须移动其它元素。(缺点)deque 是 list 和 vector 的折中方案。兼有 list 的优点,也有 vector 随机访问效率高的优点。总结:支持随机访问,但效率没有 vector 高,在头部和尾部插入或删除效率高,但在中间插入或删除效率低。

适用场景

适用于既要频繁随机访问,又要关心两端数据的插入与删除的场景。

例子

以下例子声明了一个浮点类型的 deque,并在容器尾部插入 6 个元素,最后打印出所有元素。

#include <iostream>#include <deque>using namespace std;int main(int argc, char* argv[]){	deque<float> dequeTemp;	for (int i = 0; i<6; i++)		dequeTemp.push_back(i);	for (int i = 0; i<dequeTemp.size(); i++)		cout << dequeTemp[i] << " "; // 输出:0 1 2 3 4 5	return 0;}
2.3 list

List 由双向链表(doubly linked list)实现而成,元素存放在堆中,每个元素都是放在一块内存中。没有空间预留习惯,所以每分配一个元素都会从内存中分配,每删除一个元素都会释放它占用的内存。

特点

内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,因此它没有提供 [] 操作符的重载。(缺点)由于链表的特点,在任意位置的插入和删除效率都较高。(优点)只支持首尾两个元素的直接存取,想获取其他元素(访问时间一样),则需要遍历链表。(缺点)总结:不支持随机访问,在任意位置的插入和删除效率都较高。

适用场景

适用于经常进行插入和删除操作并且不经常随机访问的场景。

例子

以下例子产生一个空 list,准备放置字符,然后将 'a' 至 'z' 的所有字符插入其中,利用循环每次打印并移除集合的第一个元素,从而打印出所有元素:

#include <iostream>#include <list>using namespace std;  C++资料领取进裙:580475042int main(int argc, char* argv[]){	list<char> listTemp;	for (char c = 'a'; c <= 'z'; ++c)		listTemp.push_back(c);	while (!listTemp.empty())	{		cout <<listTemp.front() << " ";		listTemp.pop_front();	}	return 0;}

成员函数empty()的返回值告诉我们容器中是否还有元素,只要这个函数返回 false,循环就继续进行。循环之内,成员函数front()会返回第一个元素,pop_front()函数会删除第一个元素。

注意:list<指针> 完全是性能最低的做法,还不如直接使用 list<对象> 或使用 vector<指针> 好,因为指针没有构造与析构,也不占用很大内存。

2.4 set

set(集合)顾名思义,就是数学上的集合——每个元素最多只出现一次,并且 set 中的元素已经从小到大排好序。

特点

使用红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复。每次插入值的时候,都需要调整红黑树,效率有一定影响。(缺点)map 和 set 的插入或删除效率比用其他序列容器高,因为对于关联容器来说,不需要做内存拷贝和内存移动。(优点)总结:由红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复,且插入和删除效率比用其他序列容器高。

适用场景

适用于经常查找一个元素是否在某集合中且需要排序的场景。

例子

下面的例子演示 set(集合)的两个特点:

#include <iostream>#include <set>using namespace std;int main(int argc, char* argv[]){	set<int> setTemp;	setTemp.insert(3);	setTemp.insert(1);	setTemp.insert(2);	setTemp.insert(1);	set<int>::iterator it;	for (it = setTemp.begin(); it != setTemp.end(); it++)	{		cout << *it << " ";	}	return 0;}

输出结果:1 2 3。一共插入了 4 个数,但是集合中只有 3 个数并且是有序的,可见之前说过的 set 集合的两个特点,有序和不重复。

当 set 集合中的元素为结构体时,该结构体必须实现运算符 ‘<’ 的重载:

#include <iostream>#include <set>#include <string>using namespace std;struct People{	string name;	int age;	bool operator <(const People p) const	{		return age < p.age;	}};int main(int argc, char* argv[]){	set<People> setTemp;	setTemp.insert({"张三",14});	setTemp.insert({ "李四", 16 });	setTemp.insert({ "隔壁老王", 10 });	set<People>::iterator it;	for (it = setTemp.begin(); it != setTemp.end(); it++)	{		printf("姓名:%s 年龄:%d\n", (*it).name.c_str(), (*it).age);	}	return 0;}  C++资料领取进裙:580475042/*输出结果姓名:王二麻子 年龄:10姓名:张三 年龄:14姓名:李四 年龄:16 */ 

可以看到结果是按照年龄由小到大的顺序排列。另外 string 要使用c_str()转换一下,否则打印出的是乱码。

另外 Multiset 和 set 相同,只不过它允许重复元素,也就是说 multiset 可包括多个数值相同的元素。这里不再做过多介绍。

2.5 map

map 由红黑树实现,其元素都是 “键值/实值” 所形成的一个对组(key/value pairs)。

map 主要用于资料一对一映射的情况,map 内部自建一颗红黑树,这颗树具有对数据自动排序的功能,所以在 map 内部所有的数据都是有序的。比如一个班级中,每个学生的学号跟他的姓名就存在着一对一映射的关系。

特点

每个元素都有一个键,且只能出现一次,不允许重复。根据 key 值快速查找记录,查找的复杂度基本是 O(logN),如果有 1000 个记录,二分查找最多查找 10次(1024)。(优点)每次插入值的时候,都需要调整红黑树,效率有一定影响。(缺点)增加和删除节点对迭代器的影响很小,除了那个操作节点,对其他的节点都没有什么影响。(优点)对于迭代器来说,可以修改实值,而不能修改 key。总结:元素为键值对,key 和 value 可以是任意你需要的类型,每个元素都有一个键,且只能出现一次,不允许重复,根据 key 快速查找记录。

适用场景

适用于需要存储一个数据字典,并要求方便地根据key找value的场景。

例子

#include "stdafx.h"#include <iostream>#include <map>#include <string>using namespace std;int main(int argc, char* argv[]){	map<int, string> mapTemp;	mapTemp.insert({ 5,"张三" });	mapTemp.insert({ 3, "李四"});	mapTemp.insert({ 4, "隔壁老王" });	map<int, string>::iterator it;	for (it = mapTemp.begin(); it != mapTemp.end(); it++)	{		printf("学号:%d 姓名:%s\n", (*it).first, (*it).second.c_str());	}	return 0;}/*输出结果:学号:3 姓名:李四学号:4 姓名:隔壁老王学号:5 姓名:张三*/

multimap 和 map 相同,但允许重复元素,也就是说 multimap 可包含多个键值(key)相同的元素。这里不再做过多介绍。

2.6 容器配接器

除了以上七个基本容器类别,为满足特殊需求,STL还提供了一些特别的(并且预先定义好的)容器配接器,根据基本容器类别实现而成。包括:

1、stack

名字说明了一切,stack 容器对元素采取 LIFO(后进先出)的管理策略。

2、queue

queue 容器对元素采取 FIFO(先进先出)的管理策略。也就是说,它是个普通的缓冲区(buffer)。

3、priority_queue

priority_queue 容器中的元素可以拥有不同的优先权。所谓优先权,乃是基于程序员提供的排序准则(缺省使用 operators)而定义。Priority queue 的效果相当于这样一个 buffer:“下一元素永远是queue中优先级最高的元素”。如果同时有多个元素具备最髙优先权,则其次序无明确定义。

三、总结

各容器的特点总结

vector 支持随机访问,在头部和中间插入或删除效率低,但在尾部插入或删除效率高。支持随机访问,但效率没有 vector 高,在头部和尾部插入或删除效率高,但在中间插入或删除效率低。list 不支持随机访问,在任意位置的插入和删除效率都较高。set 由红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复,且插入和删除效率比用其他序列容器高。map 的元素为键值对,key 和 value 可以是任意你需要的类型,每个元素都有一个键,且只能出现一次,不允许重复,根据 key 快速查找记录。

在实际使用过程中,到底选择这几种容器中的哪一个,应该根据遵循以下原则:

1、如果需要高效的随机访问,不在乎插入和删除的效率,使用 vector。

2、如果需要随机访问,并且关心两端数据的插入和删除效率,使用 deque。

3、如果需要大量的插入和删除元素,不关心随机访问的效率,使用 list。

4、如果经常查找一个元素是否在某集合中且需要排序,唯一存在的情况使用 set,不唯一存在的情况使用 multiset。

5、如果打算存储数据字典,并且要求方便地根据 key 找到 value,一对一的情况使用 map,一对多的情况使用 multimap。

各容器的时间复杂度分析

vector 在头部和中间位置插入和删除的时间复杂度为 O(N),在尾部插入和删除的时间复杂度为 O(1),随机访问的时间复杂度为 O(1),查找的时间复杂度为 O(N);deque 在中间位置插入和删除的时间复杂度为 O(N),在头部和尾部插入和删除的时间复杂度为 O(1),随机访问的时间复杂度为 O(1),查找的时间复杂度为 O(N);list 在任意位置插入和删除的时间复杂度都为 O(1),查找的时间复杂度为 O(N);set 和 map 都是通过红黑树实现,因此插入、删除和查找操作的时间复杂度都是 O(log N)。

各容器的共性

各容器一般来说都有下列函数:默认构造函数、复制构造函数、析构函数、empty()、max_size()、size()、operator=、operator<、operator<=、operator>、operator>=、operator==、operator!=、swap()。

顺序容器和关联容器都共有下列函数:

begin() :返回容器第一个元素的迭代器指针;end():返回容器最后一个元素后面一位的迭代器指针;rbegin():返回一个逆向迭代器指针,指向容器最后一个元素;rend():返回一个逆向迭代器指针,指向容器首个元素前面一位;clear():删除容器中的所有的元素;erase(it):删除迭代器指针it处元素。

标签: #c加加结构体排序