给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
link
public class TopKFrequent {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
//最小堆,lambda表达式 等同于下方的重写Comparator接口的compare方法
PriorityQueue<Integer> heap = new PriorityQueue<Integer>((n1, n2) -> map.get(n1) - map.get(n2));
// PriorityQueue pq = new PriorityQueue<>(new Comparator() {
// @Override
// public int compare(Integer n1, Integer n2) {
// return map.get(n1) - map.get(n2);
// }
// });
for(int i : nums){
map.put(i, map.getOrDefault(i, 0) + 1);
}
//每次加入一个元素到堆中,会先放在堆的底部,然后向上筛选(siftUpUsingComparator)
//如果它的父节点比它的优先级低,则替换两个节点,直到父节点不再比它的优先级低
//这里我使用的是最小堆,也就是说数字越小,优先级越高,会被放到堆顶点。 达到K个数之后,就会每次移除堆顶元素。
//也就是移除那些小的元素
for(int n : map.keySet()){
heap.add(n);
if(heap.size() > k)
heap.poll();
}
int[] res = new int[k];
int j = 0;
while (!heap.isEmpty()){
res[j++] = heap.poll();
}
return res;
}
}
起初我并不了解 到底怎样创建是最大堆 怎样是最小堆; 对于优先级队列太久没用了,加上以前也是囫囵吞枣,今天才发现自己并没有实际了解清楚
今天看了下源码,才算分清楚了
这是优先级队列的构造函数
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
可以看到有个初始容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
所以优先级队列创建时如果没有指定容量,默认就是11
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
而且看到其实queue底层就是一个Object数组实现的。
那是如何保证优先级队列以什么样的标准维护插入元素之后依然会保持整个队列的优先级顺序呢
我们去看 offer方法 (add方法底层就是调用的offer方法)
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
这里可以看到插入的步骤
/**
* @param k the position to fill
* @param x the item to insert
*/
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer n1, Integer n2) {
return map.get(n1) - map.get(n2);
}
});
我们自定义的比较方法是 第一个元素与第二个元素的值进行减法运算。返回的是差值。
相当于 如果第一个元素 >= 第二个元素 那么返回值就 >= 0。那么第一个元素大于第二个元素返回值 > 0 按照里面的逻辑就会发现:
插入元素的值比父节点的元素大,就不会进行向上筛选;
如果比父节点小小,插入元素就会赋给父节点,继续往上筛选。
那么很明显,就是小的元素会往堆顶走,大的元素会沉淀在堆底。
这就是最小堆。
相反,只需要把比较方法调换一下,就变成了最大堆。
PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer n1, Integer n2) {
return map.get(n2) - map.get(n1);
}
});
这就是最大堆
然后现在来看Leecode这道题。是需要返回频率最高的前K个数,那么最小堆很合适。
我们只需要创建一个最小堆,一直往里面丢元素进去。
等优先级队列的size达到K之后,就每次都删除一个堆顶元素出来。
因为删除的都是最小的元素,所以最后优先级队列里面存放的肯定是出现频率最高的前K个数。