[数据结构]优先级队列
1.优先级队列
- 队列 : 是一种先进先出 (FIFO) 的数据结构
- 但在有些情况下 , 操作的数据可能带有优先级 , 一般出队列时 , 可能需要优先级高的元素下先出队列
- 在这种场景下 , 数据结构应该提供两个最基本的操作 , 一个是返回最高优先级对象 , 一个是添加新的对象
- 这种数据结构就称为 优先级队列
2.优先级队列的模拟实现
2..1 堆的概念
如果有一个集合 , 把集合中的所有元素 按照完全二叉树的顺序存储方式 存储在一个一维数组中
- 将根节点最大的堆叫做大根堆
- 将根节点最小的堆叫做小根堆
[总结] :
- 堆中某个结点的值 总是 不大于或者不小于其父亲结点的值
- 堆总是一颗完全二叉树
以小根堆为例
逻辑结构 :
存储结构 :
2.2 堆的创建
2.2.1 堆的向下调整
① 创建一个堆类
public class TestHeap{public int[] elem;public int usedSize;public TestHeap(){this.elem = new int[10];}
② 初始化这个堆,让这个堆中开始有元素
public void initElem(int[] array){for(int i = 0;i<array.length;i++){this.elem[i] = array[i];this.usedSize++;}
}
③ 向下调整创建堆
public void createHeap(){for(int parent = (usedSize-1)/2;parent>=0;parent--){siftDown(parent,this.usedSize);}
}private void siftDown(int parent, int usedSize) {int child = parent*2+1;while(child<usedSize){if(child+1<usedSize&&elem[child]<elem[child+1])child++;//让child指向较大值if(elem[parent]<elem[child]){swap(elem,parent,child);//交换parent = child;//继续向下调整child = child*2+1;}else {break;}}}
public void swap(int[] elem,int i,int j ){int tmp = elem[i];elem[i] = elem[j];elem[j] = tmp;
}
[注意]:
createHeap()
逻辑:堆化过程从最后一个非叶子节点开始,依次对每个节点执行siftDown
,直到根节点(索引 0)
siftDown(int parent, int usedSize) 核心逻辑:对指定父节点,找到其左右子节点中的最大值,若父节点小于该最大值,则交换两者,并继续对新的子节点执行下沉操作,直到满足大根堆性质
- 计算左子节点索引(
parent*2+1
)。 - 比较左右子节点,让
child
指向较大的子节点(若右子节点存在)。 - 若父节点小于
child
指向的子节点,交换两者,并更新parent
和child
继续下沉。 - 若父节点 ≥ 子节点,说明当前节点已满足堆性质,退出循环
2.2.2 堆的向上调整
向上调整建堆
public void swap(int[] elem,int i,int j ){int tmp = elem[i];elem[i] = elem[j];elem[j] = tmp;}public void siftUp(int child, int usedSize){int parent = (child-1)/2;while (parent>=0) {if (elem[child] > elem[parent]) {swap(elem, parent, child);child = parent;parent = (child - 1) / 2;}else {break;}}}
[注意]:
维度 | 向上调整(siftUp) | 向下调整(siftDown) |
操作方向 | 从下往上(子 → 父) | 从上往下(父 → 子) |
触发场景 | 插入元素(末尾新增元素) | 删除堆顶元素、堆初始化 |
比较对象 | 当前节点与父节点 | 父节点与左右子节点中的最优者 |
终止条件 | 到达堆顶或满足堆性质 | 子节点超出范围或满足堆性质 |
时间复杂度 | O (log n)(堆的高度) | O (log n)(堆的高度) |
2.3.1 插入操作(上浮 Adjust Up)
步骤 :
① 尾插 : 将新元素加入数组的末尾(对应二叉树的最后一个位置) , 保证其结构是一颗完全二叉树
② 向上调整 : 比较新元素与其父亲结点的值 , 若不满足堆属性 , 则交换二者 , 重复此过程直到新元素称为堆顶
代码 :
public void swap(int[] elem,int i,int j ){int tmp = elem[i];elem[i] = elem[j];elem[j] = tmp;}public void siftUp(int child, int usedSize){int parent = (child-1)/2;while (parent>=0) {if (elem[child] > elem[parent]) {swap(elem, parent, child);child = parent;parent = (child - 1) / 2;}else {break;}}}
public boolean isFull(){return usedSize == elem.length;}
public void offer(int val){//在尾部增加元素if(isFull()){elem = Arrays.copyOf(elem,elem.length*2);}elem[usedSize] = val;//在最末尾添加元素siftUp(elem[usedSize],usedSize);//向上调整 先调整再++; 如果先++,则可能会超出范围usedSize++;
}
2.3.2 删除堆顶操作(下沉 Adjust Down)
步骤:
① 替换堆顶 : 将数组尾元素与堆顶元素位置互换 , 然后 usedSize--
② 向下调整 : 对指定父节点,找到其左右子节点中的最大值,若父节点小于该最大值,则交换两者,并继续对新的子节点执行下沉操作,直到满足大根堆性质
代码 :
private void siftDown(int parent, int usedSize) {int child = parent*2+1;while(child<usedSize){if(child+1<usedSize&&elem[child]<elem[child+1])child++;//让child指向较大值if(elem[parent]<elem[child]){//创建大根堆swap(elem,parent,child);//交换parent = child;child = child*2+1;}else {break;}}}
public void swap(int[] elem,int i,int j ){int tmp = elem[i];elem[i] = elem[j];elem[j] = tmp;
}
public int poll(){//删除顶层元素if(isFull()){return -1;}int val = elem[0];swap(elem,0,usedSize-1);//交换 0下标元素 和 最后一个元素siftDown(0,usedSize-1);//向下调整 不调整此时最后一个元素 , 因为该元素是要删除的元素,直接让usedSize--就好了usedSize--;return val;
}
public int peek() {//获取顶层元素if(isEmpty()) {return -1;}return elem[0];
}
2.3.3 基于大根堆 , 完成升序排列
public void heepSort(){//基于大根堆 , 完成升序排列int end = usedSize-1;while(end>0){swap(elem,0,end);//基于大根堆 , 顶层为最大值 , 将最大值放在末尾处 , 下一次循环将较大者放到end-1位置 , end位置是排序好的元素siftUp(0,end);//注意这个end是堆的大小 , 已经减过1了end--;//指向下一次末尾序号}
}
3.常用接口介绍
3.1 PriorityQueue 的特性
Java 集合框架中提供了 PriorityQueue 和 PriorityBlockingQueue 两种类型的优先级队列
- PriorityQueue 是线程不安全的
- PriorityBlockingQueue 是线程安全的
注意:
- 使用时需要导入包 : import java.util.PriorityQueue;
- PriorityQueue 中放入的元素必须能够比较大小 , 不能插入无法比较大小的对象 , 否则抛出 ClassCastException 异常
- 不能插入 null 对象 , 否则抛出 NullPointerException
- 没有容量限制 , 可以插入任意多个元素 , 其内部可以自动扩容
- PriorityQueue 底层使用了堆数据结构
- PriorityQueue 默认情况下是小堆--即每次获取到的元素都是最小元素
3.2 优先级队列的构造
代码 :
public static void main(String[] args) {//创建一个空的优先级队列,默认是小根堆排序,可以在括号里指定初始大小PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();//默认是小根堆priorityQueue.offer(44);priorityQueue.offer(23);priorityQueue.offer(4);System.out.println(priorityQueue);//[4, 44, 23]//用比较器创建一个优先级队列,是以大根堆排序PriorityQueue<Integer> priorityQueuebig = new PriorityQueue<>(new IntCmp());//利用比较器创建出来的是大根堆priorityQueuebig.offer(44);priorityQueuebig.offer(23);priorityQueuebig.offer(4);System.out.println(priorityQueuebig);//[44, 23, 4]//用一个集合来创建优先级队列List<Integer> list = Arrays.asList(3, 1, 4, 1, 5);PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>(list);System.out.println(priorityQueue1);//[1, 1, 4, 3, 5]
}
class IntCmp implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o2.compareTo(o1);}
3.3 常用方法
3.3.1 基本操作方法
方法 | 功能描述 |
| 向队列中添加元素,成功返回 ,如果元素为 或不满足比较规则会抛出异常 |
| 向队列中添加元素,成功返回 ,失败返回 (更推荐使用,不会抛出异常) |
| 移除并返回队列的头部元素(优先级最高的元素),队列为空时返回 |
| 移除并返回队列的头部元素,队列为空时抛出 |
| 清空队列中的所有元素 |
3.3.2 元素查询方法
方法 | 功能描述 |
| 返回队列的头部元素(优先级最高的元素),但不移除它,队列为空时返回 |
| 返回队列的头部元素,队列为空时抛出 |
| 判断队列中是否包含指定元素,返回 值 |
| 返回队列中元素的数量 |
| 判断队列是否为空,返回 值 |
3.3.3 批量操作与迭代
方法 | 功能描述 |
| 将集合中的所有元素添加到队列中 |
| 移除队列中与集合交集的元素 |
| 保留队列中与集合交集的元素,移除其他元素 |
| 返回迭代器,用于遍历队列元素(注意:迭代顺序不保证优先级顺序) |
代码 :
import java.util.Arrays;
import java.util.PriorityQueue;public class PriorityQueueMethods {public static void main(String[] args) {// 创建一个小顶堆PriorityQueue<Integer> pq = new PriorityQueue<>();// 添加元素pq.offer(5);pq.offer(2);pq.offer(8);pq.offer(1);System.out.println("添加元素后: " + pq); // 内部存储:[1, 2, 8, 5]// 查看头部元素System.out.println("头部元素(peek): " + pq.peek()); // 1// 移除并返回头部元素System.out.println("移除的元素(poll): " + pq.poll()); // 1System.out.println("poll后队列: " + pq); // [2, 5, 8]// 判断是否包含元素System.out.println("是否包含5: " + pq.contains(5)); // true// 批量添加元素pq.addAll(Arrays.asList(3, 6));System.out.println("批量添加后: " + pq); // [2, 3, 8, 5, 6]// 遍历元素(注意:迭代顺序不是优先级顺序)System.out.print("迭代器遍历: ");for (int num : pq) {System.out.print(num + " "); // 可能输出:2 3 8 5 6}System.out.println();// 按优先级顺序输出所有元素System.out.print("按优先级出队: ");while (!pq.isEmpty()) {System.out.print(pq.poll() + " "); // 2 3 5 6 8}}
}
4. 应用
Top K 问题(求前 K 个最大 / 小元素)
堆是解决 Top K 问题的高效方案,时间复杂度为 O (n log K),适合处理海量数据。
方法 ①
public int[] smallestK1(int[] arr, int k) {PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();for(int i = 0;i<arr.length;i++){priorityQueue.offer(arr[i]);}int[] ret = new int[k];for(int i = 0;i<k;i++){ret[i] = priorityQueue.poll();}return ret;
}
方法 ②
public int[] smallestK(int[] arr, int k) {int[] ret = new int[k];if(k == 0||arr == null){return ret;}PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp1());for(int i = 0;i<k;i++){priorityQueue.offer(arr[i]);}for(int i = k;i<arr.length;i++){int tmp = priorityQueue.peek();if(tmp>arr[i]){priorityQueue.poll();priorityQueue.offer(arr[i]);}}for(int i = 0;i<k;i++){ret[i] = priorityQueue.poll();}return ret;}
class IntCmp1 implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o2.compareTo(o1);}}