目录
java中的堆——优先队列
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 ≤ 数组的长度。
要找到第k个最大的元素,首先想到的就是对数组进行排序,然后输出第k个值。这样本质上是使用的快排,时间复杂度为O(ogn)
控件复杂度为O(1)。
另外,也可以考虑使用容量为k的小根堆,并且将所有数组元素依次进堆。最后堆顶元素就是所求的第k大的元素,整个堆中的元素就是前K个大的元素。
值得一提的是,在JAVA中堆的实现是通过PriorityQueue优先队列来实现的。
解题代码:
class Solution {
public int findKthLargest(int[] nums, int k) {
// 定义小根堆
PriorityQueue<Integer> heap = new PriorityQueue<Integer>((n1, n2) -> n1 - n2);
// 控制堆的容量为k
for (int n: nums) {
heap.add(n);
if (heap.size() > k)
heap.poll();
}
// 输出
return heap.poll();
}
}
PriorityQueue的特点
- PriorityQueue 是基于优先级堆的无限优先级队列。
- PriorityQueue是一种无界的,线程不安全的队列。
- PriorityQueue通过数组实现的。
- 优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。
- 不允许使用 null 元素也不允许插入不可比较的对象(没有实现Comparable接口的对象)。
- 队列的头部是相对于指定顺序的最小元素。 如果多个元素被绑定到最小值,那么头就是这些元素之一。
- 优先级队列是无限制的,但是具有管理用于在队列上存储元素的数组的大小的内部容量 。 它始终至少与队列大小一样大。 当元素被添加到优先级队列中时,其容量会自动增长。
PriorityQueue优先级规则可以由我们根据具体需求而定制, 方式有2种:
- 添加元素自身实现了Comparable接口,确保元素是可排序的对象。
- 如果添加元素没有实现Comparable接口,可以在创建PriorityQueue队列时直接指定比较器。
eg:
public class Test {
public static void main(String[] args) {
//通过改造器指定排序规则
PriorityQueue<Student> q = new PriorityQueue<Student>(new Comparator<Student>() {
public int compare(Student o1, Student o2) {
//按照分数低到高,分数相等按名字
if(o1.getScore() == o2.getScore()){
return o1.getName().compareTo(o2.getName());
}
return o1.getScore() - o2.getScore();
}
});
//入列
q.offer(new Student("dafei", 20));
q.offer(new Student("will", 17));
q.offer(new Student("setf", 30));
q.offer(new Student("bunny", 20));
//出列
System.out.println(q.poll()); //Student{name='will', score=17}
System.out.println(q.poll()); //Student{name='bunny', score=20}
System.out.println(q.poll()); //Student{name='dafei', score=20}
System.out.println(q.poll()); //Student{name='setf', score=30}
}
}
源码解析
1. 默认属性
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
transient Object[] queue; //队列容器, 默认是11
private int size = 0; //队列长度
private final Comparator<? super E> comparator; //队列比较器, 为null使用自然排序
//....
}
2.入堆操作
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;
}
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x); //指定比较器
else
siftUpComparable(k, x); //没有指定比较器,使用默认的自然比较器
}
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
从源码上看PriorityQueue的入列操作并没对所有加入的元素进行优先级排序。仅仅保证数组第一个元素是最小。
3.出堆操作
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x); //指定比较器
else
siftDownComparable(k, x); //默认比较器
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
从源码可知,当第一个元素出堆之后,对剩下的元素进行再排序,挑选出最小的元素排在数组第一个位置,相当每次只排出最小的那个。
另外从出堆和进堆的操作可以看出来,PriorityQueue是线程不安全的,因为它的入堆和出堆都没有加锁。
PriorityQueue常用方法
Modifier and Type | Method and Description |
---|---|
boolean | add(E e) 将指定的元素插入到此优先级队列中。 |
void | clear() 从此优先级队列中删除所有元素。 |
Comparator<? super E> | comparator() 返回用于为了在这个队列中的元素,或比较 |
boolean | contains(Object o) 如果此队列包含指定的元素,则返回 |
Iterator<E> | iterator() 返回此队列中的元素的迭代器。 |
boolean | offer(E e) 将指定的元素插入到此优先级队列中。 |
E | peek() 检索但不删除此队列的头,如果此队列为空,则返回 |
E | poll() 检索并删除此队列的头,如果此队列为空,则返回 |
boolean | remove(Object o) 从该队列中删除指定元素的单个实例(如果存在)。 |
int | size() 返回此集合中的元素数。 |
Spliterator<E> | spliterator() 在此队列中的元素上创建late-binding和失败快速 |
Object[] | toArray() 返回一个包含此队列中所有元素的数组。 |
<T> T[] | toArray(T[] a) 返回一个包含此队列中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。 |