【C++】priority_queue使用和模拟实现——仿函数


首先,在这里还是推荐一下我正在用的一个C++的查询文档网站,这里是关于 priority_queue的使用文档。有什么本文中没有讲清楚的东西,可以去参考这个网站的内容。

1. priority_queue的使用

1.priority_queue的介绍

priority_queue(优先级队列),是包含在<queue>头文件下的一个容器适配器。下面是cplusplus网站对priority_queue的介绍。

image-20230427143323115

可以看到,priority_queue是一个容器适配器默认的容器是vector。他和队列在使用上是类似的,只是出队列的规则不同,queue是按照入队列的顺序出队列,priority_queue是按照优先级出队列,这里的优先级是按照类模板的第三个参数决定的,这里的第三个参数是一个仿函数,关于仿函数的概念将会在下文中详细讲解。

2.priority_queue的结构

priority_queue在底层的逻辑上,是一个堆,每次pop的都是堆定的数据,关于堆的讲解,可以去看一下博主之前写的【数据结构】树与二叉树,里面对堆这个数据结构讲解的还是比较清楚的。

3. 主要接口

priority_queue是一个容器适配器,所以接口基本上都差不多,我们看一下文档:

image-20230427145626883

可以看到,接口和stack、queue基本相同,这里给出几个重点的函数接口:

函数接口接口说明
priority_queue()构造一个空的优先级队列
priority_queue(first, last)按照迭代器区间构造一个优先级队列
empty()判断优先级队列是否为空,返回bool类型
top()返回堆顶元素(被cosnt修饰)
push(x)插入一个数据
pop()删除堆顶数据

4. 使用示例

了解了上述的一些接口之后,总是要实践一下的,接下来我们使用以下这个结构,来测试一下:

//这里放一下测试代码,读者可以自行拷贝下去测试
void Test1()
{
	priority_queue<int> heap;
	heap.push(5);
	heap.push(3);
	heap.push(7);
	heap.push(10);
	heap.push(1);
	heap.push(9);
	while (!heap.empty())
	{
		cout << heap.top() << " ";
		heap.pop();
	}
	cout << endl;
}

image-20230427150918002

可以看到,默认情况pop出来的是当前堆内的最大值,所以可知默认的堆是大堆

❓那么如果想让堆变成小堆需要怎么做呢?
✅这里就要注意到,在文章的开头,我们讲到的类模板的第三个参数。可以看到默认传的参数是less,这就是表示默认建立大堆,如果想建小堆的话需要传的仿函数就是greater
image-20230427152520257

接下来,就用刚学的priority_queue做一道OJ题吧:215. 数组中的第K个最大元素 - 力扣(LeetCode).

我是题解,点我

2. 仿函数

1. 仿函数的概念

在上文中,我们看到priority_queue类模板中,有三个模板参数,其中第三个就是仿函数

仿函数到底是什么呢?

仿函数(functors),也叫函数对象(function objects),是STL六大组件中的一部分,这里我们没办法一次性讲完它,所以就基于priority_queue的实现稍微介绍一下。

实际上,就实现意义而言,函数对象这个名字更加贴切:一种具有函数特质的对象。但是,仿函数似乎能更加符合的描述他的行为。所以这里我们就采用仿函数这种叫法。

在学习STL之前我们就已经了解了泛型编程的概念,C++引入了模板让我们的编程能够随意的控制数据类型,现在引入了仿函数的概念,让我们能够控制逻辑。

那么现在让我们见见仿函数:

image-20230428000620364

image-20230428000825943

这两个就是我们在priority_queue的参数列表中可能用到的仿函数。

image-20230428002052512

可以看到,在使用的时候,实际上类似一个函数的调用,因此被称为仿函数

2.尝试实现仿函数

仿函数的本质就是一个运算符重载operator(),重载的是函数调用的运算符。由于仿函数本身也就是一个类模板,所以我们的实现如下

template<class T>
class less
{
    public:
    bool operator()(const T& x, const T& y)//less和greater需要实现的就是一个比较,所以这里的返回值是bool类型
    {
        return x < y;
    }
};
template<class T>
class greater
{
    public:
    bool operator()(const T& x, const T& y)
    {
        return x > y;
    }
};

这样就算是实现了less和greater的仿函数。

这里只是稍微提了一下仿函数的概念,仿函数毕竟是STL六大组件之一,其中包含的东西还有很多,我们对仿函数的学习还在路上

3.priority_queue的模拟实现

有了上述只是的铺垫,现在我们已经有了模拟实现priority_queue的能力,那么just do it。

1.priority_queue的结构

首先,对于函数模板的设计,我们和库里面对其,给了三个参数,分别表示参数存入容器的参数类型,容器类型和仿函数,其中默认的仿函数是less,建大堆。

template<class T, class Container = std::vector<T>, class Compare = less<T>>
class priority_queue
{
public:
    //...
private:
    Container _con;
}; 

2. 接口实现

按照我们之前在【数据结构】树与二叉树实现堆的经验,一定需要实现的两个接口是向上调整和向下调整,这也是整个堆的核心接口,所以我们就先着重实现这两个功能性接口,这两个接口实现完成之后,其他的结构性接口都很容易实现啦。

1.向下调整算法

image-20230428004532849

这里,我们以小堆为例(在这里偷个懒:小堆的图有以前画过的),我们需要做的事情就是找到两个孩子中较小的,然后与父节点比较大小,如果父节点大于子节点就执行交换,然后原来的子节点成为新的父节点,再次进行上述步骤。直到符合堆的结构

void adjust_down(size_t parent)
{
    Compare cmp;//实例化仿函数
    int minchild = 2 * parent + 1;
    while (minchild < _con.size())
    {
        if (minchild + 1 < _cin.size() && cmp(_con[minchild], con[minchild + 1]))//使用仿函数找到符合条件的子节点
        {
            minchild++;
        }
        if (cmp(_con[parent], _con[minchild]))//判断是否满足堆结构
        {
            std::swap(_con[parent], _con[minchild]);
            //父子节点迭代
            parent = minchild;
            minchild = 2 * parent + 1;
        }
    }
}

2. 向上调整算法

image-20230428010029365

这里向上调整的操作,从指定的孩子节点位置开始,和父节点比较,判断是否满足堆结构,如果不满足,就交换父子节点,然后原来的父节点编程子节点,再次进行上述操作,直到满足堆结构为止。

void adjust_up(size_t child)
{
    Compare cmp;//实例化仿函数
    size_t parent = (child - 1) / 2;
    while (child > 0)
    {
        if (cmp(_con[parent], _con[child]))
        {
            std::swap(_con[parent], _con[child]);
            //父子迭代
            child = parent;
            parent = (child - 1) / 2;
        }
    }
}

3.构造函数

构造函数分为无参的构造和迭代器区间构造

//无参的构造函数
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)
    {
        adjust_down(i);
    }
}

4.修改数据

priority_queue的数据修改只有push和pop两种情况

push数据就直接尾插,然后向上调整堆,直到满足堆的结构即可,pop数据就交换堆顶数据和最后一个数据,然后容器pop_back,然后向下调整直到剩余数据满足堆结构即可

void push(const T& val)
{
    _con.push_back(val);//尾插数据
    adjust_up(_con.size() - 1);//向上调整堆结构
}
void pop()
{
    swap(_con[0], _con[_con.size() - 1]);//交换堆顶和堆内最后一个元素
    _con.pop_back;//容器尾删
    adjust_down(0);//向下调整堆结构
}

5.获取数据

剩下的接口就直接复用容器提供的接口即可

bool empty() const
{
    return _con.empty();
}
size_t size() const
{
    return _con.size();
}
const T& top() const
{
    return _con[0];
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

凌云志.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值