目录
五、vector和list中,删除末尾元素时指针和迭代器如何变化?若删除中间元素呢?
八、vector和list的具体实现方式,常规操作的时间复杂度是多少?
一、STL容器动态链接可能产生的问题
1、问题
容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库哈数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
2、原因
容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏的问题。
二、map和unordered_map的区别
map和unordered_map的区别在于他们的实现机理不同。
1、map实现机理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡的二叉搜索树),红黑树有自动排序功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作相当于是对红黑树进行的操作。map中的元素是按照二叉树存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
2、unordered_map实现机理
unordered_map内部实现了一个哈希表(也称散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。
三、vector和list的区别以及适用场景
vector和list的区别在于底层实现机理不同,因而特性和适用场景也不同。
1、vector(一维数组)
元素在内存连续存放,动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后内存也不会释放。
优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高,时间复杂度O(1)。
缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动,比较麻烦,时间复杂度O(n),另外当空间不足时还需要进行扩容。
应用场景:拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随机访问而不在乎插入和删除的效率,可以使用vector。
2、list(双向链表)
元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。
优点:底层实现是循环双链表,当对大量数据进行插入删除时,时间复杂度是O(1)。
缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历,时间复杂度O(n),没有提供 [ ] 操作符的重载。
应用场景:拥有一段不连续的内存空间,如果需要高效率的插入和删除,而不关心随机访问,则应该使用list。
四、vector的实现原理
底层实现是一维数组(元素连续存放)。
1、新增元素
vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素。插入新的数据分为:在最后插入push_back和通过迭代器在任何位置插入。这里说一下通过迭代器插入,通过迭代器与第一个元素的距离知道要插入的位置,即int index=iter-begin()。这个元素后面的所有元素都向后移动一个位置,在空出来的位置上存入新增的元素。
//新增元素
void insert(const_iterator iter, const T& t)
{
int index=iter-begin();
if(index<size_)
{
if(size_==capacity_)
{
int capa=calculateCapacity();
newCapacity(capa);
}
memmove(buf+index+1,buf+index,(size_-index)*sizeof(T));
buf[index]=t;
size_++;
}
}
}
2、删除元素
删除和新增相似,也分两种:删除最后一个元素pop_back和通过迭代器删除任意一个元素erase(iter)。通过迭代器删除还是先找到要删除元素的位置,即int index=iter-begin()。这个位置后面的每个元素都向前移动一个元素的位置。同时,我们知道erase不释放内存,只初始化成默认值。删除全部元素clear是循环调用了erase,所以删除全部元素的时候,不释放内存。内存是在析构函数中释放的。
//删除元素
iterator erase(const_iterator iter)
{
int index=iter-begin();
if(index < size_ && size_>0)
{
memmove(buf+index,buf+index+1,(size_-index)*sizeof(T));
buf[--size_]=T();
}
return iterator(iter);
}
3、迭代器
迭代器iterator是STL的一个重要组成部分,通过iterator可以很方便的存储集合中的元素。STL为每个集合都写了一个迭代器,迭代器其实是对一个指针的包装,实现一些常用的方法,如++、--、!=、==、*、->等。通过这些方法可以找到当前元素或是别的元素。vector是STL集合中比较特殊的一个,因为vector中的每个元素都是连续的,所以在自己实现vector的时候可以用指针代替。
//迭代器的实现
template<class _Category,
class _Ty,
class _Diff=ptrdiff_t,
class _Pointer=_Ty *,
calss _Reference = _Ty& >
struct iterator
{
//base type for all iterator classes
typedef _Category iterator_category;
typedef _Ty value_type;
typedef _Diff difference_type;
typedef _Diff distance_type;
typedef _Pointer pointer;
typedef _Reference reference;
};
五、vector和list中,删除末尾元素时指针和迭代器如何变化?若删除中间元素呢?
1、迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。它只是模拟了指针的一些功能,重载了指针的一些操作符,-->、++、--等。迭代器封装了指针,是一个“可遍历STL容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提升了比指针更高级的行为,相当于一种智能指针。可以根据不同类型的数据结构来实现不同的++、--等操作。迭代器返回的是对象而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
2、vector增删元素
删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
3、list增删元
删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。
六、map和set有何区别,如何实现?
1、set是一种关联式容器,特性如下:
(1)set以RBTree作为底层容器;
(2)所得元素的只有key没有value,value就是key;
(3)不允许出现键值重复;
(4)所有的元素都会被自动排序;
(5)不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的。
2、map和set一样是关联式容器,特性如下:
(1)map以RBTree作为底层容器;
(2)所有元素都是键+值存在;
(3)不允许键重复;
(4)所有元素是通过键进行自动排序的;
(5)map的键是不能修改的,但是其键对应的值是可以修改的。
综上所述,map和set底层实现都是红黑树。map和set的区别在于map的值不作为键,键和值是分开的。
七、push_back和emplace_back的区别
如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class A {
public:
A(int i){
str = to_string(i);
cout << "构造函数" << endl;
}
~A(){}
A(const A& other): str(other.str){
cout << "拷贝构造" << endl;
}
public:
string str;
};
int main()
{
vector<A> vec;
vec.reserve(10);
for(int i=0;i<10;i++){
vec.push_back(A(i)); //调用了10次构造函数和10次拷贝构造函数,
// vec.emplace_back(i); //调用了10次构造函数一次拷贝构造函数都没有调用过
}
八、vector和list的具体实现方式,常规操作的时间复杂度是多少?
1. vector 一维数组(元素在内存连续存放)是动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放; 如果新增大小当前大小时才会重新分配内存。
扩容方式:
a. 倍放开辟三倍的内存
b. 旧的数据开辟到新的内存
c. 释放旧的内存
d. 指向新内存
2. list 双向链表(元素存放在堆中) 元素存放在堆中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点,使得它的随机存取变得非常没有效率,因此它没有提供[ ]操作符的重载。但是由于链表的特点,它可以很有效的支持任意地方的删除和插入操作。
特点:
a. 随机访问不方便
b. 删除插入操作方便
3. 常见时间复杂度
(1)vector插入、查找、删除时间复杂度分别为:O(n)、O(1)、O(n);
(2)list插入、查找、删除时间复杂度分别为:O(1)、O(n)、O(1)。