前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【深入探索 C++ STL 容器适配器 stack 和 queue】 —— 数据战场的神秘指挥官,掌控代码节奏的幕后黑手

【深入探索 C++ STL 容器适配器 stack 和 queue】 —— 数据战场的神秘指挥官,掌控代码节奏的幕后黑手

作者头像
换一颗红豆
发布2024-12-20 11:01:17
发布2024-12-20 11:01:17
9200
代码可运行
举报
文章被收录于专栏:C++进阶之路C++进阶之路
运行总次数:0
代码可运行

C++ STL系列学习参考:STL 数据结构与算法__Zwy@的博客-CSDN博客

https://blog.csdn.net/bite_zwy/category_12852141.html

学习C++ STL的三个境界,会用,明理,能扩展,STL中的所有容器都遵循这个规律,下面我们就按照这三个境界来学习stack和queue

一、stack的介绍及使用

参考文档

cplusplus.com/reference/stack/stack/

https://cplusplus.com/reference/stack/stack/

1、标准库中的stack

栈是STL中的容器适配器,也是一个类模板,遵循后进先出(LIFO - Last In First Out)的原则,元素只从容器的一端插入和提取。栈是作为容器适配器实现的,这些适配器是使用特定容器类的封装对象作为其底层容器的类,提供一组特定的成员函数来访问其元素。

1)、 模板参数

​​​​​其中T表示存储的元素类型,Container 表示stack内部底层容器的类型。

2)、成员类型

value_type(T) 表示存储元素的类型

container_type (Container) 表示底层容器的类型

reference 表示对value_type 元素类型的引用

const_reference 表示对value_type 元素类型的const 引用

size_type 表示无符号整数类型 通常与size_t类型相同

3)、结构示意图

2、stack的常用接口

1)、Constructor 构造函数
Ⅰ、 stack<T> stk

默认构造函数,构造空的stack,这是最基本的构造方式,用于创建一个空的stack。这里的T是模板参数,代表 stack中存储元素的类型,默认构造函数会初始化stack内部使用的底层容器,将其设置为空状态。

示例代码:

代码语言:javascript
代码运行次数:0
复制
void TestConstructor()
{
	//构造空的stack
	stack<int> st;
	stack<string> st_str;
	stack<stack<int>> st_st;
	stack<vector<int>> st_vec;
 }
Ⅱ、 stack<T, Container> stk(cntr)

使用底层容器初始化的构造函数,这种构造函数允许指定stack的底层容器类型,并使用一个已有的该类型容器来初始化stack。构造函数会将给定容器中的元素按照顺序复制到stack中,并且遵循stack的后进先出原则。

注意,指定的底层容器类型必须支持back(),push_back()和pop_back()操作。

示例代码:

代码语言:javascript
代码运行次数:0
复制
void TestConstructor()
{	
	// 此时st_v1的底层容器是v1
	// 栈顶为5,栈底为1
	vector<int>	v1= { 1, 2, 3 ,4, 5 };
	stack<int,vector<int>>  st_v1(v1);

	// 此时st_v2的底层容器是v2
	//栈顶为stack 栈底为queue
	vector<string>  v2 = { "queue","stack" };
	stack<string, vector<string>> st_v2(v2);
 }
Ⅲ、stack<T> stk(Stk)

拷贝构造函数

示例代码:

代码语言:javascript
代码运行次数:0
复制
void TestConstructor()
{	
	vector<int> v{ 1,3,5,7,9 };
	stack<int,vector<int>> st(v);
	//拷贝构造
	stack<int,vector<int>> st_cp(st);
 }

当使用一个已存在的同类型stack对象来初始化新的stack时,会调用拷贝构造函数。新构造的stack会和被拷贝的stack对象一模一样。

2)、stack_Element access 元素操作
Ⅰ、empty()

检测stack是否为空,为空返回true,否则返回false

示例代码:

代码语言:javascript
代码运行次数:0
复制
void Testempty()
{	
	//构造非空的stack
	vector<int> v{ 1,3,5,7,9 };
	stack<int,vector<int>> st1(v);
	if (!st1.empty())
		cout << "st1不为空" << endl;
	else
		cout << "st1为空" << endl;

	//构造空的stack
	stack<int> st2;
	if (!st2.empty())
		cout << "st2不为空" << endl;
	else
		cout << "st2为空" << endl;
 }

输出:

Ⅱ、size()

返回stack中有效元素的个数

Ⅲ、push()

向栈顶压入元素,即压栈

代码语言:javascript
代码运行次数:0
复制
void Test_size_push()
{	
	vector<int> v{ 1,2,3,4,5};
	stack<int,vector<int>> st(v);
	cout << "st当前有效数据个数为:" << st.size() << endl;
	st.push(6);
	st.push(7);
	cout << "st当前有效数据个数为:" << st.size() << endl;
 }

输出:

Ⅳ、top()

获取栈顶元素的引用

Ⅴ、pop()

移除栈顶的元素

示例代码:

代码语言:javascript
代码运行次数:0
复制
void Test_top_pop()
{	
	vector<int> v{ 1,2,3,4,5};
	stack<int,vector<int>> st(v);
	//压栈 栈顶压入元素
	st.push(6);
	st.push(7);
	st.push(8);
	cout << "遍历前st有效元素个数:" << st.size() << endl;
	//使用top和pop接口遍历stack
	cout << "开始遍历st:" << endl;
	while (!st.empty())
	{
		//获取栈顶元素
		cout << st.top() << " ";
		//移除栈顶元素
		st.pop();
	}
	cout << endl;
	cout << "遍历后st有效元素个数:" << st.size() << endl;
 }

输出:

Ⅵ、swap()

交换两个stack的底层容器

示例代码:

代码语言:javascript
代码运行次数:0
复制
void Test_swap()
{
	vector<int> v1{ 1,3,5,7 };
	stack<int, vector<int>> st1(v1);
	vector<int> v2{ 2,4,6,8,10,12 };
	stack<int, vector<int>> st2(v2);
	//交换前
	cout << "swap前st1的size()为:" << st1.size() << endl;
	while (!st1.empty())
	{
		cout << st1.top() << " ";
		st1.pop();
	}
	cout << endl;
	st1.swap(st2);
	//交换后
	cout << "swap后st1的size()为:" << st1.size() << endl;
	while (!st1.empty())
	{
		cout << st1.top() << " ";
		st1.pop();
	}
	cout << endl;
}

输出:

3、stack的模拟实现

从栈的接口中可以看出,栈实际是一种特殊的vector,因此使用vector完全可以模拟实现stack,

所以我们可以使用vector作为栈底层的容器来模拟实现栈。

示例代码:

代码语言:javascript
代码运行次数:0
复制
#include<iostream>
#include<vector>
using namespace std;
namespace _Zwy
{
	template <class T,class Container=vector<T>>
  class Stack
  {
  public:
	  void push(const T& x)
	  {
		  _con.push_back(x);
	  }
	  void pop()
	  {
		  _con.pop_back();
	  }
	size_t size()
	  {
		return _con.size();
	  }
	const T& top()const
	{
		return _con.back();
	}
	bool empty()
	{
		return _con.empty();
	}
  private:
	  Container _con;
  };
}

二、queue的介绍及使用

1、标准库中的queue

参考文档:

cplusplus.com/reference/queue/queue/

icon-default.png?t=O83A
icon-default.png?t=O83A

https://cplusplus.com/reference/queue/queue/

队列也是STL中的一种容器适配器,专门用于在 FIFO上下文(先进先出 )中操作,其中从容器一端插入元素,另一端提取元素。队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列

1)、模板参数

queue的模板参数与stack相同,​​​​​其中T表示存储的元素类型,Container 表示queue内部底层容器的类型。

2)、成员类型

queue的成员类型也与stack完全相同

member type

definition

notes

value_type

The first template parameter (T)

Type of the elements

container_type

The second template parameter (Container)

Type of the underlying container

reference

container_type::reference

usually, value_type&

const_reference

container_type::const_reference

usually, const value_type&

size_type

an unsigned integral type

usually, the same as size_t

value_type(T) 表示存储元素的类型

container_type (Container) 表示底层容器的类型

reference 表示对value_type 元素类型的引用

const_reference 表示对value_type 元素类型的const 引用

size_type 表示无符号整数类型 通常与size_t类型相同

3)结构示意图

2、queue的常用接口

1)、Constructor 构造函数
Ⅰ、queue<value_Type> q

默认构造函数 这是最基本的构造方式,用于构造一个空的queue。其中value_Type表示存储的元素类型。

示例代码:

代码语言:javascript
代码运行次数:0
复制
void TestConstructor()
{
	//默认构造函数
	queue<int> qi;
	queue<string> qs;
	queue<vector<int>> qv;
	queue<char> qc;
}
Ⅱ、queue<value_Type, Container> q(cntr)

使用底层容器初始化的构造函数这种构造函数,允许指定queue的底层容器类型,并使用一个已有的该类型容器来初始化queueContainer必须满足一定的序列容器要求,通常可以是listdeque等。构造函数会将给定容器中的元素按照顺序复制到queue中,并且遵循queue的先进先出原则。

注意,指定的底层容器类型必须支持front()back()push_back()pop_front()操作。

示例代码:

代码语言:javascript
代码运行次数:0
复制
void TestConstructor()
{
	//使用底层容器初始化的构造函数
	list<int> Mylist{ 1,3,5,7,9 };
	//使用Mylist作为q1的底层容器
	queue<int, list<int>> q1(Mylist);

	//使用Myvector作为q2的底层容器
	vector<char> Myvector{ 'a','c','e','g' };
	queue<char, vector<char>> q2(Myvector);
}
Ⅲ、queue<Type> q(Q)

拷贝构造函数,当使用一个已存在的同类型queue对象(Q)来初始化新的queueq)时,会调用拷贝构造函数。这个函数会构造一个新的queue,并将Q中的元素逐个复制到新的queue中。新构造的queue会和被拷贝的queue对象一模一样。

示例代码:

代码语言:javascript
代码运行次数:0
复制
void TestConstructor()
{
	//拷贝构造函数
	list<int> Mylist{ 1,3,5,7,9 };
	//使用Mylist构造 q1
	queue<int, list<int>> q1(Mylist);
	
	//使用q1拷贝构造q2
	queue<int, list<int>> q2(q1);
}
2)、queue_Element access 元素操作
Ⅰ、empty()

判断queue队列是否为空,为空返回true,否则返回false

示例:

代码语言:javascript
代码运行次数:0
复制
void Testempty()
{
	//构造非空的queue
	list<int> l{ 1,3,5,7,9 };
	queue<int, list<int>> q1(l);
	if (!q1.empty())
		cout << "q1不为空" << endl;
	else
		cout << "q1为空" << endl;

	//构造空的queue
	queue<int> q2;
	if (!q2.empty())
		cout << "q2不为空" << endl;
	else
		cout << "q2为空" << endl;
}

输出:

Ⅱ、size()

返回当前queue中有效元素的个数

示例:

代码语言:javascript
代码运行次数:0
复制
void Testsize()
{
	list<int> l{ 1,3,5,7,9 ,11};
	queue<int, list<int>> q1(l);
	cout << "q1的有效数据个数size():" << q1.size() << endl;

	queue<int> q2;
	cout << "q2的有效数据个数size():" << q2.size() << endl;
}

输出:

Ⅲ、push()

向队列尾部插入元素,入队列,这个成员函数有效地调用底层容器对象的成员函数push_back。

Ⅳ、pop()

移除队列头部的元素,出队列,这个成员函数有效地调用底层容器对象的成员函数pop_front。

示例:

代码语言:javascript
代码运行次数:0
复制
void Test_push_pop()
{
	//使用list作为底层容器构造queue
	list<int> l{ 1,2,3,4,5};
	queue<int, list<int>> q(l);
	//push元素 入队列
	q.push(6);
	q.push(7);
	cout << "当前队列size为:" << q.size() << endl;
	//遍历队列
	cout << "开始遍历队列:" << endl;
	while (!q.empty())
	{
		//获取队头元素并删除
		cout << q.front() << " ";
		q.pop();
	}
	cout << endl<< "当前队列size为:" << q.size() << endl;
}

输出:

Ⅴ、front()

获取队列头部元素的引用,这个成员函数有效地调用底层容器对象的成员函数front

Ⅵ、back()

获取队列尾部元素的引用,此成员函数有效地回调底层容器对象的成员函数back

示例:

代码语言:javascript
代码运行次数:0
复制
void Test_front_back()
{
	//使用list作为底层容器构造queue
	list<int> l{ 1,2,3,4,5};
	queue<int, list<int>> q(l);
	cout << "初始队头元素front为:" << q.front() << endl;
	cout << "初始队尾元素back为:" << q.back() << endl;
	//push元素 入队列
	q.push(6);
	//pop 队头元素出队列
	q.pop();
	cout << "当前队头元素front为:" << q.front() << endl;
	cout << "当前队尾元素back为:" << q.back() << endl;
}

输出:

Ⅶ、swap()

交换两个queue的底层容器

示例:

代码语言:javascript
代码运行次数:0
复制
void Test_swap()
{
	list<int> l1{ 10,9,8,7,6 };
	queue<int, list<int>> q1(l1);
	list<int> l2{ -1,0,1,2,3,4,5 };
	queue<int, list<int>> q2(l2);
	//交换前
	cout << "swap前q1的size()为:" << q1.size() << endl;
	while (!q1.empty())
	{
		cout << q1.front() << " ";
		q1.pop();
	}
	cout << endl;
	q1.swap(q2);
	//交换后
	cout << "swap后q1的size()为:" << q1.size() << endl;
	while (!q1.empty())
	{
		cout << q1.front() << " ";
		q1.pop();
	}
	cout << endl;
}

输出:

3、queue的模拟实现

因为queue的接口中存在头删和尾插,因此使用vector来封装效率太低,故可以借助list来模拟实

现queue,list封装效率较高

代码语言:javascript
代码运行次数:0
复制
namespace _Zwy
{
	template <class T, class Container = list<T>>
	class Queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_front();
		}
		size_t size()
		{
			return _con.size();
		}
		const T& front()const
		{
			return _con.front();
		}

		const T& back()const
		{
			return _con.back();
		}
		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

三、stack和queue的OJ练习

相信大家看到这里,已经对stack和queue的使用和简单的模拟实现有了大概的了解,下面给大家分享几道经典的OJ题,帮助大家在尝试解题中更深刻的理解stack和queue!

1、155. 最小栈 - 力扣(LeetCode)

icon-default.png?t=O83A
icon-default.png?t=O83A

https://leetcode.cn/problems/min-stack/description/

题解:

代码语言:javascript
代码运行次数:0
复制
class MinStack
{
public:
	void push(int x)
	{
		// 只要是压栈,先将元素保存到_elem中
		_elem.push(x);
		// 如果x小于_min中栈顶的元素,将x再压入_min中
		if (_min.empty() || x <= _min.top())
			_min.push(x);
	}
	void pop()
	{
		// 如果_min栈顶的元素等于出栈的元素,_min顶的元素要移除
		if (_min.top() == _elem.top())
			_min.pop(); _elem.pop();
	}
	int top() { return _elem.top(); }
	int getMin() { return _min.top(); }
private:
	// 保存栈中的元素
	std::stack<int> _elem;
	// 保存栈的最小值
	std::stack<int> _min;
};

2、栈的压入、弹出序列_牛客题霸_牛客网

icon-default.png?t=O83A
icon-default.png?t=O83A

https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&&tqId=11174&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking

题解:

代码语言:javascript
代码运行次数:0
复制
class Solution {
public:
	bool IsPopOrder(vector<int> pushV, vector<int> popV) {
		//入栈和出栈的元素个数必须相同
		if (pushV.size() != popV.size())
			return false;
		// 用s来模拟入栈与出栈的过程
		int outIdx = 0;
		int inIdx = 0;
		stack<int> s;
		while (outIdx < popV.size())
		{
			// 如果s是空,或者栈顶元素与出栈的元素不相等,就入栈
			while (s.empty() || s.top() != popV[outIdx])
			{
				if (inIdx < pushV.size())
					s.push(pushV[inIdx++]);
				else
					return false;
			}
			// 栈顶元素与出栈的元素相等,出栈
			s.pop();
			outIdx++;
		}
		return true;
	}
};

3、150. 逆波兰表达式求值 - 力扣(LeetCode)

icon-default.png?t=O83A
icon-default.png?t=O83A

https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/

题解:

代码语言:javascript
代码运行次数:0
复制
class Solution 
{
public:
	int evalRPN(vector<string>& tokens) {
		stack<int> s;
		for (size_t i = 0; i < tokens.size(); ++i) 
		{
			string& str = tokens[i];
			// str为数字
			if (!("+" == str || "-" == str || "*" == str || "/" == str))
			{
				s.push(atoi(str.c_str()));
			}
			else
			{
				// str为操作符
				int right = s.top();
				s.pop();
				int left = s.top();
				s.pop();
				switch (str[0])
				{
				case '+':
					s.push(left + right);
					break;
				case '-':
					s.push(left - right);
					break;
				case '*':
					s.push(left * right);
					break;
				case '/':
					// 题目说明了不存在除数为0的情况
					s.push(left / right);
					break;
				}
			}
		}
		return s.top();
	}
};

四、容器适配器

1、什么是适配器?

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设

计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。

这就是大家所熟知的电源适配器,通过适配器,将原本不兼容的插头和插座可以一起工作。容器适配器也是一样的道理。

2、STL标准库中stack和queue的底层容器

在STL标准库中stack和queue的底层容器默认情况下为deque,deque(双端队列)也是C++标准库提供的一种序列容器,下面我们来认识deque!

3、deque的介绍(了解)

参考文档:

cplusplus.com/reference/deque/deque/

icon-default.png?t=O83A
icon-default.png?t=O83A

https://cplusplus.com/reference/deque/deque/

deque又叫做双端队列,Double - ended queue, 头文件为<deque>,是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。它的元素在内存中不是连续存储的,而是由多个固定大小的数组块组成,这些块在逻辑上是连续的。

deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个

动态的二维数组其底层结构如下图所示:

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其 整体连续 以及随机访问

的假象,落在了 deque 的迭代器身上, 因此deque 的迭代器设计就比较复杂,如下图所示

那deque是如何借助其迭代器维护其假想连续的结构呢?

通过对迭代器的特殊封装,

4、deque的优点和缺陷

1)、deque的优点

与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩

容时,也不需要搬移大量的元素,因此其效率是必vector高的。与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。

2)、deque的缺陷

不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,迭代器不仅要记录当前元素在块内的位置,还需要记录当前块的信息。

当迭代器在块内移动时,操作相对简单,但当迭代器移动到块边界时,需要进行块切换操作,这涉及到更新块指针和重新计算元素在新块内的位置等操作,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

5、为什么选deque作为stakc和queue底层默认容器

stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;

queue是先进先出的特殊线性数据 结构,只要具有push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如 list。

但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:

1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进 行操作。

2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。 结合了deque的优点,而完美的避开了其缺陷。

6、STL标准库中对于stack和queue的模拟实现

1)、STL中stack的模拟实现

代码语言:javascript
代码运行次数:0
复制
#include<deque>
namespace _Zwy
{
	template<class T, class Con = deque<T>>
	//template<class T, class Con = vector<T>>
	//template<class T, class Con = list<T>>
	class stack
	{
	public:
		stack() 
		{}
		void push(const T& x) 
		{
			_deq.push_back(x);
		}
		void pop()
		{
			_deq.pop_back();
		}
		T& top() 
		{ 
			return _deq.back();
		}
		const T& top()const 
		{ 
			return _deq.back(); 
		}
		size_t size()const 
		{ 
			return _deq.size();
		}
		bool empty()const 
		{ 
			return _deq.empty(); 
		}
	private:
		Con _deq;
	};
}

1)、STL中queue的模拟实现

代码语言:javascript
代码运行次数:0
复制
#include<deque>
namespace _Zwy
{
	template<class T, class Con = deque<T>>
	//template<class T, class Con = list<T>>
	class queue
	{
	public:
		queue() 
		{}
		void push(const T& x) 
		{ 
			_deq.push_back(x); 
		}
		void pop() 
		{ 
			_deq.pop_front();
		}
		T& back()
		{ 
			return _deq.back();
		}
		const T& back()const 
		{ 
			return _deq.back();
		}
		T& front() 
		{
			return _deq.front();
		}
		const T& front()const 
		{ 
			return _deq.front();
		}
		size_t size()const 
		{
			return _deq.size();
		}
		bool empty()const
		{ 
			return _deq.empty(); 
		}
	private:
		Con _deq;
	};
}

五、小结

stack和queue是STL中很基础同时也很重要的容器,是非常重要的数据结构之一,其底层容器deque大家现阶段了解即可,下篇博文将带大家深度剖析deque以及STL中另一个容器适配器priority_queue 优先级队列。敬请期待!

创作不易,还请多多互三支持!!!如上讲解如有不足之处,还请各位大佬评论区斧正!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、stack的介绍及使用
    • 1、标准库中的stack
      • 1)、 模板参数
      • 2)、成员类型
      • 3)、结构示意图
    • 2、stack的常用接口
      • 1)、Constructor 构造函数
      • 2)、stack_Element access 元素操作
    • 3、stack的模拟实现
  • 二、queue的介绍及使用
    • 1、标准库中的queue
      • 1)、模板参数
      • 2)、成员类型
      • 3)结构示意图
    • 2、queue的常用接口
      • 1)、Constructor 构造函数
      • 2)、queue_Element access 元素操作
    • 3、queue的模拟实现
  • 三、stack和queue的OJ练习
  • 四、容器适配器
    • 1、什么是适配器?
    • 2、STL标准库中stack和queue的底层容器
    • 3、deque的介绍(了解)
    • 4、deque的优点和缺陷
      • 1)、deque的优点
      • 2)、deque的缺陷
    • 5、为什么选deque作为stakc和queue底层默认容器
    • 6、STL标准库中对于stack和queue的模拟实现
  • 五、小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档