欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

9.Java中PriorityQueue详解及堆相关的算法题

程序员文章站 2022-03-15 22:04:48
...

Java中PriorityQueue详解

Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示。PriorityQueue其实是一种特殊的队列,即优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次取最小元素)。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator,类似于C++的仿函数)。

Java中PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。

二、常用方法

peek()//返回队首元素
poll()//返回队首元素,队首元素出队列
add()//添加元素
size()//返回队列元素个数
isEmpty()//判断队列是否为空,为空返回true,不空返回false
offer(E e) //offer()方法用于将给定元素(e)添加到此PriorityQueue中。 
new PriorityQueue();//默认构造函数,构建一个小顶堆
new PriorityQueue(Comparator.reverseOrder());//构建一个大顶堆

算法题215: 数组中的第K个最大元素

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

思路:利用PriorityQueue构建一个大顶堆,则堆顶的永远是最大的。将前k-1个数移出,第k个数就是数组中第 k 个最大的元素。

public int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>(Comparator.reverseOrder());//自定义比较器,降序排列
    for (int i:nums)
        priorityQueue.add(i);
    while (-- k > 0)
        priorityQueue.poll();
    return priorityQueue.peek();
}

算法692. 前K个高频单词

给一非空的单词列表,返回前 k 个出现次数最多的单词。

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

示例 1:

输入: [“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 次。

注意:

假定 k 总为有效值, 1 ≤ k ≤ 集合元素数。
输入的单词均由小写字母组成。

扩展练习:

尝试以 O(nlogk) 时间复杂度和 O(n) 空间复杂度解决。

方法一:排序

算法:
计算每个单词的频率,并使用使用这些频率的自定义排序关系对单词进行排序。然后取前 k。

class Solution {
    public List<String> topKFrequent(String[] words, int k) {
        Map<String, Integer> count = new HashMap();
        for (String word: words) {
            count.put(word, count.getOrDefault(word, 0) + 1);
        }
        List<String> candidates = new ArrayList(count.keySet());
        Collections.sort(candidates, (w1, w2) -> count.get(w1).equals(count.get(w2)) ?
                w1.compareTo(w2) : count.get(w2) - count.get(w1));

        return candidates.subList(0, k);

复杂度分析

时间复杂度:O(Nlog⁡N)。其中 N 是 words 的长度。我们用 O(N) 时间计算每个单词的频率,然后用 O(Nlog⁡N) 时间对给定的单词进行排序。
空间复杂度:O(N),用来存放候答案的地方

方法二:堆

算法:

计算每个单词的频率,然后将其添加到存储到大小为 k 的小根堆中。它将频率最小的候选项放在堆的顶部。最后,我们从堆中弹出最多 k 次,并反转结果,就可以得到前 k 个高频单词。

class Solution {
    public List<String> topKFrequent(String[] words, int k) {
        Map<String, Integer> count = new HashMap();
        for (String word: words) {
            count.put(word, count.getOrDefault(word, 0) + 1);
        }
        PriorityQueue<String> heap = new PriorityQueue<String>(
                (w1, w2) -> count.get(w1).equals(count.get(w2)) ?
                w2.compareTo(w1) : count.get(w1) - count.get(w2) );

        for (String word: count.keySet()) {
            heap.offer(word);
            if (heap.size() > k) heap.poll();
        }

        List<String> ans = new ArrayList();
        while (!heap.isEmpty()) ans.add(heap.poll());
        Collections.reverse(ans);
        return ans;
    }
}

复杂度分析

时间复杂度: O(Nlog⁡k)。其中 N 是 words 的长度。我们用 O(N) 的时间计算每个单词的频率,然后将 N 个单词添加到堆中,添加每个单词的时间为 O(log⁡k) 。最后,我们从堆中弹出最多 k 次。因为 k≤N 的值,总共是 O(Nlog⁡k)。
空间复杂度:O(N),用于存储我们计数的空间