STL一些容器的底层特点及实现。
序列式容器:
1.vector:
数据结构:
数据成员只有三个:1.iterator start;//指向目前正在使用的空间头 2.iterator end;//指向目前正在使用的空间尾 3.iterator end_of_storage;//指向可用空间尾
迭代器:
vector的迭代器属于随机访问型迭代器,迭代器类型就是原始指针,即,当value_type为T时,迭代器类型就为T*。
内存管理:
使用前文所说的alloc为默认空间配置器(alloc根据宏来决定自己是第一级配置器还是第二级配置器)。
当可用空间已满时:
此时若往中push_back或者insert,将会溢出。每次push或插入前,都会先判断finish是否达到end_of_storage.若未达到则插入,否则会调用insert_aux对整个内存空间进行扩展并重新移动。
若无可用空间时,首先判断当前大小,若为0,则需重新配置一个为1大小的空间。若不为0,则配置一个原两倍大小的空间。调用allocate配置2n个空间,首先将原来的n个移动到新空间,然后释放掉原空间的vector,并调整start,end与end_of_storage三个迭代器成员变量。
注意,由于insert_aux整体迁移了空间,原vector的迭代器全部失效。erase也是如此,因为erase中会调用destory掉iterator,之后这个iterator会成为野指针,任何针对它的操作都是非法的。因此必须通过erase的返回值来重新获取指向position位置的迭代器。
vector迭代器的失效情况:
1.当插入(push_back)一个元素后,end操作返回的迭代器肯定失效。
2.当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时begin和end操作返回的迭代器都会失效。
3.当进行删除操作(erase,pop_back)后,指向删除点的迭代器全部失效;指向删除点后面的元素的迭代器也将全部失效。
2.list
STL中,list不仅仅是一个双向链表,更是一个环状双向链表。
数据成员:
数据成员只有一个指针,该指针指向整个链表的尾端的空白节点,返回end()时即返回此节点,又由于是环状列表,因此begin()返回此节点的next指针所指向的节点。
迭代器:
list的迭代器属于双向迭代器。迭代器内部维护一个指针,该指针指向list的节点。
内存管理:
依然使用alloc作为空间配置器。list的空间管理简单很多,创建一个节点,然后直接在插入位置插入即可,不用考虑vector那样整体搬移的问题。
且由于插入只是指针的更改操作,不会造成insert或push后因内存搬移而造成的迭代器失效问题。但erase某个迭代器后,该迭代器成为野指针,是不可使用的了。
由于STL的sort只支持随机访问迭代器,因此list的sort算法需要封装到自己的数据结构中。
3.deque
vector使用的是纯连续空间,list使用的是完全不连续的空间。而deque使用的是连续空间+指针链接的形式。
首先用一段连续空间(称为map,但这个map不是RBTree那个map),map中每个元素为一个指针,指向另一端连续空间,称为缓冲区。缓冲区才是存储deque中元素的主体。
总体来说,map等价于一个T**,map[i]为一个T*,map[i][j]才是真正存储元素的位置。
数据成员:
一个指针map(T**),该map指向map主控区;一个map_size,表示map指向的主控区中能容纳多少个缓冲区指针,以及一个start指向第一缓冲区的第一个元素和一个finish迭代器指向最后一个缓冲区的最后一个元素的下一个位置。
迭代器:
deque的迭代器属于随机访问迭代器。迭代器维护4个成员变量:一个T*型cur,指向现行(current)元素;T*型first,指向cur元素所在缓冲区的头;T*型last,指向cur元素所在缓冲区的尾;T**型node,指向该缓冲区在主控区map中所对应的位置。
虽然deque的迭代器属于随机访问迭代器,但本质上并不是连续空间,因此在某些地方出现断层时需要跳转。
如在某个缓冲区的边界,此时++,则调用set_node()跳一个缓冲区,set_node重新设置迭代器的node变量,并根据新的node重新设置first和last,然后设置cur=first。
内存管理:
最开始配置内存时,需要的节点个数为(元素个数/每个缓冲区可容纳元素个数)+1个节点。如果所需要配置的节点数不足8个,则会配置8个,超过8个时,配置所需节点+2个节点(这两个防范在头插入与在尾插入,预留)。也就是max(8,num_nodes+2);deque中的start指向map中第二个节点(第一个是预留给头部插入的),finish指向倒数第二个节点(最后一个预留给尾部插入)。
在尾部插入时,如果尾部缓冲区只剩一个元素备用空间了,即finish(deque维护的指向最后一个元素所在缓冲区的迭代器).cur==finish.last-1,此时等价于只剩下finish.last了,而last一般是不放元素只用来表示开区间[,)中的)的,因此需要增加缓冲区。如果finish所在的位置已经是map尾了,则需要重新配置一块map,并将数据迁移后再插入。
在头部插入时同理。
deque迭代器失效问题:
1.在deque容器首部或者尾部插入元素不会使得任何迭代器失效。
2.在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
3.在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效。
4.RBTree:
首先介绍下二叉搜索树,AVL树,RB树。
二叉搜索树:小在左,大在右,这样树的最大层数就是搜索的时间复杂度。
AVL树:二叉搜索树中,由于树的结构是根据根节点确定的,如果根节点为最小值或最大值,则树只会在根节点的单侧构造,最坏的情况为O(n)。因此引入二叉搜索树,每次插入或删除后都对树结构调整,根节点不是定死的,根节点会发生变化。AVL树的特点是任意节点的左子树与右子树高度差的绝对值不能超过1。这样树的结构是整体平衡的,复杂度为logN。
红黑树:也是为了解决二叉搜索树的问题。每次插入删除都会对树进行调整。
红黑树的特点:
1.每个节点不是黑色就是红色
2.根节点黑色
3.如果节点为红,子节点一定为黑
4.任意节点走任意路径到它的一个叶子节点,路径上的黑色节点数相同。
红黑树的特点保证了,最短路径是连续黑色的,最长路径是红黑相间的。
对红黑树的插入,插入节点必须是红色节点,当插入点的父节点是黑节点时,必须调整红黑树。
红黑树的调整:
调整主要看三个点:
1.插入位置为外侧还是内侧。
2.插入节点的父节点的兄弟节点是红色还是黑色。
3.插入节点的曾祖父(父的父的父节点)是红色还是黑色。
情况1).伯父节点为黑色,且是外侧插入:此时只需要对父节点做一次单旋转。单旋转后父节点来到原来的祖父节点位置,且为红色;原来的祖父节点来到原来伯父节点的位置,且为黑色。目前颜色失衡,需要将原父节点颜色换成黑,原祖父换成红色。
情况2).伯父节点为黑色,且是内侧插入:对于内侧插入的情况,首先以插入节点为基点做一次外旋,外旋后插节点取代原来父节点位置。这种情况下等价于父节点成为插入节点,且是外侧插入。则按照情况1)处理即可。
情况3).伯父节点为红色:这种情况需要考虑曾祖父节点,因为处理完后的祖父节点会变成红色,而如果曾祖父节点仍为红色的话还是违背RBTree的特点,因此此时:首先对插入节点的父节点及伯父节点均变色为黑色节点,祖父节点变为红色节点。此时再看曾祖父节点,如果曾祖父为黑色,则完成调整。若曾祖父为红色,则此时问题出现在祖父节点为红色,曾祖父也为红色,则可以将祖父节点看成一个插入节点,然后向上处理,如果祖父节点的伯父节点为黑色,则根据祖父节点的位置按照1)或2)处理;若祖父节点的伯父节点为红色,则按情况3)继续往上变色。
RBTree的节点设计:
标识节点颜色的变量color;三个指针:parent指向父节点,left与right分别指向左右孩子节点,以及一个标识节点值的变量。
数据结构:
1.一个记录树大小的变量node_count,记录节点个数。
2.一个RBTree node*型的指针head。head的父节点为RBTree的根节点root,而root的根节点指向header。
3.一个用于比较节点大小的仿函数key_compare。
RBTree的迭代器:
属于双向迭代器。
迭代器的++:调用increment().
如果有右节点,则走到右节点,然后一直从左子树一直走到头,这个结点就是刚好比++前的迭代器指向的节点大一点的节点。
如果没有右节点,则找它的父节点,如果当前节点是父节点的右节点,则继续上溯其父节点直到为其父节点的左节点(因为右节点的父节点一定是小于右节点的,只有当上溯到的节点时左节点时,以这个节点为根的子树相当于它父节点的左子树,这个父节点才是刚好大于++前节点的节点)
迭代器的--:调用decrement().
如果当前节点是红节点且父节点的父节点等于自己(说明node为header,而header实际上是end()返回的node),此时--应该是最后排序中的最后一个元素。而header的左节点指向最小元素,右节点指向最大元素,此时返回header->right即可。
如果不是上述情况,且有左子节点,则从左子节点开始,向右遍历到底,即为刚好比--前的节点小一点的节点。
如果既非根节点,也没有左子节点,则上溯到某个节点,直到该节点不是该节点父节点的左节点,此时,这个节点的父节点就是刚好比--前的节点小一点的节点。
5.set
set与map的区别在于,set没有key-value,key值即实值。
数据结构:
底层以RBTree为实现,数据成员为一个红黑树。
迭代器:
与红黑树树一样,双向迭代器。
且set与list一样,由于插入删除都只是指针操作,迭代器失效只会使被删除的迭代器失效,不会影响其他迭代器。
6.map
map与set一样,使用RBTree为底层实现。
双向迭代器,迭代器失效只存在于被删除的迭代器。
7.hashtable
妈个比要面试来不及写了。
hashtable以开链法完成实现。
分配一组连续地址空间,称为bucets.每个bucket中维护一系列的节点。
节点node的设计为:一个表示值的变量val;一个指向下一个节点node的指针next。
迭代器:
hashtable的迭代器没有后退操作--。属于前进迭代器。
迭代器维护两个成员:指向当前节点的指针cur,以及一个指向buckets的指针ht.
在前进时,通过next跳转,如果next达到尾部,则通过ht跳转到下一个bucket.
8.hashset
hashset以hashtable为底层应用。相比于set,hashset没有自动排序功能。
9.hashmap
hashmap以hashtable为底层应用。相比于map,hashmap没有自动排序,相比于hashset,hashmap用key值来插入与查找。