详细PriorityQueue信息请看这篇博文:csdn-JAVA中PRIORITYQUEUE详解,本文只描述我在使用时遇到的对poll()的疑惑和小结
一.问题来源
java库函数中有个PriorityQueue是使用动态数组来实现最小堆的(默认最小堆,也可以在初始化时传入自己制定的Comparator来把它变成最大堆)。
实现堆排序的算法中,有一种算法需要用到堆的"删除"方法.前面说了PriorityQueue是使用动态数组来实现最小堆的,那删除堆顶就表示堆变小了,那是不是动态数组也变小了?
众所周知数组一旦初始化了,那它的长度是不可改变的。只有新建一个数组然后把原元素复制到一个新数组,才能实现"数组长度的改变"。 如果动态数组会变小,就表示每次删除根节点,就要新建一个数组,算法的时间复杂度和空间复杂度会大大上升
所以我很好奇PriorityQueue是怎么实现的,查了下API文档,有了以下的总结。如果出错请在评论区留言讨论,谢谢
二、初始化
我们先来看PriorityQueue的构造函数
//一个常量,当你调用构造函数时没有传入数组的长度,则这个就是新建数组的默认长度
private static final int DEFAULT_INITIAL_CAPACITY = 11;
//PriorityQueue中就是维护这个数组,用来存放最小堆
transient Object[] queue;
//当前堆中的元素个数,初始值为0;这个有什么作用呢?后面就会知道
private int size = 0;
//这其实就是传入一个数字,数字作为上面那个queue数组的初始长度
//再传入一个比较器,排序时就按照这个比较器中写的来排序
//如果比较器为null,将使用元素的自然排序(数字的自然排序就是从小到大)
public PriorityQueue(int initialCapacity,Comparator<?super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
//无参构造函数,会传入默认的数组数值11(看上面的第一个属性,它默认是11)
//就是说什么都不传入,初始维护的数组大小为11
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
三、删除堆顶poll()
//把E看成是Integer就行了(当然他们并不相同,只不过好理解点)
//E是数组内的元素的数据类型——还记得吗,PriorityQueue维护了一个数组来存放堆
public E poll() {
//size是当前堆的元素个数,上面定义了
//size=0表示当前堆没元素,就没有堆顶
if (size == 0)
return null;
//删除堆顶元素,那整个堆的元素数量就减1了
//其实这里好像都没必要新建个整数存新的size了,
//因为size是个全局变量,一改全改。我猜可能是s短一点,好写
int s = --size;
//modCount是堆的修改次数(数组的修改次数),不用管,这是和线程安全有关的属性,这里不用管
modCount++;
//数组的第一个元素是堆的堆顶,poll()要返回堆顶的,所以我们要取出来
E result = (E) queue[0];
//这里涉及堆的删除知识,不懂可以去查查
//假如一个数组的长度为10,
//那这个数组的最后一个元素的下标是什么?是9;是(10-1)
//所以可以理解queue[s]是当前堆(还没开始删除)的最后一个元素
//因为s = size-1;
E x = (E) queue[s];
//把最后一个元素置为null
queue[s] = null;
//把最后一个元素(我们在置为空之前把它取出来了)放到数组的第一个位置
//进行堆的排序,堆的排序只在这个数组内进行,不会额外添加新的数组
if (s != 0)
siftDown(0, x);
return result;
}
1.假如有个初始数组
0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 3 | 5 | 2 | 6 |
2.进行堆排序(最小堆)后
0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 2 | 5 | 3 | 6 |
3.poll()掉堆顶
(1)首先:把堆的最后一个元素拿出来临时存放,然后把它置为空
0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 2 | 5 | 3 | null |
在数组外开辟个临时空间存放它的值 |
---|
6 |
(2)然后,把最后那个元素的值赋给数组的第一个元素(堆顶元素)
0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
6 | 2 | 5 | 3 | null |
(3)最后,调整数组元素来满足堆的定义
0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
2 | 3 | 5 | 6 | null |
四、代码验证
public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException {
//我们给一个初始数组,等下加进去
int[] array = {1,3,5,2,6,2,7,9,10,11,1,2,34,5,6,7};
//新建一个PriorityQueue,没输入数组长度,默认11
PriorityQueue<Integer> heap = new PriorityQueue<Integer>();
//通过反射获取queue数组
Class c = heap.getClass();
Field f = c.getDeclaredField("queue");
f.setAccessible(true);
Object[] len = (Object[]) f.get(heap);
System.out.println("没添加元素时,初始数组长度: " + len.length);
//把我们给出的数组的元素加进去,我们的数组中有16个元素
//不用管数组增长算法,感兴趣的话可以自己到api文档去看
for(int e : array){
heap.add(e);
}
len = (Object[]) f.get(heap); //要重新取数组的值
System.out.println("添加16个元素后,数组长度: " + len.length);
//删除堆顶
heap.poll();
len = (Object[]) f.get(heap); //要重新取数组的值
System.out.println("删除1个元素后,数组长度: " + len.length);
//多次删除堆顶
int i = 6;
while(i-- > 0) heap.poll();
len = (Object[]) f.get(heap); //要重新取数组的值
System.out.println("再删除6个元素后,数组长度: " + len.length);
}
五、结论:
1.可以看到删除栈顶元素后,原数组并不会变小,而是把堆的末尾元素赋给数组的第一个元素(堆顶),然后末尾元素置为null,剩余元素再调整顺序来满足堆的定义
2.所以全局变量size就有大作用了,size表示堆的元素数量,就可以用来定位堆的最后一个元素在数组中的位置,来确定堆在数组中的位置
3.突然又发现PriorityQueue不允许插入null,但是它本身是可以存null甚至里面许多空间存的就是null