【堆】《深入剖析优先级队列(堆):数据结构与算法的高效搭档》

news2025/4/7 21:41:07

文章目录

  • 前言
  • 例题
    • 一、最后一块石头的重量
    • 二、数据流中的第 K 大元素
    • 三、前K个高频单词
    • 四、数据流的中位数
  • 结语


在这里插入图片描述

前言

什么是优先级队列算法呢?它的算法原理又该怎么解释?

优先级队列(堆)算法是一种特殊的数据结构和算法,它就像一个特殊的排队系统,在这个系统中,每个元素都有一个优先级,优先级高的元素会先被处理。下面用一个形象的例子来解释它的原理。
想象有一个幼儿园的小朋友们要排队领糖果。一般的排队是按照先来后到的顺序,但是在这个特殊的队列里,是按照小朋友的 “乖巧程度” 来决定谁先领糖果的,“乖巧程度” 就是我们说的优先级。
堆这种数据结构,就像是一个特殊的队伍形状,它通常是用一个完全二叉树来实现的。简单来说,完全二叉树就是一种很有规律的树状结构,它的每一层都是填满的,只有最下面一层可能从右到左缺少一些节点。
在这个 “小朋友排队领糖果” 的例子里,我们把每个小朋友看作是树中的一个节点,而小朋友的 “乖巧程度” 就是节点的值。在堆中,有两种常见的类型,一种是大根堆,一种是小根堆。大根堆的特点是每个节点的值都大于或等于它的子节点的值,就好像在这个排队系统中,每个位置上的小朋友都比他后面的小朋友更乖巧(或者一样乖巧)。小根堆则相反,每个节点的值都小于或等于它的子节点的值。
当有新的小朋友要加入排队时,我们会把他放在树的最后一个位置,然后再根据他的 “乖巧程度” 来调整他的位置,让他和他的 “家长”(父节点)比较。如果他比他的 “家长” 更乖巧,就会和 “家长” 交换位置,一直到找到他合适的位置,保证堆的特性不变。这就好比在排队时,来了一个新小朋友,我们要看看他是不是比前面的小朋友更乖巧,如果是,就让他往前排,直到他找到自己合适的位置。
当要给小朋友发糖果时,我们总是先从队首(也就是树的根节点)开始,因为那里是最乖巧的小朋友。然后,我们把最后一个位置的小朋友放到根节点的位置,再重新调整堆,让它再次满足堆的特性。这样,我们就可以保证每次发糖果都是先发给最乖巧的小朋友,而且整个排队系统(堆)始终保持有序。
通过这样的方式,优先级队列(堆)算法能够高效地处理元素的插入和删除操作,快速找到优先级最高(或最低)的元素,在很多需要根据元素优先级来进行操作的场景中都非常有用,比如任务调度、数据排序等。

下面本文将通过例题来为大家详细讲解有关优先级队列算法的算法问题

例题

一、最后一块石头的重量

  1. 题目链接:最后一块石头的重量
  2. 题目描述:

有一堆石头,每块石头的重量都是正整数。 每⼀回合,从中选出两块最重的石头,然后将它们⼀起粉碎。假设石头的重量分别为 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],这就是最后剩下那块石头的重量。 提示:
1 <= stones.length <= 30
1 <= stones[i] <= 1000

  1. 解法:(利用堆):
    算法思路: 其实就是⼀个模拟的过程:
    • 每次从石堆中拿出最大的元素以及次大的元素,然后将它们粉碎;
    • 如果还有剩余,就将剩余的⽯头继续放在原始的石堆里面
    重复上面的操作,直到石堆里面只剩下⼀个元素,或者没有元素(因为所有的石头可能全部抵消了) 那么主要的问题就是解决:
    • 如何顺利的拿出最⼤的石头以及次大的石头;
    • 并且将粉碎后的石头放入石堆中之后,也能快速找到下⼀轮粉碎的最大石头和次大石头; 这不正好可以利用堆的特性来实现嘛?
    • 我们可以创建⼀个大根堆;
    • 然后将所有的石头放入大根堆中;
    • 每次拿出前两个堆顶元素粉碎⼀下,如果还有剩余,就将剩余的石头继续放入堆中; 这样就能快速的模拟出这个过程。
  2. 代码示例:
  public int lastStoneWeight(int[] stones) {
        // 1. 创建⼀个⼤根堆
        PriorityQueue<Integer> heap = new PriorityQueue<>((a, b) -> b - a);
        // 2. 把所有的⽯头放进堆⾥⾯
        for (int x : stones) {
            heap.offer(x);
        }
        // 3. 模拟
        while (heap.size() > 1) {
            int a = heap.poll();
            int b = heap.poll();
            if (a > b) {
                heap.offer(a - b);
            }
        }
        return heap.isEmpty() ? 0 : heap.peek();
    }

二、数据流中的第 K 大元素

  1. 题目链接:数据流的第K大元素
  2. 题目描述:

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。 请实现KthLargest 类:
KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
示例:
输入:[“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); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 5
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8
提示:1 <= k <= 10^4
0 <= nums.length <= 10^4
-104 <= nums[i] <= 10^4
-104 <= val <= 10^4
最多调用 add 方法 10^4 次 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素

  1. 解法(优先级队列):
    算法思路: 我相信,大家看到 TopK 问题的时候,兄弟们应该能立马想到「堆」当然本题也是一样通过堆来解决的

  2. 代码示例:

  class KthLargest {
        // 创建⼀个⼤⼩为 k 的⼩根堆
        PriorityQueue<Integer> heap;
        int _k;

        public KthLargest(int k, int[] nums) {
            _k = k;
            heap = new PriorityQueue<>();
            for (int x : nums) {
                heap.offer(x);
                if (heap.size() > _k) {
                    heap.poll();
                }
            }
        }

        public int add(int val) {
            heap.offer(val);
            if (heap.size() > _k) {
                heap.poll();
            }
            return heap.peek();
        }
    }

三、前K个高频单词

  1. 题目链接:前K个高频单词
  2. 题目描述:

给定⼀个单词列表 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 次。
注意:1 <= words.length <= 500
1 <= words[i] <= 10
words[i] 由小写英文字母组成。 k 的取值范围是 [1, 不同 words[i] 的数量]
进阶:尝试以 O(n log k) 时间复杂度和 O(n) 空间复杂度解决

  1. 解法(堆):
    算法思路: • 稍微处理⼀下原数组:
    a. 我们需要知道每⼀个单词出现的频次,因此可以先使用哈希表,统计出每⼀个单词出现的频次;
    b. 然后在哈希表中,选出前 k大的单词(为什么不在原数组中选呢?因为原数组中存在重复的单词,哈希表里面没有重复单词,并且还有每⼀个单词出现的频次)
    • 如何使用堆,拿出前 k 大元素:
    a. 先定义⼀个自定义排序,我们需要的是前 k 大,因此需要⼀个小根堆。但是当两个字符串的频次相同的时候,我们需要的是字典序较小的,此时是⼀个大根堆的属性,在定义比较器的时候需要注意!
    ▪ 当两个字符串出现的频次不同的时候:需要的是基于频次比较的小根堆
    ▪ 当两个字符串出现的频次相同的时候:需要的是基于字典序比较的大根堆
    b. 定义好比较器之后,依次将哈希表中的字符串插人到堆中,维持堆中的元素不超过 k 个;
    c. 遍历完整个哈希表后,堆中的剩余元素就是前 k 大的元素

  2. 代码示例:

  public List<String> topKFrequent(String[] words, int k) {
        // 1. 统计⼀下每⼀个单词出现的频次
        Map<String, Integer> hash = new HashMap<>();
        for (String s : words) {
            hash.put(s, hash.getOrDefault(s, 0) + 1);
        }
        // 2. 创建⼀个⼤⼩为 k 的堆
        PriorityQueue<Pair<String, Integer>> heap = new PriorityQueue<>(a, b) ->{
			// 频次相同的时候,字典序按照 ⼤根堆的⽅式排列
            if (a.getValue().equals(b.getValue())) {
                return b.getKey().compareTo(a.getKey());
            }
               return a.getValue() - b.getValue();
             });
        // 3. TopK 的主逻辑
        for (Map.Entry<String, Integer> e : hash.entrySet()) {
            heap.offer(new Pair<>(e.getKey(), e.getValue()));
            if (heap.size() > k) {
                heap.poll();
            }
        }
        // 4. 提取结果
        List<String> ret = new ArrayList<>();
        while (!heap.isEmpty()) {
            ret.add(heap.poll().getKey());
        }
        // 逆序数组
        Collections.reverse(ret);
        return ret;
    }

四、数据流的中位数

  1. 题目链接:数据流的中位数
  2. 题目描述:

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
• 例如 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
提⽰:
-10^5 <= num <= 10^5
在调用findMedian 之前,数据结构中⾄少有⼀个元素 最多 5 * 10^4 次调用addNum 和 findMedian

  1. 解法(利用两个堆):
    算法思路: 这是⼀道关于「堆」这种数据结构的⼀个「经典应用」。
    我们可以将整个数组「按照大小」平分成两部分(如果不能平分,那就让较小部分的元素多⼀个),较小的部分称为左侧部分,较大的部分称为右侧部分:
    • 将左侧部分放入「大根堆」中,然后将右侧元素放入「小根堆」中;
    • 这样就能在 O(1) 的时间内拿到中间的⼀个数或者两个数,进而求的平均数。
    如下图所示:
    在这里插入图片描述
    于是问题就变成了「如何将⼀个⼀个从数据流中过来的数据,动态调整到大根堆或者小根堆中,并且保证两个堆的元素⼀致,或者左侧堆的元素比右侧堆的元素多⼀个」为了方便叙述,将左侧的「大根堆」记为 left ,右侧的「小根堆」记为 right ,数据流中来的 「数据」记为 x 。 其实,就是⼀个「分类讨论」的过程:
    如果左右堆的「数量相同」, left.size() == right.size() :
    a. 如果两个堆都是空的,直接将数据 x 放⼊到 left 中;
    b. 如果两个堆非空:
    i. 如果元素要放入左侧,也就是 x <= left.top() :那就直接放,因为不会影响我们制定的规则;
    ii. 如果要放入右侧
    • 可以先将 x 放入 right 中,
    • 然后把 right 的堆顶元素放入left 中 ;
    如果左右堆的数量「不相同」,那就是 left.size() > right.size() :
    a. 这个时候我们关心的是 x 是否会放⼊ left 中,导致 left 变得过多:
    i. 如果 x 放⼊ right 中,也就是 x >= right.top() ,直接放;
    ii. 反之,就是需要放入 left 中:
    • 可以先将 x 放⼊ left 中,
    • 然后把 left 的堆顶元素放入right 中 ; 只要每⼀个新来的元素按照「上述规则」执行,就能保证 left 中放着整个数组排序后的「左半部分」, right 中放着整个数组排序后的「右半部分」,就能在 O(1) 的时间内求出平均数。

  2. 代码示例:

  class MedianFinder {
        PriorityQueue<Integer> left;
        PriorityQueue<Integer> right;

        public MedianFinder() {
            left = new PriorityQueue<Integer>((a, b) -> b - a); // ⼤根堆
            right = new PriorityQueue<Integer>((a, b) -> a - b); // ⼩根堆
        }

        public void addNum(int num) {
            // 分情况讨论
            if (left.size() == right.size()) {
                if (left.isEmpty() || num <= left.peek()) {
                    left.offer(num);
                } else {
                    right.offer(num);
                    left.offer(right.poll());
                }
            } else {
                if (num <= left.peek()) {
                    left.offer(num);
                    right.offer(left.poll());
                } else {
                    right.offer(num);
                }
            }
        }

        public double findMedian() {
            if (left.size() == right.size()) return (left.peek() + right.peek()) /
                    2.0;
            else return left.peek();
        }
    }

结语

本文到这里就结束了,主要通过几道优先级队列相关例题介绍了数据结构堆(优先级队列)在算法中的应用,更深一步了解了优先级队列的用法。

以上就是本文全部内容,感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!

最后,大家再见!祝好!我们下期见!

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

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

相关文章

【CMOS输出缓冲器驱动强度】

一 、学习笔记 原始资料&#xff1a;https://www.ti.com.cn/cn/lit/an/zhcae18/zhcae18.pdf?ts1743589394832 Q1、电平转换芯片的其中一个关键指标是转换速率&#xff0c;转换速率跟什么因素有关系呢&#xff1f; 1、瞬态驱动强度 上升或下降时间用于评估瞬态驱动强度。需要…

【C++】Cplusplus进阶

模板的进阶&#xff1a; 非类型模板参数 是C模板中允许使用具体值&#xff08;而非类型&#xff09;作为模板参数的特性。它们必须是编译时常量&#xff0c;且类型仅限于整型、枚举、指针、引用。&#xff08;char也行&#xff09; STL标准库里面也使用了非类型的模板参数。 …

透明的卡组收费模式IC++

IC是信用卡处理商用来计算每笔交易相关费用的定价模型。与统一或混合定价相比&#xff0c;IC提供了额外的透明度。 作为企业主&#xff0c;了解IC定价的来龙去脉至关重要&#xff0c;以确定它是否对您的运营有意义。 什么是IC&#xff1f; IC或interchange plus是一种流行的定…

吾爱置顶软件,吊打电脑自带功能!

今天我给大家带来一款超棒的软件&#xff0c;它来自吾爱论坛的精选推荐&#xff0c;每一款都经过精心挑选&#xff0c;绝对好用&#xff01; S_Clock 桌面计时软件 这款软件的界面设计特别漂亮&#xff0c;简洁又大方。它是一款功能齐全的时钟计时倒计时软件&#xff0c;既能正…

使用MFC ActiveX开发KingScada控件(OCX)

最近有个需求&#xff0c;要在KingScada上面开发一个控件。 原来是用的WinCC&#xff0c;WinCC本身是支持调用.net控件&#xff0c;就是winform控件的&#xff0c;winform控件开发简单&#xff0c;相对功能也更丰富。奈何WinCC不是国产的。 话说KingScada&#xff0c;国产组态软…

【AI论文】CodeARC:评估归纳程序合成中大语言模型代理的推理能力基准

摘要&#xff1a;归纳程序合成&#xff0c;或称示例编程&#xff0c;要求从输入输出示例中合成能够泛化到未见输入的函数。尽管大型语言模型代理在自然语言指导下的编程任务中展现出了潜力&#xff0c;但它们在执行归纳程序合成方面的能力仍待深入探索。现有的评估协议依赖于静…

加密解密工具箱 - 专业的在线加密解密工具

加密解密工具箱 - 专业的在线加密解密工具 您可以通过以下地址访问该工具&#xff1a; https://toolxq.com/static/hub/secret/index.html 工具简介 加密解密工具箱是一个功能强大的在线加密解密工具&#xff0c;支持多种主流加密算法&#xff0c;包括 Base64、AES、RSA、DES…

抖音短视频安卓版流畅度测评 - 真实

对于抖音短视频安卓版的流畅度&#xff0c;实际体验可以受到多方面因素的影响&#xff0c;比如设备性能、系统优化、网络情况和应用本身的优化程度。以下是一些常见的测评维度和抖音安卓版本流畅度的实际表现&#xff1a; 1.启动速度 抖音的启动速度通常较快&#xff0c;但如果…

基于javaweb的SSM+Maven机房管理系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…

发动机试验台底座:汽车研发的关键支撑(北重制造厂家)

发动机试验台底座是汽车研发过程中的重要组成部分&#xff0c;它承载着发动机及相关部件&#xff0c;在试验过程中提供稳定的支撑。底座的设计和制造对于发动机试验的精度和可靠性至关重要&#xff0c;它需要具备足够的承载能力、稳定性和耐久性&#xff0c;以确保试验过程的准…

Linux红帽:RHCSA认证知识讲解(九)标准输入输出、重定向、过滤器与管道

Linux红帽&#xff1a;RHCSA认证知识讲解&#xff08;九&#xff09;标准输入输出、重定向、过滤器与管道 前言一、标准输入与输出、重定向&#xff0c;使用过滤器筛选文件信息1.1 Linux 的标准输入与输出1.2 什么是输入重定向1.3 输出重定向1.4 标准错误输出重定向1.5 使用过滤…

智慧园区大屏如何实现全局监测:监测意义、内容、方式

智慧园区的价值不容小觑呀&#xff0c;可以说园区的大部分数据都在这个大屏上&#xff0c;监测数据越多&#xff0c;那么大屏的价值就越大。很多小伙伴拿到需求后感觉无从下手&#xff0c;本文在这里智慧园区大屏可以监测哪些内容、监测的意义、监测的方式等&#xff0c;欢迎点…

LangChain核心解析:掌握AI开发的“链“式思维

0. 思维导图 1. 引言 🌟 在人工智能快速发展的今天,如何有效地利用大语言模型(LLM)构建强大的应用成为众多开发者关注的焦点。前面的课程中,我们学习了正则表达式以及向量数据库的相关知识,了解了如何处理文档并将其附加给大模型。本章我们将深入探讨LangChain中的核心概…

思维链编程模式下可视化医疗编程具体模块和流程架构分析(全架构与代码版)

引言 随着人工智能在医疗领域的广泛应用&#xff0c;医疗AI思维链可视化编程工具应运而生&#xff0c;旨在为非技术背景的医疗从业者提供便捷的AI模型开发平台。这个工具通过直观的可视化界面&#xff0c;简化了AI模型的构建过程&#xff0c;帮助用户高效完成数据处理、模型训…

AI与玩具结合的可行性分析

文章目录 一、市场需求&#xff1a;教育与陪伴的双重驱动&#xff08;一&#xff09;教育需求&#xff08;二&#xff09;情感陪伴需求&#xff08;三&#xff09;消费升级 二、技术发展&#xff1a;赋能玩具智能化&#xff08;一&#xff09;AI技术的成熟&#xff08;二&#…

软考又将迎来新的改革?

3月26日&#xff0c;工信部所属事业单位发布了一则招聘公告&#xff0c;其中&#xff0c;工信教考中心面相符合条件的博士招聘1名“考务处技术研究岗”的人员&#xff0c;具体岗位内容&#xff1a; 其岗位简介中&#xff0c;有一条“研究、制定考试技术改革方案&#xff0c;并组…

怎么让一台云IPPBX实现多家酒店相同分机号码一起使用

下面用到的IPPBX是我们二次开发后的成品&#xff0c;支持各种云服务器一键安装&#xff0c;已经写好了一键安装包&#xff0c;自动识别系统环境&#xff0c;安装教程这里就不再陈述了&#xff01; 前言需求 今天又遇到了一个客户咨询&#xff0c;关于部署一台云IPPBX&#xf…

蓝桥杯2024JavaB组的一道真题的解析

文章目录 1.问题描述2.问题描述3.思路分析4.代码分析 1.问题描述 这个是我很久之前写的一个题目&#xff0c;当时研究了这个题目好久&#xff0c;发布了一篇题解&#xff0c;后来很多人点赞&#xff0c;我都没有意识到这个问题的严重性&#xff0c;我甚至都在怀疑自己&#xf…

计算机视觉算法实战——基于YOLOv8的行人流量统计系统

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​​​ ​​​​​​​​​ ​​ 引言:智能客流分析的市场需求 在零售、交通、安防等领域,准确的行人流量统计对于商业决策、公共安全管理…

机器学习ML极简指南

机器学习是现代AI的核心&#xff0c;从推荐系统到自动驾驶&#xff0c;无处不在。但每个智能应用背后&#xff0c;都离不开那些奠基性的模型。本文用最简练的方式拆解核心机器学习模型&#xff0c;助你面试时对答如流&#xff0c;稳如老G。 线性回归 线性回归试图通过"最…