C++ STL:优先级队列priority_queue的使用方法和模拟实现

Posted 【Shine】光芒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ STL:优先级队列priority_queue的使用方法和模拟实现相关的知识,希望对你有一定的参考价值。

目录

一. 什么是priority_queue

二. priority_queue常见接口的使用

三. priority_queue的模拟实现

3.1 仿函数

3.2 构造函数的模拟实现

3.3 插入数据函数的模拟实现

3.4 删除堆顶数据函数的模拟实现

3.4 判空、统计数据量及获取堆顶数据函数的模拟实现

附录:优先级队列priority_queue的模拟实现完整代码


一. 什么是priority_queue

优先级队列priority_queue,即数据结构中的堆,堆是一种通过使用数组来模拟实现特定结构二叉树的二叉树的数据结构,根据父亲节点与孩子节点的大小关系,可以将堆分为大堆和小堆:

  • 大堆:所有父亲节点的值都大于或等于它的孩子节点的值。
  • 小堆:所有父亲节点的值都小于或等于它的孩子节点的值。

在C++ STL中,priority_queue的声明为:template <class T, class Container = vector<T>, class Compare = std::less<T>>  class priority_queue;

其中,每个模板参数的含义为:

  • T:优先级队列中存储的数据的类型
  • Container:用于实现优先级队列的容器,默认为vector
  • Comapre:比较仿函数,用于确定是建大堆还是建小堆。Compare默认为std::less<T>,建大堆,如果要建小堆,需要显示传参std::greater<T>,同时还有显示的声明容器类型。
图1.1 大堆和小堆在内存中的存储及抽象结构示意图

二. priority_queue常见接口的使用

接口函数功能
(construct) -- 构造priority_queue(InputIterator first, InputIterator last)  -- 通过迭代器区间构造+初始化
    priority_queue()  -- 默认初始化(构造没有数据的堆)
empty判断堆是否为空
size获取堆中数据个数
push向堆中插入数据
pop删除堆顶数据
top获取堆顶数据
#include<iostream>
#include<queue>
#include<functional>

int main()

	std::priority_queue<int> maxHeap;   //建大堆
	int data[10] =  56,12,78,23,14,34,13,78,23,97 ;
	//让arr中的数据依次入大堆
	for (int i = 0; i < 10; ++i)
	
		maxHeap.push(data[i]);
	

	std::cout << maxHeap.empty() << std::endl;  //判空 -- 0
	std::cout << maxHeap.size() << std::endl;   //获取堆中数据个数 -- 10

	//依次提取大堆堆顶数据并打输出
	while (!maxHeap.empty())
	
		//97 78 78 56 34 23 23 14 13 12
		std::cout << maxHeap.top() << " ";
		maxHeap.pop();
	
	std::cout << std::endl;

	std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;  //建小堆
	//让arr中的数据依次入小堆
	for (int i = 0; i < 10; ++i)
	
		minHeap.push(data[i]);
	

	//依次提取堆顶数据并打输出
	while (!minHeap.empty())
	
		//12 13 14 23 23 34 56 78 78 97
		std::cout << minHeap.top() << " ";
		minHeap.pop();
	
	std::cout << std::endl;

	return 0;

三. priority_queue的模拟实现

这里使用容器vector作为实现优先级队列的默认容器

3.1 仿函数

仿函数,就是使用struct定义的类对象,通过重载操作符(),即operator()(参数列表)实现类似函数的功能。仿函数的调用语法为:

  • 类对象名称(参数列表)

直白的说,仿函数其实并不是函数,而是通过在类中定义运算符()的重载函数,通过类对象,来使调用运算符重载函数的语法在功能实现和外观上都与真实的函数一致。

在priority_queue的构造函数中,就经常使用less和greater两个仿函数,less和greater都是C++标准库中给出的判断两数之间大小关系的仿函数,他们被包含在头文件<functional>中:

  • less:给两个操作数,判断前者是否小于后者。
  • greater:给两个操作数,判断前者是否大于后者。

演示代码3.1,模拟实现了less和greater仿函数,具体的实现方法就是重载运算符()

演示代码3.1:(less和greater的模拟实现)

	template <class T>
	struct Less
	
		bool operator()(const T& x, const T& y)
		
			return x < y;
		
	;

	template <class T>
	struct Greater
	
		bool operator()(const T& x, const T& y)
		
			return x > y;
		
	;

综上所述,我们模拟实现的优先级队列,应当包含两个成员变量:

  1. Container _con -- 容器。
  2. compare _comp -- 用于调用比较函数的类对象。

其中Container和compare都为priority_queue类的模板参数类型,其声明为:

template<class T, class Container = vector<int>, class compare = std::less<T>>

3.2 构造函数的模拟实现

构造函数有两种重载形式:(1)构造空堆,这时构造函数无需额外编写代码进行任何工作,容器成员和_con和类对象_comp都会被调用他们的默认构造函数被创建出来。 (2)通过迭代器区间进行构造,此时先通过迭代器区间构造容器对象,然后执行向下调整建堆操作。

向下调整函数AdjustDown需要有2个参数,分别为:堆中数据个数n和开始向下调整的父亲节点下标parent,其中还会用到类的成员(容器和用于比较的对象),AdjustDown执行的操作依次为(以建大堆为例):

  1. 根据父亲节点的下标,获取其左孩子节点的下标child。
  2. 判断孩子节点下标是否越界,如果越界(child>=n),则函数终止。
  3. 判断左孩子节点和右孩子节点那个较大,如果右孩子节点值较大,则将child更新为右孩子节点下标。
  4. 判断父亲节点是否小于较大的孩子节点,如果小于,则交换父子节点值,然后将父节点更新为孩子节点,然后回到1继续执行向下调整操作,如果大于或等于,则终止向下调整操作。

注意:对于开始执行向下操作的父节点parent,一定要保证它的左子树和右子树都为大或小堆。

图3.1 向下调整的执行流程

构造函数实现代码:

priority_queue()  //默认构造函数(空堆)
 
        
//通过迭代器区间初始
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
: _con(first, last)

	for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
	
		adjustDown(_con.size(), i);
	

向下调整函数代码: 

void adjustDown(int n, int parent)  //向下调整函数

	int child = 2 * parent + 1;
	while (child < n)
	
		if (child + 1 < n && _comp(_con[child], _con[child + 1]))
		
			++child;
		

		if (_comp( _con[parent], _con[child]))
		
			std::swap(_con[parent], _con[child]);
			parent = child;
			child = 2 * parent + 1;
		
		else
		
			break;
		
	

3.3 插入数据函数的模拟实现

向堆中插入数据需要两步操作:先向容器中尾插数据,然后调用AdjustUp函数上调整数据。

向上调整函数AdjustUp执行的操作流程为:(建大堆为例)

  1. 根据开始执行向上调整的孩子节点下标,计算出其父亲节点下标。
  2. 判断孩子节点下标是否>0,如果是,继续执行向上调整操作,否则终止函数。
  3. 判断孩子节点值是否大于父亲节点,如果大于,交换父子节点值,然后更新孩子节点为当前父亲节点,根据更新后孩子节点下标计算父亲节点下标,然后回到1继续执行向上调整操作。如果孩子节点值小于或等于父亲节点值,那么终止向上调整操作。
图3.2  向上调整操作流程图

向堆中插入数据函数:

void push(const T& x)   //向堆中插入数据函数

	_con.push_back(x);  //向容器尾部插入数据
	adjustUp(_con.size() - 1);  //执行向上调整操作

向上调整操作函数:

void adjustUp(int child)  //向上调整建堆函数

	int parent = (child - 1) / 2;
	while (child > 0)
	
		if (_comp(_con[parent], _con[child]))
		
			std::swap(_con[parent], _con[child]);
			child = parent;
			parent = (child - 1) / 2;
		
		else
		
			break;
		
	

3.4 删除堆顶数据函数的模拟实现

如果直接将除堆顶之外的全部数据向前移动一个单位,那么数组中剩余的数据大概率无法满足堆的结构要求,重新再建堆效率过低。那么,就需要一些额外的技巧来解决问题:

  1. 交换堆顶数据和数组中最后一个数据。
  2. 将数组中的最后一个数据删除。
  3. 以当前根节点为起始父亲节点,执行向下调整操作,使数组中的数据重新满足堆的结构。
图3.3 删除数据函数函数操作流程图
void pop()  //删除堆顶数据

	assert(!_con.empty());
			
	std::swap(_con[0], _con[_con.size() - 1]);  //交换堆顶数据和最后面的数据
	_con.pop_back();   //删除容器最后面的数据(即:堆顶数据)

	adjustDown(_con.size(), 0);  //执行向下调整操作

3.4 判空、统计数据量及获取堆顶数据函数的模拟实现

size_t size()  //获取堆中数据个数

	return _con.size();


bool empty()  //判空

	return _con.empty();


const T& top()  //获取堆顶数据(根节点数据)

	assert(!_con.empty());  
	return _con[0];

附录:优先级队列priority_queue的模拟实现完整代码

#include<vector>
#include<functional>
#include<cstdbool>
#include<cassert>

namespace zhang

	template <class T>
	struct Less
	
		bool operator()(const T& x, const T& y)
		
			return x < y;
		
	;

	template <class T>
	struct Greater
	
		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:
		priority_queue()  //默认构造函数(空堆)
		 

		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last)
			: _con(first, last)
		
			for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
			
				adjustDown(_con.size(), i);
			
		

		void push(const T& x)   //向堆中插入数据函数
		
			_con.push_back(x);  //向容器尾部插入数据
			adjustUp(_con.size() - 1);  //执行向上调整操作
		

		void pop()  //删除堆顶数据
		
			assert(!_con.empty());
			
			std::swap(_con[0], _con[_con.size() - 1]);  //交换堆顶数据和最后面的数据
			_con.pop_back();   //删除容器最后面的数据(即:堆顶数据)

			adjustDown(_con.size(), 0);  //执行向下调整操作
		

		size_t size()  //获取堆中数据个数
		
			return _con.size();
		

		bool empty()  //判空
		
			return _con.empty();
		

		const T& top()  //获取堆顶数据(根节点数据)
		
			assert(!_con.empty());  
			return _con[0];
		

	private:
		void adjustUp(int child)  //向上调整建堆函数
		
			int parent = (child - 1) / 2;
			while (child > 0)
			
				if (_comp(_con[parent], _con[child]))
				
					std::swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				
				else
				
					break;
				
			
		

		void adjustDown(int n, int parent)  //向下调整函数
		
			int child = 2 * parent + 1;
			while (child < n)
			
				if (child + 1 < n && _comp(_con[child], _con[child + 1]))
				
					++child;
				

				if (_comp( _con[parent], _con[child]))
				
					std::swap(_con[parent], _con[child]);
					parent = child;
					child = 2 * parent + 1;
				
				else
				
					break;
				
			
		

	private:
		Container _con;  //存储堆数据的容器
		compare _comp;   //用于实现仿函数的类对象
	;

C++:STL——栈队列和优先级队列的模拟实现


前言

在讲这些之前,我们需要了解一下双端队列:deque

deque:是一种双开口的"连续"空间的数据结构,其含义其实就是可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,插效率高,不需要搬移元素;与list比较,空间利用率比较高。

deque的底层并不是真正连续的空间,而是由不连续的连续空间段组成的。其效果类型下图:

在这里插入图片描述

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

1.stack

1.1 stack的介绍和使用

  • stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
  • stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
  • stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类
  • 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque

更为详细的可以查看stack文档介绍

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

1.2 stack的模拟实现

STL标准库中通常都是使用双端队列来作为stack默认的底层数据结构的,因此,本次模拟实现也是使用deque作为底层数据结构。

代码如下:

#include <iostream>
#include <deque>

using namespace std;

namespace myTest{
    template<typename T,class Cond = deque<T>>
        class stack{
            public:
                stack(){}
                ~stack(){}
                void push(T& x)
                {
                    c_.push_back(x);
                }
                void pop()
                {
                    c_.pop_back();
                }
                T& top() const
                {
                    return c_.back();
                }
                size_t size()const 
                {
                    return c_.size();
                }
                bool empty()const
                {
                    return c_.empty();
                }

            private:
                Cond c_;
        };
}

2. queue

2.1 queue的介绍与使用

  • 队列是一种容器适配器,专门用于在FIFO上下文(先进先出) 中操作,其中从容器一端插入元素,另一端提取元素。
  • 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列
  • 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。

更为详细的可以查看queue文档介绍

2.2 queue的模拟实现

要实现一个queue,标准容器类deque和list均满足了queue的接口的要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque

代码如下:

#include <iostream>
#include <deque>

namespace myTest{
    template<typename T, class Cond = deque<T>>
        class queue
        {
            public:
                queue(){}
                ~queue(){}

                void push(const T& x)
                {
                    c_.push_back(x);
                }
                void pop()
                {
                    c_.pop_front();
                }
                T& back()const
                {
                    return c_.back();
                }
                T& front()const
                {
                    return c_.front();
                }
                size_t size() const
                {
                    return c_.size();
                }
                bool empty()const
                {
                    return c_.empty();
                }
            private:
                Cond c_;
        };
}

3. priority_queue

3.1 priority_queue的介绍和使用

  • 优先队列是一种容器适配器根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。

  • 此上下文类似于堆在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。

  • 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。

  • 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问.

  • 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。

  • 需要支持随机访问迭代器以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。

更为详细的可以查看priority_queue文档介绍

3.2 priority_queue的模拟实现

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

3.2.1 使用算法函数来实现priority_queue

① make_heap方法

template <class RandomAccessIterator, class Compare>
  void make_heap (RandomAccessIterator first, RandomAccessIterator last);

它的作用是将范围[first,last]中的元素重新排列,使它们形成一个堆

② push_heap方法

template <class RandomAccessIterator>
  void push_heap (RandomAccessIterator first, RandomAccessIterator last);

它的作用是给定一个范围[first,last-1]的堆,这个函数通过将(last-1)中的值放入其中的相应位置,将该范围视为堆扩展到[first,last]。

③ pop_heap方法

template <class RandomAccessIterator>
  void pop_heap (RandomAccessIterator first, RandomAccessIterator last);

它的作用是重新排列堆范围[first,last]中的元素,使被认为是堆的部分缩短了一个。具有最高值的元素被移到(last-1)。虽说是对堆进行删除,但是其本身的数组的值并没有被删除,只是仅仅作为堆的发生了变化,每次删除,堆的长度均会减一。

通过调用make_heap,一个范围可以被组织成一个堆。之后,如果分别使用push_heap和pop_heap从其中添加和删除元素,其堆的属性将被保留下来。

这些算法函数均包含在#include <algorithm>

使用算法函数来实现priority_queue的代码如下:

#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>

using namespace std;

namespace myTest{
    //less<T> 是一个仿函数,表示当前比较的准则是小于,则建的堆就是大堆
    //greater<T> 也是仿函数,表示比较的准则是大于,则建的堆就是小堆
    template<typename T,class Cond = vector<T> , class Pred = less<T>>
        class priority_queue{
            public:
                explicit priority_queue() {}
                priority_queue(const T* first,const T* last)
                {
                    //1.首先建立一个数组
                    c_.insert(c_.begin(),first,last);
                    //2.其次再将建立的数组使用算法函数建成一个堆
                    make_heap(c_.begin(),c_.end(),pr_);
                }

                void push(const T x)
                {
                    //首先将数据插入到数组中
                    c_.push_back(x);
                    //调整数组,使其再次成为堆
                    push_heap(c_.begin(),c_.end(),pr_);
                }

                void pop()
                {
                    //首先需要将堆顶元素进行调整,在删除
                    pop_heap(c_.begin(),c_.end(),pr_);

                    c_.pop_back();
                }

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

                size_t size() const
                {
                    return c_.size();
                }
                T& top()const
                {
                    return c_.front();
                }
                //为了测试方便,我们编写一个打印函数
                void Print() const
                {
                    auto it = c_.begin();
                    while(it != c_.end())
                    {
                        cout << *it << " ";
                        it++;
                    }
                    cout << endl;

                }

            private:
                Cond c_;
                Pred pr_;//比较谓词
        };
}

void test()
{
    int iv[] = {1,7,9,0,3,5,2,4};
    int n = sizeof(iv) / sizeof(iv[0]);
    cout << "normal vector is ";
    for(int i = 0 ;i < n; ++i)
    {
        cout << iv[i] << " ";
    }
    cout << endl;
    myTest::priority_queue<int,vector<int>,greater<int>> pq(iv,iv+n);

    cout << "make_heap" << endl;
    pq.Print(); 
    cout << "push_heap(12)" << endl;
    pq.push(12);
    pq.Print();
    cout << "pop_heap" << endl;
    pq.pop();
    pq.Print();
}


int main()
{
    test();
    return 0;
}

运行验证如下:

在这里插入图片描述

3.2.2 使用向上、向下调整算法来实现priority_queue

关于向上调整算法和向下调整算法,大家可以查看面试考点–堆(向下调整算法,向上调整算法,建堆,堆排序)以及堆排序、建堆的时间复杂度分析(图文并茂)

这里就不再做过多描述,直接上代码。

代码如下:

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

namespace myTest{
    template<typename T,class Cond = vector<T>,class Pred = less<T>>
        class priority_queue{
            public:
                explicit priority_queue() 
                {}
                priority_queue(const T* first,const T* last)
                {
                    c_.insert(c_.begin(),first,last);

                    //建堆----使用向下调整算法
                    //从最后一个非叶子节点开始调
                    for(int i = ((c_.size()-1)-1) >> 1;i >= 0; --i)
                    {
                        AdjustDown(i);
                    }

                }

                void push(const T x)
                {
                    c_.push_back(x);

                    //插入到数组的末尾,使用向上调整算法进行调堆
                    AdjustUp(c_.size()-1);

                }


                void pop()
                {
                    //首先交换堆顶元素和堆尾元素,然后再进行调堆
                    std::swap(c_.front(),c_.back());
                    
                    c_.pop_back();

                    AdjustDown(0);

                }

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

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

                bool empty()const
                {
                    return c_.empty();
                }
                //为了测试方便,我们编写一个打印函数
                void Print() const
                {
                    auto it = c_.begin();
                    while(it != c_.end())
                    {
                        cout << *it << " ";
                        it++;
                    }
                    cout << endl;

                }

            protected:
                void AdjustDown(int parent)
                {
                    int child = parent * 2 + 1;
                    while(child < c_.size())
                    {
                        if(child + 1 < c_.size() && pr_(c_[child],c_[child + 1]))
                            ++child;
                        if(pr_(c_[parent],c_[child]))
                        {
                            std::swap(c_[child],c_[parent]);
                            parent = child;
                            child = parent * 2 + 1;
                        }
                        else
                            break;
                    }
                }
                void AdjustUp(int child)
                {
                    int parent = (child-1) >> 1;
                    while(parent > 0)
                    {
                        if(pr_(c_[parent],c_[child]))
                        {
                            std::swap(c_[child],c_[parent]);
                            child = parent;
                            parent = (child-1) >> 1;
                        }
                        else
                            break;
                    }
                }

            private:
                Cond c_;
                Pred pr_;

        };
}


void test()
{
    int iv[] = {1,7,9,0,3,5,2,4};
    int n = sizeof(iv) / sizeof(iv[0]);
    cout << "normal vector is ";
    for(int i = 0 ;i < n; ++i)
    {
        cout << iv[i] << " ";
    }
    cout << endl;
    myTest::priority_queue<int,vector<int>,greater<int>> pq(iv,iv+n);

    cout << "create heap" << endl;
    pq.Print();
    cout << "push_heap(12)" << endl;
    pq.push(12);
    pq.Print();
    cout << "pop_heap" << endl;
    pq.pop();
    pq.Print();

}

int main()
{
    test();
    return 0;
}

运行结果如下:

在这里插入图片描述

以上是关于C++ STL:优先级队列priority_queue的使用方法和模拟实现的主要内容,如果未能解决你的问题,请参考以下文章

C++:STL——栈队列和优先级队列的模拟实现

C++ 初阶优先级队列(Priority_Queue)底层框架模拟实现

C++ 初阶优先级队列(Priority_Queue)底层框架模拟实现

比较器函数在优先队列 C++ STL 中如何工作?

c++ STL queue:deque+优先队列

c++ stl 优先队列怎么输出全部数据,同时又获取top数据?