更多STL面试题参见:C++面试题系列:STL
1.常见面试题
1.1 STL中map的数据存放形式
请你说说 map 实现原理,各操作的时间复杂度是多少
得分点 红黑树
标准回答
- map 实现原理
map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而 AV L是严格平衡二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。
因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。
map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值,使用中序遍历可将键值按照从小到大遍历出来。
- 各操作的时间复杂度 插入: O(logN) 查看: O(logN) 删除: O(logN)
1.2 请你说说 unordered_map 实现原理
unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)。整个存储结构如下图(其中,Pi 表示存储的各个键值对):
可以看到,当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。
不仅如此,在 C++ STL 标准库中,将图中的各个链表称为桶(bucket),每个桶都有自己的编号(从 0 开始)。当有新键值对存储到无序容器中时,整个存储过程分为如下几步:
- 将该键值对中键的值带入设计好的哈希函数,会得到一个哈希值(一个整数,用 H 表示);
- 将 H 和无序容器拥有桶的数量 n 做整除运算(即 H % n),该结果即表示应将此键值对存储到的桶的编号;
- 建立一个新节点存储此键值对,同时将该节点链接到相应编号的桶上。
另外,哈希表存储结构还有一个重要的属性,称为负载因子(load factor)。该属性同样适用于无序容器,用于衡量容器存储键值对的空/满程度,即负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率;反之,负载因子越小,容器肯定越空,但并不一定各个链表中挂载的键值对就越少。如果设计的哈希函数不合理,使得各个键值对的键带入该函数得到的哈希值始终相同(所有键值对始终存储在同一链表上)。这种情况下,即便增加桶数是的负载因子减小,该容器的查找效率依旧很差。无序容器中,负载因子的计算方法为:负载因子 = 容器存储的总键值对 / 桶数
默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。这也就解释了,为什么我们在操作无序容器过程中,键值对的存储顺序有时会“莫名”的发生变动。
1.2 map与unordered_map
- map
优点:
有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
红黑树,内部实现一个红黑书使得map的很多操作在的时间复杂度O(nlogn)下就可以实现,因此效率非常的高
缺点:
空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点,孩子节点以及红/黑性质,使得每一个节点都占用大量的空间
适用处,对于那些有顺序要求的问题,用map会更高效一些
- unordered_map
优点:
因为内部实现了哈希表,因此其查找速度非常的快
缺点:
哈希表的建立比较耗费时间
适用处,对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
头文件
map:
#include <map>
unordered_map:
#include <unordered_map>
1.3 STL中Map和Multimap
1、Map
映射,map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
底层实现:红黑树
适用场景:有序键值对不重复映射
2、Multimap
多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。
底层实现:红黑树
适用场景:有序键值对可重复映射
1.4 STL中的map和set是怎么是实现的
1.Set,
所有元素都会根据元素的值自动被排序,且不允许重复。
底层实现:红黑树
set 底层是通过红黑树(RB-tree)来实现的,由于红黑树是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的 STL 的 set 即以 RB-Tree 为底层机制。又由于 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 set 操作行为,都只有转调用 RB-tree 的操作行为而已。
适用场景:有序不重复集合
2、map
映射。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
底层实现:红黑树
适用场景:有序键值对不重复映射
1.5 请你来说一下map和set有什么区别,分别又是怎么实现的?
map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。
map和set区别在于:
-
(1)map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
-
(2)set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
-
(3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。
1.6 请你说一说STL迭代器是怎么删除元素的呢
这个主要考察的是迭代器失效的问题。
- 1.序列容器 :vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
- 2.关联容器:map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
- 3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
1.7 面试题(28)|STL(2):vector、map等迭代器失效场景
面试题(28)|STL(2):vector、map等迭代器失效场景
2.用法小结
- 2.1 map的大小
- 2.2 数据的清空与判空
- 2.3 输出map的值
- 2.4 Map删除元素的三种方式
- 2.5 Map查找是否存在某个key
- 2.6 Map四种插入方法
2.1 map的大小
int msize=mmap.size()
2.2 数据的清空与判空
清空map中的数据可以用clear()函数,判定map中是否有数据可以用empty()函数,它返回true则说明是空map
2.3 输出map的值
for(auto it=mmap.begin();it!=mmap.end();it++)
{
cout<<it->first<<" "<<it->second<<endl;
}
map<int,string>::iterator iter;
for(iter=mmap.begin();iter!=mmap.end();iter++)
{
cout<<iter->first<<" "<<iter->second<<endl;
}
2.4 Map删除元素的三种方式
- 删除键为bfff指向的元素
cmap.erase("bfff");
- 删除迭代器 key所指向的元素
map<string,int>::iterator key = cmap.find("mykey");
if(key!=cmap.end())
{
cmap.erase(key);
}
- 删除所有元素
cmap.erase(cmap.begin(),cmap.end());
2.5 Map查找是否存在某个key
- N.find(key):返回迭代器,判断是否存在。
1 iter = m.find(key);
2 if(iter!=m.end())
3 {
4 return iter->second;
5 }
6 return null;
- M.count(key):由于map中不包含重复的key,因此m.count(key)取值为0,或者1,表示是否包含。
1 if(m.count(key)>0)
2 {
3 return m[key];
4 }
5 return null;
2.6 Map四种插入方法
- 方法1:pair
- 方法2:make_pair
- 方法3:value_type
- 方法4:下标操作[]
- 方法1:pair
map<int, string> mp;
mp.insert(pair<int,string>(1,"mp1"));
- 方法2:make_pair
map<int, string> mp;
mp.insert(make_pair<int,string>(2,"mp_b"));
- 方法3:value_type
map<int, string> mp;
mp.insert(map<int,string>::value_type(3,"mp_c"));
- 方法4:下标操作[]
map<int, string> mp;
mp[4]="mp_d";
四种方法异同:
前三种方法当出现重复键时,编译器会报错,而第四种方法,当键重复时,会覆盖掉之前的键值对。
- 综合测试
#include<iostream>
#include<map>
using namespace std;
int main()
{
map<int, string> mp;
//map的插入方法有4种
//insert返回值为pair 原型:typedef pair<iterator, bool> _Pairib
//方法1.pair 在插入重复键的情况下前三种方法类似,这里只测试第一种
pair<map<int,string>::iterator, bool> pair1 = mp.insert(pair<int,string>(1,"aaaaa11111"));
if (pair1.second == true)
{
cout<< "插入成功" <<endl;
}
else
{
cout<< "插入失败" <<endl;
}
pair<map<int,string>::iterator, bool> pair2 = mp.insert(pair<int,string>(1,"aaaaa22222"));
if (pair2.second == true)
{
cout<< "插入成功" <<endl;
}
else
{
cout<< "插入失败" <<endl;
}
//方法2.make_pair
mp.insert(make_pair<int,string>(3,"bbbbb33333"));
mp.insert(make_pair<int,string>(4,"bbbbb44444"));
//方法3.value_type
mp.insert(map<int, string>::value_type(5,"ccccc55555"));
mp.insert(map<int, string>::value_type(6,"ccccc66666"));
//方法4.[]
mp[7] = "ddddd77777";
mp[7] = "ddddd88888";
for (map<int,string>::iterator it = mp.begin(); it != mp.end(); it++)
{
cout<< it->first << "\t" << it->second <<endl;
}
cout<< "--------------------------------" <<endl;
//删除
while(!mp.empty())
{
map<int,string>::iterator it = mp.begin();
cout<< it->first << "\t" << it->second <<endl;
mp.erase(it);
}
return 0;
}