STL容器详解(C++)


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)交换当前 vectorother_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)setmultiset 插入一个元素 value
erase(value)删除 setmultiset 中的元素 value
find(value)查找 value 是否在 setmultiset 中,返回迭代器。
count(value)返回 setmultisetvalue 的出现次数(set 返回 0 或 1)。
clear()删除 setmultiset 中的所有元素。
size()返回 setmultiset 中元素的数量。
empty()检查 setmultiset 是否为空,返回布尔值。
begin()返回指向 setmultiset 第一个元素的迭代器。
end()返回指向 setmultiset 末尾后一个位置的迭代器。
rbegin()返回指向 setmultiset 最后一个元素的逆迭代器。
rend()返回指向 setmultiset 开始前一个位置的逆迭代器。
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)mapmultimap 插入一个键值对 pair
erase(key)删除 mapmultimap 中键为 key 的元素。
find(key)查找 key 是否在 mapmultimap 中,返回迭代器。
count(key)返回 mapkey 的出现次数(multimap 返回出现次数)。
clear()删除 mapmultimap 中的所有元素。
size()返回 mapmultimap 中元素的数量。
empty()检查 mapmultimap 是否为空,返回布尔值。
begin()返回指向 mapmultimap 第一个元素的迭代器。
end()返回指向 mapmultimap 末尾后一个位置的迭代器。
rbegin()返回指向 mapmultimap 最后一个元素的逆迭代器。
rend()返回指向 mapmultimap 开始前一个位置的逆迭代器。
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值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值