面试中常问的STL

1Vector

1.1 底层是如何实现的

  在堆中或栈中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放.如果新值>当前大小时才会再分配内存.当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。扩容一般是扩大1.5倍或者2倍,vs中扩容的是1.5倍。内部使用allocator类进行内存管理,程序员不需要自己操作内存。对 vector 的任何操作,一旦引起空间重新配置,指向原 vector 的所有迭代器就都失效了。

  vector就是一个动态数组,里面有一个指针指向一片连续的内存空间,当空间不够装下数据时,会自动申请另一片更大的空间(一般是增加当前容量的50%或100%),然后把原来的数据拷贝过去,接着释放原来的那片空间;当释放或者删除里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

template<class _Ty, class _Ax>
 
class vector: public _Vector_val<_Ty, _Ax> {  //varying size array of values
    public:
    protected:
        pointer _Myfirst;   // pointer to beginning of array
        pointer _Mylast;    // pointer to current end of sequence
        pointer _Myend; // pointer to end of array
};
v.begin()  v.end()  v.size()  v.capacity()  
v.push_back()  v.pop_back()
v.erase(iterator position)  v.erase(iterator begin, end)  :执行后后面元素自动前移
v.clear()
v.insert(position, size_type n, const &x)

1.2 为什么vector 插入操作可能导致迭代器失效

  vector动态增加大小时,并不是在原空间后增加新的空间,而是以原大小的两倍在另外配置一个较大的新空间,然后将内容拷贝过来,并释放原来的空间,由于操作改变了空间,所以迭代器失效。
理论会失效,理论上每次insert或者erase之后,所有的迭代器就重新计算的,所以都可以看做是失效的,原则上就是不能使用过期的内存。
  但vector一般底层是用数组实现的,仔细考虑数组的特性,得出结论:
1.insert时,假设insert位置为p,分2种情况:
  a)容器还有空余空间,不重新分配内存,p前的迭代器有效,p后的失效。
  b)重分内存,p之后的迭代器失效
2.erase时,假设erase位置为p,则p之前的迭代器都有效,并且p指向下一个元素位置,(如果之前p在尾巴上,则p指向无效尾end),p之后的迭代器都无效。

2 list

2.1 list的实现,vector和list的区别

在这里插入图片描述

  list和vector的区别:

(1)实现方式:vector是基于动态数组,内存空间是连续的,因此支持下标访问;list是基于双向链表,内存是不连续的,不支持下标访问,支持指针访问。

(2)如果需要高效的存取,而不在乎插入和删除的效率,选取vector。如果需要大量的插入删除而关心随机存取,选用list。

  List.erase(it)执行后,迭代器it指向的内存已失效,再访问违规.但其他元素不受影响。

push_back()
push_front()  
front()
back() 
insert()

3 Deque

3.1 实现

在这里插入图片描述

  和 vector 容器采用连续的线性空间不同,deque 容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。为了管理这些连续空间,deque 容器用数组(数组名假设为 map)存储着各个连续空间的首地址。也就是说,map 数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间(如图 1 所示)。

  • 在进行头部插入的时候,假如第一段的连续空间没有满的话,那么就直接查到最前的第一个空白的区域,假如全满的话,那么就需要再重新分配一段内存空间,并且将值存在这个空间的最后一个格子里,并且在那个map容器里面记录下这个区域的地址。
  • 在尾部插入的时候也是同样,假如空间足够的话,就直接插入,假如空间不够那就重新分配区域。
  • 假如map满的话,那么再重新找一快连续的区域存放map。
  • deque 容器擅长在序列的头部和尾部添加或删除元素
  • Deque支持随机访问和存取,支持下标访问,插入删除的效率也很高。是由一段一段定量的连续空间组成的。
      实现:map+buffer
      插入元素时随时可以重新增加一段新的空间并连接起来。
      提供的函数:
Push_back()  pop_back()  push_front()  pop_front()
Clear()  erase()  insert()
Deque.at()  deque.front()  deque.back()
Deque.begin()  deque.end()  deque.rbegin()  deque.rend()

3.2 deque与vector区别

  1. vector是单向开口的连续线性空间,deque是双向开口的连续线程空间。
  2. deque没有提供空间保留功能,vector则要提供空间保留功能。
  3. deque也提供随机访问迭代器,但是其迭代器比vector复杂很多

4 map

4.1 底层实现

  1. 自平衡的二叉搜索树。自动排序效果不错。
    通过map的迭代器不能修改键值,只能修改其实值。
    查找复杂度:O(logN)
    不可以,map不像vector,它在容器进行erase操作后不会返回后一个元素的迭代器,不能遍历第往后插删
  2. 哈希表

4.2 map与unordered_map的区别

  • map底层实现的红黑树,是默认排序好的。而unordered_map底层实现是哈希表,是没有排好序的,但是可以通过将转化成vector<pair<int,int>>进行排序。
  • map的速度会比unordered_map慢,因为每一次插入一个元素,红黑树都需要堆红黑树重新进行调整。而hash表则不需要。

5 set

5.1 底层实现

  set 的底层实现也是红黑树

5.2 为何map和set的插入删除效率比其他序列高

  因为不需要内存拷贝和移动

5.3 为何map和set每次insert之后,以前保存的iterator不会失效?

  因为插入操作只是结点指针换来换去,结点内存没有改变,而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

5.4 当数据元素增多时,(从10000到20000),map、set查找速度会怎样变化?

  红黑树用二分查找法,时间复杂度为logN,所以查找次数从log100000=14变为log20000=15,多了1次而已。

6 哈希表

6.2 哈希表如何避免地址冲突

  • 线性探测:hash函数计算某个元素的插入位置后,如果该位置的空间已经被占用,则继续向下寻找,直到找到一个可用空间为止。
  • .二次探测:如果计算的位置被占用,就依次尝试H+12,H+22 等
  • 开链:每一个表格元素中维护一个list,在那个list中执行插入、删除

6.3 hashtable、hash_set、hash_map的区别

  • hash_set以hashtable为底层,不具有排序功能,能快速查找,其键值就是实值。(set以红黑树为底层,具有排序功能)
  • -hash_map以hashtable为底层,没有自动排序功能,能快速查找,每一个元素同时拥有一个实值和键值。(map以红黑树为底层,有排序功能)

6.4hash_map与map的区别、什么时候用它们两个?

  构造函数:hash_map需要hash function 和等于函数,而 map需要比较函数(大小或小于)
  存储结构:hash_map以hashtable为底层,而map以红黑树为底层。
  总的来说,hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的查找速度是logN级别。但不一定常数就比log小,而且hash_map还有hash function耗时。
  如果考虑效率,特别当元素达到一定数量级时,用hash_map
  考虑内存,或者元素数量较少时,用map

  选用map还是hash_map,关键是看关键字查询操作次数,以及你所需要保证的是查询总体时间还是单个查询的时间。如果是要很多次操作,要求其整体效率,那么使用hash_map,平均处理时间短。如果是少数次的操作,使用 hash_map可能造成不确定的O(N),那么使用平均处理时间相对较慢、单次处理时间恒定的map,考虑整体稳定性应该要高于整体效率,因为前提在操作次数较少。如果在一次流程中,使用hash_map的少数操作产生一个最坏情况O(N),那么hash_map的优势也因此丧尽了。

6.5hashtable和hashmap的区别

  hashmap以hashtable为底层。有以下不同:

  1. hashtable是Dictionary的子类,而hashmap是Map接口的一个实现类
  2. hashtable中的方法是同步的,而hashmap的方法不同步

7 序列容器的异同

  • 首先需要知道,STL中标准关联容器set、multiset、map、multimap的内部采用的是一种非常高效的平衡检索二叉树,即红黑树。红黑树是一种平衡二叉树,但是红黑树的统计性能要好于一般的平衡二叉树。红黑树与一般的平衡二叉树相比,红黑树基本上是平衡的,而AVL树是完全平衡的。而为什么map和set的底层使用红黑树呢?那是因为红黑树是平衡二叉树,其插入和删除的效率都是logn,与AVL树相比,红黑树的插入和删除最多只需要3次旋转,而AVL树为了维持其完全平衡性,在最坏的情况下要旋转的次数太多。

  • 关于map的实现,我们需要知道,map中的所有元素都是以键值对形式出现的,同时拥有实值value和键值key。底层是通过红黑树来实现的,其中的key值是经过排序的,而且map不允许两个元素拥有相同的键值,map的插入和删除操作不会使迭代器失效。增加和删除结点对于迭代器的影响很小,除了正在进行操作的结点,对其他的结点都没有什么影响。对于迭代器来说,可以修改value,但不能修改key。既然map的底层是红黑树,那么红黑树插入和查找的优点map也具备。map可以根据key值快速查找记录,查找的时间复杂度基本上是logn。

  • 为什么map和set的插入删除效率要比其他顺序容器高?

  对于关联容器说,不需要做内存拷贝和内存移动。map和set容器内所有的元素都是以结点的方式来存储,其结点结构和链表差不多,指向父结点和子结点。因此插入的时候只需要稍微做变换,把结点的指针指向新的结点就可以了。删除的时候也类似,把待删除结点的指针指向其他的结点即可。也就是说,map和set的插入删除操作只是简单的将指针换来换去,和内存移动并没有关系。

  • 为什么每次插入之后,以前保存的迭代器不会失效?

  迭代器就相当于指向结点的指针,内存没有变,指向内存的指针当然也不会变,因此迭代器不会失效。对于vector容器来说,每一次的插入和删除,指针都有可能失效,调用push_back在尾部插入时也可能会导致迭代器失效,因为vector容器内的数据是连续存放的,所以为了保证内部数据的连续存放,迭代器指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者已经被释放了。即使push_back的时候,容器内部空间可能不够,需要一块更大的内存,只有把以前的内存释放,申请更大的内存,复制已有的数据元素到新的内存中,最后再把新元素插入到最后,那么以前的内存指针就不可用了。

  • 为什么map和set不能像vector一样有个reserve()函数来预分配数据?

  因为在map和set中存储的已经不是元素本身,而是包含元素的结点。其实在map和set中往往使用的是allocator分配器。

  • 当数据元素增多时(10000和20000个比较),map和set的插入和搜索如何变化?

  在map和set中查找使用的是二分查找法,即当有16个元素时,最多需要比较4次就能找到。当有10000个元素时,最多需要14次,而20000个元素的时候,最多需要15次。可见,当数据量增加一倍的时候,搜索的次数只不过多了1次而已。

  • map和set的区别

(1)map中的元素是key-value(关键字-值)对:关键字起到索引的作用,值则表示与索引相关联的数据。set只是关键字的简单集合,set中每个元素只包含一个关键字。

(2)set的迭代器是const的,不允许修改元素的值,map允许修改value,但是不允许修改key值。因为map和set是根据关键字排序来保证其有序型的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致迭代器失效。所以STL中将set的迭代器设置为const的,不允许修改迭代器的值,而map的迭代器则不允许修改key值,允许修改value值。

(3)map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map中需要慎用。

8 红黑树

8 .1红黑树的基本概念

  1. 每个节点都是红色或黑色
  2. 根节点为黑色
  3. 叶节点为黑色的NULL结点。
  4. 如果结点为红,其子节点必须为黑
  5. 任一节点到NULL的任何路径,所含黑结点数必须相同

9 优先队列

  1. 升序队列,小顶堆
  2. priority_queue <int,vector,greater > q;
  3. 降序队列,大顶堆
  4. priority_queue <int,vector,less >q;
  5. greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
    priority_queue 默认从大到小(大根堆)
  std::priority_queue<int> mypq;
 
  mypq.push(30);
  mypq.push(100);
  mypq.push(25);
  mypq.push(40);
 
  std::cout << "Popping out elements...";
  while (!mypq.empty())
  {
     std::cout << ' ' << mypq.top();
     mypq.pop();
  }
  std::cout << '\n';
215. 数组中的第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/kth-largest-element-in-an-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


# 9 STL底层数据结构实现
1. vector:数组,支持快速随机访问
2. list 双向链表,支持快速增删
3. deque:一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,支持随机访问
4. stack:底层用deque或者list实现,不用vector的原因是扩容耗时
5.  queue 底层用deque或者list实现,不用vector的原因是扩容耗时
6. priority_queue:一般以vector为底层容器,heap为处理规则来管理底层容器实现
7. set 红黑树 有序,不重复
8.  multiset 红黑树 有序,可重复
9.  map 红黑树 有序,不重复
10.  multimpap 红黑树 有序,可重复
11. hash_set hash表 无序,不重复
12.  hash_map hash表 ,无序,不重复
13. hashtable 底层结构为vector

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STL是C++非常重要的一个库,也是面试经常会涉及到的话题。以下是一些常见的STL面试题: 1. STL的容器有哪些?它们的特点和用途是什么? STL的容器有vector、list、deque、set、map等。它们的特点和用途如下: - vector:支持快速随机访问,尾部插入和删除,适用于需要经常进行插入和删除操作的场景。 - list:支持快速插入和删除操作,但不支持随机访问,适用于需要进行频繁插入和删除操作的场景。 - deque:支持随机访问和尾部插入和删除,但不支持在间进行插入和删除操作,适用于需要经常进行尾部操作的场景。 - set:内部元素有序,不允许重复元素,适用于需要元素有序并且不能重复的场景。 - map:内部元素有序,不允许重复元素,但每个元素包含一个键值对,适用于需要键值对有序并且键不能重复的场景。 2. STL的迭代器有哪些?它们的特点和用途是什么? STL的迭代器有输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。它们的特点和用途如下: - 输入迭代器:只能读取容器的元素,适用于遍历容器的场景。 - 输出迭代器:只能写入容器的元素,适用于向容器添加元素的场景。 - 前向迭代器:支持顺序遍历、读写元素和单向移动,适用于需要顺序遍历容器的场景。 - 双向迭代器:支持顺序遍历、读写元素和双向移动,适用于需要顺序遍历容器且需要在间进行插入和删除操作的场景。 - 随机访问迭代器:支持随机读写和双向移动,适用于需要随机访问容器的场景。 3. STL的算法有哪些?它们的特点和用途是什么? STL的算法有排序、查找、遍历、拷贝、删除等。它们的特点和用途如下: - 排序:对容器的元素进行排序,包括快速排序、归并排序、堆排序等。 - 查找:在容器查找指定元素,包括二分查找、线性查找等。 - 遍历:对容器的元素进行遍历,包括for_each、transform等。 - 拷贝:将容器的元素拷贝至另一个容器,包括copy、copy_if等。 - 删除:从容器删除指定元素,包括remove、remove_if等。 4. STL的智能指针有哪些?它们的特点和用途是什么? STL的智能指针有unique_ptr、shared_ptr和weak_ptr。它们的特点和用途如下: - unique_ptr:独占式智能指针,只能有一个指针指向某个对象,适用于需要独占资源的场景。 - shared_ptr:共享式智能指针,多个指针可以同时指向同一个对象,适用于需要共享资源的场景。 - weak_ptr:弱引用智能指针,不控制资源生命周期,适用于需要引用计数但不独占资源的场景。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值