priority_queue的使用与模拟实现(容器适配器+stack与queue的模拟实现源码)

news2024/9/24 11:21:28

priority_queue的使用与模拟实现

  • 引言(容器适配器)
  • priority_queue的介绍与使用
    • priority_queue介绍
    • 接口使用
      • 默认成员函数
    • size与empty
    • top
    • push与pop
  • priority_queue的模拟实现
    • 构造函数
    • size与empty
    • top
    • push与pop
    • 向上调整建堆与向下调整建堆
      • 向上调整建堆
      • 向下调整建堆
  • 源码概览
  • 总结

引言(容器适配器)

在STL中,有这样一种“容器”,自身并不完成对数据的操作,而是以其底部的容器来完成所有对数据的工作,类似于对容器的封装。这种“容器”称为container adapter(容器适配器)

stack(栈),queue(队列),priority_queue(优先级队列)都是容器适配器。
栈与队列的实现大家可以参考之前C语言的栈和队列实现,关于stack与queue容器适配器的实现会放在文末:
戳我看栈的模拟实现哦
戳我看队列的模拟实现哦

这篇文章就来介绍一下优先级队列的使用与实现:
(优先级队列的实现需要具备堆数据结构的知识,可以查看相关文章戳我看堆详解哦,戳我看堆排序详解哦)

priority_queue的介绍与使用

priority_queue介绍

在这里插入图片描述

优先级队列与队列类似,我们不能随意访问其中的元素,只能访问队头的首元素。但是在优先级队列中,根据某种优先级(某个标准),任意时刻队列的首元素都会是整个队列中优先级最高的元素(默认情况下队头的元素为最大的元素,可以通过迪三个模板参数来调整);

优先级队列的底层容器必须实现有empty()size()front()push_back()pop_back()。默认的底层容器为vector;

优先级队列的结构类似于堆结构,堆结构可以使堆顶的元素为所有元素中最大/小的元素(通过建大堆或小堆来实现)。所以优先级队列的底层实现其实就是堆结构。在容器适配器中会在需要的时刻建堆,以保证优先级队列队头的元素始终是整个队列中优先级最高的。

优先级队列有三个模板参数
第一个参数 T即优先级队列中存储的数据类型(默认为vector);
第二个参数 Container即优先级队列使用的底层容器类型
第三个参数 Compare是一个仿函数类型,即优先级的比较接口类型(默认为less<T>,即当第一个参数的优先级小于第二个元素时返回真,与之相反的仿函数为greaterless/greater都是库实现的,底层调用T类型的<>)。

接口使用

默认成员函数

构造
在这里插入图片描述
优先级队列的构造函数有两种重载版本,即无参构造使用迭代器区间构造(在构造函数中会使用指定底层容器与比较仿函数的默认构造函数来初始化一个容器对象仿函数对象,所以我们传参构造的时候可以不用去管它们);
对于迭代器区间构造,这是一个函数模板,即可以通过任意一种InputIterator迭代器来构造优先级队列:

int main()
{
	vector<int> v(10);
	for (int i = 0; i < 10; ++i)
	{
		v[i] = i + 1;
	}

	priority_queue<int> pq1; //无参构造
	priority_queue<int> pq2(v.begin(), v.end()); //迭代器区间构造
	cout << pq2.top() << endl;

	priority_queue<int, vector<int>, greater<int>> pq3(v.begin(), v.end()); //指定优先级
	cout << pq3.top() << endl;

	return 0;
}

在这里插入图片描述
pq1使用无参构造;
pq2使用迭代器vbeginend构造,默认的优先级队列头为最大值;
pq3使用v迭代器区间构造的同时,指定了优先级比较的仿函数,队列头为最小值。

优先级队列的包装器没有定义拷贝构造,赋值运算符重载、析构函数,因为编译器会自动生成,自动生成的这些默认成员函数会去调用底层容器的这些默认成员函数

size与empty

在这里插入图片描述
size用于获取优先级队列中元素的个数;
在这里插入图片描述
empty用于返回优先级队列是否为空:为空返回真,否则返回假:

int main()
{
	vector<int> v(10);
	for (int i = 0; i < 10; ++i)
	{
		v[i] = i + 1;
	}

	priority_queue<int> pq(v.begin(), v.end()); //迭代器区间构造
	cout << pq.size() << endl;
	cout << pq.empty() << endl;

	return 0;
}

在这里插入图片描述

top

在这里插入图片描述
top用于获取优先级队列头的元素:

int main()
{
	vector<int> v(10);
	for (int i = 0; i < 10; ++i)
	{
		v[i] = i + 1;
	}

	priority_queue<int> pq(v.begin(), v.end()); //迭代器区间构造
	cout << pq.top() << endl;

	return 0;
}

在这里插入图片描述

push与pop

在这里插入图片描述
push用于在优先级队列中插入一个元素,由于优先级队列会维持队列头的元素始终优先级最大,所以我们不能进行随机插入:

int main()
{
	vector<int> v(10);
	for (int i = 0; i < 10; ++i)
	{
		v[i] = i + 1;
	}

	priority_queue<int> pq(v.begin(), v.end()); //迭代器区间构造
	cout << "before top:" << pq.top() << endl;

	pq.push(12);
	cout << "after top:" << pq.top() << endl;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
pop用于在优先级队列中删除队列头的元素,即删除优先级最高的那个元素:

int main()
{
	vector<int> v(10);
	for (int i = 0; i < 10; ++i)
	{
		v[i] = i + 1;
	}

	priority_queue<int> pq(v.begin(), v.end()); //迭代器区间构造
	cout << "before top:" << pq.top() << endl;

	pq.pop();
	cout << "after top:" << pq.top() << endl;

	return 0;
}

在这里插入图片描述

priority_queue的模拟实现

为避免命名冲突,我们创建一个新的命名空间qqq来放我们的模拟实现;
作为一个类模板,有三个模板参数;
在优先级队列中需要两个成员变量:底层的容器对象与仿函数对象

namespace qqq 
{
    template <class T, class Container = vector<T>, class Compare = less<T> >
    class priority_queue
    {
    
    private:
        Container c;
        Compare comp;
    };
};

需要提醒的是:这里的ContainerCompare是底层容器与仿函数的类型,用这两个类型创建的ccomp才是容器对象与仿函数对象,在传参或者指定模板参数时不要搞错了。

构造函数

除了构造函数之外的其余默认成员函数,系统生成的默认成员函数会调用底层容器的默认成员函数,就足够了。

对于无参构造,我们其实不需要去实现,但是因为要实现迭代器区间构造,所以编译器不会再生成一个默认构造函数,所以我们写一个接口即可:

    priority_queue()
    {}

对于迭代器区间构造,这是一个函数模板:
我们首先通过循环遍历将迭代器区间中的元素拷贝到优先级队列中的成员容器c中;
然后通过循环调用向下调整建堆的成员函数adjust_down实现队列顶的元素优先级最高(对于已有的数组,向下调整建堆的效率要略高于向上调整建堆,这一点在后面详细介绍这两个建堆函数时会介绍):

    template <class InputIterator>
    priority_queue(InputIterator first, InputIterator last) //向下调整建堆
    {
        while (first != last) //遍历拷贝
        {
            c.push_back(*first);
            ++first;
        }
        for (int i = (c.size() - 2) / 2; i >= 0; --i) //向下调整建堆
        {
            adjust_down(i);
        }
    }

size与empty

sizeempty的实现就是调用底层容器的sizeempty接口即可,这也是为什么要求底层容器必须要定义有这些接口:

    bool empty() const
    {
        return c.empty();
    }
    size_t size() const
    {
        return c.size();
    }

top

top函数在库实现中,调用了底层容器的front接口,返回值为T&
我们在模拟实现时直接返回c的第一个元素即可:

    T& top() const
    {
        return c[0];
    }

push与pop

对于 push 函数:
首先调用底层容器的push_pack在底层容器尾插入一个元素;
然后使用向上调整建堆将新插入的元素调整至正确的位置,保证队列头的元素为优先级最大的元素:
在这里插入图片描述

    void push(const T& x)
    {
        c.push_back(x);
        adjust_up(c.size() - 1);
    }

对于 pop 函数:
因为如果直接将底层容器中的元素整体向前移动一个元素来删除第一个元素的话,就会破坏整个堆结构。所以在这里先将底层容器中的首尾元素互换,这样只破坏了一个元素的堆的位置,并且使要删除的元素正好处于底层容器的尾部。
pop_pack删除尾元素后,再将那个被交换到堆顶的元素通过向下调整建堆移动到正确的位置即可:
在这里插入图片描述
在这里插入图片描述

    void pop() //Remove top element
    {
        //交换后pop,再向下调整
        swap(c[0], c[c.size() - 1]);
        c.pop_back();
        adjust_down(0);
    }

向上调整建堆与向下调整建堆

关于堆这个数据结构的详细知识,可以参考我之前的文章:戳我看堆详解哦

这里只提一个要点,即结点数据的下标与结点位置的规律:
父亲结点的下标 = (孩子结点下标-1)/2;
左孩子结点的下标 = 父亲结点的下标2+1;
右孩子结点的下标 = 父亲结点的下标2+2。

这些规律使我们在根据逻辑结构访问堆中的元素时,会很方便。

向上调整建堆

向上调整建堆就是将元素由下到上调整至合适的位置:
类似于二叉树的尾插,然后将尾插的元素向上调整放到合适的位置(若子节点的值小于父亲结点的值,就将父子结点的值交换,直到再次形成一个大堆):

   void adjust_up(int child)
    {
        assert(0 <= child && child < size());
        while (child > 0)
        {
            int parent = (child - 1) / 2;

            if (comp(c[parent], c[child]))
            {
                swap(c[parent], c[child]);
            }

            child = parent;
        }
    }

向下调整建堆

向下调整建堆就是将元素由上到下调整至合适的位置:
向下调整建堆时,即将每一个根结点向下调整,放在其合适的位置。这要求该根的子树都是正确的大堆,所以我们需要从倒数第二层的根结点开始向下调整(比较父亲结点与较大的子结点的值,若父亲结点小于较大的子节点,则交换它们的值。直到再次形成大堆):

    void adjust_down(int parent)
    {
        assert(0 <= parent && parent < size());
        int bigger_child = parent * 2 + 1;
        while (bigger_child < size())
        {
            if (bigger_child + 1 < size() && comp(c[bigger_child], c[bigger_child + 1]))
            {
                ++bigger_child;
            }
            
            if (comp(c[parent], c[bigger_child]))
            {
                swap(c[parent], c[bigger_child]);
            }

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

(这里示意图就不画了,在关于堆的详解中有详细的图示)

关于在对整个数组的数据进行建堆时,为何向下调整建堆的效率高于向上调整建堆的效率,这里做简单的说明(大家可以去计算他们的时间复杂度,这里只从逻辑上分析):
面对一个未成形的堆,最后一行的元素个数是最多的,向上调整建堆从最后一层的最后一个元素向上调整,每个元素最大需要调整高度次。即最多的元素需要调整最多的次数
而向下调整建堆从倒数第二行的最后一个元素开始向下调整,每个元素最大只需要调整一次。即最多的元素需要调整最少的次数。在向下调整建堆中,最多需要调整高度次的元素只有一个,那就是堆顶的元素。
所以向下调整建堆相对于向上调整建堆的效率较高,但他们都属于n*logn级的时间复杂度,效率都很高

源码概览

priority_queue模拟实现源码:

namespace qqq 
{
    template <class T, class Container = vector<T>, class Compare = less<T> >
    class priority_queue
    {
    private:
        void adjust_up(int child)
        {
            assert(0 <= child && child < size());

            while (child > 0)
            {
                int parent = (child - 1) / 2;

                if (comp(c[parent], c[child]))
                {
                    swap(c[parent], c[child]);
                }

                child = parent;
            }
        }
        void adjust_down(int parent)
        {
            assert(0 <= parent && parent < size());

            int bigger_child = parent * 2 + 1;
            while (bigger_child < size())
            {
                if (bigger_child + 1 < size() && comp(c[bigger_child], c[bigger_child + 1]))
                {
                    ++bigger_child;
                }

                if (comp(c[parent], c[bigger_child]))
                {
                    swap(c[parent], c[bigger_child]);
                }

                parent = bigger_child;
                bigger_child = parent * 2 + 1;
            }
        }
    public:
        priority_queue()
        {}

        template <class InputIterator>
        priority_queue(InputIterator first, InputIterator last) //向下调整建堆
        {
            while (first != last)
            {
                c.push_back(*first);
                ++first;
            }
            for (int i = (c.size() - 2) / 2; i >= 0; --i)
            {
                adjust_down(i);
            }
        }
        bool empty() const
        {
            return c.empty();
        }
        size_t size() const
        {
            return c.size();
        }
        T& top() const
        {
            return c[0];
        }
        void push(const T& x)
        {
            c.push_back(x);
            adjust_up(c.size() - 1);
        }
        void pop() //Remove top element
        {
            //交换后pop,再向下调整
            swap(c[0], c[c.size() - 1]);
            c.pop_back();
            adjust_down(0);
        }

		//测试代码
        //void print()
        //{
        //    for (auto e : c)
        //    {
        //        cout << e << " ";
        //    }
        //}
    private:
        Container c;
        Compare comp;
    };

};

stack与queue模拟实现源码:

namespace qqq
{

    //stack
    template<class T, class Con = deque<T>>
    class stack
    {
    public:

        stack()
        {}

        void push(const T& x)
        {
            _c.push_back(x);
        }
        void pop()
        {
            if (_c.empty())
                return;
            _c.pop_back();
        }
        T& top()
        {
            return _c.back();
        }
        const T& top()const
        {
            return _c.back();
        }
        size_t size()const
        {
            return _c.size();
        }
        bool empty()const
        {
            return _c.empty();
        }

    private:
        Con _c;
    };

    //queue
    template<class T, class Con = deque<T>>
    class queue
    {
    public:

        queue()
        {}

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

总结

到此,关于priority_queue的使用与实现的所有知识就介绍完了
由于堆的知识在之前C语言部分有详细介绍,所以在这篇文中介绍的不是特别详细,大家有需要可以跳转去相关文章,蟹蟹支持

如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1397625.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UE5 蓝图编辑美化学习

虚幻引擎中干净整洁蓝图的15个提示_哔哩哔哩_bilibili 1.双击线段成节点。 好用&#xff0c;爱用 2.用序列节点 好用&#xff0c;爱用 3.用枚举。 好用&#xff0c;能避免一些的拼写错误 4.对齐节点 两点一水平线 5.节点上下贴节点 &#xff08;以前不懂&#xff0c;现在经常…

【AJAX框架】AJAX入门与axios的使用

文章目录 前言一、AJAX是干什么的&#xff1f;二、AJAX的安装2.1 CDN引入2.2 npm安装 三、基础使用3.1 CDN方式3.2 node方式 总结 前言 在现代Web开发中&#xff0c;异步JavaScript和XML&#xff08;AJAX&#xff09;已经成为不可或缺的技术之一。AJAX使得网页能够在不刷新整个…

SQL注入实战操作

一&#xff1a;SQl注入分类 按照注入的网页功能类型分类&#xff1a; 1、登入注入&#xff1a;表单&#xff0c;如登入表单&#xff0c;注册表单 2、cms注入&#xff1a;CMS逻辑:index.php首页展示内容&#xff0c;具有文章列表(链接具有文章id)、articles.php文 章详细页&a…

NX二次开发封装自己的函数及如何导入工程

目录 一、概述 二、函数封装 三、函数引用 四、案例——在NX中运行后输出“测试”两字 一、概述 随着对NX二次开发的学习&#xff0c;我们在各种项目里面会积累很多函数&#xff0c;对于一些经常用到的函数&#xff0c;我们可以考虑将其封装为类库&#xff0c;以后在开发其…

从请购到结算,轻松搞定!云迈ERP系统助力企业采购管理全流程!

​在企业的运营过程中&#xff0c;采购管理是至关重要的环节之一。为了确保采购流程的顺畅和高效&#xff0c;许多企业选择引入ERP&#xff08;企业资源规划&#xff09;系统来进行采购管理。那么erp系统中的采购管理都有哪些功能呢&#xff1f; 云迈erp系统中的采购管理模块&a…

虚拟机下载docker

一&#xff0c;Docker简介 百科说&#xff1a;Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的Linux机器上&#xff0c;也可以实现虚拟化&#xff0c;容器是完全使用沙箱机制&#xff…

大数据导论(2)---大数据与云计算、物联网、人工智能

文章目录 1. 云计算1.1 云计算概念1.2 云计算的服务模式和类型1.3 云计算的数据中心与应用 2. 物联网2.1 物联网的概念和关键技术2.2 物联网的应用和产业2.3 大数据与云计算、物联网的关系 1. 云计算 1.1 云计算概念 1. 首先从商业角度给云计算下一个定义&#xff1a;通过网络…

第二次作业+第三次作业

第二次作业第三次作业 第二次作业 题目&#xff1a; 网站需求&#xff1a; ​ 1.基于域名[www.openlab.com](http://www.openlab.com)可以访问网站内容为 welcome to openlab!!! 2.给该公司创建三个子界面分别显示学生信息&#xff0c;教学资料和缴费网站&#xff0c;基于[ww…

Oracle架构_数据库底层原理、机制 (授人以渔)

目录 系统全局区SGA 高速缓存缓冲区(数据库缓冲区) 日志缓冲区 共享池 其他结构 用户连接进程 用户进程User Process Server Process服务进程 程序全局区PGA Oracle的connect连接和session会话与User Process紧密相关 后台进程 数据库写入进程(DBWn) 检查点(CKPT)…

php isset和array_key_exists区别

在PHP中&#xff0c;可以使用array_key_exists函数或者isset函数来判断一个字典&#xff08;关联数组&#xff09;中是否存在某个下标。 使用 array_key_exists 函数: $myArray array("key1" > "value1", "key2" > "value2",…

PyTorch视觉工具箱:图像变换与上采样技术详解(1)

目录 Pytorch中Vision functions详解 pixel_shuffle 用途 用法 使用技巧 注意事项 参数 数学理论公式 示例代码及输出 pixel_unshuffle 用途 用法 使用技巧 注意事项 参数 数学理论公式 示例代码及输出 pad 用途 用法 使用技巧 注意事项 参数 示例代码…

Verilog基础:强度建模(二)

相关阅读 Verilog基础https://blog.csdn.net/weixin_45791458/category_12263729.html?spm1001.2014.3001.5482 三、拥有单个强度和确定值的net型信号的线与组合&#xff08;线网多驱动&#xff09; 首先来说明一下什么叫信号拥有单个强度和确定值&#xff0c;其实如果一个ne…

使用KTO进行更好、更便宜、更快速的LLM对齐

KTO全称为Kahneman-Tversky Optimisation&#xff0c;这种对齐方法使在我们的数据上对大型语言模型&#xff08;LLM&#xff09;进行对齐变得前所未有地容易和便宜&#xff0c;而且不会损害性能。大型语言模型的成功在很大程度上得益于与人类反馈的对齐。如果ChatGPT曾经拒绝回…

禅道使用教程

禅道的使用 一.禅道的使用1.添加部门和批量添加用户2.以产品经理的身份登录进行使用和操作2.1创建产品2.2创建模块2.3添加产品计划2.4添加产品需求2.5创建项目2.6设置团队 3.项目经理使用禅道3.1关联需求3.2批量分解,给人员分配任务3.3假设项目完成开发,项目经理创建版本 4.测试…

《数字图像处理-OpenCV/Python》连载:傅里叶变换与频域滤波

《数字图像处理-OpenCV/Python》连载&#xff1a;空间滤波之高斯滤波器 本书京东 优惠购书链接 https://item.jd.com/14098452.html 本书CSDN 独家连载专栏 https://blog.csdn.net/youcans/category_12418787.html 第 11 章 傅里叶变换与频域滤波 空间图像滤波是图像与滤波器核…

根据基因名批量查找它的Uniprot编号

背景&#xff1a; 前几天老师交给我一个任务&#xff0c;给我一个基因列表&#xff0c;让我查找它们所编码的蛋白质的蛋白质序列。我上了一下uniprot数据库&#xff0c;发现这个任务可以分成两步&#xff1a; 找到这个基因在Uniprot数据库中所对应的蛋白质编码根据蛋白质编码…

街机模拟游戏逆向工程(HACKROM)教程:[12]68K汇编-程序流控制

在之前的文章中&#xff0c;我们测试过一些简短的一小段程序&#xff0c;这些程序都有一个共同的程序运行流程&#xff0c;就是一句一句地向下执行&#xff0c;比如&#xff1a; movea.l #$325, a0 * ↓move.b #$01, (a0) * ↓move.b #$02, $01(a…

【软件测试常见Bug清单】

软件测试中&#xff0c;bug的类型有很多种&#xff0c;比如&#xff1a;代码错误、界面优化、设计缺陷、需求补充和用户体验等&#xff1b; 一般情况下&#xff0c;需求补充和设计缺陷比较好区分&#xff0c;但是代码错误、界面优化和用户体验区分不是很明显&#xff1b; 下面…

主动轮廓——计算机视觉中的图像分割方法

​ 一、说明 简单来说&#xff0c;计算机视觉就是为计算机提供类似人类的视觉。作为人类&#xff0c;我们很容易识别任何物体。我们可以很容易地识别山丘、树木、土地、动物等&#xff0c;但计算机没有眼睛&#xff0c;也没有大脑&#xff0c;因此它很难识别任何图像。计算机只…

PostgreSQL 的对象层次

所有的数据库离开数据量来谈性能都是耍流氓。 就你那几万条的数据库&#xff0c;用啥都行&#xff0c;典型的就是怎么方便怎么来。 不过 PostgreSQL 上手确实比 MySQL 概念更多。 PostgreSQL 比 MySQL 多了一层。 PostgreSQL 是从PostgreSQL 是从 Database&#xff0c;到 S…