C++ [STL容器适配器]

news2024/11/14 5:47:59

STL容器适配器

本文已收录至《C++语言和高级数据结构》专栏!
作者:ARMCSKGT

CSDN


STL容器适配器

  • 前言
  • 正文
    • 容器适配器
    • stack 栈
      • stack的使用
      • stack模拟实现
    • queue 队列
      • queue的使用
      • queue模拟实现
    • priority_queue 优先级队列
      • priority_queue的使用
      • priority_queue模拟实现
    • deque 双端队列
      • deque的使用
      • deque底层思想
  • 最后


前言

前面我们介绍了适配器模式中的反向迭代器,反向迭代器通过容器所支持的正向迭代器适配为具有反向迭代功能的迭代器,本节我们介绍STL中另一种适配器:容器适配器
容器适配器式容器


正文

容器适配器


前面我们提到过STL适配器模式,关于适配器的解释:STL适配器思想

适配器思想是利用已有的对象对其进行封装以满足我们的需要!

容器适配器与反向迭代器相似;容器适配器是对每个容器进行封装,以适合容器适配器的使用要求,只要被封装容器所提供的接口和功能满足容器适配器容器则可以使用,所以容器适配器是对另一个容器进行封装!

既然是封装容器,那么只要满足容器适配器容器的条件,则都可以作为底层容器,所以容器适配器的底层容器可以不固定

在STL中适配器式容器有三个

  • stack 栈
  • queue 队列
  • priority_queue 优先级队列(堆)

    这些容器有一个特性,都是使用线性表作为底层容器;所以,对于已经有满足条件的容器可以充当这个容器时,可以采用对象之间的组合封装进行合理使用。
    当然,这是大范围和小范围的概念,例如stack和queue都可以使用vector和list封装,因为list和vector所支持的功能大于stack和queue所需要的功能!

stack 栈


stack的使用

STL库为我们提供了 stack栈 容器,我们只需要声明头文件stack即可使用!
参考文档:stack
stack

接口说明

  • empty:判断栈是否为空
  • size:获取栈元素个数
  • top:访问栈顶元素(两个函数版本:分别为返回值为 引用 和 const引用)
  • push:在栈顶插入一个元素(实际是容器尾插)
  • pop:删除栈顶元素
  • swap:stack提供的交换函数
  • relational operators:关于stack支持的运算符(==,!=,>,<,<=,>=)
  • swap(stack):算法库中swap支持stack对象的交换

栈的思想是 先进后出(LIFO) ,栈在容器的一端进行插入和删除,只能访问栈顶元素,所以栈没有迭代器,接口也比较少!

栈的使用非常简单,常用的接口都是插入删除取元素等等,但stack先入栈的元素后出栈!

stack的模板参数有两个
stack

  • T:实例化数据类型
  • Container:底层容器


    对于stack,我们可以指定容器进行实例化,如果不指定,则默认是 deque双端队列

stack只有一个构造函数,那就是实例化一个栈对象!
stack
所以说stack的使用非常简单,接下来我们把大部分接口都使用一遍大家就知道了!

#include <iostream>
#include <stack>
using namespace std;

int main()
{
	stack<int> st1; //实例化容器栈st1和st2
	stack<int> st2;
	for (int i = 1; i < 6; ++i) st1.push(i);
	st2.swap(st1);
	while (!st2.empty())
	{
		cout << st2.top() << " ";
		st2.pop();
	}
	cout << endl;
	return 0;
}

实例演示
当然,我们也可以指定容器进行实例化!
注意:在指定底层容器时,传递的容器模板参数Container需要指定底层容器的实例化数据类型,与实例化stack数据类型保持一致!

#include <iostream>
#include <stack>
#include <vector>
#include <list>
using namespace std;

int main()
{
	stack<int,vector<int>> st1; //实例化栈st1 底层容器为vector<int>
	stack<int,list<int>> st2; //实例化栈st2 底层容器为list<int>
	for (int i = 1; i < 6; ++i)
	{
		st1.push(i);
		st2.push(6 - i);
	}
	cout << "st1:";
	while (!st1.empty())
	{
		cout << st1.top() << " ";
		st1.pop();
	}
	cout << endl;
	cout << "st2:";
	while (!st2.empty())
	{
		cout << st2.top() << " ";
		st2.pop();
	}
	cout << endl;
	return 0;
}

stack不同容器实例化
但是两个stack如果实例化的容器不一样,则不能使用swap函数,因为底层调用的是封装容器的swap,类型会不匹配!
stack底层容器不同不能swap
关于emplace函数,也是一种插入函数,但是其作用更广!


stack模拟实现

这里着重讲对栈的封装,关于栈的思想和算法学习:数据结构-栈

对于封装容器适配器 所需要的容器函数

  • push_back 尾插
  • pop_back 尾删
  • back 访问尾部元素
  • empty 容器是否为空
  • size 数据个数

    底层容器只要满足stack适配器所需的函数,那么就可以为其适配出一个容器适配器!
    stack的默认底层容器是 deque双端队列,当然也可以使用vector和list等线性容器!
    stack适配器容器
    stack默认在容器尾部插入删除,在尾部操作对于适配的容器效率可能较高!
    库中之所以选择 deque 是因为其头尾操作效率较高,后面我们会简单介绍 deque !

关于stack的封装实现,就是对其要求的几个函数进行封装,调用的都是底层容器的函数,所以也比较简单!

#pragma once
#include<iostream>
#include<deque>

namespace my_stack
{
	//stack是一个适配器模式,可以借助其他容器实现
	template<class T, class Container = std::deque<T>>//
	class stack
	{
		typedef stack<T, Container> _stack; //stack对象类型
	public:
		stack()
			:_con(Container())
		{}

		//尾插
		void push(const T& val) { _con.push_back(val); }

		//尾删
		void pop() { _con.pop_back(); }

		//取栈顶数据
		T& top() { return _con.back(); }
		const T& top() const { return _con.back(); }

		//数据个数
		size_t size() const { return _con.size(); }

		//是否为空
		bool empty()const { return _con.empty(); }

		//交换
		void swap(_stack& _s) { _con.swap(_s._con); };
	private:
		Container _con; //底层容器对象
	};
}

queue 队列


queue的使用

STL库为我们提供了 queue队列 容器,我们只需要声明头文件queue即可使用!
参考文档:queue
queue

接口说明

  • empty:判断队列是否为空
  • size:查询队列元素个数
  • front:查看队头元素(两个函数版本:分别为返回值为 引用const引用)
  • back:查看队尾元素(两个函数版本:分别为返回值为 引用const引用)
  • push:插入一个元素(插在队尾)
  • pop:删除一个元素(从队头删除)
  • swap:queue提供的交换函数
  • relational operators:关于queue支持的运算符(==,!=,>,<,<=,>=)
  • swap(queue):算法库中swap支持queue对象的交换

队列是一种 先进先出(FIFO) 的思想,头删尾插,在STL中也是容器适配器,但受其规则限制,也没有迭代器!

queue的使用也很简单,与stack不同的是queue先插入的先出队!

queue的模板参数有两个
queue

  • T:实例化数据类型
  • Container:底层容器

对于queue,我们可以指定容器进行实例化,如果不指定,则默认也是 deque双端队列

queue也只有一个构造函数,那就是实例化一个队列对象!
queue
queue的使用也非常简单,在实例化时也可以指定底层容器,只不过插入和删除元素的顺序不同,所以栈和队列根据不同场景使用即可!

#include <iostream>
#include <queue>
#include <list>
using namespace std;

int main()
{
	queue<char> q1; //实例化队列 默认deque容器
	queue<char, list<char>> q2; //实例化队列 底层容器list
	for (char i = 'a'; i <= 'z'; ++i)
	{
		q1.push(i);
		q2.push(i - 32);
	}
	cout << endl << "q1:";
	while (!q1.empty())
	{
		cout << q1.front() << " ";
		q1.pop();
	}
	cout << endl << "q2:";
	while (!q2.empty())
	{
		cout << q2.front() << " ";
		q2.pop();
	}
	cout << endl;
	return 0;
}

实例演示

注意

  • 同样的,queue在指定底层容器后,不同容器实例化出的queue对象不能交换
  • queue底层需要头删函数,对于不支持头删的容器不能适配和实例化(例如vector)

queue模拟实现

这里着重讲对队列的封装,关于队列的思想和算法学习:数据结构-队列

对于封装容器适配器 队列 所需要的容器函数

  • push_back 尾插
  • pop_front 头删
  • empty 判断容器是否为空
  • size 获取容器中元素个数
  • front 访问头部元素
  • back 访问尾部元素

    注意:队列要求容器支持尾插和头删,vector因为头删性能较差所以没有实现,对于不支持尾插和头删的容器将无法适配!

queue的默认底层容器是 deque双端队列,当然也可以使用list线性容器!
queue
关于queue的封装,与stack一样,也是调用底层容器的接口,适配器本身只是封装,这里为了跟库保持一致,我们仍然使用deque作为默认底层容器!

#pragma once
#include<iostream>
#include<deque>

namespace my_queue
{
	template<class T,class Container = std::deque<T>>//也可以使用list链表头尾入出效率较高
	class queue
	{
		typedef queue<T, Container> _queue; //queue对象类型
	public:
		queue()
			:_con(Container())
		{}

		//尾插
		void push(const T& x) { _con.push_back(x); }

		//头删
		void pop() { _con.pop_front(); }

		//取尾部数据
		T& back() { return _con.back(); }
		const T& back() const { return _con.back(); }

		//取头部数据
		T& front() { return _con.front(); }
		const T& front() const { return _con.front(); }

		//数据个数
		size_t size() const { return _con.size(); }

		//是否为空
		bool empty()const { return _con.empty(); }

		//交换
		void swap(_queue& _q) { _con.swap(_q._con); }

	private:
		Container _con;
	};
}

priority_queue 优先级队列


优先级队列这个词大家可能没有听说过,但是 数据结构-堆 一定有所耳闻,没错,优先级队列就是,而且优先级队列也是容器适配器!
priority_queue
priority_queue

以上图片出自《STL源码剖析》

priority_queue的使用


STL库为我们提供了 priority_queue优先级队列 容器,我们只需要声明头文件 queue 即可使用!
参考文档:priority_queue
priority_queue接口

模板参数
priority_queue模板参数
priority_queue有三个模板参数:

  • T:实例化数据类型
  • Container:底层容器
  • Compare:比较函数(仿函数-默认升序)


    注意:priority_queue要求Container底层容器迭代器支持随机访问,因为堆需要随时进行调整!

构造函数
priority_queue构造函数
priority_queue有两个构造函数:

  • 默认构造:构造一个空对象
  • 迭代器区间构造:通过迭代器区间的遍历元素插入priority_queue对象

实例演示

#include <iostream>
#include <queue>
using namespace std;

int main()
{
	priority_queue<int> pq1; //构造一个空堆

	int arr[] = { 9,10,1,2,4,6,5,3,8,7 };
	priority_queue<int> pq2(arr,arr+10); //迭代器区间构造

	return 0;
}

构造函数实例演示
堆序列二叉树表示图
我们将堆调整过的序列以二叉树的形式画出来,可以发现是一个大堆,父节点总是比子节点要大;所以,默认情况下,priority_queue建大堆!


大堆:父节点大于子节点
小堆:父节点小于子节点


当然,我们也可以通过传递Compare比较函数的方式控制建堆方式!

//T表示实例化数据类型 对于用于比较的仿函数也需要传递比较的值类型T
priority_queue<T,less<T>> BigPile; //通过less仿函数比较建大堆
priority_queue<T, vector<T>,greater<T>> SmallHeaps; //通过greater仿函数比较建小堆

上面我们演示了建大堆,下面我们用greater仿函数建小堆!

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main()
{
	int arr[] = { 9,10,1,2,4,6,5,3,8,7 };
	//通过迭代器区间构造并建小堆
	priority_queue<int, vector<int> ,greater<int>> pq(arr,arr+10);
	return 0;
}

小堆演示
小堆二叉树图表示
这里需要注意的是,在priority_queue的模板参数列表,其顺序依次是 数据类型T,底层容器Container和比较函数Compare,Container和Compare都有缺省参数,但是如果我们需要指定比较函数,需要先指定底层容器,这是缺省参数的使用规定,否则按照顺序传参的话,我们的比较函数就传给底层容器参数Container了!
关于priority_queue模板参数


关于剩下的接口,使用起来就非常简单了,无非是出堆入堆或者取堆顶数据等等!

#include <iostream>
#include <queue>
using namespace std;

int main()
{
int main()
{
	//堆排序 - 降序输出
	int arr[] = { 9,10,1,2,4,6,5,3,8,7 };
	//迭代器区间构造	
	priority_queue<int> pq(arr,arr+10);
	pq.push(668); //插入一个最大元素668数字
	cout << "heap size:" << pq.size() << endl; //输出当前的元素数
	while (!pq.empty()) //循环取堆顶数据输出 - 呈有序序列
	{
		cout << pq.top() << " ";
		pq.pop(); //输出一个 出堆一个
	}
	cout << endl;
	return 0;
}

对于某些特殊场景,例如存入指针,但需要按指针所指向的值去比较的场景,就需要我们自己实现比较函数!
优先级队列存入指针
可以发现,存入指针的情况下,如果我们想按其地址存入的值进行比较,必须自己实现比较函数,否则默认按地址的大小进行比较!


priority_queue模拟实现

priority_queue属于容器适配器的一种,因为自身规则限制与栈和队列一样,无法遍历,没有迭代器,同时也不需要实现自己的具体功能,调用底层容器的函数接口就行了,不过因为堆比较特殊,需要具备 向上调整 和 向下调整 的能力,这个能力需要底层容器支持迭代器的随机访问,确保符合堆的规则!

这里主要介绍对底层容器的封装,关于堆的详细介绍:数据结构-堆


对于封装容器适配器 优先级队列 所需要的容器函数

  • push_back 尾插
  • pop_back 尾删
  • empty 判断容器是否为空
  • size 获取堆元素个数
  • front 获取容器首元素(栈顶元素)

这里需要说明一下,大家可能看到容器适配器是能用尾插尾删来封装容器就不用头插头删,是因为尾上的操作相当于头部操作相对于大部分线性容器来说是性能较好的,例如vector没有头部操作(因为性能极差),容器适配器想尽可能适配更多容器,所以会兼容容器中实现最多的接口进行统一!

基本框架及构造函数
我们是对容器的封装加持可以控制堆的大小,所以与库中保持一致,三个模板参数:

  • 数据类型T
  • 底层容器Container
  • 比较函数Compare

关于priority_queue的框架设计和构造函数:

  • priority_queue有两个构造函数,默认构造迭代器区间构造
    –对于默认构造,只需要初始化两个成员变量即可
    –对于迭代器区间构造,需要初始化成员变量后通过迭代器将值逐一插入容器中
  • priority_queue底层默认容器是vector,其随机访问性能相对于deque来说要好一些!
  • 我们使用命名空间封装容器,并在命名空间中实现两个仿函数作为默认比较函数供使用者选择,对于priority_queue默认的比较方式,我们使用less大堆作为缺省参数!
namespace my_Priority_queue //priority_queue命名空间
{
	//仿函数实现判断小于
	template<class T>
	struct less
	{
		bool operator()(const T& left, const T& right)
		{
			return left < right;
		}
	};

	//仿函数实现判断大于
	template<class T>
	struct greater
	{
		bool operator()(const T& left, const T& right)
		{
			return left > right;
		}
	};

	//通过仿函数可以自定义比较方法!相当于qsort传指针来说,方便易懂
	template<class T, class Container = std::vector<T>, class Comapre = less<T>>
	class priority_queue
	{
		typedef priority_queue<T, Container, Comapre> _priority_queue; //priority_queue对象类型
	public:
		priority_queue() //默认构造
			:_c(Container()) //初始化底层容器对象
			, _com(Comapre()) //初始化比较函数对象
		{}

		template <class InputIterator> //迭代器区间构造
		priority_queue(InputIterator first, InputIterator last)
			:_c(Container())
			, _com(Comapre())
		{
			while (first != last)
			{
				push(*first);
				++first;
			}
		}

	private:
		Container _c; //底层容器对象
		Comapre _com; //比较函数对象
}

关于仿函数: 仿函数是通过重载运算符 () 实现的,在一些涉及比较大小的函数例如sort排序和部分STL容器中会使用,通常less是小,greater是大,在使用时进行实例化就行了,之所以会有仿函数这样的语法,是因为C语言对于将函数作为参数传递十分不友好,C++使用仿函数传递参数极大的方便了函数作为参数进行传递的不足!


查询交换类
对于查询类无非就是三个函数:empty判空,size查询元素个数和top取栈顶元素
对于这些函数,我们直接调用底层容器通过的函数即可!

//查询元素个数
size_t size() const { return _c.size(); }

//取栈顶元素 返回const引用类型
const T& top() const { return _c.front(); }

//检查容器中是否为空
bool empty() const { return _c.empty(); }

//交换
void swap(_priority_queue& _pq) { _c.swap(_pq._c); }

注意:top不能随意修改,为了提高性能使用const引用,修改堆中任意元素都有可能破坏堆结构;而且除了swap其他的函数不会涉及修改数据,可以使用const修饰this指针保证容器安全!


插入删除类
堆的插入和删除都需要进行检查和调整以确保堆结构的完整!
对于大堆:父节点值大于子节点
对于小堆:父节点值小于子节点


​​
插入数据
对于数据的插入,尾插在容器中,然后通过随机访问特性对比父子节点进行向上调整;一旦向上调整过程中,父子节点满足关系了就break跳出(因为上面的堆元素一定是满足堆条件的)!

void push(const T& val) //插入数据
{
	_c.push_back(val); //尾插数据
	adjust_up(_c.size() - 1); //向上调整
}

向上调整

void adjust_up(size_t child) //向上调整
{
	size_t parent = (child - 1) / 2; //计算父节点下标
	while (child > 0)
	{
		if (_com(_c[parent], _c[child])) //调用仿函数比较
		{
			std::swap(_c[parent], _c[child]); //库函数交换父子元素
			child = parent; //更新子节点下标
			parent = (child - 1) / 2; //更新父节点下标
		} else break;	
	}
}

删除数据
删除数据的原理是删除堆顶数据,将堆顶数据与容器尾部数据交换,然后容器尾删进行向下调整,这样堆顶的数据就被删除且保证了堆的完整性,之所以不直接删除堆顶元素然后向上调整,向上调整的性能没有向下调整的性能优秀!

void pop() //删除堆顶数据
{
	assert//	
	std::swap(_c[0], _c[_c.size() - 1]); //堆顶数据与容器尾数据交换
	_c.pop_back(); //容器尾删淘汰堆顶数据
	adjust_down(0); //从堆顶开始向下调整
}

向下调整
对于向下调整也是一样的,如果父子关系满足条件则break,不满足则继续向下调整!

void adjust_down(size_t parent) //向下调整
{
	size_t child = (parent * 2) + 1; //确定子节点下标
	while (child < _c.size())
	{
		//左右子节点最大或最小的
		if (child + 1 < _c.size() &&_com( _c[child], _c[child+1]))  { ++child; }

		if (_com(_c[parent], _c[child])) //判断父子元素关系
		{
			std::swap(_c[parent], _c[child]); ///交换父子元素
			parent = child; //关系父节点下标
			child = (parent * 2) + 1; //更新子节点下标
		} else break; //满足则直接break
	}
}

这里有一个细节问题,我们通过父节点向下调整,一开始确定的子节点是左子节点,需要与通过计算与右节点对比出最符合条件的哪个与父节点比对,否则会出现问题!


priority_queue模拟实现完整源码

#pragma once
#include<iostream>
#include<vector>

namespace my_Priority_queue
{
	//仿函数实现判断小于
	template<class T>
	struct less
	{
		bool operator()(const T& left, const T& right)
		{
			return left < right;
		}
	};

	//仿函数实现判断大于
	template<class T>
	struct greater
	{
		bool operator()(const T& left, const T& right)
		{
			return left > right;
		}
	};

	//通过仿函数可以自定义比较方法!相当于qsort传指针来说,方便易懂

	template<class T,class Container = std::vector<T>,class Comapre = less<T>>
	class priority_queue
	{
		typedef priority_queue<T, Container, Comapre> _priority_queue; //priority_queue对象类型
	public:
		priority_queue()
			:_c(Container()) //初始化底层容器对象
			,_com(Comapre()) //初始化比较函数对象
		{}

		template <class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
			: _c(Container())
			, _com(Comapre())
		{
			while (first != last)
			{
				push(*first);
				++first;
			}
		}

		void push(const T& val)
		{
			_c.push_back(val); //插入数据
			adjust_up(_c.size() - 1); //向上调整
		}

		void pop()
		{
			std::swap(_c[0], _c[_c.size() - 1]); //堆顶数据与容器尾数据交换
			_c.pop_back(); //容器尾删淘汰堆顶数据
			adjust_down(0); //从堆顶开始向下调整
		}

		size_t size() const { return _c.size(); }

		const T& top() const { return _c.front(); }

		bool empty() const { return _c.empty(); }

		void swap(_priority_queue& _pq) { _c.swap(_pq._c); }

	private:
		Container _c;
		Comapre _com;

		void adjust_up(size_t child) //向上调整
		{
			size_t parent = (child - 1) / 2;
			while (child > 0)
			{
				if (_com(_c[parent], _c[child]))
				{
					std::swap(_c[parent], _c[child]);
					child = parent;
					parent = (child - 1) / 2;
				} else break;
			}
		}

		void adjust_down(size_t parent) //向下调整
		{
			size_t child = (parent * 2) + 1;
			while (child < _c.size())
			{
				if (child + 1 < _c.size() &&_com( _c[child], _c[child+1])) { ++child; }
				
				if (_com(_c[parent], _c[child]))
				{
					std::swap(_c[parent], _c[child]);
					parent = child;
					child = (parent * 2) + 1;
				} else break;
			}
		}

	};
}

deque 双端队列


deque双端队列是stack和queue底层指定默认容器,其结构上的特殊设计决定了它只适合于头尾操作需求高的场景;双端队列 = vector + list,设计时就想汲取二者的优点,即下标随机访问极致的空间使用,但鱼和熊掌不可兼得,在复杂结构的加持之下,双端队列趋于中庸,无法做到 vector 和 list 那样极致,因此实际中也很少使用,比较适合当作适配器的底层容器!
deque

图片出自《STL源码剖析》

deque的使用

使用deque只需要声明deque头文件即可!
参考文档:deque
deque模板参数
deque有两个模板参数,空间配置器Alloc我们暂时不管,只需要传递实例化模板参数T即可!

默认成员函数
deque有4个构造函数,三个初始化构造函数和一个拷贝构造
构造函数
初始化拷贝构造:构造一个空双端队列构造一个有n个值为val的双端队列迭代器区间构造
实例演示
这里拷贝构造就不演示了,与其他容器基本相同!


deque支持赋值重载函数
deque赋值重载


迭代器部分
deque支持正向和反向迭代器,且deque的迭代器是随机迭代器,非常强大!
deque-iterator

#include <iostream>
#include <deque>
using namespace std;

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	deque<int> dq(arr, arr + 10);

	deque<int>::iterator it = dq.begin(); //正向迭代器
	for (int i = 0; (it + i) != dq.end(); ++i)
		cout << *(it + i) << " "; //随机访问
	cout << endl;

	deque<int>::reverse_iterator rit = dq.rbegin(); //反向迭代器
	for (int i = 0; (rit + i) != dq.rend(); ++i)
		cout << *(rit + i) << " "; //随机访问
	cout << endl;
	return 0;
}

deque迭代器


容量和数据访问类
deque容量和数据访问类

接口说明

  • size:获取元素个数
  • max_size:获取当前deque对象可存储的最大元素数
  • resize(n,val):增加n个val值,如果 n<size() 则什么也不做,val有缺省值,为类型的默认值
  • empty:判断当前deque是否为空
  • shrink_to_fit:缩容,缩小到一个合适的范围
  • [ ] :下标随机访问,下标非法就报错
  • at :下标随机访问,下标非法就抛异常
  • front :获取头部元素的 引用 或 const引用 (根据实际场景由编译器调用)
  • back :获取尾部元素的 引用 或 const引用 (根据实际场景由编译器调用)

deque支持增加容器元素以及缩容,但我们极其不建议使用shrink_to_fit缩容,因为其底层的结构负责,缩容会造成更大的性能损失!

deque支持方便的下标随机访问,但这个随机访问并不代表其底层是连续的内存空间,会有一定性能损失,迭代器与此也有相同的问题!

#include <iostream>
#include <deque>
using namespace std;

int main()
{
	deque<int> dq(3, 668);
	cout << "size:" << dq.size() << endl;
	cout << "max_size:" << dq.max_size() << endl;
	cout << "empty:" << dq.empty() << endl;
	dq.resize(5, 888);
	for (int i = 0; i < dq.size(); ++i) cout << dq[i] << " ";
	cout << endl;
	for (int i = 0; i < dq.size(); ++i) cout << dq.at(i) << " ";
	cout << endl;
	cout << "dq front:" << dq.front() << endl;
	cout << "dq back:" << dq.back() << endl;
	return 0;
}

deque容量和数据访问演示


数据插删类
deque数据插入与删除

接口说明

  • assign:重新分配,先情况deque中的原数据再分配数据,类似于 operator= 的功能,有两个版本:
    – 分配n个val值元素
    – 迭代器区间分配

  • push_back:尾插
  • push_front:头插
  • pop_back:尾删
  • pop_front:头删
  • insert 任意位置插入,有三个版本:
    – 在指定的position迭代器位置插入一个val值,并返回新插入值位置的迭代器
    – 在指定的position迭代器位置插入n个val值
    – 在指定的position迭代器位置插入一个迭代器区间

  • erase 任意位置删除,有两个版本:
    – 删除position迭代器所指向的元素并返回该迭代器的下一个指向的元素迭代器
    – 删除一个迭代器区间,返回这个区间的下一个元素迭代器


    swap:deque提供的交换函数,交换两个deque的底层数据
    clear:清空deque容器中的所有数据
#include <iostream>
#include <deque>
using namespace std;

void Print(const deque<int>& dq)
{
	for (const auto& x : dq) cout << x << " ";
	cout << endl;
}

int main()
{
	deque<int> dq(3,668);
	Print(dq);

	int arr[] = { 1,2,3 };
	dq.assign(arr, arr + 3);
	Print(dq);

	dq.pop_front();
	dq.pop_back();
	Print(dq);

	dq.push_front(128);
	dq.push_back(1024);
	Print(dq);

	dq.insert(dq.begin(), 2048); //相当于头插
	dq.erase(--(dq.end())); //相当于尾删
	Print(dq);

	dq.clear();
	Print(dq);

	return 0;
}

deque插入删除类


deque底层思想

抛开底层不谈,deque是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
deque
deque底层是 vector + list 的思想,其想对标 vector和list,即想拥有vector那样随机访问的功能,又想如list一样,头尾删除轻松自如,但实际上并不是很突出,雨露均沾但并没有两者的优点极限!

deque底层组成

  • 由一个一个的vector组成的数组buffer,buffer中存储数据
  • 由list中的节点指向每一个buffer首地址,这是一个中控数组,控制每一个buffer,这个主控数组在底层叫map,但底层并不是map容器,只是名字相同

将二者结合起来,就得到了复杂的双端队列!
deque底层
deque的扩容与buffer数组无关,只对中控数组map进行扩容,再将原中控数组中指向各buffer的指针拷贝过去即可!

deque随机访问实现

  • (下标 - 前预留位) / 单个数组长度 = 对应小数组buffer在map中的位置
  • (下标 - 前预留位) % 单个数组长度 = 数据在该buffer中下标位置

由扩容和随机访问的计算可以发现,单个数组buffer的大小需要固定,否则访问时计算会比较麻烦(冗余计算),但长度定长后,又会引发中间位置插入删除效率低的问题;对此 SGI 版的 STL 选择牺牲中间位置插入,提高下标随机访问速度,令小数组定长,这也是将它作为 栈和队列 默认底层容器的原因之一,因为 栈和队列 不需要对中间进行操作!

deque迭代器结构
由于deque的中控数组只记录了每个buffer数组的起始位置,所以其设计也比较复杂!
迭代器含有以下成员对deque进行控制:

  • cur指针:指向当前需要的数据位置
  • first指针:指向 buffer 数组起始位置
  • last指针: 指向 buffer 数组终止位置
  • node指针:指向中控数组(当迭代器指向buffer临界区时,进行buffer切换)
    deque迭代器
    deque的迭代器支持随机访问,所以可以使用sort进行排序,但是其随机访问牺牲性能,不如直接导入vector排完序再导入deque!

简单了解了其底层结构,我们盘点一下deque缺点:

  • 结构设计复杂,性能没有vector和list各种的优点极致
  • 中间位置插入删除非常麻烦,可以通过对buffer扩容的方式解决,但是该操作会影响随机访问的性能
  • 虽然支持随机访问,但是牺牲性能,所以不适合遍历,因为迭代器需要频繁检测是否移动某段小空间的边界,效率很低

通过上面的介绍我们发现,deque天生适合stack和queue的底层容器,不需要在中间位置进行插入和删除,所以底层默认使用deque,而因为其随机访问没有vector极致,所以priority_queue底层默认并不是deque,而是vector!(当然,我们也可以使用deque作为priority_queue底层容器,但是性能没有vector好)
因为deque结构复杂,且平时用处不多,所以我们简单了解即可,需要深究的同学可以参考 《STL源码剖析》 中对deque的讲解!


以上演示图片出自《STL源码剖析》

最后

本节容器适配器的讲解就到这里了,本节我们又介绍了STL适配器模式下的容器适配器,容器适配器的学习让我们对类和对象的封装又有了进一步的认识,类和类之间的组合应用有了初步的了解,最后我们学习了deque这个适配器容器的底层容器,了解了其底层结构的复杂和优缺点,这些将为我们后面的学习打下基础!

本次 <C++ STL容器适配器> 就先介绍到这里啦,希望能够尽可能帮助到大家。

如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
C-PLUS-PLUS

🌟其他文章阅读推荐🌟
C++ <STL容器反向迭代器> -CSDN博客
C++ <STL之list模拟实现> -CSDN博客
C++ <STL之list使用> -CSDN博客
C++ <STL之vector模拟实现> -CSDN博客
C++ <STL之vector的使用> -CSDN博客
🌹欢迎读者多多浏览多多支持!🌹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/672605.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第二章 VGG网络详解

系列文章目录 第一章 AlexNet网络详解 第二章 VGG网络详解 第三章 GoogLeNet网络详解 第四章 ResNet网络详解 第五章 ResNeXt网络详解 第六章 MobileNetv1网络详解 第七章 MobileNetv2网络详解 第八章 MobileNetv3网络详解 第九章 ShuffleNetv1网络详解 第十章…

Debian12中Grub2识别Windows

背景介绍&#xff1a;windows10 debian11,2023年6月&#xff0c;Debian 12正式版发布了。抵不住Debian12新特性的诱惑&#xff0c;我将Debian11升级至Debian12。升级成功&#xff0c;但Debian12的Grub2无法识别Window10。于是执行如下命令&#xff1a; debian:~# update-grub G…

图片加载错误的捕获及处理

引言 前端开发中&#xff0c;图片是我们在网页中加载最多的静态资源类型之一&#xff0c;但是图片加载过程中也有可能出现加载失败的情况&#xff0c;这是十分影响用户体验的。那么如何正确的判断图片是否成功加载&#xff0c;以及图片加载失败的时候&#xff0c;如何处理&…

了解 Dockerfile 和搭建 Docker 私有仓库:让容器化部署变得更简单

目录 1、Dockerfile 1.1什么是Dockerfile 1.2常用命令 1.3使用脚本创建镜像 2、Docker私有仓库 2.1私有仓库介绍&#xff1a; 2.2私有仓库搭建与配置 2.3上传镜像到私有仓库&#xff1a; 1、Dockerfile 1.1什么是Dockerfile Dockerfile是由一些列命令和参数构成的脚本…

【服务器数据恢复】IBM存储分配的卷无法访问的数据恢复案例

服务器故障&#xff1a; 一台IBM DS存储出现故障&#xff0c;存储分配给aix小机的卷无法访问。从底层查看分配给aix小机的3个卷的lvm信息丢失。 服务器数据恢复过程&#xff1a; 1、将存储中所有磁盘编号后取出&#xff0c;以只读方式做全盘镜像&#xff0c;后续的数据分析和数…

【C++篇】字符串:标准库string类

友情链接&#xff1a;C/C系列系统学习目录 知识总结顺序参考C Primer Plus&#xff08;第六版&#xff09;和谭浩强老师的C程序设计&#xff08;第五版&#xff09;等&#xff0c;内容以书中为标准&#xff0c;同时参考其它各类书籍以及优质文章&#xff0c;以至减少知识点上的…

Eyeshot 2023 Added NuGet packages.

Added Microsoft Visual Studio 2022 Extensions menu item.Microsoft .NET 6 Windows Toolbox items.Added NuGet packages.Planar curve projection on Sketch plane.Improved fillet surfaces quality and speed.Added ICurve.ConverToLinearPath() family of methods.   …

RabbitMQ学习笔记4(小滴课堂)RabbitMQ工作队列模型实战

Java项目整合RabbitMQ 创建一个maven项目。 然后我们在maven里加上jdk和rabbitmq的依赖设置&#xff1a; 我们写一段生产者的代码&#xff1a; 然后我们去运行它&#xff1a; 可以看到这里有一个队列。 现在我们是可以查看到队列的。 我们去写消费者代码&#xff1a; 这里的之…

LeetCode 2090. K Radius Subarray Averages【前缀和,滑动窗口,数组】中等

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

Redis是什么,如何学习,如何整合SpringBoot?

目录 一、Redis是什么&#xff1f; 二、如何学习Redis 三、如何整合SpringBoot 一、Redis是什么&#xff1f; Redis 是一个高性能的开源 NoSQL 数据库&#xff0c;支持多种数据结构&#xff0c;包括字符串、哈希、列表、集合和有序集合等。它采用内存存储&#xff0c;可以快…

Python3 函数与数据结构 | 菜鸟教程(十一)

目录 一、Python3 函数 &#xff08;一&#xff09;定义一个函数 1、你可以定义一个由自己想要功能的函数&#xff0c;以下是简单的规则&#xff1a; 2、语法 3、实例 ①让我们使用函数来输出"Hello World&#xff01;"&#xff1a; ②更复杂点的应用&#xff…

axios 发送请求请求头信息不包含Cookie信息

问题 axios 发送请求请求头信息不包含Cookie信息 详细问题 使用VueSpringBoot进行项目开发&#xff0c;axios进行网络请求&#xff0c;发送请求&#xff0c;请求头信息不包含Cookie信息 具体如下 实际效果 预期效果 解决方案 作用域 Vue项目全局配置 打开Vue项目的入口…

SpringSecurity是什么,如何学习SpringSecurity?

目录 一、SpringSecurity是什么 二、如何学习SpringSecurity 三、SpringSecurity整合springboot 一、SpringSecurity是什么 Spring Security是一个功能强大的安全框架&#xff0c;它为企业级应用程序提供了完整的身份验证和授权管理。它是一个开源项目&#xff0c;由Pivota…

Linux:第五章课后习题及答案

第五章 Linux常用命令 Q1&#xff1a;常用的文本内容显示命令有哪些&#xff1f;区别是什么&#xff1f; 文本内容显示的命令有cat&#xff0c;more&#xff0c;less&#xff0c; head&#xff0c;tailcat&#xff1a;显示文本文件&#xff0c; 也可以把几个文件 内容附加到另…

第一章 AlexNet网络详解

系列文章目录 第一章 AlexNet网络详解 第二章 VGG网络详解 第三章 GoogLeNet网络详解 第四章 ResNet网络详解 第五章 ResNeXt网络详解 第六章 MobileNetv1网络详解 第七章 MobileNetv2网络详解 第八章 MobileNetv3网络详解 第九章 ShuffleNetv1网络详解 第十章…

python绘画多边形(turtle)

目录 前言 正三角形 正四边形 正多边形 总结&#xff1a; 前言 事情的起因是&#xff0c;我今天心血来潮想让openai生成路飞的图像看效果怎么样&#xff0c;他是这样回我的。 我这一想&#xff0c;这不稳了吗&#xff0c;这么轻松。结果…… import turtle# 定义画笔颜色…

【Visual Studio】报错 C1083,使用 C++ 语言,配合 Qt 开发串口通信界面

知识不是单独的&#xff0c;一定是成体系的。更多我的个人总结和相关经验可查阅这个专栏&#xff1a;Visual Studio。 文章目录 问题解决方案Ref. 问题 使用 C 语言&#xff0c;配合 Qt 开发串口通信界面时&#xff0c;报错代码 C1083。 复制一下错误信息&#xff0c;方便别人…

【C语言进阶】编译链接

文章目录 &#x1f4d6;程序的两种环境 &#x1f516;翻译环境&#x1f516;执行环境 &#x1f4d6;详解翻译环境&#x1f516;从人的角度去看编译链接&#x1f516;预编译&#x1f516;编译&#x1f516;汇编&#x1f516;链接&#x1f516;符号表的作用 &#x1f4d6;执行环境…

外贸订单管理平台有哪些?

外贸订单的管理是外贸出口业务中非常重要的一项管理工作&#xff0c;订单能否实现准时交付则需要涉及到各种流程顺畅的支持。那么外贸订单管理平台有哪些&#xff1f;有孚盟软件。 首先&#xff0c;外贸订单管理平台主要是解决外贸公司的订单查询与管理&#xff0c;面对大量不…

2.4G无线收发芯片 XL2400,SOP8封装,外挂MCU使用

XL2400 芯片是工作在 2.400~2.483GHz 世界通用 ISM 频段的单片无线收发芯片。该芯片集成射频收发机、频率收生器、晶体振荡器、调制解调器等功能模块&#xff0c;并且支持一对多组网和带 ACK 的通信模式。发射输出率、工作频道以及通信数据率均可配置。芯片已将多颗外围贴片阻容…