1、优先队列的经典问题,在1000000个元素中选出前100名元素,题型模式如在N个元素中选出前M个元素。
在这里面的关键就是M远远小于N的,如果M是1,是很简单的,只需要遍历一遍,此时时间复杂度是O(n)级别的,但是此时要选出前M个元素,如果M不等于1的话,就有点麻烦了,此时也可以将100万个元素进行一下排序,对于100万个元素,使用归并排序还是快速排序,都可以在O(NlogN)的时间复杂度内完成任务,排序之后可以直接取出前一百名元素。
但是如果使用优先队列的话,可以在O(NlogM)的时间复杂度内解决问题。此时O(NlogM)比O(NlogN)时间复杂度更好的。
如何使用优先队列解决这个问题呢,可以使用优先队列,维护当前看到的前M个元素。对于一百万个元素,也就是对于这N个元素,肯定是要从头到尾扫描一遍的,在扫描的过程中,我们首相将这N个元素中的前M个元素放进优先队列里面,之后每次看到一个新的元素,如果这个新的元素比当前的这个优先队列中最小的那个元素还要大的话,那么,我们就将这个优先队列中最小的那个元素扔出去,取而代之的换上我们这个新的元素,用这样的方式,相当于这个优先队列中一直维护着当前可以看到的前M个元素,直到我们将这N个元素全部扫描完,在优先队列中最终留下了这M个元素就是我们要求得这个结果。
需要注意的是这里虽然要选出前M个元素,前M个元素默认是前M个最大的元素,但是实际上需要的是一个最小堆,我们要能够非常快速的取出当前看到的前M个元素中的最小的那个元素,我们不断的将当前可以看到的前M大的元素中那个最小的元素进行替换,此时,这里需要一个最小堆,但是呢,实际上,解决这个问题,并不需要真的使用一个最小堆的,这里依然使用最大堆。
这里面的关键就是,什么是优先级,并不是规定越大的元素优先级越高的,事实上,在这个例子中,由于每次都要先取出优先队列中最小的那个元素,所以,实质上,这里完全可以自己去定义,元素的值越小它的优先级越高,在这样的一个定义下,依然可以使用优先队列的底层是一个最大堆,却依旧完成了这个问题。大和小其实是相对的。
2、给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
1 输入: nums = [1,1,1,2,2,3], k = 2 2 输出: [1,2]
示例 2:
1 输入: nums = [1], k = 1 2 输出: [1]
说明:
- 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
- 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
3、自己设计的优先队列,如下所示:
1 package com.queue; 2 3 import com.maxHeap.MaxHeap; 4 5 /** 6 * 优先队列的底层实现可以使用最大堆进行实现, 7 * 由于优先队列本身就是一个队列,所以可以复用队列的接口。 8 * 9 * @param <E> 由于优先队列要具有可比较性,所以要进行继承Comparable 10 */ 11 public class PriorityQueue<E extends Comparable<E>> implements Queue<E> { 12 13 // 使用最大堆实现优先队列 14 private MaxHeap<E> maxHeap; 15 16 /** 17 * 无参构造函数,直接创建一个MaxHeap即可 18 */ 19 public PriorityQueue() { 20 maxHeap = new MaxHeap<>(); 21 } 22 23 @Override 24 public void enqueue(E e) { 25 // 入队操作,直接调用最大堆的add方法 26 maxHeap.add(e); 27 } 28 29 @Override 30 public E dequeue() { 31 // 出队操作,将最大元素提取出来 32 return maxHeap.extractMax(); 33 } 34 35 @Override 36 public E getFront() { 37 // 查看队首的元素是谁 38 // 其实对应的是最大堆对应的堆顶的元素 39 E maxHeapMax = maxHeap.findMax(); 40 // 获取到队列中队首的元素。对应栈的查看栈顶的元素。 41 return maxHeapMax; 42 } 43 44 @Override 45 public int getSize() { 46 // 直接返回最大堆中的大小 47 return maxHeap.size(); 48 } 49 50 @Override 51 public boolean isEmpty() { 52 // 直接返回最大堆中的是否为空 53 return maxHeap.isEmpty(); 54 } 55 56 57 public static void main(String[] args) { 58 PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(); 59 // 优先队列入队操作 60 for (int i = 0; i < 1000; i++) { 61 priorityQueue.enqueue(i); 62 } 63 64 65 // 优先队列出队操作 66 for (int i = 0; i < 1000; i++) { 67 if (i % 30 == 0) { 68 System.out.println(); 69 } 70 System.out.print(priorityQueue.dequeue() + " "); 71 } 72 73 74 } 75 76 }
4、解决力扣问题的代码,如下所示:
1 package com.leetcode; 2 3 import com.queue.PriorityQueue; 4 5 import java.util.*; 6 7 /** 8 * 给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 9 */ 10 public class HighFrequency { 11 12 /** 13 * 私有内部类,频次类 14 * <p> 15 * <p> 16 * 优先队列的泛型元素是要具有可比较性的,所以实现可比较接口 17 */ 18 private class Frequency implements Comparable<Frequency> { 19 public int e;// 元素内容 20 public int frequency;// 元素的频次 21 22 /** 23 * 含参构造函数 24 * 25 * @param e 26 * @param frequency 27 */ 28 public Frequency(int e, int frequency) { 29 this.e = e; 30 this.frequency = frequency; 31 } 32 33 /** 34 * 重写比较的方法 35 * <p> 36 * <p> 37 * 将当前类对象和另外一个频次的类对象进行比较 38 * 39 * @param another 40 * @return 41 */ 42 @Override 43 public int compareTo(Frequency another) { 44 // 对于优先队列,要非常容易的取出频次较低的那个元素 45 // 这里可以定义优先级,可以定义什么是优先级高 46 // 这里定义,频次越低优先级越高 47 if (this.frequency < another.frequency) { 48 // Java的compareTo方法,当前元素比传进来的元素大返回1,小的话,返回-1,等于的话返回0; 49 50 // 而此处的设计是,当前元素的频次小于传进来的元素,就是当前元素频次低的话,返回1, 51 // 这里定义的优先级高的意思,是频次低的优先级高, 52 // 对于优先队列,底层虽然是最大堆,取出优先级高的那个元素,但是这个优先级最高的这个元素是频次最低的那个元素。 53 return 1; 54 } else if (this.frequency > another.frequency) { 55 // 如果当前元素大于传进去的元素,此时是返回-1,就是优先级低的 56 return -1; 57 } else { 58 // 如果当前元素相等传进去的元素此时是返回0 59 return 0; 60 } 61 } 62 } 63 64 /** 65 * 给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 66 * 67 * @param nums 非空数组 68 * @param k 前k高的元素 69 * @return 将前k高的集合返回 70 */ 71 public List<Integer> topKFrequent(int[] nums, int k) { 72 // 创建一个map对象用于统计数组元素的频数 73 Map<Integer, Integer> map = new TreeMap<Integer, Integer>(); 74 // 循环遍历数组 75 for (int num : nums) { 76 // 如果数组元素已经存储在map集合中 77 if (map.containsKey(num)) { 78 // 此时将num的value值加一。 79 map.put(num, map.get(num) + 1); 80 } else { 81 map.put(num, 1); 82 } 83 } 84 85 // 此时Map集合中保存了数组中每一个元素以及每一个元素的频数 86 // 此时,求出,前k个频数最高的元素 87 // 由于此时,优先队列承载的元素类型是什么类型呢,此时map是键值对的形式 88 // 要依据元素所对应的频类来决定优先级,与此同时,也关心频率对应的元素是谁 89 // 最后,要将这些元素返回。 90 PriorityQueue<Frequency> priorityQueue = new PriorityQueue<Frequency>(); 91 // 遍历map集合的key 92 for (int key : map.keySet()) { 93 // 如果优先队列的元素数是小于k的,此时还没有存够k个元素,直接进行入队 94 if (priorityQueue.getSize() < k) { 95 // 直接对于key进行入队操作 96 priorityQueue.enqueue(new Frequency(key, map.get(k))); 97 98 99 // 队首的元素就是对于优先队列来说,优先级最高的那个元素, 100 // 在这里的优先级定义下,优先级最高的就是频次最低的那个元素, 101 } else if (map.get(key) > priorityQueue.getFront().frequency) { 102 // 此时,优先队列里面已经有k个元素了,是我们当前看到的前k个频次最高的元素, 103 // 此时,新遍历的一个key,此时这个key的频次可能比当前这前k个频次最高的元素中 104 // 那个频次最小的那个元素的频次更高。这种情况下,此时应该将优先队列中频次最小的那个元素替换掉。 105 106 // 此时,让队首元素出队 107 priorityQueue.dequeue(); 108 // 然后让新的元素入队, 109 priorityQueue.enqueue(new Frequency(k, map.get(key))); 110 } 111 112 } 113 114 // 循环遍历结束,优先队列里面剩下的就是频次最高的前K个元素。 115 // 这样的算法,时间复杂度的O(nlogk) 116 List<Integer> list = new LinkedList<Integer>(); 117 // for (int i = 0; i < priorityQueue.getSize(); i++) { 118 // list.add(priorityQueue.dequeue().e); 119 // } 120 121 while (!priorityQueue.isEmpty()) { 122 list.add(priorityQueue.dequeue().e); 123 } 124 // 将符合条件的元素返回即可。 125 return list; 126 } 127 128 public static void main(String[] args) { 129 int[] nums = new int[]{1, 1, 1, 2, 2, 3}; 130 int k = 3; 131 HighFrequency highFrequency = new HighFrequency(); 132 List<Integer> list = highFrequency.topKFrequent(nums, k); 133 for (int i = 0; i < list.size(); i++) { 134 System.out.println(list.get(i)); 135 } 136 } 137 138 }