STL(standard template libaray):
标准模板库,是C++程序设计语言的标准程序库,是一个包罗算法与数据结构的软件框架。
STL的的目的是标准化组件,所以在STL中使用了泛型编程的思想,对我们常用的数据结构:顺序表、链表、树、哈希以及常用的查找、排序等算法使用模板进行了封装,而且从运行效率以及内存使用上都基本达到了最优。引入STL后,再也不需要我们重新造轮子,而且写出来的代码更加简洁,容易修改,可移植性高。万一STL所提供的容器或者算法不能满足我们的要求,我们也可以实现自己的容器或算法与STL中的其他组件进行交融
容器:
STL中的容器是用来放数据的,因此也称为数据容器。由于对数据进行的操作不同,使用的场景各异,可能需要相应的数据结构来管理数据,常见的数据结构:array、list、tree、stack、queue、hash table、map、set等。因此STL中的容器便是对各种数据结构的封装。
根据数据在容器中的排序特性,容器分为序列式容器和关联式容器
序列式容器中的元素次序:即是按照其加入的容器中先后次序,不一定有序
序列式容器之vector
vector底层维护了一段动态的连续空间。随着元素的不断插入,vector的内部机制会自动检测,决定是否需要进行扩容以容纳新元素。在SGI版本的源码中,具体的操作是:当vector检测到空间不足时,动态开辟原空间大小两倍的空间,接着将旧空间中的元素高效的拷贝到新空间中,最后释放空间,所有的操作对用户来说都是透明的。
vector的迭代器:
因为vector底层是一段连续空间,因此vector的迭代器被设计为一个原生态指针,即:所存储元素类型的指针
vector的操作:
使用vector时,必须包含vector头文件:<vector>
必须引入标准命名空间std
list
vector底层搭载一段连续空间,因此在其任意位置进行数据的插入或删除时效率是非常低下(时间复杂度:O(N)),因此当集合中需要进行大量的插入和删除操作的时候,可以考虑list
list的底层结构:
list的底层是一个带头结点的双向循环链表,因此在其任意位置进行数据插入和删除操作非常方便,时间复杂度均为O(1)
list的迭代器:
由于list底层是带头结点的双向循环链表,因此list的迭代器需要list的实现者自己提供,否则当让迭代器 ++ 依次朝后访问链表时,迭代器将不知道如何朝后移动
迭代器的本质是指针,是将指针封装出来的一种新的类型,因此指针有的操作,迭代器也要视情况支持这些操作。迭代器在其类中只需将这些操作重载出来即可,最后只需将list迭代器类型与list容器绑定在一起即可(将迭代器作为list的一种内部类型使用)。当利用迭代器来遍历list时,在迭代器上进行++操作,它内部根据自己的底层的结构就知道如何朝后去移动。
deque:
double ended queue,vector是单向开口的真正连续空间,deque是双向开口的假想连续空间。所谓双向开口:是指可以在头尾两端分别进行元素的插入和删除操作,因此deque要求在常数时间内对头端进行数据的插入和删除操作。
deque底层结构:
deque的底层由许多段定量的连续空间构成,一旦有需要,其内部机制会自动配置一段定量连续空间,串接在整个deque空间的头端和尾端。deque实际是维护一些分段固定大小的连续空间,将这些分段连续的空间管理好,造成其连续空间的假象,用户在使用时如同在操作一段连续空间
deque的迭代器:
deque是分段连续空间,维护其“整体连续”假象的任务就落在其迭代器operator++和operator--上。因此deque的迭代器必须能够知道分段连续空间在哪里,其次迭代器还必须能够识别出自己是否已经处于其所在某段连续空间的边缘,如果是,一旦前进或后退时就必须跳跃至下一个或上一个分段空间
deque的操作:
使用deque时,必须包含其头文件<deque>和命名空间std
deque在STL中其实是一种比较鸡肋的容器,一般不会用到。比如:在数据集合中尾插和尾删方式较多时,则可以考虑vector,在数据集合中任意位置插入或删除方式较多时,则可以考虑list。
适配器:
适配器是设计模式的一种,该模式是将一个类的接口转换成用户希望的另外一个接口。简单的说,就是需要的东西就在眼前,但却不能直接用或者使用不是很方便,而短时间内无法实现它,如果想要快速实现,有一种办法就是通过已存在的东西去适配它
STL中的适配器共有三种类型,应用于容器的即容器适配器,比如stack和queue就是对deque的接口进行了转调;应用于迭代器的即迭代器适配器,比如反向迭代器就是对迭代器进行了转调;应用于仿函数的即函数适配器
容器适配器:stack和queue都是一种特殊的线性数据结构,要求在固定端进行数据的插入和删除操作;比如:
stack:要求在其一端进行数据的插入和删除即入栈和出栈,称为栈顶;另一端是栈底,
queue:要求在尾部进行数据的插入操作即入队列,在其头部进行数据的删除即出队列,因此queue是一种后进先出的线性结构
deque是双开口的结构,因此STL将其作为栈和队列的底层结构,将deque稍加改装就实现stack和queue。像这样,经某个类的接口进行重新包装而实现出的新结构,称为适配器
priority_queue
priority_queue是一个拥有权值关键的队列,允许用户以任意次序将元素插入容器内,但取出时每次都是取优先级最高(低)的元素,这正是heap的特性。因此priority_queue最终选择以vector作为底层空间结构,将heap算法进行转调应用于vector,实现了优先级队列
关联式容器:
序列式容器中元素大小杂乱无章没有次序,因此在其中找某个元素只能使用顺序查找,效率有点低下(时间复杂度为O(n))
为了提高数据查询的效率以及数据的有效性,关联式容器采用了平衡搜索树作为底层结构,并且容器中存放的不再是一个一个的数据,而是具有关联关系的键值对<key,value>
键值对实际是一个结构体,该结构体中有两个字段,一个为key,一个为value,key和value具有一一对应的关系。
二叉搜索树:
又称二叉排序树,它或者是一颗空树,或者是具有以下性质的二叉树:
若左子树不为空,则左子树上所有节点的值都小于根节点的值
若右子树不为空,则右子树上所有节点的值都大于跟节点的值
它的左右子树也分别称为二叉搜索树
但如果向容器中插入元素时,元素序列恰好是有序或接近有序,二叉搜索树将会退化为单支树,类似于链表,查询效率又降低到O(N)
因此,序列式容器底层并没有直接采用二叉搜索树结构,而是对二叉搜索树进行优化,使二叉搜索树中每个结点的左右子树高度差的绝对值不超过1来保证树的平衡性
AVL树:
一颗AVL树或者是空树,或者是具有以下性质的二叉搜索树:
它的左右子树都是AVL树
左子树和右子树高度之差的绝对值不超过1
当插入新节点后,AVL树的平衡性可能会被破坏,一旦破坏,就必须对树的结构进行旋转处理,以保证AVL树的平衡性
因为AVL树中每个结点的左右子树高度差的绝对值不超过1,因此其查找效率可以达到log2(N)。但每次向AVL树中插入或删除结点时,只要发现树不平衡,就要进行旋转,而删除时有可能旋转多次。在实际的应用中也发现AVL的插入和删除效率不是很高,因此关联式容器底层没有使用AVL树,而是采用了一种近似平衡的二叉搜索树
红黑树:
红黑树是一颗二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是red或者black,通过对任何一条从根结点到叶子结点简单路径上结点的颜色来约束,最后保证最长路径不超过最短路径的两倍,因而近似平衡
红黑树的性质:
每个结点不是红色就是黑色
根结点是黑色的
如果一个结点时红色的,则它的两个孩子结点是黑色的
对于每个结点,从该结点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点
每个叶子结点都是黑色的(此处的叶子结点指的是空节点)
虽然红黑树是一颗近似平衡的搜索树,但是在实际应用中表现出的性能确实比AVL树更优,因此关联式容器选择将红黑树作为其底层结构
map:
map的特性:
所有元素都会根据元素的键key自动排序,其中的每个元素都是<key,value>的键值对,map中不允许有键值相同的元素,因此map中的键key不能修改,但是可以修改key对应的value。如果一定要修改与value对应的key,可将已存在的key删除掉,然后重新插入
map与multimap的区别:
map中的key必须是唯一的,而multimap中的key是可以重复的,其底层结构及方法接口与map完全相同
set
与map的相同点:底层均采用红黑树,因此所有元素都会根据元素的键值自动排序
与map的不同点:map中存放的是真正的键值对<key,value>,而set中存放的是<value,value>
multiset与set唯一的不同就是:multiset中的元素可以重复,而set不能重复
unordered系列关联式容器:
哈希桶结构:
为了调高查询效率,C++11又添加了以哈希作为底层结构的关联式容器:unordered_map/unordered_set/unordered_multimap/unordered_multiset。这4个关联式容器与map/multimap/set/multiset功能基本相似,最主要就是底层结构不同,使用场景不同。如果需要一个有序序列,使用红黑树系列的关联式容器,如果需要更高的查询效率,使用以哈希表为底层的关联式容器。
迭代器:
迭代器(iterater)是一种抽象的设计概念,是设计模式的一种,其定义如下:提供一种方法,使之能够依次访问某个容器中所包含的所有元素,而又无需暴露该容器底层的结构
STL设计的中心思想为:将数据容器和算法分离开,彼此独立设计,算法要操作容器中的元素时,通过迭代器去访问即可。因此算法不需要去关心所操作数据底层的结构,只要能够按照迭代器去访问所需的数据即可,实现其通用性。
迭代器本质:
迭代器实质是一种行为类似指针的对象,因此指针的所有操作迭代器都必须要支持,使用迭代器时可以像使用指针一样去使用。比如:指针的解引用、成员访问、前置/后置++,前置后置--,==,!=等,迭代器都要支持,而迭代器是一种行为类似指针的新类型,因此迭代器的实现只需将指针的上述操作在类中重载即可
迭代器的实现:
迭代器是算法和容器的粘合剂,同一个算法可以操作不同类型的容器,而容器类型不同,意味着底层数据结构不同,数据结构不同,迭代器寻访的方式就不同,那迭代器是如何知道按照某种方式去访问的呢?
答案:容器的设计者负责该容器的迭代器的实现,根据容器底层数据结构的特点,选择指针的相应操作去重载即可。比如vector和list的迭代器就不同,因为vector底层是一段连续的空间,其迭代器实现需将原生态指针重新进行封装,最后再以统一的接口出现即可。
注意:迭代器失效的问题!
仿函数:
又叫函数对象,一种行为类似函数的对象,调用者可以像函数一样使用该对象。其实现起来也比较简单:用户只需要实现一种新类型,在类中重载()即可,参数根据用户所要进行的操作选择匹配。
算法:
STL所提供的算法往往有两个版本,其中一个版本是最常用的某种运算,第二个给出的是泛化版本,对用户的数据范围,算法不支持特定的操作,此时就需要用户通过仿函数对象来定制其所需要的操作。