9.Java中PriorityQueue详解及堆相关的算法题
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(NlogN)。其中 N 是 words 的长度。我们用 O(N) 时间计算每个单词的频率,然后用 O(NlogN) 时间对给定的单词进行排序。
空间复杂度: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(Nlogk)。其中 N 是 words 的长度。我们用 O(N) 的时间计算每个单词的频率,然后将 N 个单词添加到堆中,添加每个单词的时间为 O(logk) 。最后,我们从堆中弹出最多 k 次。因为 k≤N 的值,总共是 O(Nlogk)。
空间复杂度:O(N),用于存储我们计数的空间