C++——STL库
stl是标准模板库,经常用的容器std::vector std::map std::string std::list std::set都是标准模板库里的
类,他们定义了一套标准的模板,每个模板类实现的原理也不同,想真正理解stl的库的前提是
理解数据结构的原理,所以stl的学习过程中,要把常见的数据结构了解以下,并且知道他们的优缺点
插入,排序,查找的时间复杂度和空间复杂度,这样才有助于我们去掌握stl的使用。
stl有三个最重要的概念 容器 迭代器 算法。容器就是指的std::vector std::map这种模板类,用不同的数据结构存储数据
迭代器是一种访问容器的指针,通常遍历或者修改他们都可以用迭代器去做这个事情,不同的容器有不同的迭代器
再就是算法,这些容器给一些通用问题提供了一些算法,如排序查找之类的
stl类有很多通用的方法,只在前面介绍std::vector的时候做详细介绍,后面没有特殊处理的地方不做更多解释,
例如 begin() end() cbegin() cend() rbegin() rend() crbegin() crend(),
再例如 size() empty() clear() 等这些都是所有stl类中都有的用法,自身的意义也相似,并没有什么特殊之处。
这里分别总结一些常用的容器和遇到的坑
##各种容器操作都时间复杂度对比
// https://blog.csdn.net/dieju8330/article/details/108551007
// ========================================================================================================================================
// | 容器 | 原理 | 访问 | push_back() | push_front() | insert() | pop_back() | pop_front() | erace() | find() |
// |--------------------------------------------------------------------------------------------------------------------------------------|
// | list | 双向循环链表 | O(n) | O(1) | O(1) | O(1) | O(1) | O(1) | O(1) | \ |
// | vector | 动态数组 | O(1) | O(1) | \ | O(n) | O(1) | \ | O(n) | \ |
// | deque | 双向队列 | O(1) | O(1) | O(1) | O(n) | O(1) | O(1) | O(n) | \ |
// | map | 红黑树 | O(log n) | \ | \ | O(log n) | \ | \ | O(log n) | O(log n) |
// | multimap | 红黑树 | O(log n) | \ | \ | O(log n) | \ | \ | O(log n) | O(log n) |
// | set | 红黑树 | O(log n) | \ | \ | O(log n) | \ | \ | O(log n) | O(log n) |
// | multiset | 红黑树 | O(log n) | \ | \ | O(log n) | \ | \ | O(log n) | O(log n) |
// ========================================================================================================================================
//-----------------------------------------------------------------------------------------------------------------------------------
map, set, multimap, multiset 四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:
插入: O(logN) 查看:O(logN) 删除:O(logN)
//-----------------------------------------------------------------------------------------------------------------------------------
hash_map, hash_set, hash_multimap, hash_multiset四种容器采用哈希表实现,不同操作的时间复杂度为:
插入:O(1),最坏情况O(N)。 查看:O(1),最坏情况O(N)。 删除:O(1),最坏情况O(N)。
//-----------------------------------------------------------------------------------------------------------------------------------
stl的差异性是根据其底层的数据结构决定的,想学号stl,最主要的是要了解其数据结构。但是除了数据结构外,大多数函数是具有通用性的。
empty() 判断容器是否为空
size() 判断容器数据多少
swap() 交换内容
begin() 起始迭代器
end() 结尾迭代器
cbegin() const起始迭代器
cend() const结尾迭代器
rbegin() 反向起始迭代器
rend() 反向结束迭代器
crbegin() const反向起始迭代器
crend() const反向结束迭代器
// 这些通用部分,用法和作用都是一样的,这里在vector中做介绍,其他地方不多加赘述
//-----------------------------------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------
//倍数开辟二倍的内存
例如
std::vector a;
for (int i = 0; i < 4; i++) {
a.push_back(1);
std::cout << a.size() << " " << a.capacity() << std::endl;
}
// 1 1 // 超过申请的空间会申请新的空间
// 2 2
// 3 4 // 申请的空间成指数上升
// 4 4
//-----------------------------------------------------------------------------------------------------------------------------------
//旧的数据开辟到新内存
//释放旧的内存
//指向新内存
例如
std::vector v;
for (int i = 0; i < 4; i++) {
v.push_back(1);
for (int j = 0; j < v.size(); j++) {
std::cout << “&v[” << j << “] =” << &(v[j]) << std::endl;.
}
}
//&v[0] =0x2117010
//
//&v[0] =0x2117030 // 这里重新申请了内存,并且重新将数据考了过来
//&v[1] =0x2117034
//
//&v[0] =0x2117010
//&v[1] =0x2117014
//&v[2] =0x2117018
//
//&v[0] =0x2117010
//&v[1] =0x2117014
//&v[2] =0x2117018
//&v[3] =0x211701c
//-----------------------------------------------------------------------------------------------------------------------------------
拥有一段连续的内存空间,并且起始地址不变,因此能够非常好的支持随机存取,即[]操作符,
但是由于它的内存空间是连续的,所以在头部和中间进行插入和删除操作会造成内存块的拷贝,
另外,当该数组的内存空间不够时,需要重新申请一块足够大得内存并且进行内存的拷贝,
这些都大大的影响了vector的效率。
对头部和中间进行添加删除元素操作需要移动内存,如果你得元素是结构或类,那么移动的同时还会
进行构造和析构操作\,所以性能不高
对任何元素的访问时间都是O(1),所以常用来保存需要经常进行随机访问的内容,并且不需要经常对中间
元素进行添加删除操作,属性与string差不多,同样可以使用capacity看当前保留的内存,使用swap来
减少它使用的内存,如push_back 1000个元素,capacity返回值为16384
对最后元素操作最快(在后面添加删除元素最快),此时一般不需要移动内存,只有保留内存不够时才需要
//-----------------------------------------------------------------------------------------------------------------------------------
构造函数
std::vector v; // 不初始化
std::vector v(10); // 初始化10个0
std::vector v(10,10); // 初始化10个10
std::vector v({0,0,0});// 初始化3个0
std::vector v1(v2); // 拷贝构造
std::vector v1 = v2; // 赋值构造
//-----------------------------------------------------------------------------------------------------------------------------------
赋值函数
std::vector v = {1,2,3};
assign(); // 重新赋值,覆盖原有的值
v.assign(10,10); // 赋值10个10
v.assign({1,2,3}); // 1,2,3
//-----------------------------------------------------------------------------------------------------------------------------------
元素访问
std::vector v = {1,2,3};
at() // 直接访问
v.at(1); // 2
v.at(3); // 捕获异常
v.at(1) = 3;// 允许修改
[] // 下标访问
v[1]; // 2
v[3]; // 崩溃或者越界
v[1] = 3; // 允许修改
front() // 返回第一个值
v.front() // 1
v.front() = 1; // 允许修改
back() // 返回最后一个值
v.back() // 3
v.back() = 1; // 允许修改
data() // 返回头指针
*(v.data()) // 1
*(v.data()) = 1;// 允许修改
(v.data())[0]; // 1
//-----------------------------------------------------------------------------------------------------------------------------------
迭代器
std::vector v = {1,2,3};
begin() // 起始迭代器
end() // 结束迭代器
for (auto it = v.begin(); it != v.end(); it++) {
std::cout << *it << std::endl; // 1,2,3
*it = 10;// 允许修改
}
cbegin() // c++11 起始迭代器,返回const迭代器
cend() // c++11 结束迭代器,返回const迭代器
for (auto it = v.cbegin(); it != v.cend(); it++) {
std::cout << *it << std::endl; // 1,2,3
*it = 10;// error 不允许修改
}
rbegin() // c++11 反向起始迭代器
rend() // c++11 反向结束迭代器
for (auto it = v.rbegin(); it != v.rend(); it++) {
std::cout << *it << std::endl; // 3,2,1 顺序与begin end是刚好相反
*it = 10;// 允许修改
}
crbegin() // c++11 反向起始迭代器 返回const迭代器
crend() // c++11 反向结束迭代器 返回const迭代器
for (auto it = v.crbegin(); it != v.crend(); it++) {
std::cout << *it << std::endl; // 3,2,1 顺序与begin end是刚好相反
*it = 10;// error 不允许修改
}
//-----------------------------------------------------------------------------------------------------------------------------------
容量
std::vector v = {1,2,3};
empty() // 是否为空
v.empty(); // false
size() // 元素个数
v.size(); // 3个
max_size() // 可以存放的最大数量
v.max_size(); // 根据内存大小得到结果
capacity() // 已经申请的空间
v.capacity() // 3 正常push_back()的时候总是 * 2,
v.push_back(1); // v.capacity() = 6
reserve() // 主动开辟空间
v.reserve(100); // 开辟100个元素的空间
v.size(); // 3
v.capacity(); // 100
shrink_to_fit() // 主动释放没用的空间
v.reserve(100); // 开辟100个元素的空间
v.shrink_to_fit(); // c++11 释放掉没用的97个
v.capacity(); // 3
//-----------------------------------------------------------------------------------------------------------------------------------
修改器
std::vector v = {1,2,3};
clear() // 清除所有元素
v.clear(); // 清除掉了所有元素
v.size(); // 0 元素被清掉
v.capacity(); // 3 申请的空间不会清
insert() // 从指定位置插入数据
v.insert(v.begin(),1); // 1,1,2,3 从开始的位置插入1个1
v.insert(v.begin(), 2, 1); // 1,1,1,2,3 从开始的位置插入2个1
v.insert(v.begin(), {1,2,3}); // 1,2,3,1,2,3, 从开始的位置插入1,2,3
v.insert(v.begin(), v.begin(), v.end());// 1,2,3,1,2,3从开始的位置插入v.begin()-v.end()中间的数
erase() // 擦除元素
v.erase(v.begin()); // 2,3 删掉第一个元素
v.size(); // 2 只有2个元素
v.capacity(); // 3 申请的空间不变
push_back() // 从后面插入数据 vector效率最高的一种插入
v.push_back(1); // 1,2,3,1 从后端插入了1个1
v.size(); // 此时有4个数据
v.capacity(); // 6 申请了翻倍的空间
pop_back() // 从后端删除数据 vector效率最高的删除
v.pop_back(); // 1,2 删掉了最后一个数据
v.size(); // 2 只有2个数据
v.capacity() // 3 申请的空间没有改变
resize() // 重新设置元素个数 如果大于回原本数量则在后面补0 小于则从后截取
v.resize(5); // 1,2,3,0,0
v.size(); // 5 有5个元素
v.cacacity(); // 5 5个空间
v.resieze(2); // 1,2 截取的只剩下2个位置
v.size(); // 2 只剩下2个元素
v.capacity(); // 5 空间不变
swap() // 交换2个容器数据申请的空间也变了swap赋值的速度远远比拷贝快
std::vector v2 = {4,5};
v.swap(v2); // v:4,5 v2:1,2,3
//---------------------------------------------------------------------------------------------------------------
vector的特性比较明显,我们在使用vector的时候一定要考虑的使用情景,并灵活使用其中的函数
成倍申请内存空间的特性和数据移动的特性都是我们需要考虑的,如果一直在调用push_back,当数量
比较多的时候,可能会导致内存不够用的情况。这个时候,清理多余空间和预申请空间就可以排上用场了
c++11的新特性不止上面几个,但是另外几个用的并不是太多,需要用的时候可以去了解以下。
c++11在所有的迭代器中都加入了三组新的迭代器 rbegin() rend() cbegin() cend() crbegin() crend()
我们在调用函数的时候,需要注意迭代器的返回值是否匹配。
//-----------------------------------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------
std::list 是支持常数时间从容器任何位置插入和移除元素的容器。不支持快速随机访问。它通常实现为双向链表。
与 std::forward_list 相比,此容器提供双向迭代但在空间上效率稍低。
在 list 内或在数个 list 间添加、移除和移动元素不会非法化迭代器或引用。迭代器仅在对应元素被删除时非法化。
front() // 访问第一个元素
back() // 访问最后一个元素