C++基础面试(四)
STL
STL六部分:迭代器、容器、算法、仿函数、适配器、空间配制器(allocator)
1.容器
- vector:底层数据结构为数组,支持快速随机访问。
- list:底层数据结构为双向链表,支持快速增删。
- set:底层数据结构为红黑树,有序,不重复。
- multiset:底层数据结构为红黑树,有序,可重复。
- map:底层数据结构为红黑树,有序,不重复。
- multimap:底层数据结构为红黑树,有序,可重复。
//在C++11之前命名为hash_- unordered_set:底层数据结构为hash表,无序,不重复。
- unordered_multiset:底层数据结构为hash表,无序,可重复。
- unordered_map:底层数据结构为hash表,无序,不重复。
- unordered_multimap:底层数据结构为hash表,无序,可重复。
补充:
- deque:底层数据结构为一个中央控制器和多个缓冲区。支持首尾快速增删,支持随机访问,deque是一个双端队列,在堆中保存内容。
- stack:底层一般用list或deque实现,封闭头部即可,不用vector的原因考虑到容量大小有限制,扩容耗时。
- queue:底层一般用list或deque实现,封闭头部即可,不用vector的原因考虑到容量大小有限制,扩容耗时(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)。
- priority_queue:也叫优先队列,底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现。
+1.vector与list的区别(数组与链表的区别)
vector
- vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率,使用vector;
- vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随机存取。但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝(复杂度是O(n));
- 当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都影响了vector的效率。
list
- list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list;
- list是由数据结构中的双向链表实现的,因此它的内存空间可以是不连续的。因此只能通过指针来进行数据的访问,这个特点使得它的随机存取变的非常没有效率,需要遍历中间的元素,搜索复杂度O(n);
- 由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。
+2:vector的扩容
底层数据结构是一个动态数组。默认构造的大小是0, 之后插入按照1 2 4 8 16 二倍扩容。扩容后是一片新的内存,需要把旧内存空间中的所有元素都拷贝进新内存空间中去,之后再在新内存空间中的原数据的后面继续进行插入构造新元素,并且同时释放旧内存空间。由于vector 空间的重新配置,导致旧vector的所有迭代器都失效了。
+3:总体多个对比
- List封装了链表,Vector封装了数组,list和vector的最主要的区别在于vector使用连续内存存储的,支持[]运算符,而list是以链表形式实现的,不支持[]。
- Map,Set属于标准关联容器,使用了非常高效的平衡检索二叉树:红黑树,他的插入删除效率比其他序列容器高是因为不需要做内存拷贝和内存移动,而直接替换指向节点的指针即可。
- Set和Vector的区别在于Set不包含重复的数据。Set和Map的区别在于Set只含有Key,而Map有一个Key和Key所对应的Value两个元素。
- Map和Hash_Map的区别是Hash_Map使用了Hash算法来加快查找过程,但是需要更多的内存来存放这些Hash桶元素,因此可以算得上是采用空间来换取时间策略。
2.迭代器(实质内部类)
参考链接:https://blog.csdn.net/qq_37964547/article/details/81160505?ops_request_misc=&request_id=&biz_id=102&utm_term=迭代器失效&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-81160505.first_rank_v2_pc_rank_v29&spm=1018.2226.3001.4187
+1. 分类
- 输入迭代器:可遍历容器元素,但不可修改容器的数据值;
- 输出迭代器: 可修改容器的数据值,但不可遍历容器元素;
- 前向迭代器:输入和输出的结合,可用于遍历和修改容器;
- 双向迭代器:与输入迭代器相似,但是可以双向遍历;
- 随机存取迭代器:可以使用随机索引位置,访问数据值,且包含双向迭代器的所有属性。
+2. 迭代器失效问题
序列式容器迭代器失效
对于序列式容器,例如vector、deque;由于序列式容器是组合式容器,当当前元素的iterator被删除后,其后的所有元素的迭代器都会失效,这是因为vector,deque都是连续存储的一段空间,所以当对其进行erase操作时,其后的每一个元素都会向前移一个位置。
//例
// erase:因为在删除元素后,迭代器会自动指向下一个元素,如果再进行++,会跳过元素
v={1,2,3,4}
for(it=v.begin();it!=v.end();it++){
if(*it%2==0){
erase(*it); //此代码,在删除的时候2的时候,it已经指向3了,再在循环中++的时候,3会被跳过。
}
}
//正确使用:
for(it=v.begin();it!=v.end();){
if(*it%2==0){
it=erase(it); //此代码,在删除的时候2的时候,it自动指向3了。在关联式容器中,不会自加,应写it=erase(it++);
}else{
it++;
}
}
// 给出的报错信息是:vector iterator not incrementable
vector迭代器失效问题总结
- 当执行erase方法时,指向删除节点的迭代器全部失效,指向删除节点之后的全部迭代器也失效
- 当进行push_back()方法时,end操作返回的迭代器肯定失效。
- 当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。
- 当插入(push_back)一个元素后,如果空间未重新分配,指向插入位置之前的元素的迭代器仍然有效,但指向插入位置之后元素的迭代器全部失效.
关联式容器迭代器失效
对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。
map<int, int>m;
for (int i = 0; i < 10; i++)
{
m.insert(make_pair(i, i + 1));
}
map<int, int>::iterator it;
for (it = m.begin(); it != m.end(); it++)
{
if ((it->first)>5)
m.erase(it); //这里迭代器失效,不能进行++ 操作
}
//正确版本
map<int, int>m;
for (int i = 0; i < 10; i++)
{
m.insert(make_pair(i, i + 1));
}
map<int, int>::iterator it;
for (it = m.begin(); it != m.end(); )
{
if (it->first==5)
m.erase(it++); //erase(it++)的执行过程:这句话分三步走,先把it传值到erase里面,然后it自增,然后执行erase,所以iter在失效前已经自增了。
it++;
}
+3.迭代器例子
STL的迭代器本质是一个内部类,对容器的指针进行管理。
例:list容器机器迭代器
template <typename T>
struct ListNode {
T value;
ListNode* next;
};
class list {
public:
list();
void pushFront();
// ----------------------------------------------
// 内部类
class iterator {
public:
iterator(ListNode<T> ptr);
iterator& operator++();//前置++
iterator operator++(int);//后置++
ListNode<T> operator->()const;
T operator*()const;
bool operator==(const iterator& other);
bool operator!=(const iterator& other);
private:
ListNode<T>* ptr;
};
// ----------------------------------------------
iterator begin(void)const;
iterator end(void)const;
private:
ListNode<T>* m_head;
ListNode<T>* m_tail;
};
//==============================实现=============================================
List<T>::iterator List<T>::begin(void) const {
return List<T>::iterator(m_head);
}
List<T>::iterator List<T>::end(void) const {
return List<T>::iterator(m_tail->next); // end是获取尾节点的后一个节点
}
//内部类构造函数
List<T>::iterator::iterator(ListNode<T>* ptr) {
this.ptr = ptr;
}
//前置++,改变后返回
List<T>::iterator& List<T>::iterator::operator++() {
this.ptr = this.ptr->next;
return *this;
}
//后置++,将原来的返回再++
List<T>::iterator List<T>::iterator::operator++(int) {
List<T>::iterator old(this);
this.ptr = this.ptr->next;
return old;
}
//返回当前的指针
ListNode<T>* List<T>::iterator::operator->()const {
return ptr;
}
//返回当前指针的数据
T List<T>::iterator::operator*()const {
return ptr->value;
}
// 重载==
bool List<T>::iterator::operator==(const List<T>::iterator& other) {
return ptr == other.ptr;
}
//重载 !=
bool List<T>::iterator::operator==(const List<T>::iterator& other) {
return ptr != other.ptr;
}