目录
标准模板库
STL基本组成
- 容器、迭代器、仿函数、算法、分配器、配接器
- 他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数
组件 | 描述 |
---|---|
容器(Containers) | 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。 |
算法(Algorithms) | 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。 |
迭代器(iterators) | 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。 |
动态数组实现原理
// 改变数组容量的大小
void resize(int newCapacity)
{
assert(newCapacity>=size);
T *newData=new T[newCapacity];
for(int i=0;i<size;i++)
newData[i]=data[i];
deleta[] data;
data=newData;
capacity=newCapacity;
}
// 向数组中添加一个元素
void push_bakc(T e)
{
if(size == capacity)
resize(2*capacity);// 改变数组的容量为原来的两倍
data[size++]=e;
}
// 从数组中删除一个元素
T pop_back()
{
assert(size>0);
T ret=data[size-1];
size--;
if(size==capacity/4)
resize(capacity/2);// 改变数组的容量为原来的四分之一
return ret;
}
-
当数组中元素的个数等于数组的容量时,此时若再添加一个元素,则会重新分配内存,将数组的容量改变为原来的两倍,并将数组中的元素赋值到新数组中,以实现动态数组。
-
前n次赋值的时间复杂度为n,最后一次赋值的时间复杂度也为n,所以均摊时间复杂度为2n/(n+1)=2,为O(1).
-
当从数组中删除元素时,若数组中元素的个数仅等于原来的1/2,就改变数组的容量,虽然删除元素的均摊时间复杂度仍为O(1),但在此时若重复进行插入与删除操作,会不断地为数组分配内存,使数组容量变为原来的两倍或1/2,会使得均摊复杂度变为O(n),导致复杂度的震荡。
-
为避免复杂度震荡,应当在数组中元素的个数等于原来的1/4时,再改变数组的容量为原来的1/2,如上面代码所示。
vector和list
-
底层结构
- vector的底层结构是动态顺序表,在内存中是一段连续的空间。
- list的底层结构是带头节点的双向循环链表,在内存中不是一段连续的空间。
-
随机访问[]
- vector支持随机访问,可以利用下标精准定位到一个元素上,访问某个元素的时间复杂度是O(1)。
- list不支持随机访问,要想访问list中的某个元素只能是从前向后或从后向前依次遍历,时间复杂度是O(N)。
-
插入和删除
- vector任意位置插入和删除的效率低,因为它每插入一个元素(尾插除外),都需要搬移数据,时间复杂度是O(N),而且插入还有可能要增容,这样一来还要开辟新空间,导致效率低下。
- list任意位置插入和删除的效率高,他不需要搬移元素,只需要改变插入或删除位置的前后两个节点的指向即可,时间复杂度为O(1)。
-
适用场景
- vector适合需要高效率存储,需要随机访问,并且不关心插入和删除效率的场景。
- list适合有大量的插入和删除操作,并且不关心随机访问的场景。
vector迭代器失效
- 失效的两种情况:
- 当插入元素后,如果储存空间重新分配,则原迭代器指向的内存不再是vector,导致失效。
- 当删除元素时,后面所有的元素会向前移动一个位置,导致迭代器指向的下一个位置是未知内存。
int main()
{
vector<int> vec(5, 0);
cout << &vec[0] << endl; // 打印数组元素首地址
for (int i = 0; i < vec.size(); i++)
{
vec[i] = i;
}
vector<int>::iterator iter = vec.begin();
// 插入元素时,迭代器失效
vec.push_back(0);
cout << &vec[0] << endl;// 数组扩容后,首地址已经改变
//cout << *iter << endl;
// 删除元素时迭代器失效
for (iter = vec.begin(); iter != vec.end();)
{
if (*iter == 3) {
//vec.erase(iter);// 如果不给iter重新复制,则迭代器会失效
iter = vec.erase(iter);
}
cout << *iter << endl;
iter++;
}
return 0;
}
deque
- deque双端队列,由一段一段的定量连续空间构成。一旦要在 deque 的前端和尾端增加新空间,便配置一段定量连续空间,串在整个 deque 的头端或尾端。
- 因此不论在尾部或头部安插元素都十分迅速。 在中间部分安插元素则比较费时,因为必须移动其它元素。
- 优点:支持随机访问,即 [] 操作和 .at(),所以查询效率高;可在双端进行 pop,push。
- 缺点:不适合中间插入删除操作;占用内存多。
- 适用场景:适用于既要频繁随机存取,又要关心两端数据的插入与删除的场景。
set
- set集合由红黑树实现,内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复。
- map 和 set 的插入删除效率比用其他序列容器高,因为对于关联容器来说,不需要做内存拷贝和内存移动。
- 优点:使用平衡二叉树实现,便于元素查找(时间复杂度为O(logN)),且保持了元素的唯一性,以及能自动排序。
- 缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响。
- 适用场景:适用于经常查找一个元素是否在某群集中且需要排序的场景。
map
- map 由红黑树实现,其元素都是 “键值/实值” 所形成的一个对组。内部元素根据键值自动排序。每个键值只能出现一次,不允许重复。
- 优点:使用平衡二叉树实现,便于元素查找,且能把一个值映射成另一个值,可以创建字典。
- 缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响。
- 适用场景:适用于需要存储一个数据字典,并要求方便地根据key找value的场景。
map和set的区别
- set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。原因是map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。
- map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用。如果find能解决需要,尽可能用find。
map和unordered_map的区别
-
map,其底层是基于红黑树实现的
- 优点:
- 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
- map的查找、删除、增加等一系列操作时间复杂度稳定,都为logn
- 缺点:
- 查找、删除、增加等操作平均时间复杂度较慢,与n相关
- 优点:
-
unordered_map,其底层是一个哈希表
- 优点如下:
- 查找、删除、添加的速度快,时间复杂度为常数级O©
- 缺点如下:
- 因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用率高
- Unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O©,取决于哈希函数。极端情况下可能为O(n)
- 优点如下:
allocator分配器
作用
- 一般情况下,内存分配主要使用new和delete,但是new将内存分配和对象构造组合在了一起,delete将对象析构和内存释放组合在了一起。一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。
- 当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。
- **allocator允许内存分配和对象初始化的分离。**它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
使用
int test_allocator_1()
{
std::allocator<std::string> alloc; // 可以分配string的allocator对象
// 内存分配
int n = 5;
auto const p = alloc.allocate(n); // 分配n个未初始化的string
// 对象初始化
auto q = p; // q指向最后构造的元素之后的位置
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, 10, 'c'); // *q为cccccccccc
alloc.construct(q++, "hi"); // *q为hi
std::cout << *p << std::endl; // 正确:使用string的输出运算符
//std::cout << *q << std::endl; // 灾难:q指向未构造的内存
std::cout << p[0] << std::endl;
std::cout << p[1] << std::endl;
std::cout << p[2] << std::endl;
// 对象析构
while (q != p) {
alloc.destroy(--q); // 释放我们真正构造的string
}
// 内存释放
alloc.deallocate(p, n);
return 0;
}