飞道的博客

栈和队列以及认识优先级队列与双端队列(C++STL)

257人阅读  评论(0)

1. 栈和队列

栈最优实现是数组。

  • 优点:随机访问,cpu缓存利用率高
  • 缺点:头部和中间,删除,插入效率较低。且需要增容

队列最优实现是链表。

  • 优点:任意位置,插入和删除,O(1)
  • 缺点:不支持随机访问,缓存利用率低

2. 栈的模拟实现

#pragma once
//适配器模式
namespace zjn{
   

	template <class T, class Container = deque<T> >
	class stack{
   
	public:
		void push_back(const T& x)
		{
   
			_con.push_back(x);
		}
		void pop()
		{
   
			_con.pop_back();
		}

		const T& top()const
		{
   
			return _con.back();
		}
		bool empty()const
		{
   
			return _con.empty();
		}
		size_t size()const
		{
   
			return _con.size();
		}


	private:

		Container _con;
	};
}

3. 队列的模拟实现

#pragma once

namespace zjn{
   

	template <class T, class Container = deque<T> >
	class queue{
   
	public:
		void push_back(const T& x)
		{
   
			_con.push_back(x);
		}
		void pop()
		{
   
			_con.pop_front();
		}

		const T& front()const
		{
   
			return _con.front();
		}

		const T& back()const
		{
   
			return _con.back();
		}
		bool empty()const
		{
   
			return _con.empty();
		}
		size_t size()const
		{
   
			return _con.size();
		}


	private:

		Container _con;
	};
}

4. 认识双端队列

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)。队列只能先进先出,他和队列没有关系,更像一个vector和list的融合体。
与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。

vector和list现在依旧存在,证明了其实他是一个最终失败的设计,但是队列和栈依旧用它来适配,队列和栈主要就是对头尾进行操作,说明对于他们两个来说,用双端队列来实现是个不错的选择。

一个个分散的空间无法随机访问,连续的空间需要增容,所以双端队列采取折中的方式,将数据存在一段空间里。假如空间满了,不是增容而是转入下一个缓冲区。
这些缓冲区可以看做一个个数组,使用一个指针数组来管理他(即一个个指针指向缓冲区)

T* arr[]
或者
T** arr=(T**)malloc(sizeof(int*));


push_back,当前空间满了不扩容,转向下一个数组。
那中间插入呢,挪动数据吗,插入删除效率低,但单个扩容呢,【】可以使用是因为他根据每个数组的大小是一致的,相除算出你在那一个数组,单个扩容就会让数组大小不一致,那还要记录他增了多少,来计算。

迭代器是比较复杂的

有两个迭代器,一个start,一个finish。
first指向起始位置,last指向末尾的下一个位置。
cur指向当前位置。
node是下一个buf的首地址。
一般用迭代器的场景,

iterator it=begin();
while(it!=end())
{
   
*it;
it++;
}

*it实际就是对start中的cur进行解引用
it++就是让cur++。但是end()是finish中last的位置
cur++到自己buf的end()的时候,通过node找到下一个数组。
然后将找到的数组的开始和结束,给到start的开始结束。node++在指向下一个buf。
然后cur再次++。所以这个指针数组就像一个中控器。
所以总结一下,对于双端队列头删头插效率还可以,但是无法替代的原因就是,【】使用太频繁了,所以比较鸡肋

5. 优先级队列

对于入数据没有要求,但是出数据是按默认优先级高的来出,优先级是否高视场景而定。

由于他底层是堆,可以看到默认大堆,每次用的时候,取堆顶数据输出,然后删除堆顶,就完成了堆排序,所以他的pop接口里面必定有调堆的过程。

template很好理解,因为他底层是完全二叉树,使用vector来适配堆。Compare是一个仿函数,当less的时候是大堆,当传入greater的时候是小堆。

5.1 优先级队列实现

5.1.1 push

尾插数据,向上调整
大堆为例:

5.1.2 pop

删除堆顶。堆顶和堆尾交换,删除堆尾,向下调整

#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
#include<queue>
using namespace std;
namespace zjn{
   


	template<class T, class Container = vector<T>>
	class priority_queue
	{
   
	public:
		//写了有区间的,不会默认生成了,所以要再写一个无参的
		priority_queue()
		{
   };
		//对于已有的一段区间进行构造优先级队列,两种方法
		template<class iterator>
		priority_queue(iterator first, iterator last)
			:_Con(first,last)
		{
   
		    //向下调整法
			for (int i = ((_Con.size() - 1-1)/2); i >=0; i--)
			{
   
				AdjustDown(i);
			}
			//向上调整法建堆
			//for (int i = 1; i < _Con.size(); i++)
			//{
   
			//	AdjustUp(i);
			//}
		}
		//从下往上,和父亲开始比较
		void AdjustUp(int child)
		{
   
			//找出父亲
			int parnet = (child - 1) / 2;
			//本来应该是当孩子小于等于0时,父亲小于0停止,但是由于父亲是除法算出来的不会小于0,那就让孩子小于等于0时停止(孩子大于0继续)
			while (child>0)
			{
   
				//如果孩子比父亲大,那么就交换
				if (_Con[child]>_Con[parnet])
				{
   
					swap(_Con[child], _Con[parnet]);
					//向上继续迭代
					child = parnet;
					parnet = (child - 1) / 2;
				}
				//孩子小于父亲说明插入的数据刚好满足,退出
				else
				{
   
					break;
				}

			}
		}
		void push(const T& x)
		{
   
			//堆尾插入x
			_Con.push_back(x);
			//传下标,把孩子调整上去
			AdjustUp(_Con.size() - 1);
		}
		//从上往下,父亲和大的那一个孩子比较
		void AdjustDown(int parent)
		{
   
			//默认是左孩子
			size_t child = parent * 2 + 1;

			while (child<_Con.size())
			{
   
				//左右孩子大的那一个和父亲比较,注意当堆尾只有一个左孩子时,虽然child<size进入循环,但是他没有右孩子会导致越界

				if (child + 1<_Con.size() && _Con[child] < _Con[child + 1])
				{
   
					child++;
				}
				if (_Con[child]>_Con[parent])
				{
   
					swap(_Con[child], _Con[parent]);
					//向下继续迭代
					parent = child;
					child = parent * 2 + 1;

				}
				else
				{
   
					break;
				}
			}
		}
		//删除堆顶
		void pop()
		{
   
			assert(size() > 0);
			//堆顶和堆尾交换
			swap(_Con[0], _Con[_Con.size() - 1]);
			//尾删
			_Con.pop_back();
			//堆顶是原先堆尾的数据,除了他其他结构并未变化,可以使用向下调整
			AdjustDown(0);
		}
		const T& top()const
		{
   
			/*return _Con.top();*/
			return _Con[0];
		}
		size_t size()const
		{
   
			return _Con.size();
		}
		bool empty()const
		{
   
			return _Con.empty();
		}


	private:
		Container _Con;

	};
	
}

5.2 仿函数

在上述实现中,固定的是个大堆,再次实现一个小堆,就需要我们改变里面的逻辑关系。仿函数可以帮我们解决这个问题。
先来看一下仿函数

可以看出仿函数其实是一个类,里面重载了运算符(),看起来有点像函数调用的()。
那么怎么运用到刚才的代码中呢,不得不佩服实现者是真的很厉害。

	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 = vector<T>,
class Compare=less<T> >
class priority_queue
{
   
.........
};

上面用一个模板,表示这是一个比较的类,这个类里面得成员方法是x>y,还是x<y,不知道,我就传进来一个模板。根据你实例化对象时所用的参数,再来决定。怎么用起来呢?

            Compare com;
				//if(_Con[child]>_Con[parent])
				if (com(_Con[child],_Con[parent]))

这个不确定的模板,定义一个不确定的对象,里面是不确定的关系,取决于你传入的是less类还是greater类,生成对应的对象调用他们自己的成员函数。实现比较关系,不过默认给了less类。关系是x>y,体现为_Con[child]>_Con[parent]默认实现为大堆。

假如队列里面存的是自定义类型,里面没有重载大于小于,那么可以把仿函数声明成友元,从而访问他的私有数据,从而对自定义类型的基本成员进行比较,来达到比较自定义类型。至于比较哪个成员变量,视场景而定。

来看一个应用场景,比如比较一个货物,按销量排序,按价格排序。

通过调用sort函数,里面传入仿函数对象,进行自定义比较

也直接匿名对象,很类似于c语言中的函数指针

假如面试官问起什么是仿函数,仿函数的作用,该怎么表述呢?

仿函数就是一个类,里面的成员函数里重载了(),这个()函数表示针对对象的比较关系。假如是自定义对象(日期,货物?),还可以根据场景,自己实现对对象的成员变量的比较。当然原本对象对于不同场景,大于,小于等具有相同意义,所以直接重载了比较运算符,直接用即可。

6. 优先级队列最终代码

#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
#include<queue>
using namespace std;
namespace zjn{
   
	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 = vector<T>,class Compare=less<T>>
	class priority_queue
	{
   
	public:
		//写了有区间的,不会默认生成了,所以要再写一个无参的
		priority_queue()
		{
   };
		//对于已有的一段区间进行构造优先级队列,两种方法
		template<class iterator>
		priority_queue(iterator first, iterator last)
			:_Con(first,last)
		{
   
			for (int i = ((_Con.size() - 1-1)/2); i >=0; i--)
			{
   
				AdjustDown(i);
			}
			//for (int i = 1; i < _Con.size(); i++)
			//{
   
			//	AdjustUp(i);
			//}
		}
		//从下往上,和父亲开始比较
		void AdjustUp(int child)
		{
   
			//传的是模板,从而实现,实例化对象时传入不同的比较类,实现大堆小堆。
			//greater com,less com
			Compare com;
			//找出父亲
			int parent = (child - 1) / 2;
			//本来应该是当孩子小于等于0时,父亲小于0停止,但是由于父亲是除法算出来的不会小于0,那就让孩子小于等于0时停止(孩子大于0继续)
			while (child>0)
			{
   
				//如果孩子比父亲大,那么就交换
				//if(_Con[child]>_Con[parent])
				if (com(_Con[child],_Con[parent]))
				{
   
					swap(_Con[child], _Con[parent]);
					//向上继续迭代
					child = parent;
					parent = (child - 1) / 2;
				}
				//孩子小于父亲说明插入的数据刚好满足,退出
				else
				{
   
					break;
				}

			}
		}
		void push(const T& x)
		{
   
			//堆尾插入x
			_Con.push_back(x);
			//传下标,把孩子调整上去
			AdjustUp(_Con.size() - 1);
		}
		//从上往下,父亲和大的那一个孩子比较
		void AdjustDown(int parent)
		{
   
			Compare com;
			//默认是左孩子
			size_t child = parent * 2 + 1;

			while (child<_Con.size())
			{
   
				//左右孩子大的那一个和父亲比较,注意当堆尾只有一个左孩子时,虽然child<size进入循环,但是他没有右孩子会导致越界

				if (child + 1<_Con.size() && com(_Con[child + 1], _Con[child]))
				{
   
					child++;
				}
				if (com(_Con[child], _Con[parent]))
				{
   
					swap(_Con[child], _Con[parent]);
					//向下继续迭代
					parent = child;
					child = parent * 2 + 1;

				}
				else
				{
   
					break;
				}
			}
		}
		//删除堆顶
		void pop()
		{
   
			assert(size() > 0);
			//堆顶和堆尾交换
			swap(_Con[0], _Con[_Con.size() - 1]);
			//尾删
			_Con.pop_back();
			//堆顶是原先堆尾的数据,除了他其他结构并未变化,可以使用向下调整
			AdjustDown(0);
		}
		const T& top()const
		{
   
			/*return _Con.top();*/
			return _Con[0];
		}
		size_t size()const
		{
   
			return _Con.size();
		}
		bool empty()const
		{
   
			return _Con.empty();
		}


	private:
		Container _Con;

	};
	
}


转载:https://blog.csdn.net/qq_45928272/article/details/115955812
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场