PriorityQueue源码解析

PriorityQueue为优先队列,其与普通的FIFO(先进先出)队列不同的地方是,优先队列每次出队的均是队列中优先级最高的元素。

PriorityQueue使用最小(大)二叉堆来管理队列中的元素,元素的大小比较方法由用户指定的Comparator指定,每次出队元素为堆顶元素。

最小二叉堆

最小二叉堆满足如下约束:

  • 完全二叉树;
  • 所有非叶子节点的根节点value均小于其左、右子节点的value。

如下图即为典型的最小二叉堆:

在这里插入图片描述

入队

最小二叉堆的元素添加过程如下:

  • 将待添加元素添加到二叉树的最后一个位置;
  • 若元素value比父节点的value小,则交换该元素和父节点的位置,即元素上移;
  • 若上移后,其value仍然比父节点的value小,再次交换该元素和父节点的位置,继续上移;
  • 不断上移,直到元素value不比父节点的value小,结束添加流程。

在这里插入图片描述

出队

最小二叉堆的元素删除过程如下:

  • 将堆顶元素删除(返回),并将堆底最后一个元素移动到堆顶;
  • 此时二叉堆不能就不满足最小二叉堆的性质了,需要元素调整,即将堆顶元素不断下移;
  • 比较元素value和左、右子节点的value大小,若左子节点value最小,则将该元素与左子节点交换;若右子节点value最小,则将该元素与右子节点交换;否则结束下移,调整完成并返回。
  • 不断执行上一步骤,直至无法继续下移,调整完成并返回。

在这里插入图片描述

构造堆

假设有1个无序数组,要求 我们将其构造成最小二叉堆。

最简单的做法是,遍历无序数组,然后逐个元素执行入队过程,该种方法每次入队均需要伴随着元素的移动,不够高效。

另外一种做法是:

  1. 首先将无序数组构造成二叉堆;
  2. 从最后一个非叶子节点由下往上逐步执行元素下移,使各个非叶子节点value均小于其左、右子节点的value;
  3. 遍历直至root节点,结束遍历返回最小二叉堆。

在这里插入图片描述

在这里插入图片描述

最大二叉堆与最小二叉堆类似的过程,只是上移、下移操作由"比小"操作变成了"比大"操作。

源码分析

其实理解了最小二叉堆的构造、添加和删除过程,PriorityQueue源码就变得很简单了。

PriorityQueue基于数组来实现,其成员变量如下:

// 初始容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 元素数组
transient Object[] queue; // non-private to simplify nested class access
// 优先队列的元素个数
int size;
// 比较器
private final Comparator<? super E> comparator;
// 优先队列被调整的次数
transient int modCount;     // non-private to simplify nested class access

入队

入队方法有add、offer,而add又是直接调用的offer。

public boolean offer(E e) {
	// 若添加的元素为null,则直接抛出空指针异常
	if (e == null)
		throw new NullPointerException();
	// 操作数+1
	modCount++;
	int i = size;
	// 如果元素个数已大于或等于数组的长度,则执行扩容操作
	if (i >= queue.length)
		grow(i + 1);
	// 将待添加元素添加到数组最后,并执行元素上移过程
	siftUp(i, e);
	// 元素添加完成后,元素个数+1
	size = i + 1;
	// 返回true,代表添加元素成功
	return true;
}

先看一下扩容函数:

private void grow(int minCapacity) {
	// 获取现有容量
	int oldCapacity = queue.length;
	// Double size if small; else grow by 50%
	// 若现有容量<64,则新容量变为现有容量的2倍+2
	// 若现有容量>64,则新容量变为现有容量的1.5倍
	int newCapacity = oldCapacity + ((oldCapacity < 64) ?
									 (oldCapacity + 2) :
									 (oldCapacity >> 1));
	// overflow-conscious code
	// 若新容量大于Integer.MAX_VALUE - 8,执行hugeCapacity方法
	if (newCapacity - MAX_ARRAY_SIZE > 0)
		newCapacity = hugeCapacity(minCapacity);
	// 将老数组元素拷贝到新数组中
	queue = Arrays.copyOf(queue, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
	if (minCapacity < 0) // overflow
		throw new OutOfMemoryError();
	return (minCapacity > MAX_ARRAY_SIZE) ?
		Integer.MAX_VALUE :
		MAX_ARRAY_SIZE;
}

元素上移过程如下:

private void siftUp(int k, E x) {
	// 判断是否有传入的comparator
	// 若有则执行siftUpUsingComparator,否则执行siftUpComparable
	if (comparator != null)
		siftUpUsingComparator(k, x, queue, comparator);
	else
		siftUpComparable(k, x, queue);
}

private static <T> void siftUpComparable(int k, T x, Object[] es) {
	Comparable<? super T> key = (Comparable<? super T>) x;
	while (k > 0) {
		// 获取父节点的index=(k-1)/2
		int parent = (k - 1) >>> 1;
		// 拿到父节点的value
		Object e = es[parent];
		// 若当前节点value>=父节点,停止上移,直接break返回
		if (key.compareTo((T) e) >= 0)
			break;
		// 若当前节点value>=父节点,与父节点替换,元素上移
		es[k] = e;
		k = parent;
	}
	es[k] = key;
}

// 过程基本与siftUpComparable相同,区别就是在元素上移的过程中,siftUpUsingComparator方法使用传入的Comparator来进行大小比较
private static <T> void siftUpUsingComparator(
	int k, T x, Object[] es, Comparator<? super T> cmp) {
	while (k > 0) {
		int parent = (k - 1) >>> 1;
		Object e = es[parent];
		if (cmp.compare(x, (T) e) >= 0)
			break;
		es[k] = e;
		k = parent;
	}
	es[k] = x;
}

可以发现,PriorityQueue构造时若不传入自定义的Comparator,元素上移过程中进行Value比较的时候,默认使用的元素的compareTo方法。

故PriorityQueue默认是最小优先队列,若需要最大优先队列,则需要传入相应的Comparator。

出队

出队方法有peek和poll。

peek方法返回优先队列的首元素,但不删除首元素;

poll方法不仅返回优先队列的首元素,同时删除首元素。

首先看peek方法:

public E peek() {
	return (E) queue[0];
}

由于不涉及首元素的删除操作,所以直接返回首元素即可。

接着看一下poll方法:

public E poll() {
	final Object[] es;
	final E result;
	// 若数组的首元素不为null
	if ((result = (E) ((es = queue)[0])) != null) {
		// 操作数+1
		modCount++;
		final int n;
		// 获取数组的末尾元素,并将数组大小-1
		final E x = (E) es[(n = --size)];
		// 将数组末尾置为null
		es[n] = null;
		if (n > 0) {
			final Comparator<? super E> cmp;
			// 若无传入的Comparator,则执行siftDownComparable
			// 若有传入的Comparator,则执行siftDownUsingComparator
			if ((cmp = comparator) == null)
				// 传参为0,说明从首元素开始下移
				siftDownComparable(0, x, es, n);
			else
				siftDownUsingComparator(0, x, es, n, cmp);
		}
	}
	return result;
}

private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
	// assert n > 0;
	Comparable<? super T> key = (Comparable<? super T>)x;
	int half = n >>> 1;           // loop while a non-leaf
	// 从上到下,从左到右不断循环遍历非叶子节点
	while (k < half) {
		// 获取左子节点的index
		int child = (k << 1) + 1; // assume left child is least
		// 获取左子节点的value
		Object c = es[child];
		// 获取右子节点的index
		int right = child + 1;
		// 若右子节点存在,且右子节点的值小于左子节点,则将左右子节点的最小值更新为右子节点的值
		if (right < n &&
			((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
			c = es[child = right];
		// 若该元素小于左、右子节点中的偏小者,停止下移,直接break
		if (key.compareTo((T) c) <= 0)
			break;
		// 否则,将该节点与左、右子节点中的偏小者调换,继续执行下移过程
		es[k] = c;
		k = child;
	}
	es[k] = key;
}

// 与siftDownComparable方法类似,只是元素下移的过程中使用了传入的Comparator来进行元素大小比较
private static <T> void siftDownUsingComparator(
	int k, T x, Object[] es, int n, Comparator<? super T> cmp) {
	// assert n > 0;
	int half = n >>> 1;
	while (k < half) {
		int child = (k << 1) + 1;
		Object c = es[child];
		int right = child + 1;
		if (right < n && cmp.compare((T) c, (T) es[right]) > 0)
			c = es[child = right];
		if (cmp.compare(x, (T) c) <= 0)
			break;
		es[k] = c;
		k = child;
	}
	es[k] = x;
}

基于集合构造优先队列

PriorityQueue提供了initFromCollection方法将传入的集合构造成优先队列:

private void initFromCollection(Collection<? extends E> c) {
	// 拷贝元素创建初始堆
	initElementsFromCollection(c);
	// 将初始堆调整为最小(大)二叉堆
	heapify();
}

接着看一下heapify()方法:

private void heapify() {
	final Object[] es = queue;
	// 获取最后1个非叶子节点的index=(n/2)-1
	int n = size, i = (n >>> 1) - 1;
	final Comparator<? super E> cmp;
	// 由最后1个非叶子节点开始往上遍历,逐渐执行节点的下移过程
	if ((cmp = comparator) == null)
		for (; i >= 0; i--)
			siftDownComparable(i, (E) es[i], es, n);
	else
		for (; i >= 0; i--)
			siftDownUsingComparator(i, (E) es[i], es, n, cmp);
}

需要注意的是: PriorityQueue不是线程安全的,在多线程环境下,需要自己基于锁机制来实现线程安全。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值