C++优选算法十四 优先级队列(堆)

C++ 中的优先级队列(Priority Queue)是一种容器适配器,它提供队列的功能,但元素不是按照插入的顺序被访问,而是根据它们的优先级被访问。默认情况下,优先级队列是一个最大堆(Max-Heap),这意味着队列的根元素总是优先级最高的元素(即值最大的元素)。不过,你也可以通过自定义比较函数来改变这个行为,使其变成最小堆(Min-Heap)。

1.基本用法

在 C++ 中,优先级队列通常通过 <queue> 头文件来使用。下面是一个简单的例子,展示了如何声明和使用优先级队列:

#include <iostream>
#include <queue>
#include <vector>

int main() {
    // 创建一个默认的最大堆优先级队列
    std::priority_queue<int> pq;

    // 向队列中添加元素
    pq.push(10);
    pq.push(20);
    pq.push(15);

    // 访问并移除最高优先级的元素
    while (!pq.empty()) {
        std::cout << pq.top() << " ";  // 输出最大元素
        pq.pop();  // 移除最大元素
    }
    std::cout << std::endl;

    return 0;
}

 

输出将是:

20 15 10

2.自定义优先级

如果想创建一个最小堆或者基于自定义比较逻辑的优先级队列,可以使用模板参数来指定比较函数或比较对象。例如:

小根堆:

#include<iostream>
#include<vector>
#include<queue>

using namespace std;

int main()
{
	priority_queue<int, vector<int>, greater<int>> qe;
	qe.push(4);
	qe.push(2);
	qe.push(1);
	while (!qe.empty())
	{
		int a = qe.top();
		cout << a << ", ";
		qe.pop();
	}
	return 0;
}

自定义比较逻辑: 

#include <iostream>
#include <queue>
#include <vector>
#include <functional>  // std::greater

struct Custom {
    int id;
    std::string name;

    // 重载小于运算符以用于最小堆
    bool operator<(const Custom& other) const {
        return id > other.id;  // 注意这里是大于,因为我们想要最小堆
    }
};

int main() {
    // 创建一个最小堆优先级队列
    std::priority_queue<Custom, std::vector<Custom>, std::greater<Custom>> pq;

    pq.push({3, "Alice"});
    pq.push({1, "Bob"});
    pq.push({2, "Charlie"});

    while (!pq.empty()) {
        Custom top = pq.top();
        std::cout << "ID: " << top.id << ", Name: " << top.name << std::endl;
        pq.pop();
    }

    return 0;
}


在这个例子中,我们定义了一个 Custom 结构体,并通过重载小于运算符 operator< 来创建一个最小堆。我们还使用了 std::greater<Custom> 作为第三个模板参数来显式指定比较函数。

3.注意事项

  1. 效率:优先级队列的插入和删除操作(push 和 pop)的时间复杂度通常为 O(log n),其中 n 是队列中元素的数量。
  2. 排序稳定性:由于优先级队列是基于堆实现的,因此它并不保证相同优先级的元素按照它们被插入的顺序来访问。
  3. 自定义类型:当使用自定义类型时,确保提供了必要的比较逻辑(如重载 operator< 或提供比较函数对象)。

通过了解这些基本概念和用法,可以有效地利用 C++ 的优先级队列来解决需要优先级处理的问题。

4.例题

 1.最后一块石头的重量

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0

示例:

输入:[2,7,4,1,8,1]
输出:1
解释:
先选出 7 和 8,得到 1,所以数组转换为 [2,4,1,1,1],
再选出 2 和 4,得到 2,所以数组转换为 [2,1,1,1],
接着是 2 和 1,得到 1,所以数组转换为 [1,1,1],
最后选出 1 和 1,得到 0,最终数组转换为 [1],这就是最后剩下那块石头的重量。

解法(利用堆)
算法思路:

其实就是一个模拟的过程:

  • 每次从石堆中拿出最大的元素以及次大的元素,然后将它们粉碎;
  • 如果还有剩余,就将剩余的石头继续放在原始的石堆里面。

重复上面的操作,直到石堆里面只剩下一个元素,或者没有元素(因为所有的石头可能全部抵消了)
那么主要的问题就是解决:

  • 如何顺利的拿出最大的石头以及次大的石头;
  • 并且将粉碎后的石头放入石堆中之后,也能快速找到下一轮粉碎的最大石头和次大石头;

这不正好可以利用堆的特性来实现嘛?

  • 我们可以创建一个大根堆;
  • 然后将所有的石头放入大根堆中;
  • 每次拿出前两个堆顶元素粉碎一下,如果还有剩余,就将剩余的石头继续放入堆中;

这样就可以快速模拟出整个过程。

class Solution {
public:
    int lastStoneWeight(vector<int>& stones) 
    {
        priority_queue<int> qe;//默认创建时是大根堆
        for(int i=0;i<stones.size();i++)
        {
            qe.push(stones[i]);
        }
        int a=0;
        while(qe.size()>1)
        {
            int x=qe.top();//取出堆顶元素
            qe.pop();
            int y=qe.top();
            qe.pop();
            if(x!=y)
            {
                a=max(x,y)-min(x,y);
                qe.push(a);
            }
        }
        if(qe.size()==1)
        return qe.top();
        else
        return 0;
    }
};
2.数据流中的第 K 大元素

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

示例 1:

输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]

输出:[null, 4, 5, 5, 8, 8]

解释:

KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // 返回 4
kthLargest.add(5); // 返回 5
kthLargest.add(10); // 返回 5
kthLargest.add(9); // 返回 8
kthLargest.add(4); // 返回 8

解法(优先级队列)
这是经典的TopK问题。我们常用堆排序来解决问题。对于TopK问题,如果要求前K个最大的元素,可以维护一个小顶堆;如果要求前K个最小的元素,可以维护一个大顶堆。具体步骤如下:

  • 用数据集合中前K个元素来建堆。
  • 用剩余的N-K个元素依次与堆顶元素来比较,如果比堆顶的数据大(对于求前K个最大元素的情况),就替换它进堆,并调整堆结构。
  • 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

堆排序法的时间复杂度为O(nlogk),其中n是数据的个数,k是需要查询的数据个数。这种方法在处理大规模数据时非常高效。

class KthLargest {
    priority_queue<int,vector<int>,greater<int>> heap;//小根堆
    int _k;
public:
    KthLargest(int k, vector<int>& nums) 
    {
        _k=k;
        for(int x:nums)
        {
            heap.push(x);
            if(heap.size()>_k) 
                heap.pop();
        }
    }
    
    int add(int val) 
    {
        heap.push(val);
        if(heap.size()>_k)
            heap.pop();
        return heap.top();        
    }
};
3.前K个高频单词

给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。

示例 1:

输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。
    注意,按字母顺序 "i" 在 "love" 之前。

示例 2:

输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词,
    出现次数依次为 4, 3, 2 和 1 次。

解法(堆)
算法思路

稍微处理一下原数组:
        a.我们需要知道每一个单词出现的频次,因此可以先使用哈希表,统计出每一个单词出现的频次;
        b.然后在哈希表中,选出前k大的单词(为什么不在原数组中选呢?因为原数组中存在重复的单词,哈希表里面没有重复单词,并且还有每一个单词出现的频次)
如何使用堆,拿出前k大元素:
        a.先定义一个自定义排序,我们需要的是前k大,因此需要一个小根堆。但是当两个字符串的频次相同的时候,我们需要的是字典序较小的,此时是一个大根堆的属性,在定义比较器的时候需要注意!

  • 当两个字符串出现的频次不同的时候:需要的是基于频次比较的小根堆
  • 当两个字符串出现的频次相同的时候:需要的是基于字典序比较的大根堆

        b.定义好比较器之后,依次将哈希表中的字符串插入到堆中,维持堆中的元素不超过 k 个;
        c.遍历完整个哈希表后,堆中的剩余元素就是前 k大的元素

class Solution {
    typedef pair<string,int> psi;//将字符串和下标绑定
    struct cmp{//自定义比较逻辑
        bool operator()(const psi&a,const psi&b)
        {
            if(a.second==b.second)//频次相同,字典序按照大根堆的方式排列
                return a.first<b.first;
            return a.second>b.second;
        }
    };
public:
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        //1.统计一下每个单词的频次
        unordered_map<string,int> hash;
        for(auto &s:words)
        {
            hash[s]++;
        }

        //2.建堆
        priority_queue<psi,vector<psi>,cmp> heap;
        
        //3.TopK的逻辑
        for(auto& x:hash)
        {
            heap.push(x);
            if(heap.size()>k)
                heap.pop();
        }
        //4.提取结果
        vector<string> vv(k);
        for(int i=k-1;i>=0;i--)
        {
            vv[i]=heap.top().first;
            heap.pop();
        }
        return vv;
    }
};
4.数据流的中位数

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。

  • 例如 arr = [2,3,4] 的中位数是 3 。
  • 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。

实现 MedianFinder 类:

  • MedianFinder() 初始化 MedianFinder 对象。

  • void addNum(int num) 将数据流中的整数 num 添加到数据结构中。

  • double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

示例 1:

输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]

解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1);    // arr = [1]
medianFinder.addNum(2);    // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3);    // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0

解法(利用两个堆)
算法思路:

这是一道关于「堆」这种数据结构的一个「经典应用」。我们可以将整个数组「按照大小」平分成两部分(如果不能平分,那就让较小部分的元素多一个)较小的部分称为左侧部分,较大的部分称为右侧部分:

  • 将左侧部分放入「大根堆」中,然后将右侧元素放入「小根堆入中;
  • 这样就能在 O(1)的时间内拿到中间的一个数或者两个数,进而求的平均数。

 于是问题就变成了「如何将一个一个从数据流中过来的数据,动态调整到大根堆或者小根堆中,并且保证两个堆的元素一致,或者左侧堆的元素比右侧堆的元素多一个」
为了方便叙述,将左侧的「大根堆」记为 left ,右侧的「小根堆」记为 right ,数据流中来的
「数据」记为 x。
其实,就是一个「分类讨论」的过程:
1. 如果左右堆的「数量相同」,left.size()== right.size():
a.如果两个堆都是空的,直接将数据x放入到 left中;
b.如果两个堆非空
        i.如果元素要放入左侧,也就是x<= left.top():那就直接放,因为不会影响我们制定的规则;
        ii.如果要放入右侧
                可以先将 x放入 right 中,
                然后把 right 的堆顶元素放入 left中;

2.如果左右堆的数量「不相同」,那就是 left.size()>right.size():
a.这个时候我们关心的是 x是否会放入 left 中,导致 left 变得过多:
        i.如果 x放入 right 中,也就是x>= right.top(),直接放;
        ii.反之,就是需要放入 left 中:
                可以先将 x放入 left 中,
                然后把 left 的堆顶元素放入 right中
只要每一个新来的元素按照「上述规则」执行,就能保证 left 中放着整个数组排序后的「左半部分」,right 中放着整个数组排序后的「右半部分」,就能在 0(1)的时间内求出平均数。

class MedianFinder {
    priority_queue<int> left;//大根堆
    priority_queue<int,vector<int>,greater<int>> right;//小根堆
public:
    MedianFinder()
    {}
    
    void addNum(int num) 
    {
        int m=left.size();
        int n=right.size();
        //分类讨论
        if(m==n)
        {
            if(m==0)//为空直接放入左边大根堆
            {
                left.push(num);
            }
            else
            {
                if(num<=left.top())
                {
                    left.push(num);
                }
                else
                {
                    right.push(num);
                    left.push(right.top());
                    right.pop();//保证左边数量大于等于右边
                }
            }
        }
        else
        {
            if(num<=left.top())
            {
                left.push(num);
                right.push(left.top());
                left.pop();
            }
            else
            {
                right.push(num);
            }
        }
    }
    
    double findMedian()
    {
        if(left.size()>right.size())
            return left.top();
        else
        {
            return (left.top()+right.top())/2.0;
        }    
    }
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

综合练习--轮播图

本篇博客将教大家实现一个基础的轮播图。 源代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0&qu…

“AI玩手机”原理揭秘:大模型驱动的移动端GUI智能体

作者&#xff5c;郭源 前言 在后LLM时代&#xff0c;随着大语言模型和多模态大模型技术的日益成熟&#xff0c;AI技术的实际应用及其社会价值愈发受到重视。AI智能体&#xff08;AI Agent&#xff09;技术通过集成行为规划、记忆存储、工具调用等机制&#xff0c;为大模型装上…

光伏电站的智慧施工详解

光伏电站的智慧施工是利用先进的技术和管理方法&#xff0c;提高施工效率、质量和安全性&#xff0c;降低成本&#xff0c;实现光伏电站建设的智能化、数字化和绿色化。 下面从鹧鸪云智慧施工软件详细施工管理的步骤说起。 项目总览 包含我负责的项目、我参与的项目、我创建…

django——创建 Django 项目和 APP

2.创建 Django 项目和 APP 命令&#xff1a; 创建Django项目 django-admin startproject name 创建子应用 python manager.py startapp name 2.1 创建工程 在使用Flask框架时&#xff0c;项目工程目录的组织与创建是需要我们自己手动创建完成的。 在django中&#xff0c;…

李春葆《数据结构》-课后习题代码题

一&#xff1a;假设不带权有向图采用邻接矩阵 g 存储&#xff0c;设计实现以下功能的算法&#xff1a; &#xff08;1&#xff09;求出图中每个顶点的入度。 代码&#xff1a; void indegree(MatGraph g){int i,j,n;printf("各个顶点的入度&#xff1a;\n");for(i…

wsl安装

一. wsl简介 1. wsl和wsl2的区别 wsl需要把linux命令翻译为windows命令&#xff0c;性能差一些。 wsl2直接使用linux内核&#xff0c;不需要翻译&#xff0c;性能好&#xff0c;但开销相对大一点&#xff0c;因为需要多运行一个hyper-v虚拟机 (并非完整的虚拟机&#xff0c;是…

Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大数据篇正在更新&#xff01;https://blog.csdn.net/w776341482/category_12713819.html 目前已经更新到了&#xff1a; MyBatis&#xff…

GPT中转站技术架构

本文介绍阿波罗AI中转站&#xff08;https://api.ablai.top/&#xff09;的技术架构&#xff0c;该中转API的技术架构采用了分布式架构、智能调度和API中转等技术&#xff0c;确保了全球范围内的高效访问和稳定运行。以下是对该技术架构的详细分析&#xff1a; 分布式架构 分…

远程服务器Docker使用本地代理加速访问外部资源

Docker在pull镜像的时候非常缓慢&#xff0c;但是远程主机没有安装代理&#xff0c;就很为难&#xff0c;现在分享一个可以让远程服务器使用本地代理加速的方法 配置Docker代理 新建文件夹 mkdir -p /etc/systemd/system/docker.service.d 切换到这个文件夹里 cd /etc/system…

【详解】树链剖分之重链剖分

终于搞懂了树链剖分的一些皮毛了…… 树链剖分 “树链剖分”&#xff0c;顾名思义&#xff0c;就是把一棵树剖分成一条条的链…… 重链剖分 重链剖分的基本概念 重链剖分是树链剖分的一种&#xff0c;它会把树剖分成一条条重链…… 什么是重链呢&#xff1f; 重链就是连接…

RocketMQ: 部署结构与存储特点

RocketMQ 是什么 它是一个队列模型的消息中间件&#xff0c;具有高性能、高可靠、高实时、分布式特点 Producer、Consumer、队列都可以分布式Producer 向一些队列轮流发送消息 队列集合称为 TopicConsumer 如果做广播消费则一个 consumer 实例消费这个 Topic 对应的所有队列如果…

帮助中心FAQ系统:打造卓越客户服务体验的关键驱动力

在当今这个信息爆炸的时代&#xff0c;企业为了保持市场竞争力&#xff0c;必须不断提升客户服务体验。FAQ&#xff08;常见问题解答&#xff09;系统&#xff0c;作为一种高效且便捷的用户服务工具&#xff0c;正日益受到企业的青睐。本文将阐述FAQ系统的核心价值、功能特性以…

如何使用 Python 开发一个简单的文本数据转换为 Excel 工具

目录 一、准备工作 二、理解文本数据格式 三、开发文本数据转换为Excel工具 读取CSV文件 将DataFrame写入Excel文件 处理其他格式的文本数据 读取纯文本文件&#xff1a; 读取TSV文件&#xff1a; 四、完整代码与工具封装 五、使用工具 六、总结 在数据分析和处理的…

Elasticsearch向量搜索:从语义搜索到图搜图只有一步之遥

续 上集说到语义搜索&#xff0c;这集接着玩一下图搜图&#xff0c;这种场景在电商中很常见——拍照搜商品。图搜图实现非常类似语义搜索&#xff0c;代码逻辑结构都很类似… 开搞 还是老地方modelscope找个Vision Transformer模型&#xff0c;这里选用vit-base-patch16-224…

Flink【基于时间的双流联结 Demo】

前言 1、基于时间的双流联结&#xff08;Join&#xff09; 对于两条流的合并&#xff0c;很多情况我们并不是简单地将所有数据放在一起&#xff0c;而是希望根据某个字段的值将它们联结起来&#xff0c;“配对”去做处理。例如用传感器监控火情时&#xff0c;我们需要将大量温度…

大数据入门-什么是Flink

这里简单介绍Flink的概念、架构、特性等。至于比较详细的介绍&#xff0c;会单独针对这个组件进行详细介绍&#xff0c;可以关注博客后续阅读。 一、概念 Apache Flink 是一个框架和分布式处理引擎&#xff0c;用于在无边界和有边界数据流上进行有状态的计算。 Flink的四大基…

KubeVirt下gpu operator实践(GPU直通)

KubeVirt下gpu operator实践(GPU直通) 参考《在 KubeVirt 中使用 GPU Operator》&#xff0c;记录gpu operator在KubeVirt下实践的过程&#xff0c;包括虚拟机配置GPU直通&#xff0c;容器挂载GPU设备等。 KubeVirt 提供了一种将主机设备分配给虚拟机的机制。该机制具有通用性…

How to update the content of one column in Mysql

How to update the content of one column in Mysql by another column name? UPDATE egg.eggs_record SET sold 2024-11-21 WHERE id 3 OR id 4;UPDATE egg.eggs_record SET egg_name duck egg WHERE id 2;

【K8S系列】imagePullSecrets配置正确,但docker pull仍然失败,进一步排查详细步骤

如果 imagePullSecrets 配置正确,但在执行 docker pull 命令时仍然失败,可能存在以下几种原因。以下是详细的排查步骤和解决方案。 1. 检查 Docker 登录凭证 确保你使用的是与 imagePullSecrets 中相同的凭证进行 Docker 登录: 1.1 直接登录 在命令行中,执行以下命令: …

机器学习基础06

目录 1.梯度下降 1.1梯度下降概念 1.2梯度下降公式 1.3学习率 1.4实现梯度下降 1.5API 1.5.1随机梯度下降SGD 1.5.2小批量梯度下降MBGD 1.6梯度下降优化 2.欠拟合过拟合 2.1欠拟合 2.2过拟合 2.3正则化 2.3.1L1正则项&#xff08;曼哈顿距离&#xff09; 2.3.2…