【C++】stack和queue及其模拟实现

浅浅介绍一下STL的stack和queue的知识,若有兴趣,不妨垂阅!

目录

1.容器适配器(配接器)

1.1.什么是适配器

1.2.STL标准库中stack和queue的底层结构

2.stack

3.模拟实现stack 

4.queue

 5.模拟实现queue

​编辑

6.deque 

7.小知识

7.1.stack和queue没有迭代器

7.2.可能出现的编译问题


首先stack和queue不属于容器了,而是容器适配器(配接器),这是一个新的概念,在模拟实现部分可以更好的理解这个概念。

1.容器适配器(配接器)

1.1.什么是适配器

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设 计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。

1.2.STL标准库中stack和queue的底层结构

虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为 容器适配器(配接器),这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认 使用deque。如果不理解,请看下文:

2.stack

我们看一下stack文档 对于stack的介绍:

是一个类模板,有两个模板参数,第二个模板参数有缺省值deque<T>,这是个啥东西我们不用理先,也就是说不必理会第二个模板参数先,先学会stack的使用即可。

其实stack的接口就那么几个,只要浅浅涉猎过数据结构中的栈,那么这几个接口一眼秒懂,所以我就随意介绍一下几个常用接口:

1.stack() 

接口说明:构造空的栈。

2.empty()

接口说明:检查stack是否为空。

3.size()

接口说明:返回stack中元素的个数。

4.top()

接口说明:返回栈顶元素的引用。

5.push()

接口说明:将元素val压入stack中。

6.pop()

接口说明:将stack中尾部的元素弹出。

其他的接口可以去查询文档。

我们可以简单使用一下这些接口,当然记得包含头文件<stack>:

#include <stack>
#include <iostream>
using namespace std;
int main()
{
	stack<int> s;
	cout << s.empty() << endl;
	s.push(1);
	cout << s.empty() << endl;
	s.push(2);
	cout << s.size() << endl;
	cout << s.top() << endl;
	s.pop();
	cout << s.size() << endl;
	cout << s.top() << endl;
	return 0;
}

看到运行结果是符合预期的: 

3.模拟实现stack 

从stack的接口中可以看出,stack实际是一种特殊的vector,因此使用vector完全可以模拟实现stack。所以我们使用适配器模式来模拟实现stack:

我将模拟实现的stack放在了myStack.h文件中,并用命名空间HD将其封装:

#pragma once
namespace HD
{
	template <class T, class Container = vector<T>>
	class stack
	{
		Container _con;

		typedef T value_type;
		typedef size_t size_type;
	public:
		void push(const value_type& val);
		void pop()
		{
			_con.pop_back();
		}
		value_type& top()
		{
			return _con.back();
		}
		const value_type& top()const
		{
			return _con.back();
		}
		size_type size()const
		{
			return _con.size();
		}
		bool empty()const
		{
			return _con.empty();
		}
	};
	template<class T, class Container>
	typename void stack<T, Container>::push(const stack<T, Container>::value_type& val)
	{
		_con.push_back(val);
	}
}

模拟实现stack的精髓就在于第二个模板参数Container。仔细看就可以看到stack的实现就是vector套了一层壳,包括STL的stack也是这样弄的,只不过STL的stack默认不是使用vector来适配,而是选择了另一个容器deque来进行默认适配,所以说stack是容器适配器(配接器),而非容器。

至于deque是啥,有兴趣的读者老爷可以自行去学习,我就不仔细介绍了。

可以在test.cpp文件中测试一下模拟实现的stack:

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

#include "myStack.h"
int main()
{
	HD::stack<int> ms;
	cout << ms.empty() << endl;
	cout << ms.size() << endl;
	ms.push(1);
	cout << ms.top() << endl;
	ms.push(2);
	cout << ms.size() << endl;
	ms.push(3);
	ms.push(4);
	ms.push(5);
	ms.push(6);
	ms.pop();
	cout << ms.empty() << endl;
	cout << ms.size() << endl;
	cout << ms.top() << endl;
	return 0;
}

看一下运行结果,是符合预期的:

4.queue

 我们看一下queue文档对queue的介绍:

见识过适配器模式的话,对于queue这个类模板应该很好理解,我就不解释了,我们直接来浅浅介绍一下常见接口:

 1.queue()

接口说明:构造空的队列。

2.empty()

接口说明:检测队列是否为空,是返回true,否则返回false。

3.size()

接口说明:返回队列中有效元素的个数。

4.front()

接口说明:返回队头元素的引用。

5.back()

接口说明:返回队尾元素的引用。

6.push()

接口说明:在队尾将元素val入队列。

7.pop()

接口说明:将队头元素出队列。

其他接口可以去文档查阅,这里就不介绍了。

我们可以简单使用一下这些接口,当然记得包含头文件<queue>:

#include <queue>
#include <iostream>
using namespace std;
int main()
{
	queue<int> q;
	cout << q.empty() << endl;
	q.push(1);
	cout << q.empty() << endl;
	q.push(2);
	cout << q.size() << endl;
	cout << q.front() << endl;
	cout << q.back() << endl;
	q.pop();
	cout << q.size() << endl;
	cout << q.front() << endl;
	cout << q.back() << endl;
	return 0;
}

看到结果符合预期:

 5.模拟实现queue

 同样的,我们使用适配器模式来模拟实现queue,但是我们仔细思考便知,如果跟模拟实现的stack一样使用vector来默认适配的话就不合适,因为quque需要满足队尾入数据、队头出数据,如果还用vector来默认适配的话效率低下,所以我们模拟实现queue使用list来做默认适配。

我将模拟实现的queue放在了myQueue.h文件中,并用命名空间HD将其封装:

#pragma once
namespace HD
{
	template <class T, class Container = list<T>>
	class queue
	{
		Container _con;

		typedef T value_type;
		typedef size_t size_type;
	public:
		void push(const value_type& val);
		void pop()
		{
			_con.pop_front();
		}
		value_type& back()
		{
			return _con.back();
		}
		const value_type& back()const
		{
			return _con.back();
		}
		value_type& front()
		{
			return _con.front();
		}
		const value_type& front()const
		{
			return _con.front();
		}
		size_type size()const
		{
			return _con.size();
		}
		bool empty()const
		{
			return _con.empty();
		}
	};

	//模板不适合声明和定义分离到两个文件下,所以此次声明和定义分离到同一个文件下
	//其实这里本没有必要声明和定义分离,但是练练手
	template<class T, class Container>
	void queue<T, Container>::push(const queue<T, Container>::value_type& val)
	{
		_con.push_back(val);
	}
}

可以在test.cpp中测试一下模拟实现的queue:

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

#include "myQueue.h"

int main()
{
	HD::queue<int> mq;
	cout << mq.empty() << endl;

	mq.push(1);
	mq.push(2);
	mq.push(3);
	mq.push(4);
	cout << mq.front() << endl;
	cout << mq.back() << endl;

	cout << mq.size() << endl;
	mq.pop();
	cout << mq.front() << endl;
	cout << mq.back() << endl;
	cout << mq.size() << endl;
	cout << mq.empty() << endl;

	return 0;
}

结果是符合预期的: 

6.deque 

本博客不去介绍deque的底层,感兴趣的话可以自己去了解。我在这里主要介绍: 

 deque这个容器是vector和list的缝合怪,那么为什么要弄deque这么一个容器呢?就是因为vector和list的优缺点是可以互补的,那么吸收他们俩的优点搞出来了一个deque。


我们先来分析一下vector和list的优缺点:

1.vector:

优点:

尾插尾删效率不错,支持高效下标随机访问。并且得益于物理空间连续,所以高速缓存利用率高。

缺点:

空间要扩容,扩容有一些代价(效率和空间浪费)。并且头部和中间插入删除效率低。

2.list:

优点:

按需申请释放空间,没有扩容导致的效率和空间浪费。并且任意位置的插入删除效率都不错。

缺点:

不支持下标随机访问。


deque确实吸收了他们俩的优点,但是其底层设计也导致deque又有了一些缺陷,这些缺陷导致deque的应用场景不多:

1. 不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

2. 中间插入删除效率很低,要挪动数据,时间复杂度是O(N)。


当然deque吸收了他们俩的优点,且得益于其底层设计,所以:

deque和vector相比,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高;而且尾插效率也高于vector;最后在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。

deque和list相比,deque底层是连续空间,空间利用率比较高,不需要存储额外字段。而且头插和尾插效率高于list。


那么STL的stack和queue为什么deque作为stack和queue的底层默认容器,理由如下:

1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作,这就避开了deque不适合遍历的缺点。

2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。均发挥了deque的优点。

3. stack只需在栈顶操作,恰巧deque尾插效率高于vector,尾删效率也高;queue需要在队头和队尾操作,恰巧deque尾插效率高于list,头删效率也高。


7.小知识

7.1.stack和queue没有迭代器

stack和queue是没有迭代器的。我们可以想想如果有迭代器的话,还能保证stack的数据一定是先进后出吗?还能保证queue的数据一定是先进先出吗?

由于迭代器的功能,显然是不能保证的,所以也stack和queue也不应该设计迭代器。

7.2.可能出现的编译问题

为了介绍这个小知识,我创建了一个工程用于演示,该工程包含两个文件:test.h和test.cpp。


先来看一份代码:

在test.h中:

#pragma once
void func()
{
	cout << "hello world" << endl;
}

在test.cpp中:

#include <iostream>
using namespace std;
#include "test.h"
int main()
{
	func();
	return 0;
}

看一下运行结果:

看起来没什么问题,确实也没什么问题。可是也许有一个疑问:为什么在test.h中没有包含<iostream>,并且没有使用命名空间std,那么为啥test.h中的函数func可以正常使用cout和endl呢?

 因为编译器在编译的时候是没有.h这样的概念的,编译器编译的第一个过程叫预处理,预处理后所有的.h文件就在.c文件或者.cpp文件中展开,那么说函数func在预处理阶段就在test.cpp中展开了,画图方便理解:

编译器只会去编译.cpp文件或者.c文件。 预处理展开test.h文件后,在函数func中使用到了cout和endl时,编译器向上搜索可以搜索到<iostream>,并且也使用了命名空间std,所以函数func可以正常使用cout和endl。


再来看一份代码:

在test.h中:

#pragma once
void func()
{
	cout << "hello world" << endl;
}

在test.cpp中:

#include <iostream>
#include "test.h"
using namespace std;
int main()
{
	func();
	return 0;
}

看一下运行结果:

编译不通过,为什么?

我们先来看看预处理后,test.h的展开情况:

 编译器只会去编译.cpp文件或者.c文件。 预处理展开test.h文件后,在函数func中使用到了cout和endl时,编译器向上搜索可以搜索到<iostream>,但是还没有使用命名空间std,所以函数func不可以正常使用cout和endl,所以报错信息为: error C2065: “cout”: 未声明的标识符、error C2065: “endl”: 未声明的标识符。


感谢阅读,欢迎斧正!

### C++ 中 `stack` `queue` 的数据结构定义及用法 #### 定义与特性 在 C++ 标准库 `<stack>` `<queue>` 中,`std::stack` 是一种后进先出(LIFO, Last In First Out)的数据结构,而 `std::queue` 则是一种先进先出(FIFO, First In First Out)的数据结构。这两种容器适配器都基于其他标准容器(如 `std::deque` 或 `std::list`)来实现。 对于 `std::stack` 而言,默认底层容器为 `std::deque`[^1]。它提供了三个主要的操作函数:`push()` 用于向栈顶添加元素;`pop()` 用于移除栈顶元素;以及 `top()` 返回当前栈顶的元素值[^3]。 而对于 `std::queue` 来说,默认同样使用 `std::deque` 作为底层容器[^2]。它的基本操作包括:`push()` 将新元素加入到队列尾部;`front()` 获取队首元素;`back()` 获取队尾元素;以及通过调用 `pop()` 移除队首元素。 #### 使用方法对比 以下是两种数据结构的具体使用示例: ##### Stack 示例代码 ```cpp #include <iostream> #include <stack> using namespace std; int main() { stack<int> stk; stk.push(1); stk.push(2); stk.push(3); while (!stk.empty()) { cout << stk.top() << " "; // 输出顺序应为 3 2 1 stk.pop(); } return 0; } ``` ##### Queue 示例代码 ```cpp #include <iostream> #include <queue> using namespace std; int main() { queue<int> que; que.push(1); que.push(2); que.push(3); while (!que.empty()) { cout << que.front() << " "; // 输出顺序应为 1 2 3 que.pop(); } return 0; } ``` 上述两段程序分别展示了如何利用 `std::stack` 实现 LIFO 行为借助 `std::queue` 达成 FIFO 效果。 #### 主要区别总结 - **访问模式**: `std::stack` 只允许在一端进行插入删除操作,即遵循 LIFO 原则;相反地,`std::queue` 支持两端操作——一端入队另一端出队,体现 FIFO 特征。 - **功能范围**: 鉴于其单侧受限的特点,`std::stack` 更适合解决那些需要逆序处理或者递归模拟场景下的问题;而当面对诸如广度优先搜索(BFS)之类需按层次遍历的任务时,则更适合选用 `std::queue`[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X_chengonly

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值