STL剖析(二):容器底层数据结构及常见用法-创新互联
本文主要聚焦于STL容器,STL完整的容器分类体系如下所示,下文将逐一对各个容器底层的数据结构以及常见用法进行介绍。
成都创新互联公司是专业的郊区网站建设公司,郊区接单;提供成都网站建设、做网站,网页设计,网站设计,建网站,PHP网站建设等专业做网站服务;采用PHP框架,可快速的进行郊区网站开发网页制作和功能扩展;专业做搜索引擎喜爱的网站,专业的做网站团队,希望更多企业前来合作!测试环境:Ubuntu 22.04 g++ 11.3.0
顺序容器都对应着线性数据结构。
2.1 arrayarray的使用需要引入头文件,它表示固定大小的数组,与C风格的数组一致。在定义array时,需要指定类型
T
和数组大小N
,即array
,array创建及常见用法如下:
#include#include
using namespace std;
int main()
{arrayarr1;
// 对array填充特定的值
arr1.fill(5); // arr1: {5, 5, 5}
// 遍历数组方式一
for (int a : arr1) {cout<< a<< " ";
}
cout<< endl; // 5 5 5
//遍历数组方式二
for (auto it = arr1.begin(); it != arr1.end(); it++) {cout<< *it<< " ";
}
cout<< endl; // 5 5 5
// 定义时指定初值
arrayarr2 = {2, 5, 4};
// 交换两个数组的内容,前提是类型和数组大小都要相同
arr1.swap(arr2); // arr1: {2, 5, 4}, arr2: {5, 5, 5}
// 对数组进行排序
arrayarr3 = {3.14, 2.56 };
// 获取数组的大小
cout<< arr3.size()<< endl; // 2
// 获取数组的第一个元素元素
cout<< arr3.front()<< endl; // 3.14
// 获取数组的最后一个元素
cout<< arr3.back()<< endl; // 2.56
// 获取数组的首地址
cout<< arr3.data()<< endl;
return 0;
}
2.2 vectorvector表示动态数组,同array一样,vector中的元素也是连续存储在内存中的,但vector的大小是可变的。在使用vector时需要引入头文件
,一般而言使用vector时仅需要指定数据类型T
即可。
需要注意的是,vector并不是每添加一个元素就分配一次新的存储空间,实际上,当vector分配的容量全部填满元素后再添加新元素时,vector的分配器会重新分配两倍容量的新空间,然后将元素挪过去。vector示意图如下:
图源自侯捷老师的《STL源码剖析》,侵权删。
注意:各家C++的实现可能略有区别,上述的重新分配规则不一定适用,但可以确定的是vector分配容量时一般都要未雨绸缪,即容量要比实际大小更大,否则需要重新分配。
vector的创建及常见用法示例如下:
#include#includeusing namespace std;
int main()
{// 创建空vector
vectorarr1;
// vector末端插入元素
arr1.push_back(1);
arr1.push_back(3);
arr1.push_back(5);
arr1.push_back(7);
arr1.push_back(9);
// 创建时指定初值
vectorarr2 = {1,2,3 };
// 创建时指定大小和默认值
vectorarr3(3, 5); // arr3: {5, 5, 5}
// 遍历方式一
for (int a : arr1) {cout<< a<< " ";
}
cout<< endl; // 1 3 5 7 9
// 遍历方式二
for (auto it = arr2.begin(); it != arr2.end(); it++) {cout<< *it<< " ";
}
cout<< endl; // 1 2 3
// 索引vector元素
cout<< arr1[1]<< endl; // 3
// 获取vector的第一个和最后一个元素
cout<< arr2.front()<< " "<< arr2.back()<< endl; // 1 3
// 判断数组是否为空
cout<< arr1.empty()<< endl; // 0
// 获取数组的大小和实际容量
cout<< arr1.size()<< " "<< arr1.capacity()<< endl; // 5 8
// 测试是否2倍扩张容量
int n = 4;
for (int i = 0; i< n; i++)
arr1.push_back(i);
cout<< arr1.size()<< " "<< arr1.capacity()<< endl; // 9 16
// 清空vector的元素
arr1.clear();
// 插入元素,需要通过迭代器指定位置
arr3.insert(arr3.begin(), 0); // arr3: {0, 5, 5, 5}
// 删除vector末端的元素
arr3.pop_back(); // arr3: {0, 5, 5}
// 删除vector指定位置的元素,需要通过迭代器指定位置
arr3.erase(arr3.begin());
return 0;
}
注:对于vector,索引任意位置的元素的时间复杂度为 O ( 1 ) O(1) O(1),往末端插入或删除末端元素的时间复杂度为 O ( 1 ) O(1) O(1),往其它位置插入或删除其它位置元素的时间复杂度为 O ( n ) O(n) O(n)。
2.3 dequedeque表示双向队列,是一种双向开口的"连续"空间。要使用deque需要引入头文件
,并指定数据类型T
。
连续只是用户看起来,实际deque的存储并非连续的,而是由一系列单独分配的固定大小的数组组成,另外,deque中还需要记录模块(中控器)来维护记录各个段的信息。
与vector比起来,deque的扩展更廉价,因为它不涉及将现有元素复制到新的内存位置。deque的示意图如下所示,其中map便是中控器,其中每个元素都指向一段连续的空间,称之为缓冲区,缓冲器才是用来存储数据的。从下图可以看出,deque还需要维护start和finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素。此外,这两个迭代器还需要指向map,否则无法在不同的缓冲区间转移。
图源自stackoverflow: What really is a deque in STL?
deque的创建以及常见用法示例如下:
#include#includeusing namespace std;
int main()
{dequed = {1, 3, 5, 8, 10};
//在deque开头插入元素
d.push_front(0); // d: {0,1,3,5,8,10}
// 删除deque开头的元素
d.pop_front(); // d: {1,3,5,8,10}
// 在deque结尾插入元素
d.push_back(7); // d: {1,3,5,8,10,7}
//删除deque结尾处的元素
d.pop_back(); // d: {1,3,5,8,10}
// 在指定位置插入元素,会返回一个指向插入元素的迭代器
auto it = d.begin();
it++;
it = d.insert(it, 11); // d: {1,11,3,5,8,10}
for (; it != d.end();it++)
{cout<< *it<< " ";
}
cout<< endl; // {11,3,5,8,10}
// 删除指定范围的元素
d.erase(d.begin() + 2, d.begin() + 5); // {1,11,10}
//获取deque开头和结尾的元素
cout<< d.front()<< " "<< d.back()<< endl; // 1 10
// 获取deque的大小
cout<< d.size()<< endl; // 3
return 0;
}
注:deque在开头和结尾可以进行元素的快速插入和删除。
2.4 listlist表示环形双向链表,链表大的特点的大小可变且存储非连续。要使用list需要引入头文件
,创建对象时需要传入数据类型T
。
STL中list节点结构示意图如下图所示,其中包含两个指针prev
和next
分别指向当前节点的前一个和后一个节点,data
存储当前节点的数据。
图源自侯捷老师的《STL源码剖析》,侵权删。
基于节点的结构,list的可视化示意图如下所示,可以看到,由于环状要求,还需要一个空白节点(红框标记)。
list的创建与常见用法如下所示:
#include#include#include
using namespace std;
int main()
{// 创建list并初始化
listl = {3, 5, 7, 9};
// 在list前面添加一个数值为1的节点
l.push_front(1);
// 在list后面添加一个数值为11的节点
l.push_back(11);
// 遍历链表
for(int a:l){cout<< a<< " ";
}
cout<< endl; // 1 3 5 7 9 11
// 删除链表头的元素
l.pop_front(); // l: {3,5,7,9,11}
// 删除链表尾的元素
l.pop_back(); // l:{3,5,7,9}
// 寻找链表中大于6的第一个元素
auto it = upper_bound(l.begin(), l.end(), 6);
// 链表中插入元素
if(it != l.end())
l.insert(it, 6); // l: {3,5,6,7,9}
// 删除满足某些特殊条件的元素
l.remove_if([](const int x)
{return x< 5;
}); // l: {5,6,7,9}
// 删除值等于某个特定值的元素
l.remove(7); // l:{5,6,9}
listl1 = {3, 1, 7, 10};
// 对链表进行排序
l1.sort(); // l1: {1,3,7,10}
// 归并两个有序链表
l.merge(l1); // l:{1,3,5,6,7,9,10}
//对链表进行逆序
l.reverse(); // l:{10,9,7,6,5,3,1}
// 获取链表的大小
cout<< l.size()<< endl; // 7
// 获取链表头的元素
cout<< l.front()<< endl; // 10
// 获取链表尾的元素
cout<< l.back()<< endl; // 1
return 0;
}
注:list插入和和删除元素的时间复杂度为 O ( 1 ) O(1) O(1)。
2.5 forward_listfoward_list为单向链表,其行为与list类似,只不过只能单向访问。要使用forward_list需要引入头文件
,创建对象时需要指定数据类型参数T
。
forward_list的示意图如下所示:
forward_list的创建与常见用法如下所示:
#include#includeusing namespace std;
int main()
{// 创建对象并初始化
forward_listl = {2, 4, 6, 8};
// 在链表开头插入一个元素
l.push_front(0); // l {0,2,4,6,8}
// 删除链表头的元素
l.pop_front(); // {2,4,6,8}
// 在链表中指定位置后插入元素,位置通过迭代器来指定
auto it = l.begin();
it++;
l.insert_after(it, 10); // {2,4,10,6,8}
return 0;
}
三.关联容器forward_list的好多操作与list类似,这里不做重复赘述。
关联容器底层的数据结构是红黑树。
3.1 红黑树红黑树是一种平衡二叉搜索树,其示意图如下所示:
红黑树需要满足如下规则:
- 每个节点不是红色就是黑色。
- 根节点需为黑色。
- 红节点的子节点不能为红色。
- 任一节点至NULL(树尾端)的任何路径所包含的黑色节点数须相同。
当新节插入时,若未符合上述规则,则需要调整颜色并旋转树形,使得其仍然是保持上述性质的平衡二叉搜索树。
红黑树中节点是有序的,即每个节点的左子树中各个节点的值都小于根节点,右子树中所有节点的值都大于根节点。
红黑树的插入、删除和查找操作的时间复杂度都是 O ( log N ) O(\text{log}N) O(logN)。
3.2 容器说明限于篇幅,本文不对红黑树进行展开讲解,后续将会写一篇专门的博客来介绍红黑树。
对于set、multiset、map、multimap四种容器,其底层的数据结构都是红黑树,其中:
- set表示集合,其中包含一系列无重复的键(key)。set的使用需要引入头文件
,另外还需要指定键的数据类型T
和排序准则Compare
,注意后者不是必须的,只有当需要自定义键的排序准则时才需要显式指定。 - multiset与set类似,但其可以包含重复的键。
- map表示字典,即map中每个元素是一个键唯一的键值对。map的使用需要引入头文件
,另外需要指定键(key)和值(value)的数据类型
Key
和T
,此外其同样可以指定排序准则Compare
。 - multimap与map类似,但其允许键值对的键重复。
上述四种容器中,set和map用的比较多,下面仅介绍两者的用法。
set的创建和常见用法示例如下:
#include#includeusing namespace std;
int main()
{// 创建并初始化,注意set会自动去重
sets = {1, 3, 3, 5, 5};
cout<< s.size()<< endl; // 3
// 插入元素
s.insert(7); // s: {1,3,5,7}
// 删除指定位置的元素
s.erase(s.begin()); // s: {3,5,7}
// 删除特定的键
s.erase(7); // s:{3,5}
// 寻找集合中特定的key的数量(非1即0)
cout<< s.count(3)<< " "<< s.count(7)<< endl; // 1 0
s.insert(8);
s.insert(0);
s.insert(4); // s: {0,3,4,5,8}
// 返回第一个不小于给定键的元素的迭代器
auto it = s.lower_bound(6);
if(it != s.end())
cout<< *it<< endl; // 8
// 返回第一个大于给定键的元素的迭代器
it = s.upper_bound(2);
if(it !=s.end())
cout<< *it<< endl; // 3
return 0;
}
map的创建和常见用法示例如下:
#include#include
四.无序关联容器注:map中存在很多查找操作与set类似,故没有重复介绍。
无序关联容器底层的数据结构是哈希表。
4.1 哈希表哈希表(散列表)在插入、删除、查找等操作上具有平均复杂度为 O ( 1 ) O(1) O(1),其数据结构示意图如下:
图源:hast-table
假定数组有 m m m个桶(bucket),哈希表通过哈希函数(hash function)计算key映射到索引(哈希代码),然后将元素放置到索引对应的桶。
但使用哈希函数可以回带来一个问题,哈希碰撞,即不同的元素被映射到相同的位置。目前解决哈希碰撞最常见的方法为开链(separate chaining),即数组每个桶都维护一个list,通过哈希函数可以为key分配一个桶,然后可以在桶对应list上进行相应的插入、删除和查找操作,若list足够短,速度仍然是非常快的。
4.2 容器说明限于篇幅,后面写博客详细介绍。
unordered_set、unordered_multiset、unordered_map和unordered_multimap的底层数据结构都是哈希表,其中
- unordered_set:包含一组唯一的键(key),使用需要引用
,并指定键的数据类型。 - unordered_multiset:与unordered_set类似,但键可以重复。使用需要引用
。 - unordered_map:包含一组键(key)唯一的键值对,引用需要导入
,并指定键和值的数据类型。 - unordered_multimap:与unordered_map类似,但键可以重复。使用需要引用
。
注意:上述容器还可以指定哈希函数Hash
和判断键是否相等的函数KeyEqual
。
上述四种容器中,unordered_set和unordered_map用的比较多,下面仅介绍两者的用法。
unordered_set的创建和常见用法如下所示:
#include#includeusing namespace std;
int main()
{// 创建并初始化
unordered_setus = {3, 3, 4, 4, 7, 9, 1};
// 获取元素个数
cout<< us.size()<< endl; // 5
// 获取哈希表的桶个数
cout<< us.bucket_count()<< endl; // 13
// 判断容器是否为空
cout<< us.empty()<< endl; // 0
// 返回特定桶的元素数
cout<< us.bucket_size(0)<< endl; // 0
// 返回每个桶的平均元素数
cout<< us.load_factor()<< endl; // 0.384615
// 往哈希表中插入元素
us.insert(10); // us: {}
auto it = us.find(9);
if(it != us.end()) // find key!!!
cout<< "find key!!!"<< endl;
else
cout<< "not find"<< endl;
unordered_setus1 = {11, 8, 9};
us.merge(us1); // us:{11,8,10,1,9,7,4,3}
return 0;
}
unordered_map的创建和常见用法如下所示:
#include#includeusing namespace std;
int main()
{unordered_mapgrades = {{"math", 85}, {"chinese", 77}};
// 插入特定的键与值
grades["english"] = 90;
// 插入特定的元素
grades.insert({"biology", 85.5});
// 获取特定键对应的值
cout<< grades["math"]<< endl; // 85
// 返回特定键的元素个数,非1即0
cout<< grades.count("phyics")<< " "<< grades.count("math")<< endl; // 0 1
return 0;
}
五.容器适配器unordered_map的好多用法与unordered_set类似,故不重复赘述。
容器适配器是依靠其底层容器来完成特定数据结构的功能。
5.1 堆栈stack是一种先进后出的数据结构,其示意图如下所示:
图源自侯捷老师的《STL源码剖析》,侵权删。
可以看出stack只允许在栈顶进行元素的插入(push)、删除(pop)和获取。(stack不允许遍历行为)
STL中要使用stack,需要引入头文件
,并指定栈内数据类型T
,stack的默认底层容器为deque
。由于deque是双向队列,因此只要将deque的底部结构封闭,便能形成一个栈。
stack的创建以及常见用法为:
#include#includeusing namespace std;
int main()
{stacks;
// 往栈中插入元素
s.push(1);
s.push(3);
s.push(4);
s.push(9);
s.push(10);
// 获取栈中元素的个数
cout<< s.size()<< endl; // 5
// 通过empty()来判断栈是否为空
while(!s.empty()) //
{// 获取栈顶的元素
cout<< s.top()<< " ";
// 弹出栈顶的元素
s.pop();
}
cout<< endl; // 10 9 4 3 1
return 0;
}
5.2 队列queue是一种先进先出的数据结构,其示意图为:
图源自侯捷老师的《STL源码剖析》,侵权删。
可以看出,queue允许从队首获取元素,从对尾插入元素。(queue不允许遍历行为)
要使用queue,需要引入头文件
,并指定队列元素的数据类型T
,queue的默认底层容器同样为deque
。对于deque,只要封闭其底端的出口和首端的入口,便能轻易形成一个queue。
queue的创建及常见用法如下:
#include#includeusing namespace std;
int main()
{queueq;
// 往队尾插入元素
q.push(3);
q.push(5);
q.push(4);
q.push(8);
q.push(1);
// 获取队列的大小
cout<< q.size()<< endl; // 5
// 通过empty判断队列是否为空
while(!q.empty()){// 获取队首的元素
cout<< q.front()<< " ";
// 弹出队首的元素
q.pop();
}
cout<< endl; // 3 5 4 8 1
return 0;
}
5.3 优先级队列priority_queue表示具有元素优先级的队列,其示意图如下:
图源自侯捷老师的《STL源码剖析》,侵权删。
priority_queue的行为与queue类似,都是只能从队尾加入元素,从队首获取元素。另外,由于priority_queue中元素带有优先级,因此其内的元素并非依次按照入队的次序排列,而是按照元素的优先级权值来进行排序,权值高的排在前面。
要使用priority_queue需要引用头文件
,并指定元素类型
。priority_queue的底层是利用一个大堆(max-heap)完成的,而max-heap是一个以vector表现的完全二叉树。使用大堆,可以满足优先级队列中权值高的排在前面的特性。
注意,priority_queue还可以指定权值大小的比较方法Compare
。
大堆中根节点的值都不小于其孩子节点的值。
priority_queue的创建及常见用法如下:
#include#includeusing namespace std;
int main()
{priority_queuepq;
int n = 10;
for (int i = 0;i// 往优先级队列中插入元素
pq.push(rand() % 20);
}
// 获取队列大小
cout<< pq.size()<< endl; // 10
// 通过empty判断队列是否为空
while(!pq.empty())
{// 获取队首元素
cout<< pq.top()<< " ";
// 弹出队首元素
pq.pop();
}
cout<< endl; // 17 15 15 13 12 9 6 6 3 1
return 0;
}
六.参考资料本文的完成参考了如下资料:
- cppreference.com
- cplusplus.com
- 《STL源码剖析》侯捷著
以上便是本文的全部内容,要是觉得不错可以关注或点赞支持一下,有任何问题也敬请批评指正!!!。
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
网页标题:STL剖析(二):容器底层数据结构及常见用法-创新互联
分享链接:http://hbruida.cn/article/hhgph.html