前言
我们之前之前接触过很多数据结构,比如数组、栈、红黑树,队列,链表,二叉树等等。我们都知道队列是一种遵循先进先出(First-In-First-Out)的模式,但在有的时候我们需要在队列中基于优先级处理对象的。比如说下面几种情况:
- 作业系统中的调度程序,当一个作业完成后需要从所有等待调度的作业选择一个优先级最高的来执行,并且也可以添加一个新的作业到优先队列中
- Timer定时任务中我们需要获取最近执行的任务,当任务执行完成以后还需要再次放到队列中跟其他任务比较下一次执行任务的时间。
- 我们需要找出一组数据中前N个最大的数。
示例
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(10);
queue.add(-1);
queue.add(6);
queue.add(50);
queue.add(-12);
queue.add(32);
queue.add(23);
queue.add(74);
System.out.println(queue.toString());
}
结果: [-12, -1, 6, 50, 10, 32, 23, 74]
同时我们还可以通过自定义实现 Comparator接口来对队列中的数据进行排序。
public static void main(String[] args) {
//实现排序接口,使得的任何一个父节点的值都要大于其他两个子节点的值
PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
if(o1 == o2) {
return 0;
}
return o1 > o2 ? -1 : 1;
}
});
queue.add(10);
queue.add(-1);
queue.add(6);
queue.add(50);
queue.add(-12);
queue.add(32);
queue.add(23);
queue.add(74);
System.out.println(queue.toString());
}
结果: [74, 50, 32, 10, -12, 6, 23, -1]
优先队列的作用就是保证每次取出的元素都是队列中权值最小的,我们也把这个叫做是最小堆队列;反之如果每次取出的元素都是队列中权值最大的话,那么我们也可以成为最大堆队列,其内部使用数组来进行存储和操作实现的。
代码分析
- 创建对象
public class PriorityQueue {
//注意默认的构造函数是最小堆,如果要实现最大堆的话需要自己实现 Comparator 接口
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
//创建一个长度为 11的数组
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
}
在创建对象的时候,其内部就会将创建一个具体大小的数组,该数组用于存储元素,同时也非常方便的进行查找和索引的操作。当然这个大小我们是可以自己定的,当数组已经存满了以后再进行添加元素的话,则会进行数组拷贝、扩容保证数组长度的可用。
- 添加数据
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
//修改次数+1
modCount++;
int i = size;
//如果数组已经存满数据的话,则需要对数组进行扩容
if (i >= queue.length)
grow(i + 1);
//个数加1
size = i + 1;
//如果原先的数组没有数据,直接将数据添加到数组第一个位置
if (i == 0)
queue[0] = e;
else
//首先我们将新添加进来的进来的元素添加到数组最后一个位置
siftUp(i, e);
return true;
}
我们在将数据添加到堆中的
private void siftUp(int k, E x) {
//首先判断我们是否实现 Comparator接口
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
//不断循环的跟父节点比较大小,然后父节点交换位置,直到到达第一个位置。
//这里的 k 表示数组的下表索引位置,x 表示 value
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
//由于我们没有实现 Comparator接口,如果 key大于其父节点的话,直接将元素放到最后一个位置
if (key.compareTo((E) e) >= 0)
break;
//如果父节点的元素大于 key,则交换他们的位置
queue[k] = e;
k = parent;
}
queue[k] = key;
}
添加元素图解
看了上面的代码我们可能会感觉到非常的懵逼的,最后的代码说明就是通过图一步步的进行讲解和分析。下面就通过多张来源网络的图来说明怎么添加元素的。
比如说我们要往最小堆中添加一个元素 4,首先会对数组的size-1/2的位置(该位置就是第一个父节点)元素进行比较。
(注:图片来源于该 博客 复制的)
- 删除数据
//删除具体的元素
public boolean remove(Object o) {
//首先从数组中遍历找到该元素,如果没有找到该元素直接返回false
int i = indexOf(o);
if (i == -1)
return false;
else {
//删除数组中具体位置的元素
removeAt(i);
return true;
}
}
从上面的代码可以看出删除指定元素的话,首先需要找到该元素的位置(因为内部是数组实现的,所以需要不断的进行遍历寻找),然后根据索引的位置来删除该元素,由于删除元素的时候同时会破坏堆的顺序,所以重新进行排列。
删除数组中指定位置元素
E removeAt(int i) {
modCount++;
int s = --size;
//如果删除的是最后一个元素则直接置位null
if (s == i)
queue[i] = null;
else {
//首先取出最后一个元素赋值给 moved
E moved = (E) queue[s];
//然后将最后一个元素置位 null
queue[s] = null;
//然后不断的对该位置的元素进行下沉
siftDown(i, moved);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
在删除元素的同时我们还需要下沉元素。
private void siftDown(int k, E x) {
//首先判断我们是否实现 Comparator 接口
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
//其中 x 表示元素的位置, x 表示最后一个位置的内容值 value
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
//这里half表示的有多少个非叶子节点
int half = size >>> 1;
//这里不断的进行循环,
while (k < half) {
int child = (k << 1) + 1;//左节点位置
Object c = queue[child];//左节点位置的内容
//右节点位置
int right = child + 1;
//比较左节点和右节点两个值的内容大小,如果右节点的内容小于左节点,则将右节点的内容赋值给 c
if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
//同时还是需要将右节点的位置赋值给 child。
c = queue[child = right];
//同时将左右节点最小的值再跟父节点中进行比较,如果大于父节点的话,直接跳出循环
if (key.compareTo((E) c) <= 0)
break;
//交换父亲节点中和其中一个节点位置
queue[k] = c;
//同时依次的往下遍历
k = child;
}
queue[k] = key;
}
看了上面的一大堆的代码我们可能还不知道其中具体怎么删除元素的,下面我们就通过一张图来描述整个流程,因为图永远比文字容易生动和理解的,不过有人会问我有图了,为什么还需要有代码来分析了。我只要懂的其中的原理就行了,其实这种想法是非常危险的。优秀的源代码的可以锻炼我们更好的思维和提高我们的编码能力,我们在看图的过程中学到了理论,但是如果你不能实现出来又有什么用呢?下面我们以删除第一个元素为例子
[ 注:该图来源于博客网站]
- 清除所有元素
public void clear() {
//首先修改次数需要加 1,但是不知道该字段有啥用
modCount++;
//然后遍历整个数组,将元素置位 null,有利于JVM回收
for (int i = 0; i < size; i++)
queue[i] = null;
size = 0;
}
总结
Java中的优先队列是由数组实现的一种数组数据结构,它 不支持插入null数据;如果取出的第一个元素永远都是数组中最小的元素,那么我们就称该堆为最小堆;如果取出的第一个元素永远都是数组中最大的元素,那我们称该堆为最大堆。在生活的应用场景也是非常多的,这里我们就不一一进行举例了。具体的大家在百度上直接搜索其应用案例。