C++——STL容器——stack、queue、priority_queue

1.适配器模式

        这一篇文章我们将要学习栈、队列、优先级队列的使用方法和细节问题。首先我们需要考虑怎么去定义这些容器的基本结构。

        以栈stack为例,在C语言阶段,我们曾经学习过栈的结构与实现,所以我们当然可以自然地想到可以仿照C语言中的经验,以顺序表的方式组织栈的结构。于是我们就能够写出对应C++下栈这个类的定义。

	template<class T>
	class stack {
	private:
		T* _a;
		int _top;
		int _capacity;
	};

         在C语言中,我们采取一个顺序表数组来作为栈的底层结构。但是现在我们使用了C++,作为C语言的plus版本,理应有更方便的方式来定义出来栈。C语言版本下的栈因为是一个动态数组,所以需要考虑容量和元素数量等的问题,这些都需要我们自己动手处理。但是仔细一想,所谓动态数组我们曾在C++中使用过,不正是vector吗?所以我们现在为了省时省力,不禁思考是否能用vector作为栈的底层来组织栈呢?

        事实告诉我们是可以的,而且这种方法很好用。stack并不会对数据有什么特殊的组织方式,它只是对数据的存取访问有一些规则的限制,实际对数据的底层组织形式还是顺序表的方式。所以如stack、queue、priority_queue实际就是对其他容器进行了一层包装而得到的,因此我们称之为容器适配器。

        所谓容器适配器,就是为了实现自己的功能选择合适的容器进行适配,也就是对已经存在的容器进行了转换包装,让其作为自己的底层从而可以实现新的功能。

        适配器的设计模式,因为需要选择合适的容器进行适配,所以体现在代码中表现为增加了一个选择底层容器的参数。

2.deque

        在开始了解栈、队列之前,我们需要再铺垫一层知识,我们引入另外一种容器,即deque。

2.1 deque数据结构

        deque作为stl容器之一,叫做双端队列。通过它的名称我们已经窥见一些门路了,即这个容器是一个重点着眼于双端操作的结构。

        对于实例化的deque<T>,是通过一个元素为T*的中控数组(vector)来管理数据,这个中控器中的每个元素都是一段连续空间(缓冲区)(vector)。所以可以感觉到deque有种类似于二维数组的感觉,中控数组中存储着缓冲区的地址,这样可以通过中控数组访问管理各个段的缓冲区。而缓冲区各自连续,彼此之间不连续,所以中控数组的作用就是维持这样一个连续存储的假象,以方便访问。

        所以通过这种方式,deque就可以支持任意下标的访问,并且效率可观。但是因为缓冲区之间彼此不连续,所以存在着对下标进行计算,找到对应的缓冲区,再找到缓冲区中的元素,所以效率不及vector。

        deque的另一个优点是无论是头部操作还是尾部操作效率都很高,因为其特殊的缓冲区的结构,所以在头尾操作时不需要移动任何元素,只需要在最前方和最后方插入删除即可。因此deque的优点就是在可以随机访问的同时头尾操作效率高,因此很适合头尾操作频繁的场景,所以stack和queue选择了deque作为容器。

2.2 deque的迭代器

deque的迭代器定义了一个元素的四个信息:

        cur--当前元素的指针

        first--当前元素所在缓冲区的起始指针

        last--当前元素所在缓冲区的结尾指针

        node--当前元素所在缓冲区在中控数组的指针

3.stack

        在具有了以上知识的铺垫,我们再来设计栈就显得轻松许多了。

        我们采取适配器模式,在模板参数中增加一个选择容器的参数,缺省值使用deque容器,这样的话我们仅仅需要一个成员变量,即容器对象即可。在实例化栈的时候,确定了模板参数之后,实例化出的栈对象中是另一个容器的对象。于是对栈操作的成员函数就可以反应为容器对象的成员函数的操作。

        于是借助容器的操作我们就可以实现如出栈、入栈、取栈顶和判空等操作。这里也看出来了对于容器成员函数命名一致的作用,在调用的时候因为容器命名一致所以可以直接不加考虑使用同一个函数名调用同样功能但是不同容器的函数。

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

4.queue

        对于队列而言,和栈没有什么大的区别了。和栈类似,队列也使用适配器模式,同时使用容器的成员函数完成自己特定的功能。

	template<class T, class Container = std::deque<T>>
	class queue {
	public:
		size_t size()
		{
			return _con.size();
		}
		bool empty()
		{
			return _con.empty();
		}
		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();
		}
	private:
		Container _con;
	};

5.priority_queue

        对于优先级队列而言,就不像栈和队列那么容易了,优先级队列中有一些需要考虑到的细节问题。

5.1 优先级队列简介

        优先队列实际就是我们所学过的堆的数据结构的容器,默认是大堆。之所以取名叫作优先队列,是因为对于堆而言,最有价值的就是堆顶的元素,因为堆顶的元素是所有元素中的最大或最小的一个,堆的各种应用也是利用了这一点。所以因为其首元素的特殊特征,所以认为是队列中优先级最高的元素,所以称为优先队列。

        priority_queue也是采取适配器模式,默认使用的底层容器是vector。栈和队列因为是FILO和FIFO,所以操作的均是头尾的元素,而deque的出现正是为了解决头尾操作问题的,所以它们默认使用了deque的容器。而回忆堆的知识会发现,堆的最关键的向上调整和向下调整会涉及到频繁的随机下标访问,而下标访问最佳的就是vector,所以采用vector作为容器适配器。

5.2 仿函数

        既然优先级队列是堆的结构,那就必然涉及到大堆小堆的分别,而我们稍微回顾一下就可以回忆起来,大小堆区分的关键在于向上调整和向下调整的比较逻辑:大堆要求父结点大于子结点,而小堆则相反,所以只要能够合理地进行比较逻辑控制,就可以进行大小堆的控制。

        在C语言中,我们为了可以灵活控制大小堆,对于这个比较逻辑我们使用了函数来实现,而比较函数则是在调用向上向下调整函数时作为参数传入的。所以在C语言中我们是使用了函数指针的方法来选择大小堆的。那么进入C++中,是否有新的方便的方法可以控制大小堆比较函数呢?因此就提出了C++中仿函数的概念。

        仿函数,如其名,仿函数并不是一个函数,而是一个类,只是使用方法类似于函数。

函数的调用方法:函数名(参数)

        可以发现函数调用如果有运算符的话,那括号似乎就是这个运算符。所以仿函数的类重载了()这个运算符,可以使用这个类的实例化对象调用()重载函数,而运算符重载的调用方式是直接使用在对象上即可。因此这样的调用方式在形式上就类似于函数调用,对象(参数) 的方式进行调用。

运算符重载的调用方法:对象.operator运算符(参数)

                                        运算符的常规逻辑

        我们知道()操作符有两种使用方式,一个是(表达式),另一个是函数调用的函数名(形参)。而我们重载的正是函数调用的括号操作符,它是一个具有若干操作数的操作符,它有左操作数即函数名,以及若干个右操作数,即形参列表。

	template<class T>
	class less {
	public:
		bool operator()(const T& e1, const T& e2) {
			return e1 < e2;
		}
	};

	template<class T>
	class greater {
	public:
		bool operator()(const T& e1, const T& e2) {
			return e1 > e2;
		}
	};

        因此对于如上两个类,我们先分析一下。首先他们是非静态成员函数,所以它们就有一个隐藏的第一个参数,即this指针。这个对象的this指针在调用的时候会接收第一个操作数,因此说明我们在调用仿函数的时候第一个操作数(即左操作数)应该是对象,然后剩余的操作数对应着重载函数中的其他参数。所以最后的括号运算符的调用方式应该是:对象(其他操作数),这就和函数调用的形式不谋而合了。

        因此我们调用的方式就很明晰了:

less<int> lessfunc;
lessfunc(1,2);

greater<int> greaterfunc;
greaterfunc(1,2);

5.3 优先级队列的模拟

        那么再来考虑priority_queue,除了元素类型、容器适配器之外,还需要一个仿函数类来控制大堆和小堆。实现大堆利用的就是less仿函数,less的()重载了小于比较,因此调用()时完成的就是两个参数之间的小于比较;而实现小堆则利用的是greater仿函数,其将()重载为了大于比较。

template<class T>
class less {
public:
	bool operator()(const T& e1, const T& e2) {
		return e1 < e2;
	}
};

template<class T>
class greater {
public:
	bool operator()(const T& e1, const T& e2) {
		return e1 > e2;
	}
};

template<class T, class Container = std::vector<T>, class Compare = less<T>>
class priority_queue {
public:
	size_t size()
	{
		return _con.size();
	}
	bool empty()
	{
		return _con.empty();
	}
	void adjust_up(size_t child)
	{
		size_t parent = (child - 1) / 2;
		while (child > 0)
		{
			//Compare cmp;
			//if (_con[child] > _con[parent])	//大堆
			//if (_con[child] < _con[parent])	//小堆
			if(Compare()(_con[parent],_con[child]))	//仿函数(匿名对象)
			{
				std::swap(_con[child], _con[parent]);
				child = parent;
				parent = (child - 1) / 2;
			}
			else
			{
				break;
			}
		}
	}
	void push(const T& x)
	{
		_con.push_back(x);
		adjust_up(_con.size()-1);
	}
	void adjust_down(size_t parent)
	{
		Compare cmp;
		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() && _con[child] > _con[child + 1])	//小堆
			if (child + 1 < _con.size() && cmp(_con[child] , _con[child + 1]))	//仿函数(有名对象)
			{
				++child;
			}
			//if (_con[child] > _con[parent])	//大堆
			//if (_con[child] < _con[parent])	//小堆
			if (cmp(_con[parent] , _con[child]))	//仿函数
			{
				std::swap(_con[child], _con[parent]);
				parent = child;
				child = parent * 2 + 1;
			}
			else
			{
				break;
			}
		}
	}
	void pop()
	{
		std::swap(_con[0], _con[_con.size()-1]);
		_con.pop_back();
		adjust_down(0);
	}
	const T& top()
	{
		return _con.front();
	}
private:
	Container _con;
};

        其中对于仿函数的调用方式,首先要明确虽然称作仿函数,但实际上仍然只是一个类,调用仿函数实际上就是在调用类的成员函数。所以在调用时首先需要实例化对象,因为我们的目的是调用仿函数,所以可以实例化为有名对象,也可以是匿名对象。然后利用对象进行()运算符的重载函数的调用,对象(参数) ,因为()运算符的调用方式和函数不谋而合,所以才叫仿函数。

5.4 优先级队列的使用

        因为优先级队列的模板列表中由仿函数来控制比较,在C++中已经实现了less和greater两个类,而这两个类中的比较实际是通过大于小于运算符进行比较的。所以需要明确,当模板实例化为内置类型时,因为内置类型由标准库提供大于小于比较重载,所以可以直接使用

	void Test3()
	{
		priority_queue<int, std::vector<int>> s1;
		s1.push(1);
		s1.push(2);
		s1.push(9);
		s1.push(4);
		s1.push(8);
		s1.push(3);
		while (!s1.empty())
		{
			std::cout << s1.top() << ' ';
			s1.pop();
		}
		std::cout << std::endl;
	}

 9 8 4 3 2 1 

        而对于自定义类型实例化时,首先要保证自定义类型重载了>、<的操作符,否则会无法比较而产生bug。 

        对于string类型,std容器实现了其的大于小于操作符重载所以可以正常使用优先级队列。

	void Test6()
	{
		priority_queue < std::string, std::vector<std::string>, greater<std::string>> s1;
		s1.push("hello");
		s1.push("the next");
		s1.push("final");
		while (!s1.empty())
		{
			std::cout << s1.top() << std::endl;
			s1.pop();
		}
	}

final
hello
the next

        而对于其他自定义类型,则需要自己实现一个新的仿函数或者大于小于比较符重载来支持优先级队列。一般选择实现新的仿函数,因为比较符重载最后只能有一种逻辑,而对于需要多种逻辑的比较而言可以提供多种仿函数。

	//对于其他没有重载比较运算符的自定义类型,就需要自己实现比较逻辑来交给优先队列
	struct A {
		int a;
		char c;
	};
	//可以看到,仿函数的类不一定需要类模板,在priority_queue实例化时是类即可
	class A_less_a {
	public:
		bool operator()(const A& e1, const A& e2)
		{
			return e1.a < e2.a;
		}
	};
	template<class T>
	class A_less_c {
	public:
		bool operator()(const T& e1, const T& e2)
		{
			return e1.c < e2.c;
		}
	};
	std::ostream& operator<<(std::ostream& out, const A& a)
	{
		out << a.a << ',' << a.c;
		return out;
	}
	void Test7()
	{
		priority_queue<A, std::vector<A>, A_less_a> q;
		q.push({ 7,'f' });
		q.push({ 3,'j' });
		q.push({ 10,'b' });
		q.push({ 9,'a' });
		q.push({ 2,'p' });
		q.push({ 5,'k' });
		while (!q.empty())
		{
			std::cout << q.top() << std::endl;
			q.pop();
		}
	}
	void Test8()
	{
		priority_queue<A, std::vector<A>, A_less_c<A>> q;
		q.push({ 7,'f' });
		q.push({ 3,'j' });
		q.push({ 10,'b' });
		q.push({ 9,'a' });
		q.push({ 2,'p' });
		q.push({ 5,'k' });
		while (!q.empty())
		{
			std::cout << q.top() << std::endl;
			q.pop();
		}
	}

10,b
9,a
7,f
5,k
3,j
2,p

2,p
5,k
3,j
7,f
10,b
9,a

  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

犀利卓

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

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

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

打赏作者

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

抵扣说明:

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

余额充值