C++STL之容器

C++STL中数据结构主要分为序列式容器和关联式容器。

1. 序列式容器

元素可序,但未必有序的容器。包含array,vector,list,deque,stack,queue等。

1.1. array

静态空间,类似数组,但比数组更加安全,配置完毕后不能改变大小。

array<int, 5> arr; // 生成一个长度为5的整型数组
1.2. vector

具有动态改变长度的向量容器。vector里面包含capacity表示当前vector的最大长度,size表示当前vector数据的长度。当添加数据时,若当前size>capacity,则需要重新配置,重新分配一块2*capacity大小的内存空间,将原vector的内容拷贝至新的vector中,再将原vector销毁。
stl_vector
可以看出,所谓的vector动态增长只是一个假象,背后一直在执行“配置新空间/数据移动/释还旧空间”的操作罢了。

vector<int> vec; // 生成一个整型向量
vec.push_back(5); // 将数字5放入向量中
vec.erase(vec.begin(), vec.end()); // 移除所有元素
vec.erase(vec.begin()); // 移除首元素
vec.clear(); // 移除所有元素
vec.insert(vec.begin(), 2, 5); // 在首位置添加2个数值等于5的元素
1.3. list

list插入,删除操作都为 O ( 1 ) O(1) O(1)

list<int> li; // 生成一个整型链表
li.push_back(5); // 将数字5放入链表末尾
li.push_front(1); // 将数字1放入链表头
auto iter = li.erase(li.begin()); // 删除第一个元素,返回下一个元素的迭代器
li.pop_front(); // 移除首元素
li.pop_back(); // 移除尾元素
li.clear(); // 移除所有元素
li.remove(5); // 移除所有值等于5的元素
li.unique(); // 移除数值相同的连续元素
1.4. slist

单向链表,用法同list。

1.5. deque

vector只能从尾加入元素,deque则是一种双向开口的连续线性空间。deque实现通过指针数组的形式实现。它是由一段一段连续的空间构成,通过指针数组的指针指向这些连续的段,来构成一个大的数组。
stl_deque
queue进行数据添加时,当达到最大容量,不需要进行拷贝销毁操作,只需要将map数组增大,加入新的缓冲区即可。

deque<int, alloc, 32> deq; // 生成一个每个缓冲区为32,内存分配为alloc的整型双向队列
deq.push_back(5); // 将数字5放入队列末尾
deq.push_front(1); // 将数字1放入队列头
deq.erase(deq.begin()); // 删除第一个元素,返回下一个元素的迭代器
deq.erase(deq.begin(), deq.end()); // 删除所有元素
deq.pop_front(); // 移除首元素
deq.pop_back(); // 移除尾元素
deq.clear(); // 移除所有元素
deq.insert(deq.begin(), 5); // 在首位置插入数值等于5的元素
1.6. stack

先入后出(FILO)的数据结构。默认以deque作为底层实现,也可以用list作为底层实现。stack没有迭代器

stack<int> stk; // 生成一个整型的栈,内部实现默认使用deque。
stack<int, list<int> > lstk; // 生成一个内部实现使用list的整型栈。
stk.push(5); // 将数字5压入栈
stk.pop(); // 将栈内首元素弹出
stk.top(); // 获取栈首元素数值
stk.empty(); // 查看当前栈是否为空
stk.size(); // 返回当前栈的大小
1.7. queue

先入先出(FIFO)的数据结构。默认以deque作为底层实现,也可以用list作为底层实现。queue没有迭代器

queue<int> que; // 生成一个整型的队列,内部实现默认使用deque。
queue<int, list<int> > lque; // 生成一个内部实现使用list的整型队列。
que.push(5); // 将数字5压入队列
que.pop(); // 将队列内首元素弹出
que.front(); // 获取队列首元素数值
que.back(); // 获取队列末元素数值
que.empty(); // 查看当前队列是否为空
que.size(); // 返回当前队列的大小
1.8. priority_queue

优先队列是带有权值的队列,每次出队的数据均为队列中所有数据权值最高的数据。起内部实现使用的是vector/array数据结构和heap算法。priority_queue不能遍历且没有迭代器

priority_queue<int> pque; // 生成一个整型的最大优先队列
pque.push(5); // 将数字5压入队列
pque.pop(); // 将队列内首元素弹出
pque.top(); // 获取队列首元素数值
pque.empty(); // 查看当前队列是否为空
pque.size(); // 返回当前队列的大小
1.8.1. heap算法

heap算法利用完全二叉树进行设计。

  1. 元素插入
    新元素插入时,把新元素放在数组的末尾,即最右一个叶节点处。然后开始上溯,依次和父节点比较,如果比父节点大,则与父节点调换位置,再继续上溯,否则结束。
    heap_push
  2. 元素弹出
    元素弹出是,即把根节点的值弹出,此时需要重新调整二叉树,使其满足最大堆。首先将最后一个叶节点与根节点调换位置(此时这个叶子已经没有了),然后进行下溯,依次和左右子节点比较,如果小于两个子节点的最大值,则与较大的子节点调换位置,否则结束。
    heap_pop
    pop完成之后并没有将最大值删除,而是将其放在了末尾。
1.8.2. priority_queue实现

priority_queue是通过vector/array数据结构,利用heap算法实现的。根节点的索引为1(索引为0的元素可置空不用),若某个节点的索引为 i i i,则其左子节点的索引为 2 i 2i 2i,其右子节点的索引为 2 i + 1 2i+1 2i+1,其父节点的索引为 i / 2 i/2 i/2

2. 关联式容器

每个元素都有一个键值(key)和一个实值(value)的容器。关联式容器没有头尾,只有最大和最小元素。关联式容器主要有set,map,multiset,multimap,hash_set,hash_map,hash_multiset,hash_multimap。非hash的关联式容器底层采用RB-tree实现,所以这里先介绍一下RB-tree。

2.1. RB-tree

RB-tree(红黑树)是一种平衡的二叉搜索树,这里先介绍二叉搜索树,再介绍比较简单的平衡二叉树AVL-tree,最后介绍RB-tree。

2.1.1. 二叉搜索树

二叉搜索树能够提供对数时间的插入和访问。规则是:任何节点的键值一定大于其左子树的每一个节点的键值,并小于其右子树的每一个节点的键值。从根节点一直向左,就得到最小值,一直向右,就得到最大值。

  1. 二叉搜索树的插入
    从根节点开始,遇到键值大的就向左,遇到键值小的就向右走,直到到达尾端。
    bst_insert
  2. 二叉搜索树的删除
    首先找到需要删除的点,从根节点按大小向左右访问即可。
    a. 若被删除节点无子节点,直接删除即可。
    b. 若被删除节点只有一个子节点,则将该子节点连至该节点的父节点即可。
    bst_del1
    c. 若被删除节点有两个子节点,则用右子树的最小节点取代该节点即可。右子树的最小节点即为右子树的最左节点。
    bst_del2
2.1.2. AVL-tree

从二叉搜索树可以看出,搜索的效率与树的深度有关,如果树的层数太多会严重影响效率,为了提高访问的效率,提出了平衡树的概念,避免任何一个节点深度过大。AVL-tree是一种平衡树,它要求任何节点的左右子树高度相差最多1。
AVL-tree插入删除后,出现不平衡后,需要对树进行调整,使其再次平衡,调整一共有两种情况。

  1. 情况1:外侧不平衡。即不平衡后最大深度的节点在父节点和叔节点外侧。
    这里以左左为例,即最大深度点为父节点的左侧,叔节点在父节点右侧。这种情况要是二叉树恢复平衡,需要对其爷爷节点进行右旋操作。爷爷节点的父节点(祖父节点)作为爷爷节点的右子节点,爷爷节点的右子节点(叔叔节点)作为组父节点的左子节点。这样二叉树就恢复了平衡。如果此时不平衡情况为右右,则进行左旋即可。
    avl_ll
  2. 情况2:内侧不平衡。即不平衡后最大深度的节点在父节点和叔节点内侧。
    这里以左右为例,即最大深度点为父节点的右侧,叔节点在父节点右侧。这种情况要是二叉树恢复平衡,需要对其父节点进行一次左旋操作, 再对其爷爷节点进行一次右旋操作。
    avl_lr
2.1.3. RB-tree

红黑树是一个被广泛运用的平衡二叉树,满足以下性质:

  1. 每个节点不是红色就是黑色。
  2. 根节点为黑色。
  3. 如果节点为红,其子节点必须为黑色。
  4. 任一节点至NULL节点的任何路径,所含黑节点数目相同。

根据规则4,新增节点必须为红色,有根据规则3,新增节点的父节点必须为黑色。当插入新节点后不满足上述条件,则需要调整颜色并旋转树形使其满足条件。(插入和删除规则以后有空再写,图画着太累了。。。)

2.2. set

set是只包含实值,没有键值的关联式容器。其不允许有相同值的元素,且值不可改变,底层利用红黑树实现。

set<int> iset; // 生成一个整型的集合
iset.insert(3); // 将3加入集合
iset.erase(3); // 将3移除集合
iset.size(); // 返回当前集合的大小
2.3. map

map所有元素都是pair,同时拥有键值和实值,其键值不可重复。底层使用红黑树实现,所以map是按键值排好序的。

map<string, int> simap; // 生成一个键值为string,实值为整型的字典
simap[string("math")] = 97;
simap.insert(pair<string, int>(string("english"), 88));
2.4. multiset

multiset和set用法相同,不同之处在于multiset允许元素重复。

2.5. multimap

multimap和map用法相同,不同之处在于multimap允许键值重复。

2.6. hash_table

散列表就是将分散的元素通过散列函数映射在一起,使键值对的基本操作时间都为常数。
hash
其键值key通过散列函数 f ( x ) f(x) f(x),映射到hash_table,相应位置保存该key对应的实值value,则通过计算 f ( k e y ) f(key) f(key)就能在常数时间取得对应的实值。
hash_table最主要解决的就是如何避免和解决冲突,即通过hash函数将不同键值映射到哈希表的不同位置,映射到相同位置需要采取的措施。

2.6.1 线性探测

映射到相同位置是,依次向后寻找,直到找到一个空的位置。读取的时候相同,读出的值与目标不符则继续向后搜寻。删除时则标记删除符号,待表格重新整理后,再进行实际删除。

2.6.2 二次探测

线性探测处理冲突,使用 H , H + 1 , H + 2 , . . . , H + i H,H+1,H+2,...,H+i HH+1H+2...H+i,来搜寻下一个位置,这样容易造成主集团问题,即所有插入元素集中在一块,影响效率。二次探测则使用 H , H + 1 2 , H + 2 2 , . . . , H + i 2 H,H+1^2,H+2^2,...,H+i^2 HH+12H+22...H+i2,来进行搜寻,可有效的避免主集团。

2.6.3 开链法

开链法就是哈希表里面保存的是链表,每次哈希到相同值时,则在该位置的链表后插入,若链表足够短,则仍可以保证搜寻效率。STL中的hash_table就是采用的开链法。
hash_list

2.7. hash_set

hash_set用法和set一样,只不过底层实现使用哈希表而已,不同之处在于set具有自动排序功能,而hash_set没有。

2.8. hash_map

hash_map用法和map一样,只不过底层实现使用哈希表而已,不同之处在于map具有自动排序功能,而hash_map没有。

2.9. hash_multiset

允许元素重复的hash_set。

2.10. hash_multimap

允许键值重复的hash_map。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值