【C++】手把手教你写出自己的Stack和Queue类

在上一篇文章中,我介绍了如何模拟实现 list容器,今天我们来实现 栈(Stack)和队列(Queue)。

我将 栈 与队列放置在一起的原因是 这两种数据结构 是十分相似的,将他们放在一起可以相互比较,对照,让我们的学习更加简单。

在开始模拟实现之前,我需要先引入一些新的知识,比如说 优先级队列,容器适配器,deque等,这些对我们的最终效果与还原程度 至关重要,请同学们耐心阅读。

stack的介绍与使用

stack 简介

  1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。

  2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。

  3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
    empty:判空操作
    back:获取尾部元素操作
    push_back:尾部插入元素操作
    pop_back:尾部删除元素操作

  4. 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque

stack 的基本接口


函数说明接口说明
stack()构造空的栈
empty()检测stack是否为空
size()返回stack中元素的个数
top()返回栈顶元素的引用
push()将元素val压入stack中
pop()将stack中尾部的元素弹出

queue的介绍与使用

queue 简介

  1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素。
  2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列。
  3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以操作:
    empty:检测队列是否为空
    size:返回队列中有效元素的个数
    front:返回队头元素的引用
    back:返回队尾元素的引用
    push_back:在队列尾部入队列
    pop_front:在队列头部出队列
  4. 标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。

queue 的基本接口


函数声明接口说明
queue()构造空的队列
empty()检测队列是否为空,是返回true,否则返回false
size()返回队列中有效元素的个数
front()返回队头元素的引用
back()返回队尾元素的引用
push()在队尾将元素val入队列
pop()将队头元素出队列

容器适配器

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

举个例子,我们经常用到的 转换头 ,网线接头是一种 接口,路由器的接口 是USB口,两者无法直接转换,这是我们通过 转换头 就可以 将一个接口 间接 转换为另一个。

在这里插入图片描述

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

在这里插入图片描述

优先级队列

优先级队列简介

对于上面两种常见的数据结构,这里不再过多的介绍,接下来,我们介绍一种更为复杂的结构:优先级队列。

在这里插入图片描述

  1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最的。
  2. 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
  3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
  4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:

empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素

  1. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指
    定容器类,则使用vector。
  2. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数
    make_heap、push_heap和pop_heap来自动完成此操作。

优先级队列的使用

优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆

这里对”堆“这个特殊的树形结构不熟悉的话,可以看我关于堆的博客:
【C语言】堆


函数声明接口说明
priority queue() / priority queue(first,last)构造一个空的优先级队列
empty()检测优先级队列是否为空
top()返回优先级队列中最大(最小)元素,即堆顶元素
push(x)在优先级队列中插入元素x
pop()删除优先级队列中最大(最小)元素,即堆顶元素

我们来测试一下:

void Test1()
{
	vector<int> v{ 3,2,7,6,0,4,1,9,8,5 };
	priority_queue<int> q1;
	for (auto& e : v)
		q1.push(e);

	cout <<"堆顶元素:"<< q1.top() << endl;

	
}

可以看到q1中的元素按照大堆的方式排列的。
在这里插入图片描述
如果我们想要取堆顶元素时是最小值,也是可行的。

这时我们要在构造优先级队列的时候传三个参数。
之所以之前的构造中只需要传一个类型参数,是因为模板中的后两个是缺省参数, 其中第三个参数就是将大堆转化为小堆的关键,在模拟实现中我们会讲到。

priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
cout << q2.top() << endl;
// #include <functional> // greater算法的头文件

优先级队列的模拟实现

我们知道,堆的底层结构就是 堆(特殊的完全二叉树),因此核心思路就是对 普通的堆的元素进行堆的相关操作 并封装起来即可。

这里再次强调一下,不熟悉堆的同学看一下这篇博客:
【C语言】堆

这里我们先将 默认情况下(大堆)的 优先队列 模拟实现 。

基本框架

我们先将 priority_queue 的基本框架 写一下:

这里我们就可以很直观的感受 适配器 是什么了,模板参数中的第二个参数 就是 适配器,我们可以传vector 或者 list 来实现我们的 优先级队列。

Container模板 在 priority_queue类 中实例化,这时候我们就可以使用 实例化对象的接口,帮助我们实现priority_queue的接口。

namespace yyk
{
  template<class T,class Container = vector<T>>
  class priority_queue
  {
   public:
      //无参构造
      priority_queue()
      {}
      //有参构造
      termplate<class InputIterator>
      template_queue(InputIterator first, InputIterator last)
      {}
      //数据插入
      void push(const T& x) 
      {}
      //删除堆顶元素
      void pop()
      {}
      //获取堆顶元素
      const T& top()
      {} 
      //获取元素个数
      size_t size()
      {}
      //判空
      bool empty()
      {}
      
      
   private:
      Container _con;
  }
}

在具体实现接口之前,我们还要引入堆的几个算法(这里暂时默认调整为大堆):

  1. 堆的向上调整算法
  2. 堆的向下调整算法

关于具体分析,这里不再赘述,详情看 堆 的博客。

        //堆的向上调整算法
        void AdjustUp(size_t child)
		{
			
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (_con[parent] <_con[child])
				{
					swap(_con[parent]._con[child]);

					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}

			}
		}
		//堆的向下调整算法
		void AdjustDown(size_t parent)
		{
			int l_child = 2 * parent + 1;
			
			while (l_child < _con.size())
			{
				if (l_child + 1 < _con.size() && _con[l_child] < _con[l_child+1])
				{
					l_child++;
				}

				if (_con[parent] < _con[l_child])
				{
					swap(_con[l_child], _con[parent]);
					parent = l_child;
					l_child = parent * 2 + 1;
					
				}
				else
				{
					break;
				}
				
			}
		}

构造函数

对于有参构造,我们要做两件事:

  1. 将元素插入空间中
  2. 利用 建堆算法 将元素排列为 大堆

 //有参构造
 termplate<class InputIterator>
 template_queue(InputIterator first, InputIterator last)
  {
     //插入
     while (first != last)
	  {
			 _con.push_back(*first);
			 ++first;
	  }
     //建堆 (具体原理 见  堆 博客)
 	 for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
	 {
			AdjustDwon(i);
	 }			
			
  }

push

我们的push函数也很简单,我们先用 适配器的接口中的尾插函数,向 堆尾插入数据,再使用向上调整算法 重新将 数据 调整为大堆。

void push (const T& x)
{
  _con.push_back(x);
  AdjustUp(_con.size()-1);
}

pop

pop()是删除堆顶的元素。

在堆中,删除堆顶元素的时候,为了减少复杂度,我们通常:

  1. 将堆顶元素与堆中最后一个元素进行交换。
  2. 删除堆中最后一个元素
  3. 将堆顶元素向下调整到满足堆的特征为止
void pop()
{
  swap(_con[0],_con[_con.size()-1]);
  _con.pop_back();
  AjustDown();
}

其他接口

其他接口比较简单,我们一并实现。

        const T& top()
		{
			return _con[0];
		}

		size_t size()
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}

这里对于 top()要注意一下,当我们返回 堆顶引用的时候,最好加const,防止堆顶元素被篡改。

仿函数


至此,我们的优先级基本队列完成了,但是此时内部是一个大堆,如果我们取栈顶元素的时候想要最小的元素,这怎么办?

其实很容易想到,我们将两个堆的调整算法 改成 小堆版本的就可以,只需要改变部分符号就行。

但是,这样做的难度不小,”运算符“是很难作为参数或模板传入的。又或者我们直接写两份 优先级队列,一个大堆版,一个小堆版,不过这样造成了代码的低可读性,复用性。

这个时候就要用到我们的第三个 模板参数。

在这里插入图片描述
可以看到,第三个参数简单来说就是这样的,是一个缺省参数:

class Compare =less<T>

补充:Container::value 就是 模板类型参数 T ,typename 的作用是提示 编译器 这是一个类型,对于部分编译器,可以省略typename

我们注意到,这里的缺省参数是less,这就是我们要讲的 仿函数,又叫函数对象,是一种自定义类型。

以Less为例,它是一个类,实例化之后是类型的对象,可以像函数一样使用

template <class T>
class Less
{
 public:
   bool operator()(const T& x, const T& y)
   {
     return x < y;
   }
};
void test_Less()
{
  Less less;
  cout<< less.operator()(1,2) <<endl;
  //less是类型的对象,可以像函数一样使用
  cout<<less(1,2)<<endl;
}

也就是说,我们在less类中 讲 "()"重载了,使它等效于 “<” , 同理我们也可以 写出一个 “>” :

template<class T>
	class Greater
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x > y;
		}
	};

有了 仿函数,我们就可以将这个作为类模板 ,以此来控制 运算符,我们想要小堆,就将模板写为Greater,大堆就写Less:

        void AdjustUp(size_t child)
		{
			Compare com; //将Greart/Less模板实例化
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				//if (_con[parent] < _con[child])
				if (com(_con[parent],_con[child]))
				{
					swap(_con[parent], _con[child]);

					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}

		void AdjustDwon(size_t parent)
		{
			Compare com;
			size_t child = parent * 2 + 1;
			while (child < _con.size())
			{
				//if (child + 1 < _con.size() && _con[child] < _con[child+1])
				if (child + 1 < _con.size() && com(_con[child],_con[child + 1]))
				{
					++child;
				}

				//if (_con[parent] < _con[child])
				if (com(_con[parent],_con[child]))
				{
					swap(_con[parent], _con[child]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
				{
					break;
				}
			}
		}

deque(了解)

在模拟实现stack与queue之前,我们还有最后一个定义要了解:deque。

对于优先级队列和 stack,queue,都是适配器实现的,其中 优先级队列是利用 vector/queue的接口包装实现的,而 stack和queue是使用deque的接口包装实现的。

在这里插入图片描述
那么deque到底是什么?

deque的简介

deque(双端队列):是一种双开口的"连续"空间的数据结构。

双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高

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

在这里插入图片描述
这个结构比较复杂,这里只介绍其优缺点,有兴趣的同学可以看《stl原码剖析》。

deque的测试:

void test_deque()
	{
		deque<int>dp;
		dp.push_back(1);
		dp.push_back(2);
		dp.push_back(3);
		dp.push_back(4);

		dp.push_front(1);
		dp.push_front(2); 
		dp.push_front(3);
		dp.push_front(4);

		//list无法用[]访问
		//融合了vector 和list的优点,从使用的角度,避开了他们各自的缺点
		//
		for (size_t i = 0; i < dp.size(); ++i)
		{
			cout << dp[i] << " ";
		}
	}
}

deque的缺陷

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

与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。

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

为什么选择deque

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的优点,而完美的避开了其缺陷。

stack与queue模拟实现

我们已经了解过了优先级队列的实现,而stack和 queue更加简单,所以这里不再赘述:

stack

#pragma once
#include <vector>
#include<list>
#include<deque>

namespace yyk
{
	//stack是一个Container 适配(封装转换)出来的
	//给缺省参数 deque(双端队列)
  template<class T,class Container=std::deque<T>>
  class stack
  {
	  //Container 尾认为是栈顶
  public:
	  void push(const T& x)
	  {
		  _con.push_back(x);
	  }
	  void pop()
	  {
		  _con.pop_back();
	  }
	  const T& top()
	  {
		  return  _con.size();
	  }
	  bool empty()
	  {
		  return _con.empty();
	  }

  private:
	  Container _con;
  };
  void test_stack()
  {
	  stack<int, std::vector<int>>st;
	  st.push(1);
	  st.push(2);
	  st.push(3);
	  st.push(4);
	  while (!st.empty())
	  {
		  cout << st.top() << " ";
		  st.pop();

	  }
	  cout << endl;

  }
}

queue

#pragma once
#include <vector>
#include<list>
#include<deque>


namespace yyk
{
	//stack是一个Container 适配(封装转换)出来的
	//给缺省参数 deque(双端队列)
	template<class T, class Container = std::deque<T>>
	class queue
	{
		//Container 尾认为是队尾,头是队头
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_front();
		}
		const T& front()
		{
			return  _con.front();
		}
		const T& back()
		{
			return  _con.back();
		}
		bool empty()
		{
			return _con.empty();
		}

	private:
		Container _con;
	};
	void test_queue()
	{
		queue<int, std::vector<int>>st;//vector 头删效率低,不能头删
		st.push(1);
		st.push(2);
		st.push(3);
		st.push(4);
		while (!st.empty())
		{
			cout << st.front() << " ";
			st.pop();

		}
		cout << endl;

	}
	

至此,关于栈与队列的基础内容就结束了,欢迎讨论。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ornamrr

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

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

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

打赏作者

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

抵扣说明:

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

余额充值