一、优先队列场景:
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叉树,如下图的三叉堆
七、广义队列
这里我们学习了优先队列,已经前面的普通队列
栈,也可以理解成是一个队列