【C++】14:stack和queue的理解和使用,deque的介绍,stack和queue的模拟实现(超详细)

C++栈队列与deque详解

使用stack,queue和deque需要包含头文件stack,queue和deque

stack文档介绍

queue文档介绍

deque文档介绍

目录

一、stack的介绍

二、stack的常用函数

2.1 构造函数

2.1.1 默认构造函数

2.1.2 拷贝构造函数

2.1.3 使用其它的容器构造一个栈

2.2 push--入栈

2.3 size--获取元素个数

2.4 top--取栈顶元素

2.5 empty--判空

2.6 pop--出栈

三、queue的介绍

四、queue的常用函数

4.1 构造函数

4.1.1 默认构造函数

4.1.2 拷贝构造函数

4.1.3 使用其它的容器构造队列

4.2 push--入队

4.3 pop--出队

4.4 front--取队头元素

4.5 back--取队尾元素

4.6 empty--判空

4.7 size--获取队列元素个数

五、容器适配器

5.1 什么是容器适配器

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

六、栈的模拟实现

6.1 栈底层结构的构建

6.2 栈的构造函数

6.3 其他成员函数

6.4 测试

6.5 给Container设置缺省值

6.6 代码

6.6.1 stack.h

6.6.2 test.cpp

七、队列的模拟实现

八、deque的介绍

8.1 deque的介绍

8.2 deque的底层结构

8.3 deque的优缺点

8.4 stack和queue选择deque作为底层容器的原因


一、stack的介绍

stack文档介绍

 【stack】是一种 特殊的数据结构,也是一种容器适配器,主要特点为:【先进后出】,主要操作有:入栈、出栈、查看栈顶元素、判断栈空等;栈在原则上是不允许进行中部底部操作的,这样会破坏栈结构的完整性。

我们在stack的参考文档可以看见栈的定义,如下所示:

可以看到 栈【stack】是作为容器适配器被实现,容器适配器是对特定类封装作为其底层容器(容器适配器本文的后面会详细讲解,这里先了解一下它 和 栈 的关系)

可以看出,栈有两个模板参数

  • 参数1:T 栈中的元素类型,同时也是底层容器中的元素类型
  • 参数2:Container 实现栈时用到的底层容器,这里为缺省参数,缺省结构为 双端队列 deque

也就是说,【stack】的底层容器可以是任何标准的容器类模板或者一些其特定的容器类,这些容器应该支持一下操作:

  • empty :判空操作
  • top :获取栈顶元素
  • push_back :尾部插入元素操作
  • pop_back :尾部删除元素操作

标准容器 vector、deque、list 均符合上面的这些要求,默认情况下,如果没有为 【stack】指定特定的顶层容器,默认情况下使用 deque (这一点会在本文,后下容器适配器,详细讲解)

二、stack的常用函数

2.1 构造函数

2.1.1 默认构造函数

#include<iostream>
using namespace std;
#include<stack>
int main() {
	stack<int> st;
	return 0;
}

2.1.2 拷贝构造函数

#include<iostream>
using namespace std;
#include<stack>
int main() {
	stack<int> st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);
	stack<int> st2(st1);
	return 0;
}

2.1.3 使用其它的容器构造一个栈

#include<iostream>
using namespace std;
#include<stack>
#include<vector>
int main() {
	stack<int, vector<int>> st;
	return 0;
}

2.2 push--入栈

这个函数的作用是往栈里面入数据

#include<iostream>
using namespace std;
#include<stack>
int main() {
	stack<int> st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);
	st1.push(4);
	return 0;
}

2.3 size--获取元素个数

这个函数的作用是求栈中的元素个数

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

2.4 top--取栈顶元素

这个函数的作用是取出栈顶元素

#include<iostream>
using namespace std;
#include<stack>
int main() {
	stack<int> st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);
	st1.push(4);
	cout << st1.top()<< endl;
	return 0;
}

2.5 empty--判空

这个函数的作用是判断一个栈是否为空,空就返回true,非空就返回false。

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

2.6 pop--出栈

这个函数的作用是删除栈顶元素,注意这个和top函数的区别,top函数只是取出栈顶元素,不会删除,这个函数会删除栈顶元素。

#include<iostream>
using namespace std;
#include<stack>
int main() {
	stack<int> st1;
	st1.push(10);
	st1.push(20);
	st1.push(30);
	st1.push(40);
	st1.pop();//删除栈顶元素40
	cout << st1.top() << endl;  //取出栈顶元素30
	return 0;
}

三、queue的介绍

queue文档介绍

队列 【queue】是一种特殊的数结构,同时也是一种 容器适配器,遵循先进先出FIFO原则,其中从容器一端插入元素,另一端提取元素。主要操作:入队、出队、判断队空、查看队头队尾元素等;队列在原则上 是不允许进行 中部 的操作,这样会破坏队列的完整性。

我们在queue文档介绍可以看见queue的定义,如下所示:

可以看到 队列【queue】也是作为容器适配器被实现,容器适配器是对特定类封装作为其底层容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层。元素从队尾入列,从队头出列。(容器适配器本文的后面会详细讲解,这里先了解一下它 和 队列 的关系)

可以看出,队列也有两个模板参数

  • 参数1:T 栈中的元素类型,同时也是底层容器中的元素类型
  • 参数2:Container 实现栈时用到的底层容器,这里为缺省参数,缺省结构为 双端队列 deque

也就是说,【queue】的底层容器可以是任何标准的容器类模板或者一些其特定的容器类,这些容器应该支持一下操作:

  • empty :检查队列是否为空
  • size :返回队列中的有效元素个数
  • front :返回队头元素的引用
  • back :返回队尾元素的引用
  • psuh_back :在队列尾部入队列
  • pop_front :在队列头部出队列

四、queue的常用函数

4.1 构造函数

4.1.1 默认构造函数

#include<iostream>
using namespace std;
#include<queue>
int main() {
	queue<int> q1; //底层结构默认是deque
	return 0;
}

4.1.2 拷贝构造函数

#include<iostream>
using namespace std;
#include<queue>
int main() {
	queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	queue<int> q2(q1);
	return 0;
}

4.1.3 使用其它的容器构造队列

#include<iostream>
using namespace std;
#include<queue>
#include<list>
int main() {
	queue<int,list<int>> q1; //底层结构是双向链表
	return 0;
}

4.2 push--入队

#include<iostream>
using namespace std;
#include<queue>
int main() {
	queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	return 0;
}

4.3 pop--出队

#include<iostream>
using namespace std;
#include<queue>
int main() {
	queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	q1.pop();
	return 0;
}

4.4 front--取队头元素

#include<iostream>
using namespace std;
#include<queue>
int main() {
	queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	cout << q1.front() << endl;
	return 0;
}

4.5 back--取队尾元素

#include<iostream>
using namespace std;
#include<queue>
int main() {
	queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	cout << q1.back() << endl;
	return 0;
}

4.6 empty--判空

#include<iostream>
using namespace std;
#include<queue>
int main() {
	queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	cout << q1.empty() << endl;
	return 0;
}

4.7 size--获取队列元素个数

#include<iostream>
using namespace std;
#include<queue>
int main() {
	queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	cout << q1.size() << endl;
	return 0;
}

五、容器适配器

5.1 什么是容器适配器

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

其中,容器适配器 可修改底层为指定容器,如由 vector 构成的、由 list 构成的队列

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

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

注意: 容器支持 迭代器,但是容器适配器不支持迭代器,因为栈和队列这种数据结构不能随便去遍历,不然会导致发生变化,不易维护

六、栈的模拟实现

6.1 栈底层结构的构建

我们需要像string一样,使用T* _a,size_t _top和size_t _capacity来模拟构建栈嘛?,如下所示:

#pragma once
#include<iostream>
using namespace std;
namespace zx {
	template<class T>
	class stack
	{
	private:
		T* _a;
		size_t _top;
		size_t _capacity;
	};
}

我们实现栈能不能使用其他的容器封装转换一下,就转化出对应的栈呢?

栈只需要实现在栈顶插入,在栈顶删除,只有这个容器在一端进行插入和删除,就可以使用这个容器来模拟实现栈。如vector,list。所以栈就不需要来原生的来实现,来封装一下vector和list就可以了。如下所示:

#pragma once
#include<iostream>
using namespace std;
namespace zx {
	template<class T>
	class stack
	{
	private:
		vector<T> v;
	};
}

但是这样写的话就写死了,栈的底层结构就只能是vector了,如果我们有时候需要使用链表类型的栈呢?所以我们应该把这个类型写成模板,可以随时切换,如下所示:

#pragma once
#include<iostream>
using namespace std;
namespace zx {
	template<class T,class Container>
	class stack
	{
	private:
		Container _con;
	};
}

使用Container来定义一个容器,这个Container是什么类型我们也不知道,传什么就是什么,传入vector就是vector实现的栈,传入list就是list实现的栈。对Container适配转换出stack。

6.2 栈的构造函数

我们需要写stack的构造函数嘛?

因为stack里面的数据是自定义类型,无论我们写不写它都会调用Container的构造函数,所以可以不用写构造函数。

6.3 其他成员函数

因为stack的底层结构是vector或者list,我们完全就可以使用vector和list的函数来实现stack的函数,如下所示:

#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<list>
namespace zx {
	template<class T,class Container>
	class stack
	{
	public:
		//入栈
		void push(const T& x) 
		{
			_con.push_back(x);
		}
		//出栈
		void pop() 
		{
			_con.pop_back();
		}
		//取栈顶元素
		const T& top() const
		{
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con, empty();
		}
	private:
		Container _con;
	};
}

6.4 测试

#include"Stack.h"

int main() {
	zx::stack<int, vector<int>> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	while (!st.empty()) {
		cout << st.top() << endl;
		st.pop();
	}
	return 0;
}

运行结果如下:

我们在声明这个栈的时候也可以这样来声明:zx::stack<int, list<int>> st;使用list来声明栈,此时stack的底层结构就成为了双向链表了。

6.5 给Container设置缺省值

但是我们在使用库里面的stack的时候并不需要传入vector和list类型,这是为什么呢?

我们可以在设置模板参数的时候给Container设置一个缺省值,使用默认的容器,如下所示:

template<class T,class Container=vector<T>>

此时,我们创建一个stack对象就不需要传入容器了,默认是vector容器,如果我们想要使用链表作为stack的底层结构,就可以传入list容器。

库里面使用的是deque容器,这个结构是一个双端队列,下面再讲

6.6 代码

6.6.1 stack.h

#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<list>
namespace zx {
	template<class T,class Container=vector<T>>
	class stack
	{
	public:
		//入栈
		void push(const T& x) 
		{
			_con.push_back(x);
		}
		//出栈
		void pop() 
		{
			_con.pop_back();
		}
		//取栈顶元素
		const T& top() const
		{
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

6.6.2 test.cpp

#include"Stack.h"

int main() {
	zx::stack<int, vector<int>> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	while (!st.empty()) {
		cout << st.top() << endl;
		st.pop();
	}
	return 0;
}

七、队列的模拟实现

我们写完栈的模拟实现之后,队列的模拟实现也是如此,如下所示:

#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<list>
namespace zx {
	template<class T, class Container = list<T>>
	class queue
	{
	public:
		//在队尾入队
		void push(const T& x)
		{
			_con.push_back(x);
		}
		//在队头出队
		void pop()
		{
			_con.pop_front();
		}
		//取队尾元素
		const T& back() const
		{
			return _con.back();
		}
		//取队头元素
		const T& front() const
		{
			return _con.front();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

我们在上面使用list作为默认的容器,这是因为vector不直接支持头删,间接支持头删,并且头删效率很低。因此使用list。

测试代码如下:

#include"Queue.h"
int main() {
	zx::queue<int> q1;
	q1.push(1);
	q1.push(2);
	q1.push(3);
	q1.push(4);
	while (!q1.empty()) {
		cout << q1.front() << endl;
		q1.pop();
	}
	return 0;
}

八、deque的介绍

在STL中stack和queue默认用deque作为Container,下面我们就来介绍一些deque。

8.1 deque的介绍

双端队列【deque】:是一种双开口 ”连续“ 空间的数据结构,双开口的含义:可以在头尾端进行插入和删除操作,且时间复杂度为:O(1) ,与【vector】比较头插效率高,不需要移动元素与【list】比较空间利用率比较高 

8.2 deque的底层结构

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:

deque(双端队列)的底层结构通常由多个固定大小的缓冲区组成,每个缓冲区是一个连续的存储块。然后使用一个中控指针数组来记录每一个缓冲区的起始地址,这样就能记录每一个缓冲区。

deque的内部缓冲区以分块的形式存储元素。每个缓冲区有一个固定的大小,它通常是2的幂次方,例如512、1024等。缓冲区中的元素被存储在数组中,以保持元素的连续性。

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:

当需要在deque的头部或尾部插入或删除元素时,只涉及到相关缓冲区的操作,而不会涉及其他缓冲区。这种设计使得deque的插入和删除操作时间复杂度为常数级别(O(1))。

8.3 deque的优缺点

【vector】的优缺点:

  • 优点:适合尾插尾删,随机访问
  • 缺点:不适合头部或者中部插入删除,效率低,需要挪动数据;扩容有一定性能消耗,还可能存在一定程度的空间浪费。

【list】的优缺点:

  • 优点:在任意位置插入删除效率高;按需申请释放空间
  • 缺点:不支持随机访问;CPU高速缓存命中率低 

【deque】就结合了 【vector】和 【list】的优缺点而为之发明!!!

【deque】 与 【vector】相比较 的优势:头部插入和删除时,不需要搬移元素,效率特别高,而且效率特别高,而且在扩容时,也不需要移动大量元素,因此其效率是比 【vector】高。

【deque】 与 【list】相比较 的优势:其底层是连续空间,空间利用率较高,不需要存储额外字段

【deque】的致命缺陷:不适合遍历!!! 因为在遍历时,【deque】的迭代器要频繁的去检查其是否移动到某段空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑【vector】和 【list】。

8.4 stack和queue选择deque作为底层容器的原因

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

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

注意:string 、vector 不直接支持头删,只是间接支持头删除,并且头删效率低,因此无法适配 queue

但是在 STL 中对 【stack】和 【queue】默认选择 【queue】作为其底层容器,原因为:

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

在【satck】中元素增长时,【deque】比 【vector】的效率更高(扩容时不需要移动大量元素)

在【queue】中元素增长时,【deque】不仅效率高,而且内存使用率高

总结来说:【deque】结合了所有 【stack】 和 【queue】所需要的优点,而完美的避开了其缺陷

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值