文章目录
STL容器分类
容器分为序列式容器和关联式容器:
- 序列容器是按照元素在容器中的顺序来存储的,元素的位置由插入的顺序决定,每个元素都有自己的位置索引,比如vector,deque,list。
- 关联容器是按照键值对的方式来存储的,每个元素由一个键(key)和一个值(value)组成,它们根据键来快速访问和检索元素,比如map,set。
1. string容器
string是类,里面封装了char*
和操作字符串的方法。std::string
类的数据结构:
class std::string {
private:
char* _data; // 指向字符数组的指针
size_t _size; // 当前字符串的长度
size_t _capacity; // 分配的容量
// 其他成员函数和数据
};
在典型的实现中:
- 在32位系统上,
std::string
对象的大小通常是12字节:3个4字节的成员变量。 - 在64位系统上,
std::string
对象的大小通常是24字节:3个8字节的成员变量。
size_t
类型的大小与系统架构有关,32位系统上通常是4字节,64位系统上通常是8字节
常用方法:
方法 | 描述 |
---|---|
size() | 返回字符串的长度。 |
length() | 返回字符串的长度(与 size() 等价)。 |
empty() | 检查字符串是否为空,返回布尔值。 |
append(str) | 在字符串末尾追加 str 。 |
insert(pos, str) | 在指定位置插入 str 。 |
erase(pos, len) | 删除从位置 pos 开始的 len 个字符。 |
replace(pos, len, str) | 用 str 替换从位置 pos 开始的 len 个字符。 |
substr(pos, len) | 返回从位置 pos 开始的子字符串,长度为 len 。 |
find(str) | 查找 str 在字符串中的位置,返回索引或 string::npos 。 |
rfind(str) | 从后向前查找 str 的位置,返回索引或 string::npos 。 |
compare(str) | 比较当前字符串与 str ,返回整数值。 |
c_str() | 返回 C 风格字符串(以 null 结尾的字符数组)。 |
at(pos) | 返回指定位置的字符,支持越界检查。 |
operator[] | 访问指定位置的字符,不支持越界检查。 |
2. vector容器
vector容器和普通数组区别:
不同之处在于数组是静态空间,存储空间一旦确定就没法修改,而vector可以动态扩展。
动态扩展并不是在原空间之后续接新空间,而是寻找更大的内存空间,将原数据拷贝到新空间,再释放原空间。
常用方法:
方法 | 描述 |
---|---|
size() | 返回 vector 中元素的数量。 |
capacity() | 返回 vector 分配的存储容量。 |
empty() | 检查 vector 是否为空,返回布尔值。 |
push_back(value) | 在 vector 末尾添加一个元素 value 。 |
pop_back() | 删除 vector 末尾的元素。 |
insert(pos, value) | 在指定位置 pos 插入元素 value 。 |
erase(pos) | 删除指定位置 pos 的元素。 |
clear() | 删除 vector 中的所有元素。 |
operator[] | 访问指定位置的元素,不支持越界检查。 |
at(pos) | 返回指定位置的元素,支持越界检查。 |
front() | 返回 vector 的第一个元素。 |
back() | 返回 vector 的最后一个元素。 |
resize(new_size) | 更改 vector 的大小,如果新大小小于当前大小则截断。 |
swap(other_vector) | 交换当前 vector 与 other_vector 的内容。 |
begin() | 返回指向 vector 第一个元素的迭代器。 |
end() | 返回指向 vector 末尾后一个位置的迭代器。 |
rbegin() | 返回指向 vector 最后一个元素的逆迭代器。 |
rend() | 返回指向 vector 开始前一个位置的逆迭代器。 |
3. deque容器
双端数组deque与vector的区别:
-
vector头部插入/删除效率低,数据量越大效率越低;deque相对而言,对头部的插入/删除速度比vector快;
-
vector访问元素时的速度会比deque快,和内部实现有关。
deque内部工作原理:
deque内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据,中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间。
常用方法:
方法 | 描述 |
---|---|
size() | 返回 deque 中元素的数量。 |
empty() | 检查 deque 是否为空,返回布尔值。 |
push_back(value) | 在 deque 末尾添加一个元素 value 。 |
push_front(value) | 在 deque 前面添加一个元素 value 。 |
pop_back() | 删除 deque 末尾的元素。 |
pop_front() | 删除 deque 前面的元素。 |
insert(pos, value) | 在指定位置 pos 插入元素 value 。 |
erase(pos) | 删除指定位置 pos 的元素。 |
clear() | 删除 deque 中的所有元素。 |
operator[] | 访问指定位置的元素,不支持越界检查。 |
at(pos) | 返回指定位置的元素,支持越界检查。 |
front() | 返回 deque 的第一个元素。 |
back() | 返回 deque 的最后一个元素。 |
resize(new_size) | 更改 deque 的大小,如果新大小小于当前大小则截断。 |
begin() | 返回指向 deque 第一个元素的迭代器。 |
end() | 返回指向 deque 末尾后一个位置的迭代器。 |
rbegin() | 返回指向 deque 最后一个元素的逆迭代器。 |
rend() | 返回指向 deque 开始前一个位置的逆迭代器。 |
4. list容器
链表(list)是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。
STL中的链表是一个双向循环链表。
由于链表的存储方式并不是连续的内存空间,因此链表list中的迭代器只支持前移和后移,属于双向迭代器。
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效,这在vector是不成立的。如vector容器放满后,则动态扩展,开辟新的空间来拷贝原有数据,则原来空间迭代器失效。
常用方法:
方法 | 描述 |
---|---|
size() | 返回 list 中元素的数量。 |
empty() | 检查 list 是否为空,返回布尔值。 |
push_back(value) | 在 list 末尾添加一个元素 value 。 |
push_front(value) | 在 list 前面添加一个元素 value 。 |
pop_back() | 删除 list 末尾的元素。 |
pop_front() | 删除 list 前面的元素。 |
insert(pos, value) | 在指定位置 pos 插入元素 value 。 |
erase(pos) | 删除指定位置 pos 的元素。 |
clear() | 删除 list 中的所有元素。 |
operator[] | 访问指定位置的元素(不直接支持,但可用迭代器实现)。 |
front() | 返回 list 的第一个元素。 |
back() | 返回 list 的最后一个元素。 |
resize(new_size) | 更改 list 的大小(不适用于 list ,常用于 vector )。 |
begin() | 返回指向 list 第一个元素的迭代器。 |
end() | 返回指向 list 末尾后一个位置的迭代器。 |
rbegin() | 返回指向 list 最后一个元素的逆迭代器。 |
rend() | 返回指向 list 开始前一个位置的逆迭代器。 |
sort() | 对 list 中的元素进行排序。 |
unique() | 删除相邻重复的元素。 |
reverse() | 反转 list 中元素的顺序。 |
5. set/multiset容器
set/multiset属于关联式容器,底层结构是用红黑树实现。存入的元素具有自动排序的特性,排序的依据是 key ,而 set/miltiset 元素的 key 与 value是合二为一的,其value 就是 key。
不能通过迭代器改变 set/multiset 元素的值,因为set元素值就是其键值,关系到set元素的排序规则。如果任意改变set元素值,会严重破坏set组织。 set容器的迭代器为可读迭代器。
红黑树是一种自平衡二叉搜索树,它具有以下特点:
-
节点是红色或黑色。
-
根节点是黑色。
-
所有叶子节点(NIL)是黑色。
-
红色节点的两个子节点都是黑色(即不存在两个连续的红色节点)。
-
从任一节点到其每个叶子的所有路径都包含相同数量的黑色节点。
例如如下图:
红黑树与普通二叉搜索平衡树(AVL树)的比较
AVL树 | 红黑树 | |
---|---|---|
平衡性 | 高度平衡,严格保证每个节点的左右子树高度差不超过1 | 相对AVL树平衡性稍差,但仍然保证树的高度为 O(log n) |
查找 | 树高通常比红黑树略低,在查找操作上会稍快 | 允许更高的树高,查找操作的性能略逊于 AVL 树 |
插入和删除 | 需要多次旋转来保持平衡,导致较高的旋转成本 | 最多需要进行两次旋转,旋转次数更少,通常更高效 |
实现和维护 | 插入和删除的实现相对复杂,维护成本较高 | 插入和删除的实现和维护相对简单,尤其在删除操作中优势更明显 |
综上,所以查找方法都是一样的,只是树的结构实现不同,导致插入/删除方法不同。
实际应用中,对于查找性能特别敏感的应用,AVL 树可能是更好的选择,而对于需要频繁插入和删除操作的应用,红黑树则更具优势。
常用方法:
方法 | 描述 |
---|---|
insert(value) | 向 set 或 multiset 插入一个元素 value 。 |
erase(value) | 删除 set 或 multiset 中的元素 value 。 |
find(value) | 查找 value 是否在 set 或 multiset 中,返回迭代器。 |
count(value) | 返回 set 或 multiset 中 value 的出现次数(set 返回 0 或 1)。 |
clear() | 删除 set 或 multiset 中的所有元素。 |
size() | 返回 set 或 multiset 中元素的数量。 |
empty() | 检查 set 或 multiset 是否为空,返回布尔值。 |
begin() | 返回指向 set 或 multiset 第一个元素的迭代器。 |
end() | 返回指向 set 或 multiset 末尾后一个位置的迭代器。 |
rbegin() | 返回指向 set 或 multiset 最后一个元素的逆迭代器。 |
rend() | 返回指向 set 或 multiset 开始前一个位置的逆迭代器。 |
equal_range(value) | 返回 value 的范围(对于 multiset ,返回一对迭代器)。 |
6. map/multimap容器
map/multimap 是以红黑树为底层结构的,因此存入的元素具有自动排序的特性,排序的依据是 key;
map/multimap 提供遍历操作与迭代器 iterator,通过不断的 iterator++ 遍历可以获取到已经按照 key 排好序的元素;
不能通过迭代器改变 map/multimap 元素的 key 值,因为key值关系到元素的排序规则。如果任意改变key值,会严重破坏map组织。但可以修改 key 对应的 value 值。因而 map/multimap 内部将 key type 设为 const,如此可以避免对 key 的随意修改。
常用方法:
方法 | 描述 |
---|---|
insert(pair) | 向 map 或 multimap 插入一个键值对 pair 。 |
erase(key) | 删除 map 或 multimap 中键为 key 的元素。 |
find(key) | 查找 key 是否在 map 或 multimap 中,返回迭代器。 |
count(key) | 返回 map 中 key 的出现次数(multimap 返回出现次数)。 |
clear() | 删除 map 或 multimap 中的所有元素。 |
size() | 返回 map 或 multimap 中元素的数量。 |
empty() | 检查 map 或 multimap 是否为空,返回布尔值。 |
begin() | 返回指向 map 或 multimap 第一个元素的迭代器。 |
end() | 返回指向 map 或 multimap 末尾后一个位置的迭代器。 |
rbegin() | 返回指向 map 或 multimap 最后一个元素的逆迭代器。 |
rend() | 返回指向 map 或 multimap 开始前一个位置的逆迭代器。 |
operator[] | 访问或插入键为 key 的元素(对于 map )。 |
equal_range(key) | 返回 key 的范围(对于 multimap ,返回一对迭代器)。 |
7. unordered_map / unordered_set / unordered_multimap / unordered_multiset
这四种容器采用哈希表实现,不同操作的时间复杂度为:
-
插入: O(1),最坏情况O(N)
-
查看: O(1),最坏情况O(N)
-
删除: O(1),最坏情况O(N)
HashMap是数组加链表组成的复合结构,HashMap的主干是数组,其中数组被分为一个个桶(bucket),每个桶存储有一个或多个键值对,每个键值对也称为 Entry ,通过哈希值决定了Entry对象在这个数组的下标;哈希值相同的Entry对象(键值对),则以链表形式存储。
举例说明:
传入第一个键值对Entry1时,调用put方法:
map.put( 99 , “huang” );
index = Hash(99)
假定最后计算出的index是333,那么就将这个Entry1存入到数组中下标为333的位置
Entry2 与 Entry3 同理
当然,数组的长度是有限的,而插入的Entry会越来越多,这个时候又该如何存储呢?这个时候就需要用到链表了。
此时我们再调用put方法:
map.put( 520 , “liuliu”)
index = Hash(520)
假定计算出的index还是333,而下标333处有一个Entry1,这时就出现了”hash冲突“。该如何插入呢?
HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点。每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可,而插入是根据“头插法”进行插入:
当然还有一种情况,当我想要修改我的Entry1中value的值,这时的过程是怎么样呢?
调用put方法:
map.put( 99 , “gege” );
key一样,调用哈希函数计算出的值当然也一样
此时已经确定应该在下标为333的位置。此时会去查询链表,查询当前的key:99 是否存在,如果存在,则用新的Entry18覆盖旧的Entry1。如果不存在则再次使用“头插法”将当前Entry插入到链表中。
我们想要找到key为99的value,那么过程是怎么样的呢?
当我们调用Get方法:
map.get( 99 );
根据哈希函数计算出key的值,可以算出其结果为333,则此时根据下标333处的链表来查询key
使用equals方法对比Entry17的key,返回结果为false,则继续查询,比对Entry18的key,返回结果为ture。表示已经匹配到了正确的key。
get步骤如下:
-
通过hash函数计算key的值,得到一个数组下标;
-
使用equals方法去对比下标处的头节点的key;
-
遍历链表,直至找到对应的key,返回value值。