赞
踩
相关系列文章
深入理解STL空间分配器(一): new_allocator
深入理解STL空间分配器(三):pool_allocator
深入理解STL空间分配器(四):bitmap_allocator
目录
2.3.vector为什么要用加倍扩容而不是每次增加一个固定的扩容容量
2.6.size、resize、reserve、capacity的区别
2.10.频繁对vector调用push_back()对性能的影响和原因
3.6.vector和list中,如果删除末尾的元素,其指针和迭代器如何变化?若删除的是中间的元素呢?
3.7.总结一下vector和list具体是怎么实现的呢?常见操作的时间复杂度是多少?
8.1.你了解map和unordered_map嘛?底层实现呢
8.4.为什么map和set和插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效
8.5.为什么map和set不能像vector一样有个reserve函数来预分配数据
8.6.你知道map和set有什么区别嘛?分别是怎么实现的呢
8.7.vector越界访问下标,map越界访问下标,vector删除元素时会不会释放空间
8.9.hash_map与map的区别?什么时候用hash_map,什么时候用map
动态开辟的二维数组。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了 封装,比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的 空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器, 程序可能会崩溃)。
- #include <iostream>
- using namespace std;
- #include <vector>
- int main()
- {
- int a[] = { 1, 2, 3, 4 };
- vector<int> v(a, a + sizeof(a) / sizeof(int));
- // 使用find查找3所在位置的iterator
- vector<int>::iterator pos = find(v.begin(), v.end(), 3);
- // 删除pos位置的数据,导致pos迭代器失效。
- v.erase(pos);
- cout << *pos << endl; // 此处会导致非法访问
- return 0;
- }
erase删除pos位置元素后,pos位置之后的元素会往前移动,没有导致底层空间的改变,理论上讲迭代器应该不会失效,但是如果pos刚好是最后一个元素,删完之后pos刚好是end位置,而end位置是没有元素的,那么pos就失效了。所以删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。
emplace_back() 和 push_back() 的主要区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。而且,向一个vector添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移到新的空间。
- list.push_back(elem) 在尾部加入一个数据
- list.pop_back() 删除尾部数据
- list.push_front(elem) 在头部插入一个数据
- list.pop_front() 删除头部数据
- list.size() 返回容器中实际数据的个数
- list.sort() 排序,默认由小到大
- list.unique() 移除数值相同的连续元素
- list.back() 取尾部迭代器
- list.erase(iterator) 删除一个元素,参数是迭代器,返回的是删除迭代器的下一个位置
1) vector的优点
vector的底层是一段连续的物理空间,所以支持随机访问。
跟数组类似,我们能够很轻易的找到最后一个元素,并完成各种操作。
因为系统在底层拿空间的时候,是拿一段进cpu,不是只拿单独一个,会提前往后多拿一点,vector的物理地址是连续的,所以我们再拿到数据的时候,cpu访问后面的数据会更快。
2)vector的缺点
如果我们要在前面或中间插入或者删除数据,我们不能直接删,我们需要挪动数据,去覆盖或者增加一段空间,这样我们挪动数据的效率就是O(N)。
正常情况下我们vector的扩容机制是一旦达到当前空间的capacity(容量)那么我们扩容原空间的1.5倍或者2倍数(vs一般是1.5倍而g++是2倍),这样扩容就有可能导致空间浪费,而且频繁扩容也会影响效率。
3) list的优点
list是一个带头双向循环链表,那么链表就是一个个独立的空间链接起的,需要多少,就new多少,不存在空间浪费。
因为list是双向循环链表,我们需要插入新的元素只需要改变原数据的next和prev,所以我们的插入删除效率是O(1)。
4)list的缺点
因为list是链表,在存放数据的物理地址并不是连续的,所以我们也就不能支持随机访问。
主要还是跟它的物理地址不连续有关,CPU提前存的一段数据,可能跟下一个数据完全没有联系,因为它们空间不连续,所以就命中率低。
动态开辟的二维数组空间,第二维固定长度的数组空间,扩容的时候(第一维的数组进行2倍扩容)。
deque内部实现的是一个双向队列。元素在内存连续存放。随机存取任何元素都在常数时间完成(仅次于vector)。所有适用于vector的操作都适用于deque。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
deque为了维持整体连续的假象,设计一个中控器,其用来记录deque内部每一段连续空间的地址。大体上可以理解为deque中的每一段连续空间分布在内存的不连续空间上,然后用一个所谓的map作为主控,记录每一段内存空间的入口,从而做到整体连续的假象。
- static size_t buffer_size(){
- return __deque_buf_size(BufSiz, sizeof(T));
- }
- //如果n不为0,传回n,表示buffer size 由自己定义
- 如果n为0,表示buffer_size 采用默认值
- 如果sz(元素大小) < 512,传回512/sz,如果不小于512 ,传回1
- inline size_t __deque_buf_size(size_t n, size_t sz)
- {
- return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1));
- }
3. set_node函数
当迭代器处在当前缓冲区的边缘时,一旦前进或者后退,就要考虑超过当前缓冲区的情况,此时需要跳转到下一个缓冲区,这时候set_node就派上用场了。
- void set_node(map_pointer new_node)
- {
- node = new_node; // 跳转到相应缓冲区
- first = *new_node; // 更新跳转后缓冲区first信息
- last = first + difference_type(buffer_size()); // 更新跳转后缓冲区last的信息
- }
deque维护着一个map,用来记录每个缓冲区的位置。除了map外,deque的数据结构还维护着start和finish两个迭代器,分别指向deque的首尾。此外,他还必须知道map的大小,一旦map提供的节点不足,就需要配置一块更大的map。
- deque.push_back(elem)在尾部加入一个数据。
- deque.pop_back()删除尾部数据。
- deque.push_front(elem)在头部插入一个数据。
- deque.pop_front()删除头部数据。
- deque.size() 返回容器中实际数据的个数。
- deque.at(idx)传回索引idx所指的数据,如果idx越界,抛出out_of_range。
也即:
底层数据结构一般以vector为底层容器,heap为处理规则来管理底层容器实现。
优先队列(priority_queue)容器与队列一样,只能从队尾插入元素,从队首删除元素。但是它有一个特性,队列中最大的元素总是位于队首。出队时,并非按照先进先出的原则进行,而是将当前队列中最大的元素出队。这点类似于给队列里的元素进行了由大到小的顺序排序。元素的比较规则默认按元素值由大到小排序,可以重载“<”操作符来重新定义比较规则。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。
容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库函数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。
因为存储的是节点,不需要内存拷贝和内存移动。
插入操作只是节点指针换来换去,节点内存没有改变,而iterator就像指向节点的指针,内存没变,指向内存de指针也不会变。
因为在map和set内部存储的已经不是元素本身了,而是包含元素的节点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。
综上所述,map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分开的。
erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针;clear()函数,只能清空内容,不能改变容量大小;如果要想在删除内容的同时释放内存,那么你可以选择deque容器。
STL内存管理使用二级内存配置器。
1) 第一级配置器:
2)第二级配置器
第一级配置器直接调用malloc和free带来了几个问题:
如果分配的区块小于128bytes,则以内存池管理,第二级配置器维护了一个自由链表数组,每次需要分配内存时,直接从相应的链表上取出一个内存节点就完成工作,效率很高。
知识点看完了,最后还是希望大家都能拿到自己“心动的offer”!
参考书籍:<<STL源码剖析>>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。