STL标准模板库:(6大组件)
一、容器类:C++把C语言中的数据结构进行封装
1、顺序容器
(1)vector容器。它是一个矢量容器,它的底部是一个数组。是对顺序表进行了封装。头文件:#include
(2)list容器。它也是双向链表容器。它的底部是一个双向链表(环状)。所以它的底部是一个双向循环列表。头文件:#include
(3)deque容器。它是一个双端队列容器。它的底部放的是一个双端队列。头文件:#include
2、关联容器:关联容器是由红黑树组成。
(1)set容器。又称单集合容器。它的底部是红黑树。头文件:#include
(2)multiset容器。又称多重集合容器。它的底部也是红黑树。头文件:#include
(3)map容器。又称单映射容器。它的底部也是红黑树。头文件:#include
(4)multimap容器。又称多重映射容器。它的底部同样也是红黑树。头文件:#include
3、容器适配器
(1)stack
(2)queue
(3)priority_queue
这三者都是由容器适配器来实现的。它的底层放的是一个容器,默认放的是deque容器。
二、泛型算法
泛型算法和容器的关系:容器如果当成数据结构来理解的话它是来存储数据的。如果我们要对这些数据进行一系列的操作的话像增删改查这样的操作。都是以泛型算法的方式实现的。泛型算法的好处就是,他已经与类型无关,底层不管放的是什么容器,都可以对其进行操作。
sort排序算法,只支持随机访问迭代器的容器,不支持list容器,list容器不能通过sort排序,list类中自己提供排序算法
三、迭代器
一种针对于容器对象的遍历方式。
容器,泛型算法和迭代器三者之间的关系:首先,容器我们可以理解为数据,泛型算法是对数据进行的操作。这样看来数据和操作是分离开来进行设计的。那我们如何将它们又结合在一起,让泛型算法来操作容器里面的数据呢?那么就需要用到一个粘合剂,这个粘合剂就是迭代器它们的关系如下图所示:
四、函数对象
五、适配器
六、空间配置器
容器类
一、顺序容器
1、vector容器:底部是一个数组,支持随机访问迭代器(如果一个数据结构底层连续,可以通过指针+偏移直接跳转到一个位置处理,随机访问迭代器可以通过迭代器+i的方式处理)
(1)简单操作
- 增加:push_back(尾插)、insert(按位置插)
- 删除:pop_back(尾删)、erase(按位置删除)
(2)底层实现:以数组形式处理,数组动态开辟
(3)扩容方式
- 以倍数形式开辟更大的内存
- 把旧的数据拷贝到新的内存中
- 释放旧的内存
- 指向新的内存并调整总大小
(4)特点:支持在尾部快速的插入或者删除,直接访问元素
2、list容器:底部是一个双向循环链表,双向链表容器,支持双向迭代器(数据结构底层是不连续的,双向迭代器只能通过++或者--的方式处理)
(1)简单操作
- 增加:push_front(头插)、push_back(尾插)、insert(按位置插)
- 删除:pop_front(头删)、pop_back(尾删)、erase(按位置删除)
- 修改和查询交给迭代器处理
(2)底层实现:双向链表
(3)扩容方式:list开辟一个结点是动态开辟一个结点空间,是没有扩容方式的
(4)特点:6个操作时间复杂度都是O(1),可以在任意位置快速插入或者删除
(5)缺点:访问效率达到O(n),访问时list容器的缺点
(6)list容器不能通过sort排序(泛型算法),list类中自己提供排序算法
3、deque容器:底部是一个双端队列,双端队列容器,支持随机访问迭代器
(1)简单操作
- 增加:push_front(头插)O(1)、push_back(尾插)O(1)、insert(按位置插)O(n)
- 删除:pop_front(头删)O(1)、pop_back(尾删)O(1)、erase(按位置删除)O(n)
- 修改和查询迭代器处理
(2)底层实现:
(3)扩容方式
1、头部满
- 先看映射区域上面,如果有,直接开辟数据区域。
- 如果映射区域上面没有空间的话,就看映射区域下面的空间。如果映射区域下面的空间有区域的话,数据区域整体往下移动,然后再在上面开辟数据区域。
- 如果映射区域下面没有空间的话就扩充映射区域将数据移动到中间。 (以倍数形式扩充映射区)
2、尾部满
先看映射区域的下面,如果有空间的话,直接开辟数据区域。
如果映射区域下面没有空间的话,就看映射区域上面的空间。如果映射区域上面的空间有区域的话,数据区域整体往上移动,然后再在下面开辟数据区域。如果映射区域上面没有空间的话就扩充映射区域将数据移动到中间。
(4)特点:支持在头部或者尾部快速的插入或者删除O(1)
为什么deque底层内存是不连续的,但是还可以支持随机访问迭代器?
deque容器的迭代器实现屏蔽了底层内存不连续的特征,也支持随机访问迭代器
考点:
为什么在容器适配器的底部放的是deque容器?
根据容器的优缺点进行描述
首先,我们最终要实现的是栈和队列。栈是先进后出。队列是先进先出。这样的特征决定了它在两端进行操作,这样就排除了vector容器。因为vector容器只有尾插,没有头插。所以实现队列的话,vector是不行的。接着,不管是list还是deque都可以在头部和底部进行插入和删除。对于栈和队列我们访问的都是头部和尾部的元素。经过对比,我们发现deque容器对访问这些元素的效率高(O(1))因为它直接访问任何元素。所以最终选择deque容器为容器适配器的底层容器
vector容器、list容器和deque容器的区别:
(1)vector容器底层是数组,list容器底层是双向链表,deque底层是双端队列
(2)vector容器底层是随机访问迭代器,list容器底层是双向迭代器,deque底层是随机访问迭代器
(3)vector容器底层尾部快速的插入或删除,list容器任意位置快速插入或者删除,deque容器头部或者尾部快速的插入或删除
二、关联容器:底层存放红黑树,红黑树是特殊的BST树(特殊点在于在插入一个数据要求必须有序,红黑树分为红结点和黑结点,排序的过程会按照红黑结点的规则调整结点,红黑树的i/o效率会更高),BST树又称二叉搜索树或者二叉排序树,这种树形结构在数据存储的时候要求按照数据按照中序遍历排列有序
1、set容器:又称单集合容器,它的底部是红黑树。
(1)set的结点由三部分构成,分别是左孩子域、右孩子域和数据域(存储关键字)
(2)操作
- set集合不需要提定点插入,因为他的数据存储必须有序;插入只有insert_val
- 删除只有erase _where
(3)泛型算法find查找一个数据是一个顺序遍历,相当于将每个元素都遍历了一遍,所以他的时间复杂度为O(n)。set类中的find函数,它是二分查找实现的一个类成员方法。它比泛型算法库中提供的find函数(顺序遍历)的效率高,时间复杂度为O(log2 n).
(4)优点:
- 不允许关键字(数据)重复
- 基于关键字的快速查询(set中提供一个find函数,这个函数是二分查找,比顺序查找效率高)
2、map容器:又称单映射容器,是1-1的映射关系(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值),不允许关键字重复,它的底部也是红黑树,是一个树形结构。
(1)set的结点可以由三部分构成,也可以由四部分构成
- 四部分组成:左孩子域、右孩子域、存储键值对关系的键域,存储键值对关系的值域
- 三部分组成:左孩子域、右孩子域、数据域(保存键值对关系的结构,这个结构叫pair,这个结构体生成的对象叫pair对象,pair对象中保存map对应的映射关系)
(2)操作
- 插入:红黑树不需要提供顶定点插入,只需要insert插入,插入的时候需要排序,map和set都是用关键字key比较
- 删除:erase
(3)优点
- 1-1的映射
- 基于关键字的快速查询
三、容器适配器
(1)stack
(2)queue
(3)priority_queue
这三个容器适配器层都是由deque实现
迭代器:掌握迭代器思想、和失效
把容器的遍历方式封装到一个类,本质是面向对象的指针
由容器提供begin接口和end接口给迭代器赋予对应的位置,begin赋予起始位置,end赋予最后一个有效元素的下一个位置,由begin和end构成元素区间;在迭代器封装++操作,通过++找到下一个元素;在迭代器中还需要封装一个解引用的操作,这个解引用操作是用来获取当前迭代器所迭代的元素。
迭代器失效:删除数据、插入数据等操作会导致位置数据发生变化,操作另外一个迭代器为了防止出错,可以让后面的迭代器失效
vector容器:
1、push_back(尾插):尾部迭代器失效
2、insert:从插入点到尾部所有迭代器都失效
3、pop_back(尾删):尾部迭代器失效
4、erase:从删除点到尾部所有迭代器失效
5、扩容:所有迭代器失效
五中预定义的迭代器:了解
(1)输入型迭代器:把容器里面的数据放到内存,从容器读取元素
(2)输出型迭代器:把内存的数据输出到容器,给容器中写入元素
(3)正向迭代器:既能从容器读取元素,也能给容器中写入元素
(4)双向迭代器:既能++,也能--,比前三个多了个回馈--
(5)随机访问迭代器:集合了前面四个迭代器的作用,还能通过迭代器+偏移进行跳转,底层要求内存连续
上述五种迭代器都有++和解引用的功能
按照功能划分迭代器,分为三类:了解
(1)反转型迭代器:reverse_iterator、const_reverse_iterator,反转型迭代器和普通迭代器的标志相反的,由rbegin()迭代起始位置,这个起始位置是普通迭代器最后一个有效位置的当前位置;通过rend()反转型的末尾,这个末尾是普通迭代器第一个有效元素的前一个位置;由这两个构成迭代器
(2)插入型迭代器
- 前插型迭代器:front_insert_iteratort提供push_front前插这个接口对应的容器通过迭代器的方式做元素的插入
- 后插型迭代器:back_insert_iteratort提供push_back尾插这个接口对应的容器通过迭代器的方式做元素的插入
- insert_iteratort:给所有的容器通过迭代器的方式做元素的插入
(3)流式迭代器
- 输入流迭代器:istream_iterator,和输入缓冲区绑定,通过迭代器迭代输入缓冲区,把输入缓冲区的元素放进容器
- 输出流迭代器:ostream_iterator,和输出缓冲区绑定,通过迭代器迭代输出缓冲区,把输出缓冲区的元素放进容器,打印
- 这两个迭代器必须结合copy这个泛型算法做处理
适配器
相当于转换器
适配器的划分:
1、容器适配器:把容器转换成另外一个容器
2、函数适配器:
- 绑定器:把二元函数对象转成函数一元对象,bind1st绑定第一个参数,bind2nd绑定第二个参数
- 取反器:把函数对象的结果取反,not1给一元函数对象取反,not2给二元函数对象取反
空间配置器
频繁的new和delete会带来效率的问题和出现很多外碎片,为了解决这个问题提出了空间配置器
空间配置器的设置把对象的生成和销毁的四步分开用四个函数实现:allocate、construct、destory、deallocate
对象的生成:
1、开辟空间(用allocate函数开辟)
2、调用构造函数(用construct函数)
对象的销毁:
1、调用析构函数(用destory函数)
2、释放空间(用deallocate函数)
空间配置器分为一级空间配置器和二级空间配置器
- 一级空间配置器是针对于malloc和free函数的封装,也就是allocate是对malloc函数进行了调动,deallocate是对free函数的调动。一次开辟的内存>128字节,认为开辟内存足够大,不容易产生碎片(可以把大的碎片当成小的空间使用),使用一级空间配置器
- 二级空间配置器是以自由链表的形式构成内存池,也就是allocate函数是在内存池中开辟内存,deallocate释放内存也是释放在内存池。一次开辟的内存<=128字节,认为内存开辟的比较小,容易产生内存碎片,通过内存池的方式处理,解决碎片问题,这个时候使用二级空间配置器。