C++ STL库


前言

C++ STL库,容器的实现之类的以及部分底层的代码和对应的数据结构。STL库⼀共提供六⼤组件,包括容器,算法,迭代器,仿函数,配接器和配置器
在这里插入图片描述

  • heap内含一个vector,而priority-queue内含一个heap。
  • stack和queue内含deque,因为它们都是在deque的基础上关闭某些功能而形成的。
  • set、map、multiset 和 multimap都内含一颗红黑树,因此可实现自动排序功能。
  • unordered_set、unordered_map、unordered_multiset 和 unordered_multimap 内含一个哈希表,实现快速查找功能。

一、STL库的组件

容器: 各种数据结构,如 vector,list,deque,set,map,⽤来存放数据, 从实现的⻆度来讲是⼀种类模板。

算法: 各种常⽤的算法,如 sort(插⼊,快排,堆排序),search(⼆分查找), 从实现的⻆度来讲是⼀种⽅法
模板。

迭代器: 从实现的⻆度来看,迭代器是⼀种将 operator*,operator->,operator++, operator–等指针相关操作赋予重载的类模板,所有的 STL 容器都有⾃⼰的迭代器。

仿函数: 从实现的⻆度看,仿函数是⼀种重载了 operator() 的类或者类模板。 可以帮助算法实现不同的策略。

配接器: ⼀种⽤来修饰容器或者仿函数或迭代器接⼝的东⻄。

配置器: 负责空间配置与管理,从实现的⻆度讲,配置器是⼀个实现了动态空间配置、空间管理,空间释放的类模板。

二、序列式容器

1、vector

是动态空间,随着元素的加⼊,它的内部机制会⾃⾏扩充空间以容纳新元素。vector 维护的是⼀个连续的线性空间,⽽且普通指针就可以满⾜要求作为 vector 的迭代器(RandomAccessIterator)。

vector 的数据结构中其实就是三个迭代器构成的,⼀个指向⽬前使⽤空间头的 iterator,⼀个指向⽬前使⽤空间尾的iterator,⼀个指向⽬前可⽤空间尾的 iterator。当有新的元素插⼊时,如果⽬前容量够⽤则直接插⼊,如果容量不够,则容量扩充⾄两倍,如果两倍容量不⾜, 就扩张⾄⾜够⼤的容量。

扩充的过程并不是直接在原有空间后⾯追加容量,⽽是重新申请⼀块连续空间,将原有的数据拷⻉到新空间中,再释放原有空间,完成⼀次扩充。需要注意的是,每次扩充是重新开辟的空间,所以扩充后,原有的迭代器将会失效。
在这里插入图片描述

解决 迭代器 失效的问题:

用容器迭代器erase失效情形如下。

  • 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器

  • 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。(解决方式)

  • 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用

详解vector迭代器失效

  1. 会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back…

    解决方案:只需给 迭代器 重新赋值即可

  2. 任意位置元素的删除操作 erase
    1、erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效。
    2、如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。

    解决方案:删除到最后时,把这个迭代器 弃用掉 或者 重定向 就行了。
    ps : erase的过程 先将后面的元素搬到需要删除的元素处,再将后面多余的元素析构掉

#include <vector>
#include <iostream>

using namespace std;

int main()
{
	vector<int> v{ 1, 2, 3, 4, 5, 6, 7 }; //C++ 11语法
	vector<int>::iterator my_it = v.begin();

	while (my_it != v.end()){
        cout<< * my_it << endl;
		my_it = v.erase(my_it);
        
		// ++my_it;
	}
	return 0;
}
  1. 当 vector 数组超过容量,会开辟新的内存,并将原来内存的元素移过来,导致原来的内存的迭代器失效

    解决方案:使用智能指针。

2、list

list 是⼀个双向链表,普通指针已经不能满⾜ list 迭代器的需求,因为 list 的存储空间是不连续的。list 的迭代器必需具备前移和后退功能,所以 list 提供的是 BidirectionalIterator。list 的数据结构中只要⼀个指向 node 节点的指针就可以了
在这里插入图片描述

list为了方便使用,也定义了一个list_node_allocator用于以节点大小为单位分配空间。

list内部提供一个迁移操作(transfer),可以将连续范围的元素迁移到某个特定位置之前。在这个操作的基础上,可以很轻松地实现splice(接合操作,即transfer的公开接口)、sort(排序)、merge(合并两个升序排列的链表)等操作。

3、deque

deque 则是⼀种双向开⼝的连续线性空间,⽀持从头尾两端进⾏元素的插⼊和删除操作。相⽐于 vector 的扩充空间的⽅式,deque 实际上更加贴切的实现了动态空间的概念。deque 没有容量的概念,因为它是动态地以分段连续空间组合⽽成,随时可以增加⼀段新的空间并连接起来。

deque 采⽤⼀块所谓的 map 作为主控,这⾥的 map 实际上就是⼀块⼤⼩连续的空间,其中每⼀个元素,我们称之为节点 node,都指向了另⼀段连续线性空间称为缓冲区,缓冲区才是 deque 的真正存储空间主体。

STL 是允许我们指定缓冲区的⼤⼩的,默认值0表示使⽤ 512bytes 缓冲区。当 map 满载时,我们选⽤ ⼀块更⼤的空间来作为 map,重新调整配置。deque 另外⼀个关键的就是它的 iterator 的设计,deque 的 iterator 中有四个部分,cur 指向缓冲区现⾏元素,first 指向缓冲区的头,last 指向缓冲区的尾(有时会包含备⽤空间),node指向管控中⼼(map)。
在这里插入图片描述

4、stack 和 queue

  • stack 是⼀种先进后出的数据结构,只有⼀个出⼝,stack 允许从最顶端新增元素,移除最顶端元素,取得最顶端元素。deque 是双向开⼝的数据结构,所以使⽤ deque 作为底部结构并封闭其头端开⼝,就形成了⼀个 stack。
  • queue 是⼀种先进先出的数据结构,有两个出⼝,允许从最底端加⼊元素,最顶端取得(移除)元素。deque 是双向开⼝的数据结构,若以 deque 为底部结构并封闭其底端的出⼝,和头端的⼊⼝,就形成了⼀个 queue。(其实 list 也可以实现 queue)

5、priority_queue

堆(heap)并不属于 STL 容器组件,它是个幕后英雄,扮演 priority_queue 的助⼿,priority_queue 允许⽤户以任何次序将任何元素推⼊容器内,但取出时⼀定是从优先权最⾼(数值最⾼)的元素开始取。⼤根堆(binary max heap)正具有这样的性质,适合作为 priority_queue 的底层机制。大根堆和小根堆这里不说了。

priority_queue 底层是⼀个 vector,使⽤ heap 形成的算法插⼊,获取 heap 中元素的算法,维护这个 vector,以达到允许⽤户以任何次序将任何元素插⼊容器内,但取出时⼀定是从优先权最⾼(数值最⾼)的元素开始取的。

6、slist

slist为SGI STL提供的单向链表,与list的区别在于,它的迭代器属于Forward Iterator,而list的迭代器属于Bidirectional Iterator。

三、关联式容器

标准的STL关联式容器分为set(集合)和map(映射表)两大类,以及两大类的衍生体multiset(多键集合)和multimap(多建映射)。标准之外又提供了unordered_set和unordered_map两大类及其衍生体unordered_multiset和unordered_multimap。两者显而易见的区别就是是否会自动排序,而这个区别正是由于它们底层实现的不同,标准的STL关联式容器底层采用红黑树结构,而非标准STL关联式容器采用的是哈希表。 红黑树和哈希表容器是不对外开放的。

1、红黑树

红黑树的本质是 二叉搜索树(或者说是 2-3-4树的变体),红黑树查找的时间复杂度为O(logn)。

性质:

  1. 节点非黑即红
  2. 根节点是黑色
  3. 叶子节点(Nil,辅助空节点)是黑色
  4. 红色节点的子节点一定是黑色
  5. 任一节点到(非叶子节点)到叶子节点的黑色节点个数相等

旋转和变色:

  • 变色:原节点是红色,就置为黑色,若原节点是黑色,就置为红色。
  • 左旋:如下图,红色节点为旋转支点,支点往左子树移动即为左旋。左旋之后,原支点的位置被原支点的右子节点代替,新支点的左子节点变为原支点的右子节点。
    在这里插入图片描述
  • 右旋:右旋操作和左旋相反,如下图。
    在这里插入图片描述
    插入
    为了方便节点的插入,默认新插入的节点为红色。(下面描述以插入左子树为例)
  1. 红黑树为空
    插入节点作为根节点,然后变为黑色。

  2. 父节点为黑色,直接插入

  3. 父节点为红色,以父节点是祖父节点的左孩子为例
    ① 插入后为左孩子,叔节点(父节点的兄弟节点)为黑色
    第一步:父节点,祖父节点变色
    第二部:祖父节点右旋
    在这里插入图片描述
    ② 插入后为右孩子,叔节点(父节点的兄弟节点)为黑色
    第一步:父节点左旋
    第二步:变为情况①
    在这里插入图片描述
    ③ 插入后,叔节点(父节点的兄弟节点)为红色
    第一步:父节点、叔节点、祖父节点直接变色
    第二步:祖父节点的父节点是黑色就停止,是红色则根据 情况①或者情况② 继续调整。

在这里插入图片描述
删除

  1. 红黑树本质是二叉搜索树,删除需要符合二叉搜索树的删除规则:
    ①无孩子节点:直接删除
    ②有一个孩子节点:直接删除,孩子节点代替被删除节点
    ③有两个孩子节点:删除,右子树最小节点(左子树最大节点)代替被删除节点
  2. 红黑树调整 推荐B站视频
    ①红节点直接删除,不影响红黑树的平衡
    ②删除的是黑节点(影响黑高),假设代替被删节点的是X,X的兄弟节点是W,且X在左侧,一共有四种情况:
    a、W是黑色,其子节点都是黑色
    1. 首先将 W 变成红色
    2. X = X->parent
    3. 继续向上调整,直到 X 变为根节点,或者 X 为红色节点,退出循环,
    4. 退出循环时,将 X 节点设为黑色

在这里插入图片描述
b、W是黑色,右子节点是红色
1. W -> color = X -> parent -> color
2. X -> parent -> color = black;
3. W -> right -> color = black
4. X 的父节点左旋
在这里插入图片描述
c、W是黑色,子节点左红右黑
1. 将 W 和 W 左孩子颜色互换
2. W 右旋
3. W = W -> parent
4. 变成了情况 b
在这里插入图片描述
d、W 是红色

  1. W 和 X 的父节点颜色互换
  2. X 的父节点左旋
  3. W = X -> parent->right
  4. 变成前面三种情况的一种
    在这里插入图片描述

2、map 和 set 主要区别

  • map 中的元素是 key-value(关键字—值)对:关键字起到索引的作⽤,值则表示与索引相关联的数据;Set 与之相对就是关键字的简单集合,set 中每个元素只包含⼀个关键字
  • set 的迭代器是 const 的不允许修改元素的值map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么⾸先需要删除该键,然后调节平衡,再插⼊修改后的键值,调节平衡,如此⼀来,严重破坏了map和set的结构,导致 iterator 失效。所以STL中将 set 的迭代器设置成const,不允许修改迭代器的值;⽽ map 的迭代器则不允许修改key值,允许修改value值。
  • map⽀持下标操作,set不⽀持下标操作。map可以⽤key做下标,map的下标运算符[ ]将关键码作为下标去执⾏查找,如果关键码不存在,则插⼊⼀个具有该关键码和mapped_type类型默认值的元素⾄map中,因此下标运算符[ ]在map应⽤中需要慎⽤,const_map不能⽤,只希望确定某⼀个关键值是否存在⽽不希望插⼊元素时也不应该使⽤,mapped_type类型没有默认值也不应该使⽤。如果 find 能解决需要,尽可能⽤find。

3、hashtable

哈希表(HashTable)又叫做散列表,是根据关键码值(即键值对)而直接访问的数据结构。也就是说,它通过把关键码映射到表中一个位置来访问记录,这个映射函数就叫做散列(哈希)函数,存放记录的数组叫做散列表。

常见的哈希函数

  • 直接定址法:取关键字或关键字的某个线性函数值为散列地址,即H(key)=key或H(key) = a•key + b
  • 数字分析法:数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
  • 除留余数法:取关键字 被某个不大于散列表表长m的数p 除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。
  • 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
  • 平方取中法:取关键字平方后的中间几位作为散列地址。
  • 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
  • 斐波那契数列作为哈希函数
  • redis的 MurmurHash算法

哈希冲突
通过哈希函数,我们可以将键转化为数组的索引(0~M-1),但是对于两个或者多个键具有相同的索引值得情况,我们需要一种处理这种情况的方法。

解决方法:

  • 拉链法
  • 开放寻址法
    线性探查法
    平方探测法
    再哈希法

C++ 散列表解决冲突

在这里插入图片描述
STL库曾经有一个版本的hashtable是用vector+单向链表实现
在这里插入图片描述
这个版本我不敢确定是否准确,我看了C++17的源码,貌似是这么做的,不过Bucket的名字改成了 _Vec,大家参考参考把。


总结

参考文章:
C++ 通过指针访问vector中的元素失效问题解决方案
STL容器
C++ stl迭代器 (vector迭代器失效问题)
红黑树详解
HashTable详解
C++ STL 的散列表是如何解决冲突的?

之前写的红黑树删除有问题,目前我在写STL库,有兴趣的可以去看看我的 GitHub,里面有完整的红黑树实现STL

有时间我想写一下STL库的编写过程,哦里给。

我只是知识的搬运工,希望未来有一天我能够做点自己的东西吧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值