C++常见面试题总结

STL容器底层数据结构

  • vector,底层数据结构是动态数组。由于数组的特点,vector也具有以下特性:
    O(1)时间的快速访问;
    顺序存储,插入到非尾结点所需时间复杂度为O(n),删除也一样;
    扩容规则: 见下一小节。

  • list,底层数据结构为双向链表,特点是支持快速的增删,时间复杂度O(1);不能随机访问,需要进行遍历,时间复杂度为O(n)。

  • map 与 multimap,STL中的关联容器、提供一对一key-value的数据处理能力; map与multimap的区别在于,multimap允许关键字重复,而map不允许重复。map与multimap实现了以key为序排列,即它们有序。底层数据结构均为红黑树。根据红黑树的原理,map与multimap可以实现O(lgn)的查找,插入和删除。

  • unordered_map 与 unordered_multimap,容器中key为无序排列,其底层实现为hash table,因此其查找时间复杂度理论上达到了O(1)。

  • set 与 multiset,有序存储元素,这两种容器的底层实现与map一样都是红黑树,所以能实现O(lgn)的查找,插入,删除操作。set与multiset的区别在于是否允许重复。

  • unordered_set 与 unordered_multiset,与unordered_map & unordered_multimap相同,其底层实现为hash table。

  • priority_queue,优先级队列相当于一个有权值的单向队列queue,在这个队列中,所有元素是按照优先级排列的。根据堆的特性,优先级队列实现了取出最大最小元素时间复杂度为O(1),对于插入和删除,其最坏情况为O(lgn)。

  • stack,栈,为后入先出原则。

  • queue,单向队列,为先入先出原则。

  • deque,双向队列,可以实现在头尾两端高效的插入和删除操作。

vector 与 list

vector

vector(底层是动态顺序表/动态数组)和数组类似,拥有一段连续的内存空间,并且起始地址不变,不容易造成内存碎片,空间利用率高,缓存利用率高。因此能高效的进行随机访问,时间复杂度为O(1)。但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。

vector扩容

当我们新建一个vector的时候,会首先分配给他一片连续的内存空间,如std::vector vec,当通过push_back向其中增加元素时,如果初始分配空间已满,就会引起vector扩容,其扩容规则在GCC是二倍扩容(VS13是1.5倍扩容):首先重新申请一个2倍大的内存空间;然后将原空间的内容拷贝过来;最后将原空间内容进行释放,将内存交还给操作系统。并且,由于vector 空间的重新配置,导致旧vector的所有迭代器都失效了。

vector的初始的扩容方式代价太大,初始扩容效率低,需要频繁增长,不仅操作效率比较低,而且频繁的向操作系统申请内存容易造成过多的内存碎片,所以这个时候需要合理使用resize()和reserve()方法提高效率减少内存碎片的,需要resize()和 reserve()。

(1)2倍扩容

假设我们一开始申请了 16Byte 的空间。当需要更多空间的时候,将首先申请 32Byte,然后释放掉之前的 16Byte。这释放掉的16Byte 的空间就闲置在了内存中。当还需要更多空间的时候,你将首先申请 64Byte,然后释放掉之前的 32Byte。这将在内存中留下一个48Byte 的闲置空间(假定之前的 16Byte 和此时释放的32Byte 合并)。当还需要更多空间的时候,你将首先申请128Byte,然后释放掉之前的 64 Byte。这将在内存中留下一个112Byte 的闲置空间(假定所有之前释放的空间都合并成了一个块)。扩容因子为2时,上述例子表明:每次扩容,我们释放掉的内存连接起来的大小,都小于即将要分配的内存大小。

(2)1.5倍扩容

假设我们一开始申请了 16Byte 的空间。当需要更多空间的时候,将申请 24 Byte ,然后释放掉 16 ,在内存中留下 16Byte 的空闲空间。当需要更多空间的时候,将申请 36 Byte,然后释放掉 24,在内存中留下 40Byte (16 + 24)的空闲空间。当需要更多空间的时候,将申请 54 Byte,然后释放 36,在内存中留下 76Byte。当需要更多空间的时候,将申请 81 Byte,然后释放 54, 在内存中留下 130Byte。当需要更多空间的时候,将申请 122 Byte 的空间(可以复用内存中闲置的 130Byte)。

list

list是由双向链表实现的,因此内存空间是不连续的,容易造成内存碎片,空间利用率低,缓存利用率低。只能通过指针访问数据,所以list的访问效率较低,时间复杂度为O(n)。但由于链表的特点,能高效地进行插入和删除。

vector和list的区别

  • vector底层是连续结构;list底层是非连续结构。
  • vector支持随机访问;list不支持随机访问。
  • vector迭代器是原生指针;list迭代器是封装结点的一个类。
  • vector在插入和删除时可能会导致迭代器失效;list在删除的时候会导致当前迭代器指向的结点失效。
  • vector不容易造成内存碎片,空间利用率高;list容易造成内存碎片,空间利用率低。
  • vector在非尾插,删除的时间复杂度为O(n),list在任何地方插入和删除的时间复杂度都为O(1)。
  • vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等操作符。

vector::iterator和list::iterator都重载了“++”运算符。

迭代器失效问题

vector在插入元素时的时候,要重新给所有的迭代器赋值。因为插入元素有可能导致扩容,所有迭代器指针和引用都会失效。删除元素位置之后的所有迭代器和指针引用都会失效,需要重新赋值。
list在插入元素的时候不会导致迭代器失效,删除元素的时候只会导致当前迭代器失效,其他的迭代器不会受到影响。
使用场景:如果需要高效的随机访问,而不在乎插入和删除的效率,使用vector。如果需要大量的插入和删除,而不关心随机访问,则应使用list。

红黑树

我这里只简单进行介绍,更详细的可以参考这篇博客: https://www.cnblogs.com/skywang12345/p/3245399.html

红黑树(red-black tree) 是一棵满足下述性质的平衡二叉查找树:

  • 每一个结点要么是红色,要么是黑色。
  • 根结点是黑色的。
  • 所有叶子结点都是黑色的(实际上都是Null指针)。叶子结点不包含任何关键字信息,所有查询关键字都在非叶结点上。
  • 每个红色结点的两个子节点必须是黑色的。换句话说:从每个叶子到根的所有路径上不能有两个连续的红色结点。
  • 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。

红黑树的操作

左旋、右旋和颜色变更。 因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的查找操作与普通二叉查找树上的查找操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次。

红黑树的优势

红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,红黑树用非严格平衡来换取增删节点时旋转次数的降低,任何不平衡都会在3次旋转之内解决。而AVL是严格平衡树(高度平衡的二叉搜索树),因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。

set、map底层为什么用红黑树(RB-Tree)而不用AVL树?

RB-Tree和AVL树作为平衡二叉搜索树(BBST, Balance Binary Search Tree) 其实现的算法时间复杂度相同,AVL作为最先提出的BBST,貌似RB-tree实现的功能都可以用AVL树是代替,那么为什么还需要引入RB-Tree呢?

红黑树不追求"完全平衡",即不像AVL那样要求节点的 |balFact| <= 1,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。

  • 插入节点导致树失衡,AVL和RB-Tree都是最多两次树旋转来实现复衡rebalance,旋转的量级是O(1)。
  • 删除节点导致失衡,AVL需要维护从被删除节点到根节点这条路径上所有节点的平衡,旋转的量级为O(logN);而RB-Tree最多只需要旋转3次实现复衡,只需O(1)。所以说RB-Tree删除节点的rebalance的效率更高,开销更小。
  • AVL的结构相较于RB-Tree更为平衡,插入和删除引起失衡,如2所述,RB-Tree复衡效率更高;由于AVL高度平衡,其查找效率更高。

故引入RB-Tree是功能、性能、空间开销的折中结果。AVL更平衡,结构上更加直观,时间效能针对读取而言更高;维护稍慢,空间开销较大。红黑树查询略逊于AVL(因其比AVL树不平衡最多一层),维护(插入和删除)强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。

实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL。如果搜索,插入删除次数几乎差不多,应该选择RB。

为什么map用红黑树而不用哈希表? 为什么unordered_map用哈希表而不用红黑树?

  • 有序性和无序性
    map的有序性:红黑树(非严格平衡二叉树),该结构具有自动排序的功能,因此map内部的所有元素都是有序的。
    unordered_map的无序性:哈希表不会根据key值大小进行排序,存储时是根据key的hash值判断元素是否相同,因此unordered_map内部元素是无序的。
  • 运行效率
    map的运行效率:红黑树可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。
    unordered_map的运行效率:哈希表的查找的时间复杂度可达到O(1)。
  • 占用内存
    unordered_map内存占用比map高。hash表占用空间高,插入查找速度都相对快(hash如果及时扩充,平均算法复杂度是O(1),O(n)之前重新hash了);红黑树速度相对慢,空间占用低。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值