优先级队列——堆
概念
1 优先级队列
优先级队列(PriorityQueue)是具有优先级的队列, 优先级高的元素先出队, Java中的PriorityQueue底层用了堆这样的数据结构.
2 堆
2.1 堆的概念
堆实际上是一个特殊的完全二叉树, 这颗树的某个结点总是大于或小于父节点.
堆分为大根堆和小根堆, 根结点最小的堆称为小根堆, 反之称为大根堆.
2.2 堆的存储方式
堆是一个完全二叉树, 中间不会有空结点, 所以可以按层序遍历的顺序存储在一维数组中.
下标从0开始, i 表示下标, i 的父亲节点下标为 (i-1)/2
i 的左孩子下标为 2 * i + 1
i 的右孩子下标为 2 * i + 2
模拟实现堆
1 堆的创建
提供一个数组 [22, 29, 15, 42, 35, 6 2, 34, 56, 19, 31], 如何将它调整为堆呢?
以大根堆为例:
向下调整建堆
从最后一个非叶子节点开始向前遍历.
每个非叶子节点与孩子节点比较, 若孩子节点较大, 则与孩子节点交换.
交换后需要继续向下调整, 直到比孩子节点大或没有孩子节点.
代码:
// 向下调整建堆的时间复杂度为O(n)
public void createHeap(int[] array) {
elem = Arrays.copyOf(array,array.length);
usedSize = array.length;
// 从最后一个非叶子节点 开始 向下调整
for (int i = (usedSize-1-1) / 2; i >= 0; i--) {
shiftDown(i,usedSize);
}
}
/**
* @param parent 是每棵子树的根节点的下标
* @param len 是每棵子树调整结束的结束条件
* 向下调整的时间复杂度:O(log(2)n)
*/
private void shiftDown(int parent, int len) {
// 孩子结点下标
int child = parent * 2 + 1;
while (child < len) {
// 比较两个孩子结点大小
if (child+1 < len && elem[child] < elem[child + 1]) {
child++;
}
// 孩子结点大于根结点,交换
if (elem[child] > elem[parent]) {
swap(elem, child, parent);
// 因为发生了变化,所以继续向下调整
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}
2 堆的插入
在堆的末尾插入新的节点, 然后将该节点向上调整
代码:
// 插入
public void push(int val) {
if (isFull()) {
// 扩容
elem = Arrays.copyOf(elem, usedSize + usedSize / 2);
}
// 放入数组
elem[usedSize++] = val;
// 向上调整
shiftUp(usedSize - 1);
}
// 向上调整的时间复杂度是O(log(2)n)
private void shiftUp(int child) {
// 父亲结点下标
int parent = (child - 1) / 2;
while (parent >= 0) {
// 孩子结点 大于 父亲结点 交换
if (elem[child] > elem[parent]) {
swap(elem, child, parent);
// 继续向上调整
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
向上调整也可以建堆, 但是比向下调整效率低,
因为对于一个完全二叉树, 下层的节点数比较多,
下层的节点需要向上调整的次数多, 而向下调整的次数少
// 向上调整建堆
public void createHeap2(int[] array) {
elem = Arrays.copyOf(array,elem.length);
usedSize = array.length;
// 从第二个叶子结点 开始 向上调整
for (int i = 1; i < usedSize; i++) {
shiftUp(i);
}
}
3 堆的删除
删除堆顶元素后仍要保持是大根堆
public void pollHeap() {
if(isEmpty()) {
return;
}
// 将最后一个元素 与 堆顶元素交换
// 并将usedSize - 1
elem[0] = elem[--usedSize];
// 此时除了 堆顶元素,其余的都符合大根堆
// 向下调整
shiftDown(0, usedSize);
}
4 完整代码
import java.util.Arrays;
// 大根堆
public class TestHeap {
public int[] elem;
public int usedSize;
public TestHeap() {
this.elem = new int[10];
}
/**
* 向下调整建堆的时间复杂度:o(n)
*
* @param array
*/
public void createHeap(int[] array) {
elem = Arrays.copyOf(array,array.length);
usedSize = array.length;
// 从最后一个非叶子结点 开始 向下调整
for (int i = (usedSize-1-1) / 2; i >= 0; i--) {
shiftDown(i,usedSize);
}
}
/**
* 向上调整建堆
*
* @param array
*/
public void createHeap2(int[] array) {
elem = Arrays.copyOf(array,elem.length);
usedSize = array.length;
// 从第二个叶子结点 开始 向上调整
for (int i = 1; i < usedSize; i++) {
shiftUp(i);
}
}
/**
* @param parent 是每棵子树的根节点的下标
* @param len 是每棵子树调整结束的结束条件
* 向下调整的时间复杂度:O(log(2)n)
*/
private void shiftDown(int parent, int len) {
// 孩子结点下标
int child = parent * 2 + 1;
while (child < len) {
// 比较两个孩子结点大小
if (child+1 < len && elem[child] < elem[child + 1]) {
child++;
}
// 孩子结点大于根结点,交换
if (elem[child] > elem[parent]) {
swap(elem, child, parent);
// 因为发生了变化,所以继续向下调整
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}
/**
* 入队:仍然要保持是大根堆
* @param val
*/
public void push(int val) {
if (isFull()) {
// 扩容
elem = Arrays.copyOf(elem, usedSize + usedSize / 2);
}
// 放入数组
elem[usedSize++] = val;
// 向上调整
shiftUp(usedSize - 1);
}
// 向上调整的时间复杂度:O(log(2)n)
private void shiftUp(int child) {
// 父亲结点下标
int parent = (child - 1) / 2;
while (parent >= 0) {
// 孩子结点 大于 父亲结点 交换
if (elem[child] > elem[parent]) {
swap(elem, child, parent);
// 继续向上调整
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
public boolean isFull() {
return usedSize == elem.length;
}
private void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
/**
* 出队【删除】:每次删除的都是优先级高的元素
* 堆顶元素
* 仍然要保持是大根堆
*/
public void pollHeap() {
if(isEmpty()) {
return;
}
// 将最后一个元素 与 堆顶元素交换
// 并将usedSize - 1
elem[0] = elem[--usedSize];
// 此时除了 堆顶元素,其余的都符合大根堆
// 向下调整
shiftDown(0, usedSize);
}
public boolean isEmpty() {
return usedSize == 0;
}
/**
* 获取堆顶元素
* @return
*/
public int peekHeap() {
if (isEmpty()) {
return -1;
}
return elem[0];
}
}
PriorityQueue的使用
1 注意事项
关于Java中PriorityQueue的使用要注意:
- 使用时必须导入PriorityQueue所在的包,即:import java.util.PriorityQueue;
- PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
- 不能插入null对象,否则会抛出NullPointerException
public static void main(String[] args) {
PriorityQueue<Student1> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(new Student1("张三",13));
priorityQueue.offer(new Student1("李四",12));
// PriorityQueue中放置不能比较大小的数据 插入第二个元素时,会抛出ClassCastException异常
// priorityQueue.offer(null);
// 插入null对象,会抛出NullPointerException
}
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
- 插入和删除元素的时间复杂度为O(log(2)n)
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小根堆—即每次获取到的元素都是最小的元素
2 自定义类型如何插入PriorityQueue
代码:
// 自定义类
class Student implements Comparable<Student>{
public int age;
public String name;
public Student(String name, int age) {
this.age = age;
this.name = name;
}
// 重写了Comparable接口的 compareTo方法
@Override
public int compareTo(Student o) {
return this.name.compareTo(o.name);
}
}
// 两个比较器
class NameComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
public static void main(String[] args) {
PriorityQueue<Student> priorityQueue3 = new PriorityQueue<>(new NameComparator());
priorityQueue3.offer(new Student("张三",19));
priorityQueue3.offer(new Student("李四",13));
// 手动传了一个比较器
// 如果传了比较器 就优先使用手动传入的方法进行比较
PriorityQueue<Student> priorityQueue2 = new PriorityQueue<>(new AgeComparator());
priorityQueue2.offer(new Student("张三",19));
priorityQueue2.offer(new Student("李四",13));
// 没传比较器,默认用类的实现的接口Comparable 中的compareTo 方法
// 如果没有实现这个接口,也没传比较器,会抛出ClassCastException异常
PriorityQueue<Student> priorityQueue1 = new PriorityQueue<>();
priorityQueue1.offer(new Student("张三",19));
priorityQueue1.offer(new Student("李四",13));
}
3 如何建立大根堆
因为PriorityQueue默认建的是小根堆, 那我们怎么让它建一个大根堆呢?
代码:
// 比较器
class testComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
// Integer 类中默认的比较是 o1.compareTo(o2)
return o2.compareTo(o1);
}
}
public static void main(String[] args) {
int[] array = { 22,29,15,42,35,62,34,56,19,31};
PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>();
for (int i = 0; i < array.length; i++) {
priorityQueue1.offer(array[i]);
}
System.out.println(priorityQueue1);
// 默认是小根堆
// 如何创建大根堆呢
// 手动传入一个 反过来的 比较器
PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(new testComparator());
for (int i = 0; i < array.length; i++) {
priorityQueue2.offer(array[i]);
}
System.out.println(priorityQueue2);
}
运行结果: