通用的容器分为三类:顺序性容器、关联式容器和容器适配器。
一、顺序性容器
顺序性容器是一种各元素之间有顺序关系的线性表,除非用插入、删除的操作改变位置,否则元素在容器中的位置与元素本身没有关系,只与操作的时间和地点相关(时间:什么时候添加的元素,地点:元素添加到了那个位置);常用的顺序性容器有:vector、deque、list。
- Vector
vector是一个线性顺序结构,相当于数组。与数组不一样的地方是可以动态的扩容,其他特性与数组完全一样。它的扩容规则是当元素个数超出其capacity()时,会重新申请一块内存,其大小是之前capacity()的两倍,这种扩容操作很耗费时间,效率低下。所以最好在初始化申请其空间大小,这样效率比较高。
由于vector在内存中是连续的,而且支持自动扩容,所以显而易见,其在后部push_back()、pop_back()元素的动态操作是很高效的,但是在内部进行增删的动态效率非常低,所以尽量避免这样操作。
vector的初始化:
vector<int> a(10); //定义10个整型元素的向量,没有定义其初值,其值是不确定的 vector<int> a(10, 0); //定义10个整型元素的向量,初值设置为0 vector<int> a(b); //利用容器b来初始化a,整体复制 vector<int> a(b.begin(), b.begin()+ 5); //使用容器b的前五个元素来构造a
vector的常用操作:
a[i]; //返回第i个元素 a.at(i); //返回第i个元素 a.size(); //返回a中元素的个数 a.capacity(); //返回a总共可容纳的元素个数 a.empty(); //返回值为bool类型,检查容器a是否为空 a.clear(); //清空a中的所有元素 a.push_back(1); //向a的尾部推入元素1 a.pop_back(); //删除容器a的最后一个元素,没有返回值 a.back(); //返回容器a的最后一个元素 a.front(); //返回容器a的第一个元素 a.begin(); //返回一个指向容器a的首元素的迭代器 a.end(); //返回一个指向容器a的尾元素下一个位置的迭代器
注意:
- a.at(i)和a[i]的不同之处在于,a.at(i)会做安全检查,如果访问越界,则会抛出out_of_range异常,而a[i]不会做安全检查,直接中断程序。
- 相信很多人和我有同样的疑问,为什么不在pop_back()操作时直接返回删除的值,这样不是更加方便?其实pop_back()是没有返回值的,back()才返回容器尾部元素,这样做是为了出于安全考虑,把元素的得到方法和删除方法分开实现,免得在一起操作是,元素删除成功,而返回失败,造成数据丢失。这个规则在其他容器中也同样适用。
vector的一些不常用操作(慎用):
a.erase(a.begin()); //删除传入的迭代器指向的元素,返回值是一个迭代器,指向被删除元素的下一个元素的迭代器 a.erase(a.begin(), a.begin() + 3); //删除迭代器指向区间内的元素(左闭右开),返回指向“右开”元素的迭代器 a.insert(a.begin(), 5); //在传入迭代器的前部插入元素5 a.insert(a.begin(), 3, 5); //在传入迭代器的前部插入3个元素5 a.insert(a.begin(), b.begin(), b.end()); //在传入迭代器的全部插入区间的全部元素
注意:第一点是erase()函数返回值是一个迭代器,指向被删除元素的下一个元素,第二点是在使用迭代器对容器进行遍历时,对特定元素进行删除时,要注意野指针的问题,解决办法是erase(iter--)。PS:这个坑刚开始学习使用的时候很容器踩。
vector的几种重要算法(包含在头文件<algorithm>中):
sort(a.begin(), a.end()); //将传入的迭代器指向区间进行从小到大排序 reverse(a.begin(), a.end()); //将传入的迭代器指向区间进行元素颠倒操作 copy(a.begin(), a.end(), b.begin()); //将传入的迭代器指向区间的元素全部复制到b中,覆盖掉元素元素 find(a.begin(), a.end(), 10); //在传入的迭代器指向区间查找指定元素,返回首次发现该元素的迭代器(返回值是迭代器,不是位置)
- List
list容器的功能实际上与数据结构中的双向链表类似,它的内存空间是不连续的,通过指针来对数据进行访问,它在链表的任意位置的增删节点的操作都是高效的,没有提供[]来对元素进行访问,因为这种访问操作效率很低,list的迭代器只能进行“++”和“--”操作,而不能和vector的迭代器一样进行+n和-n操作。
与vector不同的一些操作:
list.push_front(1); //将传入的元素插入到list的首部 list.pop_front(); //删除链表的首元素
相比vector,增加了对list的首元素进行增加和删除的操作。
- deque
deque容器算是对list和vector的一种综合,首先在内存上来说,vector采用的是连续的内存空间存储数据,list是采用单个节点的链表形式,而deque是采用的部分连续的非连续空间进行存储,相较于vector的单端(尾部操作),deque和list一样,是双端操作,而且具备了list所不具备的at()和[]操作来访问元素,但是不像vector那样高效,综合了两种的优缺点,是两者折中的一种数据结构。
二、关联式容器
与顺序性容器不一样,关联式容器是采用非线性的树结构来存储数据,树形结构的底层采用的是平衡二叉搜索树:RB-Tree(红黑树),哈希结构底层实现为哈希表,容器内的各元素没有严格意义上的物理内存上的顺序关系。此外,关联式容器是采用key-value形式,在树形结构中保存key值,然后通过哈希表访问其value值,而顺序性容器只能保存一种类型的值。
- set和multiset
set容器顾名思义是一个集合,用来存储统一类型的数据,此外在set中的每个元素的值都是唯一的,而且系统能根据元素的值自动进行排序,默认升序排列,set中的值不能直接改变,很简单理解,对一颗红黑树的任意一个节点的值直接改变后,会直接打破红黑树的本身规则,所以一般都是先删除该节点,然后再添加新节点的形式来完成改值的操作。
set的常用操作:
s.erase(1); //在set中删除传入的元素 s.erase(s.begin()); //删除传入的迭代器所指向的元素 s.erase(s.begin(), s.end()); //删除传入两个迭代器之间的所有元素 s.insert(2); //向容器中插入传入的元素 s.find(3); //在容器中查找传入的元素,如果找到,则返回位置,没有找到则返回end() s.equal_range(5); //查找找到的第一个大于或等于指定元素的位置, s.lower_bound(5); //返回第一个大于等于指定元素的定位器 s.upper_bound(5); //返回最后一个大于等于指定元素的定位器 //最后这是三个用法我也没使用过
注意:
- set容器的find()操作的时间复杂度是O(logN),显然其查找效率高于顺序性容器的O(N),此外set容器的erase()和insert()操作的时间复杂度都是O(logN)。(这里挖个坑,有时间写红黑树等BBST的总结)
- set容器的动态操作(增删)不会改变之前保存的iterator失效,这点和list容器类似,因为内存上没有连续性
multiset容器是多重集合,其实现方式和set相似,只是multiset中的元素允许重复出现。
- map和multimap
map容器存放的是key-value形式的数据,key和value的数据类型可任意选择,单纯考虑key值,其结构形式和set类似,不同的地方在于每一个key值通过哈希表映射到一个value值,key值是唯一的,不能重复,而value是可以重复的,它的查找操作和删除操作时间复杂度也是O(logN),key值不允许修改,通过key值改变value值很简单,只需要改变映射关系就可以了。
map的构造函数:
map<int, string> map_a; //默认构造 map<int, string> map_a(map_b); //拷贝构造 map<int, string> map_a(map_b.begin(), map_b.end()); //区间构造 map<int, string> map_a{ {1,"a"},{2,"b"} }; //列表构造
map的插入操作:
map_a.insert(pair<int, string>(3, "c")); map_a.insert(map<int, string>::value_type(4, "d")); map_a[5] = "e";
注意:以上三种方法都可以完成数据的插入操作,但是还是有差异,前面两种在效果上是一直的,在使用insert()插入数据时,如果map容器中已经含有key值,则会直接跳过,不进行任何操作,不会用新的value值覆盖原始的映射关系,而采用第三种数组的方式,如果map容器中已经含有key值,会进行value值的覆盖操作。但是单就效率而言,如果确定map容器中不存在该key值,用insert()操作更高效。
map容器的其他操作都与set容器的操作相同。而multimap容器就是允许集合中出现重复元素,与multiset类似。
三、容器适配器
刚开始学的时候不理解这个概念,感觉很高端的样子。其实就是将已有的容器结构类型,改变一下接口,使其实现特有的功能,比如stack容器,其实就是deque容器改变接口实现的,你也可以使用deque容器当做stack容器使用,但是deque容器接口过多,为了避免误操作,破坏了stack容器的性质,就使用容器适配器。C++提供了三种容器适配器:stack,queue和priority_queue。stack和queue默认是基于deque实现,priority_queue默认是基于vector实现。
- stack
stack容器专门设计用于在LIFO(后进先出),其中元素仅从容器的一端插入和删除。stack容器的成员函数:
stack<int> stack; stack.empty(); //判断栈是否为空,为空返回true,否则返回false stack.size(); //返回栈中元素的个数 stack.push(1); //将传入元素压入栈中 stack.top(); //返回栈顶元素的引用 stack.pop(); //将栈顶元素弹出,无返回值
stack容器看着很简单,成员函数也很少,其实用处很大,比如在递归中,使用栈就能完成一次深度优先搜索,而且节约了递归中的函数入栈出栈开销。还有波兰表达式、逆序输出等等。
- queue
queue容器是一种“先进先出”的队列,queue的成员函数:
queue<int> queue; queue.empty(); //判断栈是否为空,为空返回true,否则返回false queue.size(); //返回队列中元素的个数 queue.push(1); //将元素入队 queue.pop(); //弹出队首元素,无返回值 queue.front(); //返回队首元素的引用 queue.back(); //返回队尾元素的引用
queue容器的应用也很广泛,比如对二叉树的层次遍历就要使用到queue容器,理解的更深刻一点的话,广度优先搜索就会使用到队列的结构。
- priority_queue
priority_queue容器就是优先级队列,按照值的大小来决定出队顺序。
priority_queue<int> p1; //默认是 最大值优先级队列 priority_queue<int, vector<int>, less<int> > p2; //提前定义好的预定以函数,第二个参数的默认是less<int> priority_queue<int, vector<int>, greater<int>> p2; //最小值优先级队列