🤖个人主页:晚风相伴-CSDN博客
思维导图链接:STL
持续更新中……
💖如果觉得内容对你有帮助的话,还请给博主一键三连(点赞💜、收藏🧡、关注💚)吧
🙏如果内容有误或者有写的不好的地方的话,还望指出,谢谢!!!
让我们共同进步
目录
1、STL的基本组成部分
2、常见容器的实现原理
3、map的实现原理
4、push_back和emplate_back的区别
5、hashtable的实现原理
6、deque的实现原理
7、list的实现原理
8、各容器的时间复杂度
9、迭代器
9.1迭代器的作用
9.2迭代器和指针的区别
9.3迭代器产生的原因
9.4迭代器失效的问题
10、resize和reserve的区别
10.1capacity和size的区别
10.2resize和reserve的区别
11、map和unordered_map的区别
11.1红黑树的特性
12、map和set的区别
13、哈希表和红黑树的比较
14、数组和链表的区别
15、智能指针
15.0RAII
15.1auto_ptr
15.2unique_ptr
15.4weak_ptr
1、STL的基本组成部分
广义上讲,STL分为三类:算法、容器和迭代器
详细的说,STL由6部分组成:容器、算法、迭代器、仿函数、适配器、空间配置器
- 容器(Container):是一种数据结构,如list、vector和deque等,以模版类的方法提供。
- 算法(Algorithm):是用来操作容器中的数据的模板函数。
- 迭代器(iterator):提供了访问容器中对象的方法。
- 仿函数(Function object):仿函数又被称之为函数对象,其实就是重载了操作符的struct,没有什么特别之处
- 适配器(Adaptor):简单的说就是一个接口类,专门用来修改现有类的接口,提供一种新的接口,或调用现有的函数来实现所需要的功能。主要包括三种适配器Container Adaptor、Iterator Adaptor、Function Adaptor。
- 空间配置器(Allocator):为STL提供空间配置的系统。其中主要工作包括两部分:(1)对象的创建与销毁
(2)内存的获取与释放
2、常见容器的实现原理
1.顺序容器
(1)vector
动态数组。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
(2)deque
双向队列。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在两端增删元素具有较佳的性能。
(3)list
双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取
2.关联式容器
元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现。包含set、multiset、map、multimap,具体实现原理如下:
(1)set/multiset 头文件
set 即集合。set中不允许相同元素,multiset中允许存在相同元素。
(2)map/multimap 头文件
map与set的不同在于map中存放的元素有且仅有两个成员变量,一个名为first,另一个名为second, map根据first值对元素从小到大排序,并可快速地根据first来检索元素。
注意:map同multimap的不同在于是否允许相同first值的元素。
3.容器适配器
(1)stack
栈,后进先出
(2)queue
队列,先进先出
(3)priority_queue
优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列
3、map的实现原理
map是关联式容器,它的底层实现是红黑树。map的所有元素都是pair,同时拥有value(实值)和key(键值)。pair的第一个元素被视为键值,第二个元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
4、push_back和emplate_back的区别
如果要将一个临时变量push到容器的末尾,push_back需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back则直接在容器的末尾构造函数,这样就省去了拷贝的过程。
5、hashtable的实现原理
hashtable采用了函数映射的思想将记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找。这决定了哈希表特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。
6、deque的实现原理
deque内部实现的是一个双向队列。元素在内存连续存放。随机存取任何元素都在常数时间完成。所有适用于vector的操作都适用于deque。在两端增删元素具有较佳的性能。
7、list的实现原理
list内部实现的是一个双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。无成员函数,给定一个下标i,访问第i个元素的内容,只能从头部挨个遍历到第i个元素。
8、各容器的时间复杂度
1.vector:采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:
插入:O(N)
查找:O(1)
删除:O(N)
2.deque:采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:
插入:O(N)
查找:O(1)
删除:O(N)
3.list:采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:
插入:O(1)
查找:O(N)
删除:O(1)
4.map、set、multimap、multiset:这四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:
插入:O(logN)
查找:O(logN)
删除:O(logN)
5.unordered_map/unordered_set/unordered_multimap/unordered_multiset这四种容器采用哈希表实现,不同操作的时间复杂度为:
插入:O(1),最坏:O(N)
查找:O(1),最坏:O(N)
删除:O(1),最坏:O(N)
9、迭代器
9.1迭代器的作用
- 用于指向顺序容器和关联容器中的元素
- 通过迭代器可以读取它指向的元素
- 通过非const迭代器还可以修改其指向的元素
9.2迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,-->、++、--等。
迭代器返回的是对象引用而不是对象的值
9.3迭代器产生的原因
iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
9.4迭代器失效的问题
- 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
- 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
- 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
10、resize和reserve的区别
10.1capacity和size的区别
capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因此此时容器中还没有创建任何对象。
size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。
10.2resize和reserve的区别
- resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert或push_back等创建对象
- resize既修改capacity大小,也修改size大小;reserve只修改capacity大小,不修改size大小。
- 两者的形参个数不一样。resize带两个参数,一个表示容器大小,一个表示初始值;reserve只带一个参数,表示容器预留的大小
11、map和unordered_map的区别
- map实现机理:map内部实现了一个红黑树(红黑树并不是非常严格的二叉搜索树,而AVL是严格的平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。map中的元素是按照二叉树存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
- unordered_map实现机理:内部实现了一个哈希表,通过把关键码映射到hash表中一个位置来访问记录,查找时间复杂度可达O(1)。
11.1红黑树的特性
红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍)
特性:
- 节点是红色或黑色
- 根是黑色
- 叶子节点都是黑色,这里所说的叶子节点是空节点
- 红色节点的子节点都是黑色
- 红色节点的父节点都是黑色
- 从根节点到叶子节点的所有路径上不能有两个连续的红色节点
- 从任意节点到叶子节点的所有路径都包含相同数目的黑色节点
时间复杂度为logN
12、map和set的区别
map和set底层实现都是红黑树,map和set的区别在于map的值不作为键,键和值是分开的
13、哈希表和红黑树的比较
当需要快速查找、删除操作,并且不要求有序性时,哈希表是更好的选择。
当需要有序性、支持范围查找和有序遍历时,或者对空间利用率要求不高时,红黑树是更好的选择。
14、数组和链表的区别
一、存储结构
数组是一种顺序存储结构,它在内存中是连续分配的一块空间
链表是一种链式存储结构,由一系列节点组成,每个节点包含数据域和指针域
二、内存分配
数组的内存分配是静态的,在创建数组时,需要预先指定数组的大小,并且一旦创建,其大小就不能再改变。
链表的大小可以动态地改变,根据需要在运行时添加或删除节点
三、插入和删除数组的插入删除的时间复杂度为O(N)
链表的插入删除的时间复杂度为O(1)
四、查找
数组支持下标随机访问进行查找
链表只能通过遍历的方式进行查找
15、智能指针
15.0RAII
资源获取即初始化,资源(如内存、文件句柄、锁等)在对象创建时自动获取,并且在对象销毁时自动释放。
15.1auto_ptr
auto_ptr就是一个管理权转移问题
两个指针同时指向一块内存,就会两次释放同一块资源,自然报错
15.2unique_ptr
unique_ptr规定一个智能指针独占一块内存资源,当两个智能指针同时指向一块内存,编译报错。
实现原理:
将拷贝构造和赋值函数声明为private或delete。不允许拷贝构造和赋值,但是支持移动构造函数,通过move把一个对象指针变成右值之后可以移动给另一个unique_ptr
15.3shared_ptr
shared_ptr可以实现多个智能指针指向相同对象,该对象和其相关资源会在引用为0时被销毁释放。
实现原理
内部有个引用计数,使用拷贝构造和赋值时,引用计数加1,当引用计数为0时,释放资源。
循环引用问题
- 循环引用会造成引用计数失效,从而导致内存泄漏问题。
- 使用weak_ptr来解决循环引用问题,weak_ptr不会使引用计数加1
线程安全性问题
在多线程的环境下,如果多个线程同时读一个shared_ptr对象是线程安全的,但是如果是多个线程对同一个shared_ptr对象进行读和写,则是线程不安全的,则需要加锁保护。
内存泄漏问题
当两个对象同时使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效从而导致内存泄漏。
15.4weak_ptr
使用weak_ptr不能知道对象计数是否为0,因为它的构造和析构不会引起计数的增加和减少