[C++ Basic] STL Data Structure
基础杂项
auto的使用
- 当你想要拷贝range的元素时,使用
for(auto x : range).
- 当你想要修改range的元素时,使用
for(auto && x : range)
- 当你想要只读range的元素时,使用
for(const auto & x : range)
.
容器间的转换
使用begin end
1 | vector<int>& nums1 |
以下是整理和改正后的迭代器笔记。
迭代器知识
随机访问迭代器
定义:随机访问迭代器是一种迭代器类型,允许在常数时间内访问序列中的任何元素,并支持灵活的迭代器操作。
运算支持:
- 支持指针算术运算,如
+
和-
运算符,允许轻松地在迭代器位置上进行移动。 - 支持下标运算符
[]
,使其用法与指针类似。 - 例如,对于一个指向数组的随机访问迭代器
it
,可以通过*(it + i)
或it[i]
语法访问第i
个元素。
- 支持指针算术运算,如
使用场景:
- STL 容器:
vector
和deque
都提供随机访问迭代器。 - 算法:一些标准库算法(例如
std::sort
、std::nth_element
)要求输入的迭代器必须是随机访问迭代器,以便快速定位和排序。
- STL 容器:
正向迭代器与反向迭代器
概述:在 C++ 中,容器一般提供正向迭代器(
begin()
到end()
)和反向迭代器(rbegin()
到rend()
),二者在遍历方向上相反。转换:正向迭代器和反向迭代器可以相互转换,但类型不同,需要显式转换。
正向迭代器到反向迭代器:可以将一个正向迭代器转换为反向迭代器。
1
2Iterator it; // 假设 it 是正向迭代器
std::reverse_iterator<Iterator> rit(it); // 将 it 转换为反向迭代器 rit反向迭代器到正向迭代器:反向迭代器的
base()
成员函数可以返回一个指向反向迭代器当前元素的下一个位置的正向迭代器。1
2std::reverse_iterator<Iterator> rit;
Iterator it = rit.base(); // 转换为正向迭代器,指向 rit 所指位置的后一个元素
注意:
- 反向迭代器
rit.base()
指向的并不是rit
当前元素本身,而是rit
指向元素的下一个位置。
- 反向迭代器
反向遍历
C++ 提供了 rbegin()
和 rend()
来支持容器的反向遍历:
rbegin()
返回一个反向迭代器,指向容器的最后一个元素。rend()
返回一个反向迭代器,指向容器反向遍历时的“结束位置”,即第一个元素之前的一个位置。- 在循环中,注意使用
++it
而非--it
,因为反向迭代器的递增操作相当于在正向迭代器上进行递减操作。
1 | for (auto it = collection.rbegin(); it != collection.rend(); ++it) { |
前后第k个元素
std::prev
函数接受两个参数:一个是指向迭代器的参数,另一个是整数偏移量。它返回从指定迭代器开始向前移动指定偏移量后的迭代器。std::next
函数接受两个参数:一个是指向迭代器的参数,另一个是整数偏移量。它返回从指定迭代器开始向后移动指定偏移量后的迭代器。
1 | auto prevIt = std::prev(it); |
bitset
bitset类型存储二进制数位。
初始化
1 | std::bitset<16> foo; |
格式转换
将数转化为其二进制的字符串表示
1 | int i = 3; |
pair
1 |
|
tuple
1 |
|
char 与 字符串
元音与index的相互映射
1 | string vowel = "AEIOUaeiou"; |
string
初始化与读取
1 | std::string s0 ("Initial string"); |
读取空格分割的
1 | stringstream txt(s); |
增
删
改
查
顺序容器与关联容器
顺序容器包括vector、deque、list、forward_list、array、string,
- 支持快速顺序访问元素。
关联容器包括set、map,
- 支持高效的关键字查找和访问。
- 不支持顺序容器的位置相关的操作。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。
- 也不支持构造函数或插入操作这些接受一个元素值和一个数量值得操作。
关联容器和顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的。与之相对,顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。
二叉树
存储方式
链表,或者数组(如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1
,右孩子就是 i * 2 + 2
。)
链式结构如下,注意左右孩子节点
1 | struct TreeNode { |
遍历方式
深度遍历: 前/中/后序遍历。
注意:这里前中后,其实指的就是中间节点/根节点的遍历顺序
heap 堆
堆(Heap)是计算机科学中一类特殊的数据结构的统称。
堆通常是一个可以被看做一棵完全二叉树的数组对象。
堆满足下列性质:
堆中某个节点的值总是不大于或不小于其父节点的值。
- 大顶堆:每个结点的值都大于或等于其左右孩子结点的值;
- 小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
堆总是一棵完全二叉树。
make_heap()
将区间内的元素转化为heap.push_heap()
对heap增加一个元素.pop_heap()
对heap取出下一个元素.sort_heap()
对heap转化为一个已排序群集.
1 |
|
map / unordered_map / hash_map
- map
- 内部数据的组织,基于红黑树实现,红黑树具有自动排序的功能,因此map内部所有的数据,在任何时候,都是有序的。其中每个键都是唯一的,可以插入或删除,但不能更改。但是与键关联的值可以更改。
- 红黑树是一种含有红黑结点并能自平衡的二叉查找/搜索树, 别称平衡二叉B树(symmetric binary B-trees)
- unordered_map(hash_map)
- 基于哈希表,数据插入和查找的时间复杂度很低,几乎是常数时间,而代价是消耗比较多的内存。底层实现上,使用一个下标范围比较大的数组来存储元素,形成很多的桶,利用hash函数对key进行映射到不同区域进行保存。
- 标准库的unordered_map,底层实现是基于hashtable的,其避免冲突的方法是使用开链(seperate chaining)法,这种做法是在每一个表格元素中维护一个list,每个表格元素称之为buket(桶)
特性 | map | unordered_map |
---|---|---|
元素排序 | 严格弱序 | 无 |
常见实现 | 平衡树或红黑树 | 哈希表 |
查找时间 | O(log(n)) | 平均 O(1),最坏 O(n)(哈希冲突) |
插入时间 | O(log(n)) + 重新平衡 | 同查找时间 |
删除时间 | O(log(n)) + 重新平衡 | 同查找时间 |
需要比较器 | 只需 < 运算符 |
只需 == 运算符 |
需要哈希函数 | 不需要 | 需要 |
常见用例 | 当无法提供良好哈希函数或哈希 | 在大多数其他情况下。当顺序不重要时 |
函数太慢,或者需要有序时 |
初始化
map的value是int,默认为0。可以直接++
1 |
|
增改
1 | // insert 不能覆盖元素,已经存在会失败 |
删除
1 | mymap.erase ('c'); // erasing by key |
查
1 | //unordered_map 类模板中,还提供有 at() 成员方法,和使用 [ ] 运算符一样,at() 成员方法也需要根据指定的键,才能从容器中找到该键对应的值; |
set
常用于 一个值是否存在 的相关问题
- 按关键字有序:
set
(关键字即值,即只保存关键字的容器);使用红黑树,自动排序,关键字唯一。multiset
(关键字可重复出现的set);
- 无序集合:
unordered_set
(用哈希函数组织的set);unordered_multiset
(哈希组织的set,关键字可以重复出现)。
set/map 的有序性
默认红黑树,使用std::less
作为比较器, 升序序列。
存放数据类型 | 排序规则 |
---|---|
整数、浮点数等 | 按从小到大的顺序排列 |
字符串 | 按字母表顺序排列 |
指针 | 按地址升序排列 |
指向某元素的指针 | 按指针地址递增的顺序排列 |
类(自定义) | 可以自定义排序规则 |
1 | // 自定义比较器,按降序排列 |
增删改查
1 | //增改 |
并差集
- 并查集主要用于处理一些不相交集合的合并和查询问题。它在很多算法问题中都有广泛的应用,特别是在图论和动态连通性问题中。
并查集的基本操作
- 初始化
UnionFind(int n)
:每个元素初始化为自己的父节点。 - 查找
int find(int x)
:查找某个元素的根节点,并进行路径压缩以优化后续查找。 - 合并
int find(int x)
:将两个元素所在的集合合并为一个集合。
核心是
- 同一个并查集内的元素会指向同一个parent
- 维护并查集深度Rank,来按秩合并。
- 数据结构用数组和map都行
容器适配器
C++ 中的容器适配器 stack
、queue
和 priority_queue
依赖不同的基础容器来实现特定的数据结构行为。每种容器适配器都有特定的成员函数要求,默认选择的基础容器是为了更好地满足这些要求。
容器适配器 | 基础容器筛选条件 | 默认使用的基础容器 |
---|---|---|
stack | 基础容器需包含以下成员函数: | deque |
- empty() |
||
- size() |
||
- back() |
||
- push_back() |
||
- pop_back() |
||
满足条件的基础容器有 vector 、deque 、list 。 |
||
queue | 基础容器需包含以下成员函数: | deque |
- empty() |
||
- size() |
||
- front() |
||
- back() |
||
- push_back() |
||
- pop_front() |
||
满足条件的基础容器有 deque 、list 。 |
||
priority_queue | 基础容器需包含以下成员函数: | vector |
- empty() |
||
- size() |
||
- front() |
||
- push_back() |
||
- pop_back() |
||
满足条件的基础容器有 vector 、deque 。 |
stack 栈
堆栈,先进先出
1 | stack<int> minStack; |
注意pop仅用于从堆栈中删除元素,并且没有返回值, 一般用法如下
1 | top_value = stIn.top(); |
queue
- 队列中的数据元素遵循“先进先出”(First In First Out)的原则,简称FIFO结构;
- 在队尾添加元素,在队头删除元素。
1 |
|
deque 双端队列
deque 容器也擅长在序列尾部添加或删除元素(时间复杂度为O(1)),而不擅长在序列中间添加或删除元素。
1 |
|
priority_queue(优先队列)
- 自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队
- 默认利用max-heap(大顶堆)完成对元素的排序,是以vector为表现形式的complete binary tree(完全二叉树)。
- 基本操作和queue一样。
初始化
1 |
|
- Type 就是数据类型,
- Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),
- Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆
排序规则
1 | //降序队列(默认less<>), map/set默认也是使用less,但是是升序序列 |
单链表 (自定义)
一般是题目要求自己实现链表,而不是使用STL提供的链表list。
1 | // 单链表 |
list 双向链表
STL list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。
- 特点:
- 可以看到,list 容器中各个元素的前后顺序是靠指针来维系的,每个元素都配备了 2 个指针,分别指向它的前一个元素和后一个元素。其中第一个元素的前向指针总为 null,因为它前面没有元素;同样,尾部元素的后向指针也总为 null。
- vector是连续的容器,而list是非连续的容器,即vector将元素存储在连续的存储器中,而list存储在不连续的存储器中。
- 优点: list 容器具有一些其它容器(array、vector 和 deque)所不具备的优势
- 可以在序列已知的任何位置快速插入或删除元素(时间复杂度为O(1))。
- 并且在 list 容器中移动元素,也比其它容器的效率高。
- 缺点
- 不能像 array 和 vector 那样,通过位置直接访问元素。
- 举个例子,如果要访问 list 容器中的第 6 个元素,它不支持容器对象名[6]这种语法格式,
- 也不支持
find()
语法,经常使用unorder_map<key, list<xxx>::iterator> listMap
保存元素位置来加速查找。- 正确的做法是从容器中第一个元素或最后一个元素开始遍历容器,直到找到该位置。
for (it = list.begin(); it != list.end(); it++) if (it->key == key) break;
- 正确的做法是从容器中第一个元素或最后一个元素开始遍历容器,直到找到该位置。
- 应用场景:需要对序列进行大量添加或删除元素的操作,而直接访问元素的需求却很少,这种情况建议使用 list 容器存储序列。
- 不能像 array 和 vector 那样,通过位置直接访问元素。
1 | //插入 |
数组
1 | int array[2][3] = { |
array
- 与数组同样,array对象的长度也是固定的,
- 分配空间的规则也与数组类似,
- 其效率与数组相同,但更方便,更安全。
1 |
|
vector
vector是变长的连续存储:
- 对于简单的类型,是直接存储
- 对于复杂的类,存储的是,该元素的信息(比如新构造元素的begin地址,end地址,capacity信息)
初始化
1 | //创建一个vector,元素个数为nSize |
1 | //改变大小,预分配空间,增加 vector 的容量(capacity),但 size 保持不变。 |
增加
vector 也支持中间insert元素,但是性能远差于list。
1 | void push_back(const T& x) //向量尾部增加一个元素X |
删除
1 | iterator erase(iterator it) :删除向量中迭代器指向元素 |
修改
1 | void swap(vector&) :交换两个同类型向量的数据 |
查找
1 | reference at(int pos) :返回pos位置元素的引用 |
其余
返回表示
1 | vector<int> func() { |
hash 哈希
1 |
|
static_cast
用于良性类型转换,一般不会导致意外发生,风险很低。
hash <K>
模板专用的算法取决于实现,但是如果它们遵循 C++14 标准的话,需要满足一些具体的要求。这些要求如下:
- 不能拋出异常
- 对于相等的键必须产生相等的哈希值
- 对于不相等的键产生碰撞的可能性必须最小接近 size_t 最大值的倒数
参考文献
[^1]: How to use unordered_set with custom types?
https://www.runoob.com/w3cnote/cpp-vector-container-analysis.html
【C++容器】数组和vector、array三者区别和联系
https://blog.51cto.com/liangchaoxi/4056308
https://blog.csdn.net/y601500359/article/details/105297918
————————————————
版权声明:本文为CSDN博主「stitching」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_40250056/article/details/114681940
————————————————
版权声明:本文为CSDN博主「FishBear_move_on」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/haluoluo211/article/details/80877558
————————————————
版权声明:本文为CSDN博主「SOC罗三炮」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/luolaihua2018/article/details/109406092
————————————————
版权声明:本文为CSDN博主「鱼思故渊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yusiguyuan/article/details/40950735