一.概述

本文主要聚焦于STL容器,STL完整的容器分类体系如下所示,下文将逐一对各个容器底层的数据结构以及常见用法进行介绍。

测试环境Ubuntu 22.04 g++ 11.3.0

二.顺序容器

顺序容器都对应着线性数据结构

2.1 array

array的使用需要引入头文件<array>,它表示固定大小的数组,与C风格的数组一致。在定义array时,需要指定类型T和数组大小N,即array<T,N>,array创建及常见用法如下:

#include <iostream>
#include <array>using namespace std;int main()
{array<int, 3> arr1;// 对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// 定义时指定初值array<int, 3> arr2 = {2, 5, 4};// 交换两个数组的内容,前提是类型和数组大小都要相同arr1.swap(arr2); // arr1: {2, 5, 4}, arr2: {5, 5, 5}// 对数组进行排序array<double, 2> arr3 = { 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 vector

vector表示动态数组,同array一样,vector中的元素也是连续存储在内存中的,但vector的大小是可变的。在使用vector时需要引入头文件<vector>,一般而言使用vector时仅需要指定数据类型T即可。

需要注意的是,vector并不是每添加一个元素就分配一次新的存储空间,实际上,当vector分配的容量全部填满元素后再添加新元素时,vector的分配器会重新分配两倍容量的新空间,然后将元素挪过去。vector示意图如下:

图源自侯捷老师的《STL源码剖析》,侵权删。

注意:各家C++的实现可能略有区别,上述的重新分配规则不一定适用,但可以确定的是vector分配容量时一般都要未雨绸缪,即容量要比实际大小更大,否则需要重新分配。

vector的创建及常见用法示例如下:

#include <iostream>
#include <vector>using namespace std;int main()
{// 创建空vectorvector<int> arr1;// vector末端插入元素arr1.push_back(1);arr1.push_back(3);arr1.push_back(5);arr1.push_back(7);arr1.push_back(9);// 创建时指定初值vector<int> arr2 = { 1,2,3 };// 创建时指定大小和默认值vector<int> arr3(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 deque

deque表示双向队列,是一种双向开口的"连续"空间。要使用deque需要引入头文件<deque>,并指定数据类型T

连续只是用户看起来,实际deque的存储并非连续的,而是由一系列单独分配的固定大小的数组组成,另外,deque中还需要记录模块(中控器)来维护记录各个段的信息

与vector比起来,deque的扩展更廉价,因为它不涉及将现有元素复制到新的内存位置。deque的示意图如下所示,其中map便是中控器,其中每个元素都指向一段连续的空间,称之为缓冲区,缓冲器才是用来存储数据的。从下图可以看出,deque还需要维护start和finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素。此外,这两个迭代器还需要指向map,否则无法在不同的缓冲区间转移。

图源自stackoverflow: What really is a deque in STL?

deque的创建以及常见用法示例如下:

#include <iostream>
#include <deque>using namespace std;int main()
{deque<int> d = {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; // 3return 0;
}

注:deque在开头和结尾可以进行元素的快速插入和删除。

2.4 list

list表示环形双向链表,链表最大的特点的大小可变且存储非连续。要使用list需要引入头文件<list>,创建对象时需要传入数据类型T

STL中list节点结构示意图如下图所示,其中包含两个指针prevnext分别指向当前节点的前一个和后一个节点,data存储当前节点的数据。

图源自侯捷老师的《STL源码剖析》,侵权删。

基于节点的结构,list的可视化示意图如下所示,可以看到,由于环状要求,还需要一个空白节点(红框标记)。

list的创建与常见用法如下所示:

#include <iostream>
#include <list>
#include <algorithm>using namespace std;int main()
{// 创建list并初始化list<int> l = {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}list<int> l1 = {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; // 1return 0;
}

注:list插入和和删除元素的时间复杂度为 O ( 1 ) O(1) O(1)。

2.5 forward_list

foward_list为单向链表,其行为与list类似,只不过只能单向访问。要使用forward_list需要引入头文件<forward_list>,创建对象时需要指定数据类型参数T

forward_list的示意图如下所示:

forward_list的创建与常见用法如下所示:

#include <iostream>
#include <forward_list>using namespace std;int main()
{// 创建对象并初始化forward_list<int> l = {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的使用需要引入头文件<set>,另外还需要指定键的数据类型T和排序准则Compare,注意后者不是必须的,只有当需要自定义键的排序准则时才需要显式指定。
  • multiset与set类似,但其可以包含重复的键
  • map表示字典,即map中每个元素是一个键唯一的键值对。map的使用需要引入头文件<map>,另外需要指定键(key)和值(value)的数据类型KeyT,此外其同样可以指定排序准则Compare
  • multimap与map类似,但其允许键值对的键重复

上述四种容器中,set和map用的比较多,下面仅介绍两者的用法。

set的创建和常见用法示例如下:

#include <iostream>
#include <set>using namespace std;int main()
{// 创建并初始化,注意set会自动去重set<int> s = {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 0s.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; // 3return 0;
}

map的创建和常见用法示例如下:

#include <iostream>
#include <map>using namespace std;int main()
{// 创建并初始化map<string, int> wc = {{"tom", 18}, {"andy", 12}};// 使用[]可以获取特定key对应的元素cout << wc["tom"] << endl; // 18// 使用[]时当不存在指定的key时,则会插入给定键的节点,若未指定值则值会设置为Value的默认值wc["jake"]; // {{"andy", 12}, {"jake":0}, {"tom", 18}}// 获取map中元素的个数cout << wc.size() << endl; // 3// 插入的另一种方式wc.insert({"smith", 22});auto it = wc.find("sala");if(it == wc.end()) // not findcout << "not find" << endl;elsecout << "find" << endl;return 0;
}

注: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_set>,并指定键的数据类型。
  • unordered_multiset:与unordered_set类似,但键可以重复。使用需要引用<unordered_multiset>
  • unordered_map:包含一组键(key)唯一的键值对,引用需要导入<unordered_map>,并指定键和值的数据类型。
  • unordered_multimap:与unordered_map类似,但键可以重复。使用需要引用<unordered_multimap>

注意:上述容器还可以指定哈希函数Hash和判断键是否相等的函数KeyEqual

上述四种容器中,unordered_set和unordered_map用的比较多,下面仅介绍两者的用法。

unordered_set的创建和常见用法如下所示:

#include <iostream>
#include <unordered_set>using namespace std;int main()
{// 创建并初始化unordered_set<int> us = {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;elsecout << "not find" << endl;unordered_set<int> us1 = {11, 8, 9};us.merge(us1); // us:{11,8,10,1,9,7,4,3}return 0;
}

unordered_map的创建和常见用法如下所示:

#include <iostream>
#include <unordered_map>using namespace std;int main()
{unordered_map<string, double> grades = {{"math", 85}, {"chinese", 77}};// 插入特定的键与值grades["english"] = 90;// 插入特定的元素grades.insert({"biology", 85.5});// 获取特定键对应的值cout << grades["math"] << endl; // 85// 返回特定键的元素个数,非1即0cout << grades.count("phyics") << " " << grades.count("math") << endl; // 0 1return 0;
}

unordered_map的好多用法与unordered_set类似,故不重复赘述。

五.容器适配器

容器适配器是依靠其底层容器来完成特定数据结构的功能。

5.1 堆栈

stack是一种先进后出的数据结构,其示意图如下所示:

图源自侯捷老师的《STL源码剖析》,侵权删。

可以看出stack只允许在栈顶进行元素的插入(push)、删除(pop)和获取。(stack不允许遍历行为)

STL中要使用stack,需要引入头文件<stack>,并指定栈内数据类型T,stack的默认底层容器为deque。由于deque是双向队列,因此只要将deque的底部结构封闭,便能形成一个栈。

stack的创建以及常见用法为:

#include <iostream>
#include <stack>using namespace std;int main()
{stack<int> s;// 往栈中插入元素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 1return 0;
}

5.2 队列

queue是一种先进先出的数据结构,其示意图为:

图源自侯捷老师的《STL源码剖析》,侵权删。

可以看出,queue允许从队首获取元素,从对尾插入元素。(queue不允许遍历行为)

要使用queue,需要引入头文件<queue>,并指定队列元素的数据类型T,queue的默认底层容器同样为deque。对于deque,只要封闭其底端的出口和首端的入口,便能轻易形成一个queue。

queue的创建及常见用法如下:

#include <iostream>
#include <queue>using namespace std;int main()
{queue<int> q;// 往队尾插入元素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 1return 0;
}

5.3 优先级队列

priority_queue表示具有元素优先级的队列,其示意图如下:

图源自侯捷老师的《STL源码剖析》,侵权删。

priority_queue的行为与queue类似,都是只能从队尾加入元素,从队首获取元素。另外,由于priority_queue中元素带有优先级,因此其内的元素并非依次按照入队的次序排列,而是按照元素的优先级权值来进行排序,权值高的排在前面

要使用priority_queue需要引用头文件<queue>,并指定元素类型<T>priority_queue的底层是利用一个最大堆(max-heap)完成的,而max-heap是一个以vector表现的完全二叉树。使用最大堆,可以满足优先级队列中权值高的排在前面的特性。

注意,priority_queue还可以指定权值大小的比较方法Compare

最大堆中根节点的值都不小于其孩子节点的值。

priority_queue的创建及常见用法如下:

#include <iostream>
#include <queue>using namespace std;int main()
{priority_queue<int> pq;int n = 10;for (int i = 0;i<n;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 1return 0;
}

六.参考资料

本文的完成参考了如下资料:

  • cppreference.com
  • cplusplus.com
  • 《STL源码剖析》侯捷著

以上便是本文的全部内容,要是觉得不错可以关注或点赞支持一下,有任何问题也敬请批评指正!!!。

STL剖析(二):容器底层数据结构及常见用法相关推荐

  1. STL容器底层数据结构

    STL容器底层数据结构 1.vector 底层数据结构为数组 ,支持快速随机访问 2.list 底层数据结构为双向链表,支持快速增删 3.deque 底层数据结构为一个中央控制器和多个缓冲区,详细见S ...

  2. Mysql——索引底层数据结构与Explain用法

    Mysql--索引底层数据结构与Explain用法 一.索引底层数据结构 1.Mysql不同引擎对应的数据结构 2.B+Tree数据结构 2.1. 二叉树 (Binary Search Trees) ...

  3. STL容器底层数据结构的实现

    C++ STL 的实现: 1.vector      底层数据结构为数组 ,支持快速随机访问 2.list            底层数据结构为双向链表,支持快速增删 3.deque       底层 ...

  4. MySQL - 剖析MySQL索引底层数据结构

    文章目录 更多干货 Pre 索引的数据结构选型 二叉树 ? 红黑树 ? B-Tree ? B+Tree Hash表 搞定MySQL 更多干货 带你搞定MySQL实战,轻松对应海量业务处理及高并发需求, ...

  5. Arduino+GM65(二维码模块)常见用法

    图片里只要有二维码就涉嫌违规??这让我做二维码模块教程的如何是好!!! 鸽了好久好久的二维码模块教程 案例跳转 例一 例二 例三(软串口) 本教程内容:通过Arduino+GM65二维码模块实现可以根 ...

  6. STL容器底层实现数据结构

    1.vector容器 底层数据结构为数组 ,支持快速随机访问 2.List 底层数据结构为双向链表,支持快速增删 3.deque double ended queue缩写,底层数据结构为双端队列.如下 ...

  7. 【C++ STL学习笔记】C++ STL序列式容器(array,vector,deque,list)

    文章目录 C++ STL容器是什么? 迭代器是什么,C++ STL迭代器(iterator)用法详解 迭代器类别 迭代器的定义方式 C++序列式容器(STL序列式容器)是什么 容器中常见的函数成员 C ...

  8. linux中find常见用法

    find命令用于查找指定目录下的文件,同时也可以调用其它命令执行相应的操作** 一.命令格式 find pathname -options [-print -exec -ok -] 二.linux中f ...

  9. STL容器的底层数据结构

    本文部分内容转自此博客 目录 vector list deque stack queue heap priority_queue set map multiset/multimap 哈希表hashta ...

最新文章

  1. 使用getopt处理shell脚本的参数
  2. Ubuntu 显示隐藏文件
  3. 释放linux 内存
  4. 向webServices请求失败
  5. Tomcat 6 数据源配置
  6. 删除本地git的远程分支和远程删除git服务器的分支
  7. 业务时间做开发,使用jeecg框架
  8. SQL中SELECT INTO和INSERT INTO SELECT语句介绍
  9. 我的5年Python7年R,述说她们的差异在哪里?
  10. 梯度离散_使用策略梯度同时进行连续/离散超参数调整
  11. HTML九宫格拼图游戏代码,js实现九宫格拼图小游戏
  12. 如何在数据库中添加示例数据库Northwind
  13. excel锁定前几行,无法选择和编辑
  14. MATLAB电话拨号音仿真,MATLAB电话拨号音的合成与识别
  15. CSGO手套武器箱直接卖还是开了再卖?
  16. urllib3如何安装的三种办法
  17. 给计算机老师发一封信,写给计算机老师的感谢信.doc
  18. Java框架-SpringBoot
  19. android的service组件不被杀死
  20. python selenium教程.pdf_Python实战:selenium教程(12)

热门文章

  1. prometheus监控常用告警规则
  2. 虚拟 DOM 是什么? 有什么优缺点?
  3. 在线制作数据库ER模型
  4. 图像处理中像素和毫米的换算
  5. 万豪国际集团公布新任首席执行官和总裁
  6. 苹果手机相册怎么分类_电子相册怎么做?用手机app可以剪辑电子相册视频吗?...
  7. 2023款联想小新pro16和Thinkbook16+ 区别选哪个 更值得入手
  8. c语言输出n转义字符串,C语言转义字符介绍和示例
  9. 小目标 | Power BI新人快速上手手册
  10. 使用pdf.js把PDF文件转图片