C++面试(3)
21、请你说说迭代器失效原因,有哪些情况
标准回答 STL 中某些容器调用了某些成员方法后会导致迭代器失效。例如 vector 容器,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能已经被复制或移到了新的内存地址。 1. 序列式容器迭代器失效 对于序列式容器,例如 vector、deque,由于序列式容器是组合式容器,当当前元素的迭代器被删除后,其后的所有元素的迭代器都会失效,这是因为 vector、deque都是连续存储的一段空间,所以当对其进行 erase 操作时,其后的每一个元素都会向前移一个位置。解决:erase 返回下一个有效的迭代器。 2. 关联式容器迭代器失效 对于关联容器,例如如 map、 set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在 erase 时,递增当前迭代器即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个节点不会对其他点造成影响。erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 自增方式删除迭代器。
22、请你说说 vector 的扩容机制,扩容以后,它的内存地址会变化吗?
得分点 申请空间、拷贝数据、释放旧空间 标准回答 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步: 1. 完全弃用现有的内存空间,重新申请更大的内存空间; 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 3. 最后将旧的内存空间释放。 因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。vector 扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
23、请你说说 deque 的实现原理
得分点
分段连续内存、中控器
标准回答
deque 是由一段一段的定量的连续空间构成。一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
既然 deque 是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。deque 采取一块所谓的 map(不是 STL 的 map 容器)作为主控,这里所谓的 map 是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是 deque的存储空间的主体。
24、请你说说 set 的实现原理
得分点 红黑树、排序 标准回答 set 底层使用红黑树实现,一种高效的平衡检索二叉树。 set 容器中每一个元素就是二叉树的每一个节点,对于 set 容器的插入删除操作,效率都比较高,原因是二叉树的删除插入元素并不需要进行内存拷贝和内存移动,只是改变了指针的指向。 对 set 进行插入删除操作 都不会引起迭代器的失效,因为迭代器相当于一个指针指向每一个二叉树的节点,对 set的插入删除并不会改变原有内存中节点的改变。 set 中的元素都是唯一的,而且默认情况下会对元素进行升序排列。不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。
25、请你说说 map 实现原理,各操作的时间复杂度是多少
得分点 红黑树 标准回答 1. map 实现原理 map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而 AV L是严格平衡二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值,使用中序遍历可将键值按照从小到大遍历出来。 2. 各操作的时间复杂度 插入: O(logN) 查看: O(logN) 删除: O(logN)
26、请你说说红黑树的特性,为什么要有红黑树
标准回答 虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于
1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点:
1、具有二叉查找树的特点;
2、根节点是黑色的;
3、每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据;
4、任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
5、每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点。
27、请你说说 unordered_map 实现原理
得分点
哈希表、链地址法
标准回答unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)。整个存储结构如下图(其中,Pi 表示存储的各个键值对):
可以看到,当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。
不仅如此,在 C++ STL 标准库中,将图中的各个链表称为桶(bucket),每个桶都有自己的编号(从 0 开始)。当有新键值对存储到无序容器中时,整个存储过程分为如下几步:
1. 将该键值对中键的值带入设计好的哈希函数,会得到一个哈希值(一个整数,用 H 表示);
2. 将 H 和无序容器拥有桶的数量 n 做整除运算(即 H % n),该结果即表示应将此键值对存储到的桶的编号;
3. 建立一个新节点存储此键值对,同时将该节点链接到相应编号的桶上。
另外,哈希表存储结构还有一个重要的属性,称为负载因子(load factor)。该属性同样适用于无序容器,用于衡量容器存储键值对的空/满程序,即负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率;反之,负载因子越小,容器肯定越空,但并不一定各个链表中挂载的键值对就越少。如果设计的哈希函数不合理,使得各个键值对的键带入该函数得到的哈希值始终相同(所有键值对始终存储在同一链表上)。这种情况下,即便增加桶数是的负载因子减小,该容器的查找效率依旧很差。无序容器中,负载因子的计算方法为:负载因子 = 容器存储的总键值对 / 桶数
默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。这也就解释了,为什么我们在操作无序容器过程中,键值对的存储顺序有时会“莫名”的发生变动。
28、请你说说导致哈希冲突的原因和影响因素,哈希冲突的解决方法
标准回答
1. 哈希冲突产生的原因 哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值,这时候就产生了哈希冲突。
2. 产生哈希冲突的影响因素 装填因子(装填因子=数据总数 / 哈希表长)、哈希函数、处理冲突的方法
3. 哈希冲突的解决方法
a.开放地址方法
b.链式地址法
c.建立公共溢出区
d.再哈希法
29、请你说说 map,unordered_map 的区别
得分点 头文件、原理 标准回答 map、unordered_map 是 C++ STL 中的两个容器,它们的区别有: 1. 导入的头文件 map:#include \<map> unordered_map:#include \<unordered_map> 2. 原理及特点 map:内部实现了一个红黑树,该结构具有自动排序的功能,因此 map 内部的所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素,因此,对于 map 进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作,故红黑树的效率决定了 map 的效率。 unordered_map:内部实现了一个哈希表,因此其元素的排列顺序是杂乱的,无序的。</unordered_map></map>
30、说说 C++ 中智能指针和指针的
标准回答 1. 智能指针 如果在程序中使用 new 从堆(自由存储区)分配内存,等到不需要时,应使用 delete 将其释放。C++ 引入了智能指针 auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL)表明,需要有更精致的机制。基于程序员的编程体验和 BOOST 库提供的解决方案,C++11 摒弃了 auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr 和 weak_ptr。所有新增的智能指针都能与 STL 容器和移动语义协同工作。 2. 指针 C 语言规定所有变量在使用前必须先定义,指定其类型,并按此分配内存单元。指针变量不同于整型变量和其他类型的变量,它是专门用来存放地址的,所以必须将它定义为“指针类型”。 3. 智能指针和普通指针的区别 智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。指针是一种数据类型,用于保存内存地址,而智能指针是类模板。
更多推荐





所有评论(0)