【c++】STL容器-priority_queue优先级队列的使用与模拟实现,仿函数讲解(由浅入深讲解实现)

小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【c++】STL容器-stack和queue的使用与模拟实现(附加deque的简单讲解)——书接上文 详情请点击<——
本文由小编为大家介绍——【c++】STL容器-priority_queue优先级队列的使用与模拟实现,仿函数讲解


一、介绍

在这里插入图片描述

优先级队列是一种容器适配器,其结构类似于堆,默认是大堆,即第一个元素总是当前所有元素中最大的,那么有了priority_queue优先级队列以后当我们想要使用堆的时候我们就不需要再自己造轮子了,直接调用priority_queue优先级队列即可

  1. 优先级队列中的结构是大堆或小堆是采用仿函数进行控制,这个小编会在文章中间位置进行讲解
  2. 优先级队列被实现为容器适配器,容器适配器即对特定容器类进行封装作为其底层容器类。并且提供一套特定的成员函数来访问其元素
  3. 底层容器可以是任何标准容器类模板,也可以是其它特定设计的容器类。并且由于优先级队列的结构类似于堆,所以建堆以及调整的过程需要大量使用下标进行随机访问,所以这个底层容器类应该支持随机访问,并且支持以下操作
  • empty
  • size
  • front
  • push_back
  • pop_back
  1. 标准容器类vector和deque都支持这些接口,但是由于deque的随机访问需要进行计算,没有vector极致,在建堆以及调整的过程需要大量使用下标进行随机访问,所以默认情况下,如果没有特定的为priority_queue类实例化指定容器类,那么默认采用vector进行实例化

本文小编不会再围绕以下知识点进行详细的讲解,而是默认读者友友已经点击蓝字学习文章学会了关于堆的知识

  1. 在小编实现priority_queue优先级队列的过程中,由于优先级队列的结构类似于堆,所以小编会使用到堆的向下调整算法,向上调整算法,在本文向后继续学习的前请读者友友先学习这两种算法,小编在之前的文章中已经讲解过这两种算法了 详情请点击<——
  2. 同时本文还会使用到向下调整算法进行建堆,因为向下调整建堆的时间复杂度O(N)优于向上调整建堆O(N*logN),关于这个知识点,小编也已经进行了讲解,请读者友友先行点击后方蓝字进行学习 详情请点击<——
  3. 本文小编还会使用到堆的插入和删除元素,关于如何进行堆的插入和删除小编同样也已经讲解过了 详情请点击<——

二、模拟实现

铺垫

  1. priority_queue是类模板,由于模板的声明和定义不分离,以及为了将我们模拟实现priority_queue的和STL中的stack进行区分,所以我们将我们的模拟实现放在PriorityQueue.h中的命名空间中
  2. 在test.cpp包含main函数以及相关的头文件进行测试
    在这里插入图片描述

在这里插入图片描述

  1. 那么我们实现priority_queue的时候同样要使用类模板来进行实现,T作为priority_queue存储的数据类型,并且观察上图,模板参数列表中的模板参数还可以有缺省值,这个缺省值与函数的缺省值(函数的缺省值的是数据)不同的是模板参数的缺省值是类型,默认情况下我们采用vector作为stack的底层容器,即vector<T>这个类型作为Container这个模板参数的缺省值
  2. priority_queue是容器适配器,即通过对底层的容器进行一定的控制,封装进而实现priority_queue的预期接口,毕竟不能直接对容器进行控制,所以我们要控制容器实例化出的对象,并且封装这个对象的接口进而实现priority_queue的需求接口,所以我们应该使用Container定义一个私有成员变量_con作为priority_queue进行控制容器的入口
  3. 观察还有一个模板参数Compare其缺省值是less<T>,这个是仿函数,在本文小编为了更好的讲解和理解会先进行忽略这个模板参数Compare及其仿函数,并且在讲完主体实现后,再主体实现的基础上进行优化引入添加仿函数进行控制大堆和小堆
  4. 在使用vector这个容器的时候要包头文件#include <vector>
  5. 那么同样的小编会将向上调整算法和向下调整算法(默认向上调整算法和向下调整算法的实现的堆是大堆的形式)的实现使用私有访问限定符封装起来,因为这是内部接口,不需要对外提供给用户
#include <vector>

namespace wzx
{
	template<typename T, typename Container = vector<T>>
	class priority_queue
	{
	private:
		void AdjustDown(int parent)
		{
			int child = parent * 2 + 1;

			if (child + 1 < _con.size() && _con[child] < _con[child + 1])
			{
				child++;
			}

			while (child < _con.size())
			{
				if (_con[parent] < _con[child])
				{
					swap(_con[parent], _con[child]);
				}
				else
				{
					break;
				}

				parent = child;
				child = parent * 2 + 1;
			}
		}

		void AdjustUp(int 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;
				}
			}
		}

	public:
		priority_queue();
		
		template<typename InputIterator>
		priority_queue(InputIterator begin, InputIterator end);
		
		void push(const T& val);
		void pop();
		const T& top() const;
		bool empty() const;
		size_t size() const;
	private:
		Container _con;
	};

priority_queue构造函数

在这里插入图片描述

  1. 关于函数参数列表中的comp和ctnr这里我们将其忽略即可,那么第一个就是一个无参的构造,第二个就是使用一段迭代器区间进行构造
  2. 迭代器区间构造,由于priority_queue优先级队列中可以存储不同类型的数据,所以同样也可以使用不同类型的迭代器进行构造,所以这里使用迭代器区间进行构造的构造函数我们应该将其设计成一个模板函数
  3. begin和end的迭代器区间是左闭区间右开区间的,使用迭代器的解引用并且不断的将数据尾插到_con对象中,当迭代到首尾迭代器相等的时候即结束插入,这时候这个迭代器区间中的数据已经全部尾插到_con对象中了,由_con对这些数据进行初步的存储和管理
  4. 仅仅将数据进行存储起来还不行,priority_queue优先级队列的结构是大堆,那么应该将_con管理的无序数据使用向下调整建堆从最后一个父亲节点开始进行调整,直到调整完根节点之后结束,这样就完成了大堆的建立,也就完成了使用迭代器区间构造大堆构造priority_queue优先级队列
template<typename InputIterator>
priority_queue(InputIterator begin, InputIterator end)
{
	while (begin != end)
	{
		_con.push_back(*begin);
		++begin;
	}

	for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(i);
	}
}
  1. 无参的构造,这里的无参的构造虽然什么都没有做,但是我们不可以不写,因为当我们显示写了使用迭代器区间进行构造函数的时候编译器就不会生成默认的构造函数了,此时当用户使用无参的构造函数去构造priority_queue优先级队列,那么就会没有构造函数去调用,因为迭代器区间的构造函数类型不匹配,且此时编译器没有生成默认的构造函数,所以此时需要我们显示写一个什么都不做的无参的构造函数
  2. 注意,对于成员变量中的vector<T>类型的_con对象,我们不需要对其进行构造,析构等操作,这些是由_con对象自动调用vector中的构造和析构函数完成对应的操作,即_con对象是类的自定义类型,自动进行管理
priority_queue()
{}

push

  1. 函数参数由于我们不知道T是内置类型还是自定义类型的,如果是自定义类型的传参消耗大,所以我们这里传引用传参,并且我们对其不进行修改,所以加const进行修饰
  2. push是在数据序列的的结尾入数据,那么我们直接调用尾插即可实现
  3. 尾插之前priority_queue优先级队列是结构是大堆,那么为了在尾插之后维持大堆的结构,那么我们应该在进行尾插的元素位置采用向上调整算法进行调整维持大堆结构
void push(const T& val)
{
	_con.push_back(val);

	AdjustUp(_con.size() - 1);
}

pop

在这里插入图片描述

  1. pop是进行删除首个数据,即priority_queue优先级队列结构的堆顶数据,这时候我们首元素和尾元素进行交换,将已经换到尾元素位置上的原首元素进行删除即尾删,在将已经交换到首元素位置上的原尾元素将其进行向下调整即可
void pop()
{
	swap(_con[0], _con[_con.size() - 1]);

	_con.pop_back();

	AdjustDown(0);
}

top

在这里插入图片描述

  1. top是用于获取priority_queue优先级队列结构的堆顶的引用(头部的数据的引用),这里我们调用front即可,其返回的就为尾部数据的引用并且观察在STL容器中的priority_queue优先级队列的top接口是返回的引用是不允许被修改的,所以这里我们返回const引用
  2. 那么既然的是const返回权限为只读,那么我们也可以使用const修饰this指针指向的对象,即让const对象和普通对象都可以进行调用
const T& top() const
{
	return _con.front();
}

empty

在这里插入图片描述

  1. empty是进行判断容器中的数据是否为空,这里我们调用empty即可实现
  2. 这里仅仅是进行判断,并不对数据进行修改,所以这里我们加const进行修饰this指针指向的对象,让普通对象和const对象都可以进行调用
bool empty() const
{
	return _con.empty();
}

size

在这里插入图片描述

  1. size是进行获取容器中的数据个数,这里我们调用size即可实现
  2. 这里仅仅是进行获取数据个数,并不对数据进行修改,所以这里我们加const进行修饰this指针指向的对象,让普通对象和const对象都可以进行调用
  3. 返回值是个数,断然不可能为负数,所以返回值类型我们采用size_t
size_t size() const
{
	return _con.size();
}
测试
  1. 使用如下代码测试priority_queue优先级队列的结构为大堆的情况下的函数接口输出情况
void test_PriorityQueue1()
{
	int arr[] = { 1,55,12 };

	priority_queue<int> pq(arr, arr + sizeof(arr) / sizeof(arr[0]));

	pq.push(111);
	pq.push(9);
	pq.push(33);
	pq.push(5);

	cout << pq.top() << endl;
	cout << pq.empty() << endl;
	cout << pq.size() << endl << endl;

	while (!pq.empty())
	{
		cout << pq.top() << ' ';
		pq.pop();
	}
	cout << endl;
}

测试结果如下,正确
在这里插入图片描述

三、仿函数/函数对象

介绍

仿函数其实就是一个类,这个类重载了operator(),使用这个类实例化出的对象可以像函数调用一样去使用

  1. 比如小编下面编写的Less类,比较两个int类型是否是小于关系,在通常的调用中com(a, b),仿函数调用形式和函数调用类似,实际上是我们重载了运算符括号(),才可以这样进行调用,等同于com.operator()(a, b)
class Less
{
public:
	bool operator()(int a, int b)
	{
		return a < b;
	}
};

int main()
{
	Less com;

	int a = 0, b = 1;
	
	cout << com(a, b) << endl;//通常的调用形式和函数调用类似
	cout << com.operator()(a, b) << endl;//实际调用形式

	return 0;
}
  1. 通常来讲,我们通常将这个仿函数的类定义为模板类,这样内置类型的对象可以进行比较,重载了小于操作符<或大于操作符>的自定义类型同样也可以使用仿函数实例化出的对象像函数调用一样去进行调用
  2. 并且用于比较的对象不一定是内置类型的,如果是自定义类型进行传值传参的消耗大,所以我们采用引用传参,并且这里仅仅是进行比较,不进行修改参数,所以函数的参数列表中的参数我们的类型我们设定为const T&
  3. 由于不对数据进行修改,所以this指针指向的对象我们加const进行修饰,这样const对象和普通对象都可以使用仿函数进行比较
  4. 小编这里实现的Less等同于库里的less其用于比较两个数的小于关系,Greater等同于库里的greater其用于比较两个数的大于关系,在使用的时候通产不自己写仿函数,而是包头文件#include <functional>进行使用,使用方式同小编使用的方式
template<typename T>
class Less
{
public:
	bool operator()(const T& a, const T& b) const
	{
		return a < b;
	}
};

template<typename T>
class Greater
{
public:
	bool operator()(const T& a, const T& b) const
	{
		return a > b;
	}
};

int main()
{
	Less<int> com;

	int a = 0, b = 1;
	
	cout << com(a, b) << endl;
	cout << com.operator()(a, b) << endl << endl;

	Greater<int> com1;

	cout << com1(a, b) << endl;
	cout << com1.operator()(a, b) << endl;

	return 0;
}

仿函数自定义控制比较指针指向的对象

  1. 当你想要比较指针指向的对象时,如果采用库中常规的less实例化出的对象直接将指针进行比较,那么此时就真的按照指针对应的地址进行比较了,指针每次的对应的地址都可能不同,那么每次比较的结果和正确性就不对了,因为根本没有比较指针指向的对象,而是去比较了指针本身
  2. 那么我们想要通过比较指针,使用仿函数进而达到比较指针指向的对象的效果应该自己实现一个特定的仿函数
  3. 那么此时控制比较关系从a<b变成*a<*b即可完成我们的预期目的
template<typename T>
class LessP
{
public:
	bool operator()(const T& a, const T& b) const
	{
		return *a < *b;
	}
};

int main()
{
	int a = 999, b = 1000;
	int* pa = &a;
	int* pb = &b;

	LessP<int*> com;
	cout << com(pa, pb) << endl;
	
	return 0;
}

测试结果如下,正确
在这里插入图片描述

四、改进模拟实现

讲解

  1. 那么我们知道,本质上我们的priority_queue优先级队列实现的结构是大堆还是小堆的结构取决于向上调整算法和向下调整算法的比较关系,即父亲和孩子的大小比较关系进而控制的堆结构
    在这里插入图片描述
  2. 那么我们就可以使用仿函数去实例化出对象,通过这个对象控制比较关系,并且观察模板参数列表中的Compare的缺省值是less<T>,那么就是说明通过less<T>去控制实现的大堆,那么与less相反的是greater,也就是说如果我们想要实现小堆结构,那么显示传入greater<T>就可以控制比较关系,控制实现小堆
  3. 那么接下来小编包头文件#include <functional>,并且对我们原本实现的priority_queue优先级队列的源代码中的模板参数列表添加仿函数,使用仿函数less实例化出的对象_com去控制向上调整算法和向下调整算法的比较关系,这样就可以实现通过仿函数控制大堆和小堆结构了,并且public访问限定符中的成员函数不需要进行修改
#include <vector>
#include <functional>

namespace wzx
{
	template<typename T, typename Container = vector<T>,typename Compare=less<T>>
	class priority_queue
	{
	private:
		void AdjustDown(int parent)
		{
			int child = parent * 2 + 1;

			if (child + 1 < _con.size() && _com(_con[child], _con[child + 1]))
			{
				child++;
			}

			while (child < _con.size())
			{
				if (_com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);
				}
				else
				{
					break;
				}

				parent = child;
				child = parent * 2 + 1;
			}
		}

		void AdjustUp(int child)
		{
			int parent = (child - 1) / 2;

			while (child > 0)
			{
				if (_com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);

					child = parent;
					parent = (child - 1) / 2;
				}
				else
				{
					break;
				}
			}
		}
	public:
	……
	
	private:
		Container _con;
		Compare _com;
	};
}

测试

  1. 通过如下代码进行测试我们显示传入greater之后结构为小堆的priority_queue优先级队列的各个函数接口
  2. 注意这里在显示传参的时候不可以跳过第二个模板参数直接传第三个模板参数的类型即priority_queue<int, , greater<int>> pq这种方式是错误的,读者友友请不要这样传参
void test_PriorityQueue1()
{
	priority_queue<int, vector<int>, greater<int>> pq;

	pq.push(111);
	pq.push(9);
	pq.push(65);
	pq.push(6);
	pq.push(33);
	pq.push(1);

	cout << pq.top() << endl;
	cout << pq.empty() << endl;
	cout << pq.size() << endl << endl;

	while (!pq.empty())
	{
		cout << pq.top() << ' ';
		pq.pop();
	}
	cout << endl;
}

测试结果如下,正确
在这里插入图片描述

五、源代码

PriorityQueue.h

#pragma once

#include <vector>
#include <functional>

namespace wzx
{
	template<typename T, typename Container = vector<T>,typename Compare=less<T>>
	class priority_queue
	{
	private:
		void AdjustDown(int parent)
		{
			int child = parent * 2 + 1;

			if (child + 1 < _con.size() && _com(_con[child], _con[child + 1]))
			{
				child++;
			}

			while (child < _con.size())
			{
				if (_com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);
				}
				else
				{
					break;
				}

				parent = child;
				child = parent * 2 + 1;
			}
		}

		void AdjustUp(int child)
		{
			int parent = (child - 1) / 2;

			while (child > 0)
			{
				if (_com(_con[parent], _con[child]))
				{
					swap(_con[parent], _con[child]);

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

	public:
		priority_queue()
		{}

		template<typename InputIterator>
		priority_queue(InputIterator begin, InputIterator end)
		{
			while (begin != end)
			{
				_con.push_back(*begin);
				++begin;
			}

			for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
			{
				AdjustDown(i);
			}
		}

		void push(const T& val)
		{
			_con.push_back(val);

			AdjustUp(_con.size() - 1);
		}

		void pop()
		{
			swap(_con[0], _con[_con.size() - 1]);

			_con.pop_back();

			AdjustDown(0);
		}

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

		bool empty() const
		{
			return _con.empty();
		}

		size_t size() const
		{
			return _con.size();
		}

	private:
		Container _con;
		Compare _com;
	};

	void test_PriorityQueue1()
	{
		priority_queue<int, vector<int>, greater<int>> pq;

		pq.push(111);
		pq.push(9);
		pq.push(65);
		pq.push(6);
		pq.push(33);
		pq.push(1);

		cout << pq.top() << endl;
		cout << pq.empty() << endl;
		cout << pq.size() << endl << endl;

		while (!pq.empty())
		{
			cout << pq.top() << ' ';
			pq.pop();
		}
		cout << endl;
	}
}

test.cpp

#include <iostream>

using namespace std;

#include "stack.h"
#include "queue.h"
#include "PriorityQueue.h"

void test_deque()
{
	deque<int> dq;

	dq.push_back(1);
	dq.push_back(2);
	dq.push_back(3);
	dq.push_back(4);

	deque<int>::iterator it = dq.begin();
	while (it != dq.end())
	{
		cout << *it << ' ';
		it++;
	}
	cout << endl;
}

//比较指针指向的对象的仿函数
template<typename T>
class LessP
{
public:
	bool operator()(const T& a, const T& b) const
	{
		return *a < *b;
	}
};

int main()
{
	int a = 999, b = 1000;
	int* pa = &a;
	int* pb = &b;

	LessP<int*> com;
	cout << com(pa, pb) << endl;
	
	return 0;
}



//template<typename T>
//class Less
//{
//public:
//	bool operator()(const T& a, const T& b) const
//	{
//		return a < b;
//	}
//};

template<typename T>
class Greater
{
public:
	bool operator()(const T& a, const T& b) const
	{
		return a > b;
	}
};


//int main()
//{
//	Less<int> com;
//
//	int a = 0, b = 1;
//	
//	cout << com(a, b) << endl;
//	cout << com.operator()(a, b) << endl << endl;
//
//	Greater<int> com1;
//
//	cout << com1(a, b) << endl;
//	cout << com1.operator()(a, b) << endl;
//
//	return 0;
//}

//Compare _com;

//int main()
//{
//	//wzx::test_stack();
//	//wzx::test_queue();
//
//	//test_deque();
//
//	wzx::test_PriorityQueue1();
//
//	//int a = 0, b = 1;
//	//Less<int> cmp;
//	//cout << cmp.operator()(a, b) << endl;
//	//cout << cmp(a, b) << endl;
//
//	return 0;
//}


总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值