C++中的优先级队列【详细分析及模拟实现】

优先级队列

1、介绍

priority_queue 是 C++ STL 中的一个标准容器,它基于堆的数据结构实现,可以方便地实现堆的基本操作,例如插入元素、弹出最大或最小元素等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-em31HxHj-1679979872476)(E:\Typora图片\image-20230324142853232.png)]

其模板参数为:

template <class T, class Container = vector<T>,
  class Compare = less<typename Container::value_type> > 

priority_queue 的模板参数包括三个:

  1. class T:表示存储在 priority_queue 中的元素类型。可以是任何可比较的类型,例如基本数据类型(如 intdouble)、结构体、类等。
  2. class Container = vector<T>:表示底层容器的类型,缺省情况下采用的是 vector<T>,也可以自定义容器类型,只要该容器支持随机访问、大小可变、在末尾快速插入元素以及在任意位置快速删除元素即可。
  3. class Compare = less<typename Container::value_type>:表示元素之间的比较函数对象的类型,缺省情况下采用的是 less<typename Container::value_type>,即使用 < 运算符来比较元素的大小关系。如果需要使用自定义的比较函数,可以通过这个模板参数来指定。这个函数对象应该接受两个参数,返回一个 bool 类型的值,表示它们之间的大小关系。
int main()
{
	priority_queue<int> pq;

	pq.push(3);
	pq.push(2);
	pq.push(5);
	pq.push(1);

	while (!pq.empty())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wXzIg5IM-1679979872477)(E:\Typora图片\image-20230324143557390.png)]

可见,默认(缺省)情况下,采用的是大数优先(大堆)(less),我们也可更改比较方式,形成其它的优先情况

例如,可以定义一个存储 int 类型元素、采用 vector<int> 作为底层容器、采用 greater<int> 函数对象作为比较函数的 priority_queue 对象:

#include <queue>
#include <functional>      //使用greater的头文件

using namespace std;

int main() {
    priority_queue<int, vector<int>, greater<int>> pq;
    return 0;
}

对上述push的3-2-5-1结果如下:(小的优先级高)(greater)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAQlTzVB-1679979872477)(E:\Typora图片\image-20230324143843900.png)]


2、应用

我们通过力扣例题的方式来体会该容器的应用

👉 数组中第K个大的元素

image-20230324144022573

思路:

对于本题来说,我们直观的思路即是排序;其次,我们在介绍学习数据结构时通过TOP-K的方法建立大堆或小堆也可以解决本题:

①建立n个数的大堆(时间复杂度为O(n)),堆顶的数时最大的数,因此找到第K大的只用pop()K次堆顶

,每次pop()之后进行向下调整,但面对n很大K很小的情况时对资源的开销较大

②建立K个数的小堆,然后对剩下n-K个数依次和堆顶的数比较,若大于堆顶则替换掉原来的堆顶并进堆,再向上调整形成新的堆,重复执行以上操作,最终堆顶的数即是第K大的数,时间复杂度为O(K+(N-K)*logK)

从算法的时间复杂度来说,两者差异都不大;由于priority_queue容器默认是建大堆,因此本题我们采用建n个数的大堆的方式实现

需要注意的是,以上所谓的向上调整向下调整是针对于C语言实现的数据结构而言的,我们使用priority_queue容器是根据我们的比较方式自动建立大堆或小堆**(即自动调整)**

题解:

class Solution 
{
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        priority_queue<int> pq(nums.begin(),nums.end());
        //走k-1次
        while(--k)
        {
            pq.pop();
        }
        return pq.top();
    }
};

我们同时也可以试一试用建K个数的小堆的方法:

class Solution 
{
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        //建立K个数的小堆
        //注意不要再用缺省模板参数构造
        priority_queue<int,vector<int>,greater<int>> pq(nums.begin(),nums.begin()+k);
        for(size_t i=k;i<nums.size();i++)
        {
            if(nums[i]>pq.top())
            {
            	pq.pop();
            	pq.push(nums[i]);
            }
        }
        return pq.top();
    }
};

3、优先级队列的底层及模拟实现

优先级队列(Priority Queue)它是一种常见的数据结构,通常用堆(Heap)来实现。堆是一种完全二叉树,具有以下两个性质:

  1. 父节点的值始终小于或等于子节点的值(最小堆),或者父节点的值始终大于或等于子节点的值(最大堆)。
  2. 对于同一深度的节点,从左到右排列,不存在空的位置。

在优先级队列中,每个元素都有一个优先级值,根据这个值决定元素在队列中的位置。最小堆中,优先级值越小的元素越靠近堆的根部,也就是队首;最大堆中,优先级值越大的元素越靠近堆的根部。

堆的底层实现通常使用数组来表示完全二叉树,每个节点在数组中的位置可以通过计算得到。堆中插入元素的操作可以先将新元素插入到数组末尾,然后不断将它与其父节点比较交换,直到满足堆的性质。删除堆顶元素的操作可以将堆顶元素与数组末尾的元素交换,然后将堆顶元素不断与其子节点比较交换,直到满足堆的性质。

模拟实现如下:

#include <iostream>
#include <vector>
using namespace std;


template<typename T, typename Container = std::vector<T>>
class PriorityQueue
{
public:
    //构造函数
	PriorityQueue() {}
	
    //插入
	void push(const T& val)
	{
		data.push_back(val);
		adjust_up(data.size() - 1);
	}
	
    //取队顶
	T top() const
	{
		return data.front();
	}
	
    //出队
	void pop()
	{
		if (data.empty())
		{
			return;
		}

		swap(data[0], data.back());
		data.pop_back();
		adjust_down(0);
	}
	
    //队列长度
	size_t size() const
	{
		return data.size();
	}
	
    //判断队列是否为空
	bool empty() const
	{
		return data.empty();
	}

private:
	Container data;
	
    //向上调整
	void adjust_up(size_t child)
	{
		while (child > 0)
		{
			size_t parent = (child - 1) / 2;

			if (data[child] > data[parent])
			{
				swap(data[child], data[parent]);
				child = parent;
			}
			else
			{
				break;
			}
		}
	}

    //向下调整
	void adjust_down(size_t parent)
	{
		size_t child = parent * 2 + 1;

		while (child < data.size())
		{
			// 将 child 赋值为左右孩子中的最大值
			if (child + 1 < data.size() && data[child + 1] > data[child])
			{
				child++;
			}

			// 如果最大子节点比当前节点更大,就将它们交换,并继续向下调整
			if (data[child] > data[parent])
			{
				swap(data[child], data[parent]);
				parent = child;
				child = parent * 2 + 1;
			}
			else
			{
				break;
			}
		}
	}
};

4、仿函数

在 C++ 中,仿函数(Functor)是一个类或者结构体,它实现了一个函数调用操作符operator(),使得对象可以像函数一样被调用**(括号操作符重载)**

一个仿函数的基本实现如下:

//模板参数
template<typename T>
//Functor为仿函数名
class Functor {
public:
    void operator()(T arg) const 
    {
        // do something with arg
    }
};

下面是一个简单的仿函数,用于比较两个字符串的长度:

class StringLengthCompare {
public:
    bool operator()(const std::string& str1, const std::string& str2) const 
    {
        return str1.length() < str2.length();
    }
};

该仿函数接受两个字符串参数,并返回一个布尔值,指示哪个字符串更短

我们可以使用仿函数的 less greater 来定义优先级队列的比较方式;在 C++ STL 中,优先级队列的默认比较方式是 less,即小顶堆,但我们也可以通过定义一个仿函数,然后将其作为模板参数传递给优先级队列来修改比较方式

其中仿函数的 less greater实现如下:

// less 仿函数
template<typename T>
struct less {
    bool operator() (const T& left, const T& right) const {
        return left < right;
    }
};

// greater 仿函数
template<typename T>
struct greater {
    bool operator() (const T& lhs, const T& rhs) const {
        return left > right;
    }
};

用在优先级队列如下:

// 使用 less 仿函数定义小顶堆
priority_queue<int, vector<int>, less<int>> min_heap;

// 使用 greater 仿函数定义大顶堆
priority_queue<int, vector<int>, greater<int>> max_heap;

而比较方式主要用于建立大堆还是小堆,因此在优先级队列模拟实现中,表现在向上向下调整算法中:

// 使用 greater 仿函数定义大顶堆
template<typename T>
void adjustUp(vector<T>& heap, int i) 
{
    int parent = (i - 1) / 2;
    while (i > 0 && greater<T>()(heap[parent], heap[i]))
    {
        swap(heap[parent], heap[i]);
        i = parent;
        parent = (i - 1) / 2;
    }
}

// 使用 less 仿函数定义小顶堆
template<typename T>
void adjustDown(vector<T>& heap, int i, int n) 
{
    int leftChild = 2 * i + 1;
    int rightChild = 2 * i + 2;
    int maxIdx = i;
    if (leftChild < n && less<T>()(heap[maxIdx], heap[leftChild])) 
    {
        maxIdx = leftChild;
    }
    if (rightChild < n && less<T>()(heap[maxIdx], heap[rightChild])) 
    {
        maxIdx = rightChild;
    }
    if (maxIdx != i) 
    {
        swap(heap[i], heap[maxIdx]);
        adjustDown(heap, maxIdx, n);
    }
}

在使用仿函数时,我们通过 less<T>() greater<T>() 的方式来创建一个 less 或者 greater 类型的仿函数,并将其传递给需要使用比较器的函数或者容器。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值