C++入门小馆: STL 之queue和stack

发布于:2025-05-01 ⋅ 阅读:(22) ⋅ 点赞:(0)

嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let's go!

我的博客:yuanManGan

我的专栏:C++入门小馆 C言雅韵集 数据结构漫游记  闲言碎语小记坊 题山采玉 领略算法真谛

目录

怎么使用队列和栈:

队列和栈的模拟实现:

deque:

priority_queue


先来了解

怎么使用队列和栈:

这些接口看起来已经很熟悉了。

直接来看看使用吧:

使用就很简单了。

队列和栈的模拟实现:

我们先来了解一下适配器:

适配器大家比较熟悉的就是电源适配器,我们的充电器。

queue和stack都是一种容器适配器。

我们用其他类来封装这两个容器。

我们回忆一下stack只需要在一方插入和删除,我们vector和list都能满足要求,所以我们来简单来实现一下。

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

namespace refrain
{
	template<class T, class Container = std::deque<T>>
	class stack
	{
	public:
		T& top() 
		{
			return _con.back();
		}
		const T& top() const
		{
			return _con.back();
		}
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

deque:

这里的缺省值给的是deque双端队列,等会再来讲解。

再来回忆一下队列,它是先进先出,一端入数据一端出数据,我们就拿头部出数据,尾部入数据,简单实现如下:

#include<deque>
#include<iostream>
using namespace std;
namespace refrain
{
	template<class T, class Container = std:: deque<T>>
	class queue
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		T& front()
		{
			return _con.front();
		}
		const T& front() const
		{
			return _con.front();
		}
		T& back()
		{
			return _con.back();
		}
		const T& back() const
		{
			return _con.back();
		}
		void pop()
		{
			_con.pop_front();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

这里我们有个问题我们的vector不是没有pop_front吗,那我们是不是不能用vector来实现队列呢?的确库里面他就是用的pop_front,库里面不想你使用vector来实现,因为vector头部操作麻烦时间复杂度高,那我们能不能强制实现呢?我们可以使用erase来实现头部删除就可以用vector来实现queue了。

这是库里实现的:

这样就适用了,但我们一般用deque这个更优秀

接下来就简单讲讲deque了:

先来对比一下list和vector的各自的优缺点:

而我们vector的优点就是list的缺点,vector的缺点就是list的优点。

优点: 缺点
vector

1.支持下标随机访问,

2.CPU高速缓存命中率高

1.头部或中间位置的插入删除操作效率低下,因为要挪动数据

2.扩容有一定的成本,存在一定的浪费。比如现在容量为100满了扩成200只插入了120空间浪费了80空间。

list

1.容易位置的插入和删除的时间复杂度都是O(1),不需要挪动数据。

2.不存在扩容,按需申请释放,不浪费空间。

1.不支持下标随机访问

2.CPU高速缓存命中率低

这里补充一个知识CPU高速缓存命中率的知识:

cpu想要访问内存的一个数据,要看这个数据是否在缓存,在就缓存命中,不在就不命中;不命中就先加载到缓存,然后再访问缓存。

它加载到缓存不是只加载一个数据而是加载一个范围的数据,因为vector创建的是连续的空间,cpu的高高速缓存就高,而list每次都得先加载到缓存然后再访问缓存。

接下来就来简单了解一下deque的底层实现了。 

我们用四个指针封装deque的迭代器。

我们创建多个buff数组,用first指向buff的第一个位置,last指向最后一个位置。用cur指向当前位置。

再创建一个中控数组map,map里面存储着指向buff数组的指针,也就是二级指针。

这样设计有什么好处呢?

当我们迭代器++时

直接++cur,如果cur是最后一个的时候就移动node代码如下:

库里面是这样实现的: 

逻辑是一样的,但它的细节 处理的更好。

减减也是相同的逻辑,就不实现了。

解引用操作就很简单了

直接解引用cur。

push_back:

如果push_back时buff没有慢,就直接cur++,last++。 

当buff已经满了,我们就得创建一个buff了,让node++,然后cur指向buff的第二个元素,last指向末尾,first指向第一个元素,如图。

再来看看push_front

当buff满了,就创buff,此时我们cur不是指向第一个元素,指向的是最后一个元素,要保持连续性,node--,last,first指向尾和头。

再头插一个:

j就直接cur--就行了。

总结一下:

优点:

         1.头尾部插入删除效率很高

         2.下标随机访问效率还可以,但不如vector。

缺点:

         1.中间位置插入和删除效率一般。

         2.对比vector和list没有那么极致。(不专一)

 适合做stack和queue的适配器

priority_queue

优先级队列也不是队列是堆,堆之前我们用c语言就实现过,我们看看stl库里面怎样实现的。

我们之前实现的堆,大家回忆一下,是不是用到了大量的下标访问 ,我们就可以使用vector来当容器适配器。

当我们插入一个数50的时候,这时候还是一个堆,就不进行操作

而当我们插入35的时候:我们要进行向上调整算法。 

 

那我们回忆一下怎么找父亲节点呢?

parent = (child - 1)/ 2。

我们就写一个向下调整算法

如果c语言实现的时候认真学习的应该没有什么问题。

同样的删除堆顶时我们交换堆顶和最后一个位置,然后尾删,进行向下调整算法

我们找左右孩子怎么找到呢?

左孩子: child = parent * 2 + 1

右孩子:child = parent* 2 + 2

一样实现的轻而易举:

 我们就来实现一下堆吧:

template<class T, class Container = std::vector<T>>
class priority_queue
{
public:

	void push(const T& x)
	{
		_con.push_back(x);
		adjustup(_con.size() - 1);
	}
	void pop()
	{
		std::swap(_con[0], _con[_con.size() - 1]);
		_con.pop_back();
		adjustdown(0);
	}
	const T& top() const
	{
		return _con[0];
	}
	bool empty()
	{
		return _con.empty();
	}
	//大堆
	void adjustup(int child)
	{
		int parent = (child - 1) / 2;
		while (child > 0)
		{
			if (_con[child] > _con[parent])
			{
				std::swap(_con[child], _con[parent]);
				child = parent;
				parent = (child - 1) / 2;
			}
			else
			{
				break;
			}
		}
	}
	void adjustdown(int parent)
	{
		int child = parent * 2 + 1;
		while (child < _con.size())
		{
			if (child + 1 < _con.size() && _con[child + 1] > _con[child])
			{
				child++;
			}
			if (_con[child] > _con[parent])
			{
				std::swap(_con[child], _con[parent]);
				parent = child;
				child = parent * 2 + 1;

			}
			else
			{
				break;
			}
		}
	}
private:
	Container _con;
};

 

这里的top就不能实现两个版本了,因为栈顶的元素不支持修改。 

这里还缺少一个参数是什么呢,这里可以用仿函数来实现:

仿函数

我们可以通过重载( )括号运算符来实现仿函数:

 

我们可以利用这个特性来让我们选择能创的是大堆还是小堆,但我们这个有点坑,传less创建的是小堆,我们只好按它的来哦。

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

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

	template<class T>
	class greater
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x > y;
		}
	};
	template<class T, class Container = std::vector<T>, class Compare = less<T>>
	class priority_queue
	{
	public:
		Compare com;
		void push(const T& x)
		{
			_con.push_back(x);
			adjustup(_con.size() - 1);
		}
		void pop()
		{
			std::swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			adjustdown(0);
		}
		const T& top() const
		{
			return _con[0];
		}
		bool empty()
		{
			return _con.empty();
		}
		//大堆
		void adjustup(int child)
		{
			int parent = (child - 1) / 2;
			while (child > 0)
			{
				if (com(_con[parent], _con[child]))
				{
					std::swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
		void adjustdown(int parent)
		{
			int child = parent * 2 + 1;
			while (child < _con.size())
			{
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
				{
					child++;
				}
				if (com(_con[parent], _con[child]))
				{
					std::swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;

				}
				else
				{
					break;
				}
			}
		}
	private:
		Container _con;
	};


}

 完了over!


网站公告

今日签到

点亮在社区的每一天
去签到