PriorityQueue 实现的是 Queue 接口 ,可以使用 Queue 提供的方法,以及自带的方法。
1、PriorityQueue概述
Java PriorityQueue 实现了 Queue 接口,不允许放入 null 元素;其通过堆实现,具体说是
通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不
大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue 的底层实现,
数组初始大小为11;也可以用一棵完全二叉树表示。
优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次
取最小元素,C++的优先队列每次取最大元素)。这里牵涉到了大小关系,元素大小的评判
可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较
(Comparator,类似于C++的仿函数)。
2、常用方法总结
public boolean add(E e); //在队尾插入元素,插入失败时抛出异常,并调整堆结构
public boolean offer(E e); //在队尾插入元素,插入失败时抛出false,并调整堆结构
public E remove(); //获取队头元素并删除,并返回,失败时前者抛出异常,再调整堆结构
public E poll(); //获取队头元素并删除,并返回,失败时前者抛出null,再调整堆结构
public E element(); //返回队头元素(不删除),失败时前者抛出异常
public E peek();//返回队头元素(不删除),失败时前者抛出null
public boolean isEmpty(); //判断队列是否为空
public int size(); //获取队列中元素个数
public void clear(); //清空队列
public boolean contains(Object o); //判断队列中是否包含指定元素(从队头到队尾遍历)
public Iterator<E> iterator(); //迭代器
3、堆结构调整
每次插入或删除元素后,都对队列进行调整,使得队列始终构成最小堆(或最大堆)。
具体调整如下:
-
插入元素后,从堆底到堆顶调整堆;
-
删除元素后,将队尾元素复制到队头,并从堆顶到堆底调整堆。
小根堆结构调整
插入( add()和offer()方法 )元素后,向上调整堆:
//siftUp()
private void siftUp(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)//调用比较器的比较方法
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
删除( remove()和poll()方法 )元素后,向下调整堆:
//siftDown()
private void siftDown(int k, E x) {
int half = size >>> 1;
while (k < half) {
//首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标
int child = (k << 1) + 1;//leftNo = parentNo*2+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;//然后用c取代原来的值
k = child;
}
queue[k] = x;
}
4、应用:topK问题
topK问题是指:从海量数据中寻找最大的前k个数据,比如从1亿个数据中,寻找最大的1万个数。
使用优先队列,能够很好的解决这个问题。先使用前1万个数构建最小优先队列,以后每取
一个数,都与队头元素进行比较,若大于队头元素,就将队头元素删除,并将该元素添加到
优先队列中;若小于队头元素,则将该元素丢弃掉。如此往复,直至所有元素都访问完。最
后优先队列中的1万个元素就是最大的1万个元素。
例1: 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。你需要找的是数组
排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4 输出: 4
public class Main {
public static void main(String[] args) {
//int[] arr = new int[]{3,2,1,5,6,4};
int[] arr = new int[]{3,2,3,1,2,4,5,5,6};
int k = 4;
test(arr,k);
}
/**
* 方法三:PriorityQueue 优先队列思想
* 返回数组第k个最大数据
* @param nums
* @param k
*/
public static int findKthLargest3(int[] nums, int k){ //时间复杂度:O(NlogK),遍历数据 O(N),堆内元素调整 O(K),空间复杂度:O(K)
int len = nums.length;
// 使用一个含有 k 个元素的最小堆
// k 堆的初始容量,(a,b) -> a -b 比较器
PriorityQueue<Integer> minTop = new PriorityQueue<>(k,(a, b) -> a -b);
for (int i = 0; i < k; i++){
minTop.add(nums[i]);
}
for (int i = k; i < len; i++){
Integer topEle = minTop.peek(); // 返回队头元素(不删除),失败时前者抛出null
// 只要当前遍历的元素比堆顶元素大,堆顶弹出,遍历的元素进去
if (nums[i] > topEle){
minTop.poll(); // 获取队头元素并删除,并返回,失败时前者抛出null,再调整堆结构
minTop.offer(nums[i]); // 在队尾插入元素,插入失败时抛出false,并调整堆结构
}
}
return minTop.peek();
}
public static void test(int[] input,int key){
long begin = System.currentTimeMillis();
int result = findKthLargest3(input,key);
System.out.println("result = " + result);
long end = System.currentTimeMillis();
System.out.println();
System.out.println("耗时:" + (end - begin) + "ms");
System.out.println("--------------------");
}
}
运行结果:
例2:
求 数组nums={6,14,71,21,9,18,1,4,23,0,45,66} 中最大的5个数
import java.util.PriorityQueue;
public class Main {
static int[] nums={6,14,71,21,9,18,7,4,23,0,45,66};
public static void main(String[] args) {
topK();
}
public static void topK() {
PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
for(int i=0; i<5; i++) {
queue.add(nums[i]);
}
for(int i=5; i<12; i++) {
if(nums[i] > queue.element()) {
queue.remove();
queue.add(nums[i]);
}
}
while(!queue.isEmpty()) {
int Ele=queue.remove();
System.out.print(Ele+" ");
}
}
}
运行结果: