c++ STL
1.STL
STL主要包含容器,算法,迭代器
容器:包含大多数数据结构,数组、链表、队列、堆、栈、树等。
算法:包含增、删、改、查找、排序等
迭代器:相当于指针,通过有序的移动把容器的元素和算法关联起来,是实现STL功能的基础。
2.模版:
提供一种通用的方法开发可重用代码,算是一种多态.
是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。模板是创建泛型类或函数的蓝图或公式。
库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如vector或vector 。
分为两类:函数模版和类模板。
函数模版:
template<typename T>
void swap(T a,T b){
T temp;
temp=a;
a=b;
b=temp;
}
T sum(T a,T b){
return a+b;
}
调用的时候可以显示的调用,也可以让它自动转换
sum(a, b);
sum<int>(a, b);
类模板:
template <typename T>
class A{
…
public:
bool cmp(T a,T b){
return a>b;
}
};
定义类模板对象:
类模板名<类型名>对象名(参数);
A<int>a1;
A<float>a2(1.0,2.5);
3.通用容器:
1)序列性容器:按照线性存储某类型值的集合,每个元素都有自己特定位置。
- vector:
动态数组,堆中分配内存,元素连续存放,可以直接访问任何位置。
会保留内存,元素减少后内存大小也不会自动释放。
来的新数值超出了内存会重新分配一个位置并地之前的元素进行拷贝,所以最开先指定好大小,对最后位置操作速度快,因为一般不需要移动内存(除非空间不够).
如果存的是类或者结构体,移动的时候会进行构造和析构,比较慢。
在末尾插入、删除元素的时间复杂度为 O(1).
- deque:
https://blog.csdn.net/u010710458/article/details/79540505
双端队列,类似vector,支持随机访问和快速插入删除,在容器某个位置操作花费线性时间。
1)中控器map:是指针数组,里面成员是分配空间Node的地址(类似二维数组最外层)
2)迭代器:cur、first、last、node四个数组指针,分别指向连续空间开始地址(first),结束地址(last),空间中当前元素的地址(cur)以及连续空间地址在map中的位置(node).
3)缓冲区:中控区每个指针指向的一段连续地址。
不止支持从末位插入,还支持从头插入删除。
在开头或末尾插入、删除元素的时间复杂度为 O(1). 这是它和 vector 的主要区别。
随机访问的时间复杂度为 O(1). 这是它和 list 的主要区别。
- list:
链表,是双线性列表,必须顺序访问,不支持随机访问。必须从头一个一个遍历。
存储和删除的时候都可以做到时间复杂度是O(1)
查找O(n)
2)关联式容器:
关联式容器关注快速高效的检索数据,根据键值对来检索。
初始化后一般都排好序了。
红黑树实现,查找删除log(N)
set:可以快速查找,自动去重复内容。
multiset:快速查找,允许重复值。
map:映射关系,一对一,根据关键字快速查找,不允许重复。
multimap:一对多映射,根据关键字查找,允许重复。
3)容器适配器,对已经有了的容器进行某些特性在封装,不是真正的新容器。
- stack:
堆栈类,后进先出。底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时。 - queue:
队列类,先进先出。底层一般用list或deque实现,头尾不可pop()即可,不用vector的原因应该是容量大小有限制,扩容耗时。
4.复杂度等:
- deque:
类似文件系统,map指向多个连续缓冲区 - List:
双向链表
查找O(n)
插入、删除、修改O(1) - set、multiset、map、multimap:
红黑树实现
插入、删除、查找时的复杂度都为 O(logN) - unordered_map、unordered_set:
hash一段连续区域
内部不再是红黑树排关键字了,而是用的哈系表;来提高查找效率。
处理冲突有开放定址法(线性探测再散列、二次探测再散列、伪随机探测再散列)、再哈希法、链地址法、建立公共溢出区。
参考: https://www.cnblogs.com/wuchaodzxx/p/7396599.html
插入、删除、查找复杂度O(1) - priority_queue:
优先级队列默认是使用vector作容器,用堆来实现。
插入、删除、查找复杂度 O(logN)
容器 | 实现 | 查询 | 插入 | 删除 | 其他 |
---|---|---|---|---|---|
vector | 一段内存连续区域 | 下标查询O(1),值查询O(N) | 尾部O(1),中间O(N) | 尾部O(1),中间O(N) | 空间只增不减,空间不够,1.5-2倍扩容 |
list | 双向链表 | O(N) | O(1) | O(1) | |
deque | 中控器map+多个连续缓冲区+迭代器(first、last、cur、node) | 下标查询O(1),值查询O(N) | 尾部O(1), 头部O(1) | 尾部O(1), 头部O(1) | List和vector折中 |
map(set) | 红黑树(无重复,有序) | 下标访问log2(N) | log2(N) | log2(N) | |
multi_map(multi_set) | 红黑树(有重复,有序) | 下标访问log2(N) | log2(N) | log2(N) | |
unordered_map/unordered_set | hash, | 最好O(1),最坏O(n) | 最好O(1),最坏O(n) | 最好O(1),最坏O(n) | |
priority_queue | 堆,默认用vector作容器存储 | log2(N) | log2(N) | 建堆O(n),less是大顶堆,可以重载比较函数 | |
stack | deque | 插入栈顶O(1) | 删除栈顶O(1) | ||
queue | deque | 插入队尾O(1) | 插入队头O(1) |
5.vector的一些问题
vector、map…都是通过stl的allocator进行内存分配,都会提供一个默认的allocator。
所以用不同的创建方式创建出来的容器对象可以在栈或者堆上,而内容数据永远都是在堆上的。栈是没法做到动态管理的。
(vector等本身并不一定占用动态内存。vector等只是管理了一片动态内存)
vector:
动态数组,内存空间只增不减;
只保存了指针以及size,通过sizeof就可以看出vector大小和它包含数据多少没有关系;
vector内的数据是放在指针指向的空间,这块空间是连续的;
clear()和erase(),实际上只是减少了size(),缓冲区大小不会改变;
空间不够会扩容当前大小1.5-2倍,拷贝数据,删除之前数据,缓冲区大小不会改变,只是清除了其中的数据,只有在析构函数调用的时候vector才会自动释放缓冲区。
capacity(),返回对象缓冲区(vector维护的内存空间)实际申请的空间大小、size(),返回当前对象缓冲区存储数据的个数。
在末尾插入、删除元素的时间复杂度为 O(1).
可以随即访问
//分配内存只发生在程序的执行过程中,如
vector<string> a;
vector<string> *b = new vector<string>();
//a在栈中分配内存,b在堆中分配。(非new的对象在栈中分配,new的对象在堆中分配)
class A {
public:
vector<string> v1;
};
//以上只是一个类的定义,还没有实例化,所以涉及到任何内存分配的,不适用于内存分配原则。
A a=new A();
//实例化后,v1变量只是a对象的一个成员变量,在堆上还是在栈上是由a对象的分配方式决定的,而不是由其声明方式决定的。
//进一步,分析一下成员变量的内存分布也能发现,v1本身就是和其父对象是同一地址。
A *b = getA();
long offset = (long)(&(b->v1)) - (long)b; //结果是0
//也就是说,当A类实例化为a对象的时候,a对象在栈上v1就在栈上,a对象在堆上v1就在堆上。
//如果分配在堆上的,是不会随着函数结束被回收的。