一、优先队列场景:
1、系统中动态选择优先级最高的任务执行
2、医院根据患者的患病情况,选择哪个患者最先做手术。
3、游戏中,士兵去攻击优先级最高的那个敌人。
二、优先队列底层数据结构复杂度对比

三、堆
1、二叉堆Binary Heap
使用二叉树表示的堆,二叉堆是一棵完全二叉树

完全二叉树: 把元素顺序排列成树的形状。
二叉堆的性质:
堆中某个节点的值总是不大于其父节点的值。
最大堆,父节点总是大于孩子节点值(相应的可以定义最小堆)
2、用数组存储二叉堆
数组索引从1开始存储


父亲节点和孩子节点的索引关系
parent(i) = i/2
left child(i) = 2 * i;
right child(i) = 2 * i +1
数组索引从0开始存储


父亲节点和孩子节点的索引关系
parent(i) = (i-1)/2
left child(i) = 2 * i + 1;
right child(i) = 2 * i +2
2.1 堆的基础表示
元素E extends Comparable<E>,说明元素是可以比较大小的。
public class MaxHeap<E extends Comparable<E>> {
private CustomArray<E> data;
private MaxHeap(int capacity){
data = new CustomArray<E>(capacity);
}
private MaxHeap(){
data = new CustomArray<E>();
}
// 返回堆中的元素个数
public int size(){
return data.getSize();
}
//返回一个布尔值,表示堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
//返回完全二叉树的数组表示,一个索引所表示的元素的父亲节点的索引
private int parent(int index){
if(index == 0){
throw new IllegalArgumentException("index-0 doesn't have parent");
}
return (index - 1 ) / 2;
}
//返回完全二叉树的数组表示,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index){
return index * 2 + 1;
}
//返回完全二叉树的数组表示,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index){
return index * 2 + 2;
}
}
2.2 向数组中添加元素
加入已经有10个元素了,现在加入第11个节点52,我们把52放在index为10的数组里。

然后index=10和它的父亲index=4进行比较,可以发现52大于16,根据最大堆的定义,52和16交互位置,交换后如下图所示:

然后index=4和它的父亲index=1进行比较,可以发现52大于41,根据最大堆的定义,52和41交互位置,交换后如下图所示:

然后index=1和它的父亲index=0进行比较,可以发现52小于62,根据最大堆的定义,52和62不用交换位置。这样插入节点52的完成就完成了,整个过程叫Sift up(元素的上浮)
代码实现:
//向堆中添加元素
public void add(E e){
data.addLast(e);
siftUp(data.getSize() - 1);
}
private void siftUp(int k){
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
data.swap(k, parent(k));
k = parent(k);
}
}
swap是动态数组CustomArray中新增的方法
//交互索引为i和j的元素值
public void swap(int i, int j){
if(i < 0 || i >= size || j < 0 || j >= size){
throw new IllegalArgumentException("Index is illegal.");
}
E t = data[i];
data[i] = data[j];
data[j] = t;
}
2.3 向数组中取出元素
取出元素只能取出最大的元素,这里为62

取出62之后,如下图所示。有两棵子树,将两棵子树融合成一棵树,还是比较复杂的

这里我们使用一个小技巧,把堆中最后一个元素放在堆顶。把最后一个原素删除

现在要把堆顶元素16往下调,这个过程叫Sift Down。选择两个孩子元素中最大的元素进行交换。这里16的孩子为52和30, 52比30大,那么16和52进行对调。
调整后如下图所示。

对于16的新的位置,可能还是不满足最大堆的性质,要继续下沉下去
16的最大孩子的元素为41,那么16和41进行交换,交换后,如下图所示

对于16的新的位置,可以发现它只有左孩子,而且16比左孩子9大,这样就不用交换了。下沉操作结束。
代码实现:
// 查看堆中最大的元素
public E findMax(){
if(data.getSize() == 0){
throw new IllegalArgumentException("Can not findMax when heap i");
}
return data.get(0);
}
//取出堆中最大的元素
public E extractMax(){
E ret = findMax();
//交互第一个元素和最后一个原素
data.swap(0, data.getSize() -1);
//删除最后一个原素
data.removeLast();
siftDown(0);
return ret;
}
private void siftDown(int k) {
//如果k不是叶子节点
while (leftChild(k) < data.getSize()){
// 找出索引k中左右孩子中最大孩子的索引
int j = leftChild(k);
//如果有右孩子 并且右孩子比左孩子大
if(j +1 < data.getSize() && data.get(j + 1).compareTo(data.get(j) )> 0){
j = rightChild(k);
}
//此时, data[j] 是leftChild和rightChild中的最大值
if(data.get(k).compareTo(data.get(j)) >= 0){
break;
}
data.swap(k, j);
//交换完成后,将j赋值给k,进行下一轮循环
k = j;
}
}
测试:
public static void main(String[] args) {
int n = 1000000;
MaxHeap<Integer> maxHeap = new MaxHeap<Integer>();
Random random = new Random();
for(int i = 0; i < n ; i++){
maxHeap.add(random.nextInt(Integer.MAX_VALUE));
}
int[] arr = new int[n];
for(int i = 0; i < n; i++){
//从最大到最小进行排列
arr[i] = maxHeap.extractMax();
}
//测试前一个元素比后一个大,否则抛出异常
for(int i = 1; i < n; i++){
if(arr[i - 1] < arr[i]){
throw new IllegalArgumentException("Error");
}
}
System.out.println("Test MaxHeap completed.");
}
测试结果:
Test MaxHeap completed.
没有抛出异常,说明取出元素正确。
2.4 堆的时间复杂度
add和extractMax时间复杂度都是O(logn)
因为堆是完全二叉树,所以它不会成为一个链表。
四、基于最大堆实现优先队列
public class PriorityQueue<E extends Comparable<E>> implements IQueue<E> {
private MaxHeap<E> maxHeap;
public PriorityQueue(){
maxHeap = new MaxHeap<E>();
}
public int getSize() {
return maxHeap.size();
}
public boolean isEmpty() {
return maxHeap.isEmpty();
}
public E getFront() {
return maxHeap.findMax();
}
public void enqueue(E e) {
maxHeap.add(e);
}
public E dequeue() {
return maxHeap.extractMax();
}
}
五、leetcode 中 347. 前 K 个高频元素
https://leetcode-cn.com/problems/top-k-frequent-elements/
题目描述:
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 示例 1: 输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
代码实现:
public class Solution {
private class Freq implements Comparable<Freq>{
//元素
int e;
//频率(出现次数)
int freq;
public Freq(int e, int freq){
this.e = e;
this.freq = freq;
}
public int compareTo(Freq another) {
//频率越小,优先级越高
if(this.freq < another.freq){
return 1;
}else if(this.freq > another.freq){
return -1;
}else {
return 0;
}
}
}
// 返回数组nums中,前k个频率最大的元素
public int[] topKFrequent(int[] nums, int k){
TreeMap<Integer,Integer> map = new TreeMap<Integer, Integer>();
for(int num : nums){
if(map.containsKey(num)){
map.put(num, map.get(num) + 1);
}else {
map.put(num , 1);
}
}
PriorityQueue<Freq> pq = new PriorityQueue<Freq>();
//算法复杂度 nlogh
for(int key: map.keySet()){
//将前k个元素放入优先队列
if(pq.getSize() < k){
pq.enqueue(new Freq(key, map.get(key)));
}
//如果可以对应的频次大于队首的频次
else if(map.get(key) > pq.getFront().freq) {
//队首元素出队(队首元素频率最小,优先级越高)
pq.dequeue();
//增加新的元素
pq.enqueue(new Freq(key, map.get(key)));
}
}
//以上操作之后,队列就是前k个频率最高的元素了。
int[] arr = new int[pq.getSize()];
int i = 0;
while (!pq.isEmpty()){
Freq f = pq.dequeue();
arr[i] = f.e;
i++;
}
return arr;
}
public static void main(String[] args) {
int[] nums = {4,1,-1,2,-1,2,3}; // 4 1次 1 1次 -1 2次 2 2次, 3 1次
int[] res = new Solution().topKFrequent(nums,2);
for(int i = 0; i < res.length; i++){
System.out.print(res[i] + ",");
}
}
}
六、d叉堆
d个孩子的完整d叉树,如下图的三叉堆

七、广义队列
这里我们学习了优先队列,已经前面的普通队列
栈,也可以理解成是一个队列