STL之priority_queue篇——深入剖析C++中优先队列的实现原理、核心特性及其底层机制

news2024/11/30 8:45:15

文章目录

  • 前言
    • 一、补充内容:堆
      • 1.1 什么是堆
      • 1.2 堆的分类与性质
      • 1.3 堆的向下调整算法(小根堆)
        • 实现流程:
        • 代码:
      • 1.4 堆的向上调整算法(小根堆)
        • 实现流程:
        • 代码:
      • 1.5 数组建堆算法实现(小根堆)
    • 二、优先队列priority_queue的使用
      • 2.1 引入头文件
      • 2.2 基本声明
      • 2.3 常用操作
      • 2.4 示例代码
    • 三、priority_queue的模拟实现
      • 3.1 基本框架
      • 3.2 迭代器范围构造函数
      • 3.3 基本函数
      • 3.4 push与pop函数(重点)
    • 四、仿函数
      • 4.1定义与特点
      • 4.2 应用场景
      • 4.3 实现方式
    • 五、优化后的全部代码
      • 5.1 头文件(包含测试函数)
      • 5.2 源文件


前言

本文旨在深入剖析C++中优先队列的实现原理、核心特性及其底层机制,同时结合丰富的实战案例,帮助读者全面掌握优先队列的使用方法,并能够灵活应用于各种复杂问题的解决中。我们将从优先队列的基本概念出发,逐步深入到其内部实现细节,包括堆(Heap)结构的应用、比较函数的自定义等关键知识点。此外,本文还将探讨优先队列在解决经典算法问题中的实际应用,通过具体代码示例,展示如何在不同场景下发挥优先队列的最大效用

一、补充内容:堆

1.1 什么是堆

堆实际上就是一个完全二叉树,那么完全二叉树又是什么呢?

  • 假如一个二叉树有k层,并且这个树的前k-1层都是满树,第k层的叶子结点全部集中紧挨着在左边,举个例子:

在这里插入图片描述

如图所示:这样大家就能更清晰明了的看出哪一个才是完全二叉树了吧

1.2 堆的分类与性质

【分类】:

  • 堆分为两类:1. 大根堆,2. 小根堆

  • 那么这两种堆的区别在哪呢?故名思义:大根堆的堆顶代表整个堆最大的元素,小根堆的堆顶代表整个堆最小的元素

【性质】:

  • 大根堆的左右子树都是大根堆,小根堆的左右子树都是小根堆
  • 堆中的结点总是不大于或不小于其父结点

我们以小根堆来举例:
在这里插入图片描述可以看到其中每一个分支都像是一个小根堆,父结点总是小于子结点

1.3 堆的向下调整算法(小根堆)

提问:为什么要设计这个算法?这个算法有什么用?

解释:众所周知,堆是一个数据结构,既然是数据结构,那必然离不开增删查改,假如我要删除堆的堆顶元素,为了不影响整个堆的结构,我们只能取最后一个元素放在堆顶,然后执行向下调整算法,直到整个堆变成我们想要的大根堆或是小根堆。或者说,当我们想要生成一个堆的时候,这种算法就有了明显的作用,举个例子:

  • 我们定义一个数组arr,想要将其变成一个小根堆
    在这里插入图片描述
实现流程:

**函数头:**如上图所示,我们要实现这样的函数,需要三个参数:

  1. 数组地址
  2. 数组元素个数即堆的结点个数
  3. 向下调整的起始位置,应该默认是0,即根结点

**函数体:**我们只需满足小根堆的性质即可

  • 跳出循环遍历的条件:遍历完所有结点
  • 父节点总是与左右孩子较小的一个比较
  • 如果子结点小于父结点,交换父子结点,继续遍历比较,否则跳出循环
代码:
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;	// 左孩子和父节点的关系
    while(child < n)
    {
        if(child + 1 < n && a[child + 1] < a[child])
    	{
       		++child;
		}
        if(a[child] < a[parent])
        {
            swap(a[child], a[parent]);
            parent = child;
            child = parent * 2 + 1;
		}
        else break;
	}
}

1.4 堆的向上调整算法(小根堆)

同样的,我们需要在堆中插入一个元素的时候,我们只能将其插入至堆的末尾,然后逐步向上调整,直到得到我们想要的大根堆或是小根堆。

实现流程:

大致内容与向下调整算法类似,只是换了个方向比较,这里不再过多赘述。

**不同的是:**这里不需要判断左右孩子的大小,因为原本这就是一个小根堆,大小已经比较完了,如果新插入的元素小于父节点,那它必然小于左孩子

代码:
void AdjustUp(int* a, int child)
{
	int parent = (child - 1) / 2;
    while(child)
    {
		if(a[child] < a[parent])
        {
			swap(a[child], a[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }
        else break;
    }
}

1.5 数组建堆算法实现(小根堆)

  • 若左右子树不是小堆——想办法把左右子树处理成小堆

  • 可以从倒数第一个非叶子节点的位置开始向下调整

  • 最后一个非叶子节点的下标为 (n-1-1)/2

	int n = sizeof(a) / sizeof(int);
	//数组建堆算法
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(arr, n, i);
	}

二、优先队列priority_queue的使用

priority_queue 是 C++ 标准模板库(STL)中的一种容器适配器,它提供了队列的功能,并且其中元素的优先级可以由用户定义。默认情况下,priority_queue 是一个最大堆,即队列中每次出队(访问队首元素)的都是优先级最高的元素。如果你想实现一个最小堆,可以自定义比较函数或使用 greater

以下是 priority_queue 的一些基本用法和示例:

2.1 引入头文件

要使用 priority_queue,你需要包含 <queue> 头文件:

#include <queue>

2.2 基本声明

你可以使用默认的比较器来声明一个 priority_queue,这样它会成为一个最大堆:

priority_queue<int> pq;

如果你想要一个最小堆,可以自定义比较器:

priority_queue<int, vector<int>, greater<int>> minHeap;

这里,vector<int> 是底层容器(虽然通常不需要显式指定,因为 priority_queue 默认使用 vector),greater<int> 是比较器,用于确定元素的优先级。

2.3 常用操作

  • push(x): 向队列中添加一个元素。
  • pop(): 移除队首元素(优先级最高的元素)。
  • top(): 返回队首元素的引用(但不移除它)。
  • empty(): 检查队列是否为空。
  • size(): 返回队列中元素的数量。

2.4 示例代码

以下是一个简单的示例,演示了如何使用 priority_queue

#include <iostream>  
#include <queue>  
#include <vector>  
using namespace std;
  
int main() {  
    // 创建一个最大堆  
    priority_queue<int> maxHeap;  
  
    // 向最大堆中添加元素  
    maxHeap.push(10);  
    maxHeap.push(5);  
    maxHeap.push(20);  
    maxHeap.push(1);  
  
    // 输出并移除最大堆中的元素,直到堆为空  
    while (!maxHeap.empty()) {  
        cout << maxHeap.top() << " "; // 访问队首元素(优先级最高的元素)  
        maxHeap.pop(); // 移除队首元素  
    }  
    cout << endl;  
  
    // 创建一个最小堆  
    priority_queue<int, vector<int>, greater<int>> minHeap;  
  
    // 向最小堆中添加元素  
    minHeap.push(10);  
    minHeap.push(5);  
    minHeap.push(20);  
    minHeap.push(1);  
  
    // 输出并移除最小堆中的元素,直到堆为空  
    while (!minHeap.empty()) {  
        cout << minHeap.top() << " "; // 访问队首元素(优先级最低的元素)  
        minHeap.pop(); // 移除队首元素  
    }  
    cout << endl;  
  
    return 0;  
}

在这个示例中,我们首先创建了一个最大堆,并向其中添加了一些整数。然后,我们循环输出并移除最大堆中的元素,直到堆为空。接着,我们创建了一个最小堆,并重复了相同的操作。

注意,priority_queue 不支持直接访问或修改除队首元素以外的其他元素,也不支持随机访问。

三、priority_queue的模拟实现

废话不多说我们直接开造!!!

3.1 基本框架

namespace xny
{
    template <class T, class Container = vector<T>>
    class my_priority_queue
    {
    public:
    	my_priority_queue(){}
        
        template <class InputIterator>
		my_priority_queue(InputIterator first, InputIterator last);
        
        bool empty();
        
        size_t size();
        
        T& top();
        
        void push(const T& x);
        
        void pop();
        
    private:
        Container c;
    };
}

3.2 迭代器范围构造函数

在此之前,我们已经声明优先队列实际上就是一个大根堆,也就是说初始化我们需要用堆的方式进初始化,所以我们应该增添一个函数在类的private内部:

  • 向下调整堆算法:
void AdjustDown(int parent)
{
    int child = parent * 2 + 1;
    while (child < c.size()) {
        if (child + 1 < c.size() && c[child + 1] < c[child]) {
            ++child;
        }
        if (c[child] < c[parent]) {
            swap(c[child], c[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else {
            break;
        }
    }
}

在之前已经分析过,这里就不再过多赘述,唯一不同的就是参数变少了,原因是类的内部已经提供了这些东西,可以直接用

迭代器范围构造函数:

template <class InputIterator>
my_priority_queue(InputIterator first, InputIterator last) {
    while (first != last) {
        c.push_back(*first);
        ++first;
    }
	// 堆的初始化
    for (int i = (size() - 1 - 1) / 2; i > 0; --i) {
        AdjustDown(i);
    }
}

3.3 基本函数

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

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

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

3.4 push与pop函数(重点)

  • 值得注意的是,堆的插入,可不仅仅是把值插入到尾端就结束了,不要忘了堆的性质,在插入之后我们就需要用到堆的向上调整算法,将堆的结构还原

向上调整算法:

void AdjustUp(int child)
{
    int parent = (child - 1) / 2;
    while (child) {
        if (c[child] < c[parent]) {
            swap(c[child], c[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }
        else {
            break;
        }
    }
}

push:

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

pop:

解释:上面我们提到,为了不影响整个堆的结构,我们只能先交换堆顶和堆尾元素,再删除交换前的堆顶元素,然后执行向下调整算法,直到整个堆变成我们想要的大根堆或是小根堆。

void pop() {
    swap(c[0], c[c.size() - 1]);
    c.pop_back();
    AdjustDown(0);
}

四、仿函数

这里为什么我们要说仿函数这个东西呢?可以发现我们上述模拟实现的只是固定的一个大根堆的优先队列,但是标准库里通过传参数的不同还能实现小根堆的优先队列,这里就是用了仿函数,下面我来介绍一下仿函数的基本要点:

4.1定义与特点

  1. 定义:仿函数本质上是一个类,它通过重载函数调用运算符(operator())来模拟函数的行为。这样,类的实例就可以像函数一样被调用。
  2. 特点
    • 仿函数可以有参数和返回值,这是通过重载的operator()实现的。
    • 仿函数可以作为参数传递给其他函数,这是函数式编程和面向对象编程结合的一种体现。
    • 仿函数可以保存状态,因为它本质上是一个对象。这意味着在多次调用仿函数时,它可以保持一些内部状态不变,这对于实现某些复杂的算法和数据结构非常有用。

4.2 应用场景

  1. STL算法:在C++的标准模板库(STL)中,许多算法如sort、for_each、transform等都接受仿函数作为参数。这允许程序员自定义排序规则、操作、条件等。
  2. 自定义容器:通过仿函数,可以实现具有特定行为的自定义容器。例如,可以定义一个堆栈容器,该容器在每次弹出元素时都返回最小的元素。
  3. 函数对象传递:仿函数可以用作函数的参数或返回值,实现更灵活的函数调用和传递。

4.3 实现方式

  1. 重载operator():要在类中实现仿函数的功能,只需重载()运算符即可。在该运算符的实现中,可以包含任何需要的逻辑和状态。
  2. 使用模板:仿函数通常与模板一起使用,以实现更通用的代码。通过模板参数,可以灵活地传递不同类型的仿函数。

下面我就来为大家实现仿函数在堆里的实现:

包含头文件:

#include<functional>

仿函数体:

template<class T>
class Less {
public:
    bool operator()(const T& x, const T& y) {
        return x < y;
    }
};

template<class T>
class Greater {
public:
    bool operator()(const T& x, const T& y) {
        return x > y;
    }
};

然后我们的类模板参数就应该变成这样了:

template <class T, class Container = vector<T>, class Compare = Less<T>>

或者:

template <class T, class Container = vector<T>, class Compare = Greater<T>>

五、优化后的全部代码

5.1 头文件(包含测试函数)

#pragma once
#include<vector>
#include<functional>

template<class T>
class Less {
public:
    bool operator()(const T& x, const T& y) {
        return x < y;
    }
};

template<class T>
class Greater {
public:
    bool operator()(const T& x, const T& y) {
        return x > y;
    }
};

namespace xny
{
    template <class T, class Container = vector<T>, class Compare = Less<T>>
    class my_priority_queue
    {
    private:
        // 向下调整堆
        void AdjustDown(int parent)
        {
            int child = parent * 2 + 1;
            while (child < c.size()) {
                if (comp(child + 1, c.size()) && comp(c[child + 1], c[child])) {
                    ++child;
                }
                if (comp(c[child], c[parent])) {
                    swap(c[child], c[parent]);
                    parent = child;
                    child = parent * 2 + 1;
                }
                else {
                    break;
                }
            }
        }

        // 向上调整堆
        void AdjustUp(int child)
        {
            int parent = (child - 1) / 2;
            while (child) {
                if (comp(c[child], c[parent])) {
                    swap(c[child], c[parent]);
                    child = parent;
                    parent = (child - 1) / 2;
                }
                else {
                    break;
                }
            }
        }

    public:
        my_priority_queue(){}

        template <class InputIterator>
        my_priority_queue(InputIterator first, InputIterator last) {
            while (first != last) {
                c.push_back(*first);
                ++first;
            }

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

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

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

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

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

        void pop() {
            swap(c[0], c[c.size() - 1]);
            c.pop_back();
            AdjustDown(0);
        }

    private:
        Container c;
        Compare comp;
    };

    void test_my_priority_queue()
    {
        my_priority_queue<int> q;
        q.push(1);
        q.push(2);
        q.push(5);
        q.push(6);
        q.push(3);
        q.push(4);
        while (!q.empty())
        {
            cout << q.top() << " ";
            q.pop();
        }
        cout << endl;

    }
}

5.2 源文件

代码:

#include <iostream>
#include <queue>

using namespace std;

#include "my_priority"

int main()
{
	xny::test_my_priority_queue();
    
    return 0;
}

输出:

1 2 3 4 5 6

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

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

相关文章

eclpsexxx

Copyright?2001-2004 International Business Machines Corp. Guidelines Eclipse 用户界面指南 2.1 版 查看目录 作者&#xff1a;Nick Edgar, Kevin Haaland, Jin Li , Kimberley Peter 译者&#xff1a;Bobbie Wang&#xff0c;Qi Liang 最新更新: 2004年2月 注意 您…

kaggle实战2信用卡反欺诈逻辑回归模型案例1

信用卡欺诈案例 数据集下载地址 https://storage.googleapis.com/download.tensorflow.org/data/creditcard.csv 参考不平衡数据的分类 文章目录 只进行特征衍生&#xff0c;未进行数据标准化、上才样处理数据不平衡问题&#xff0c;得到的准确率和召回率居然很高如果不处理数据…

李宏毅 X 苹果书 自注意力机制 学习笔记上

self attention 是一种network架构使用场景&#xff1a;输入一组向量&#xff0c;这组向量的性质&#xff1a;数量有变化&#xff0c;序列长度不一 模型输入 文字处理&#xff1a; 模型输入&#xff1a;句子&#xff08;句子的长度&#xff0c;单词都不一样&#xff09;&am…

qt QMainWindow 自定义标题栏

可以使用setMenuWidget 来将自定义的标题栏 QWidget 设置进去就可以&#xff0c; 用来替代setMenu 菜单栏单一&#xff0c;自定义不高的问题

node_exporter使用textfile collector收集业务数据

上一篇文章讲了使用Pushgateway收集业务数据的方法&#xff0c;今天讲另外一种方式textfile collector The textfile collector is similar to the Pushgateway, in that it allows exporting of statistics from batch jobs. The Pushgateway should be used for service-leve…

解决ModuleNotFoundError: No module named ‘torchcrf‘

运行深度学习程序时候&#xff0c;出现报错&#xff1a;ModuleNotFoundError: No module named torchcrf 将 from torchcrf import CRF 改为 from TorchCRF import CRF

无设计器简单实例

目录 1、界面设计Qt5元对象系统1. **QObject 类**2. **QMetaObject**3. **信号和槽机制**4. **宏&#xff1a;Q_OBJECT**5. **动态属性**6. **反射机制**7. **元对象编译器&#xff08;MOC&#xff09;** 2、完成程序功能 1、界面设计 不点创建界面 在dialog.h中 #ifndef DIA…

树莓派5里使用protobuf

由于现在protobuf越来越复杂了&#xff0c;自己去编译&#xff0c;还是比较麻烦。 比如最新的V28版本&#xff0c;就会要求使用cmake或者bazel来编译了。 如果不要求使用最新的版本&#xff0c;直接使用系统里带的版本也是可以的。 可以进行如下操作&#xff1a; sudo apt …

【算法系列-链表】交换链表节点(反转 + 交换)

【算法系列-链表】交换链表节点(反转 交换) 文章目录 【算法系列-链表】交换链表节点(反转 交换)1. 反转链表1.1 思路分析&#x1f3af;1.2 解题过程&#x1f3ac;1.3 代码示例&#x1f330; 2. 两两交换链表中的节点2.1 思路分析&#x1f3af;2.2 解题过程&#x1f3ac;2.3 …

电器自动化入门08:隔离变压器、行程开关介绍及选型

视频链接&#xff1a;3.4 电工知识&#xff1a;三相交流异步电动机自动往返行程控制及控制变压器选型_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1PJ41117PW?p8&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 1.隔离&#xff08;控制&#xff09;变压器 2.行程开…

C++_智能指针详解

什么是智能指针&#xff1f;为什么要有智能指针&#xff1f;到目前为止&#xff0c;我们编写的程序所使用的对象都有着严格定义的生命周期。比如说&#xff0c;全局对象在程序启动时分配&#xff0c;在程序结束时销毁&#xff1b;再比如说局部static对象在第一次使用前分配&…

4.5章节python中的break和continue语句的作用

在Python中&#xff0c;break 和 continue 是两个用于控制循环流程的关键字。它们提供了在特定条件下提前退出循环或跳过当前迭代并进入下一次迭代的机制。 一、break语句 break 语句用于立即终止当前的循环&#xff08;无论是 for 循环还是 while 循环&#xff09;&#xff…

最佳人力资源管理工具,6款热门产品功能对比

文章介绍了ZohoPeople、北森、i人事等六款主流人力资源管理系统&#xff0c;涵盖招聘、培训、考勤等功能&#xff0c;各有特点&#xff0c;适合不同规模企业需求。建议企业试用后选择&#xff0c;提高管理效率。 一、Zoho People Zoho People是一款强大的云端人力资源管理系统…

看Threejs好玩示例,学习创新与技术(LiquidRaymarching)

今天的示例有点超出我的想象&#xff0c;首先会科普下WGSL这种新的着色器脚本&#xff0c;然后说说示例《Liquid Raymarching Scene with Three.js Shading Language | Codrops (tympanus.net)》的技术流程。本示例最终呈现的效果如下。可以看到他跟QQ那个消息拖拽消灭的效果非…

Flink 03 | 数据流基本操作

Flink数据流结构 DataStream 转换 通常我们需要分析的业务数据可能存在如下问题&#xff1a; 数据中包含一些我们不需要的数据 数据格式不方面分析 因此我们需要对原始数据流进行加工&#xff0c;比如过滤、转换等操作才可以进行数据分析。 “ Flink DataStream 转换主要作…

C++ -引用-详解

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【C】 欢迎点赞&#x1f44d;收藏⭐关注❤️ C -引用-详解 1.引用基础1.1是什么1.2特点 2.引用的意义3.引用的应用场景3.1作为参数3.2作为返回值传值返回引用返回 4.权限问题5.与指针的区别6.总结 1.引用基础 1.1是什么 …

SpringBoot整合异步任务执行

同步任务&#xff1a; 同步任务是在单线程中按顺序执行&#xff0c;每次只有一个任务在执行&#xff0c;不会引发线程安全和数据一致性等 并发问题 同步任务需要等待任务执行完成后才能执行下一个任务&#xff0c;无法同时处理多个任务&#xff0c;响应慢&#xff0c;影响…

小红书三面被问 RAG 原理,秒挂…

最近这一两周看到不少互联网公司都已经开始秋招发放Offer。 不同以往的是&#xff0c;当前职场环境已不再是那个双向奔赴时代了。求职者在变多&#xff0c;HC 在变少&#xff0c;岗位要求还更高了。 最近&#xff0c;我们又陆续整理了很多大厂的面试题&#xff0c;帮助一些球…

MySQL高阶2082-富有客户的数量

目录 题目 准备数据 分析数据 题目 编写解决方案找出 至少有一个 订单的金额 严格大于 500 的客户的数量。 准备数据 Create table If Not Exists Store (bill_id int, customer_id int, amount int)Truncate table Storeinsert into Store (bill_id, customer_id, amoun…

openpnp - 图像传送方向要在高级校正之前设置好

文章目录 openpnp - 图像传送方向要在高级校正之前设置好笔记END openpnp - 图像传送方向要在高级校正之前设置好 笔记 图像传送方向和JOG面板的移动控制和实际设备的顶部摄像头/底部摄像头要一致&#xff0c;这样才能和贴板子时的实际操作方向对应起来。 设备标定完&#xf…