问题

(1)什么是优先级队列?

(2)怎么实现一个优先级队列?

(3)PriorityQueue是线程安全的吗?

(4)PriorityQueue就有序的吗?

简介

优先级队列,是0个或多个元素的集合,集合中的每个元素都有一个权重值,每次出队都弹出优先级最大或最小的元素。

一般来说,优先级队列使用堆来实现。

还记得堆的相关知识吗?链接直达【拜托,面试别再问我堆(排序)了!】。

那么Java里面是如何通过“堆”这个数据结构来实现优先级队列的呢?

让我们一起来学习吧。

源码分析

主要属性

// 默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 存储元素的地方
transient Object[] queue; // non-private to simplify nested class access
// 元素个数
private int size = 0;
// 比较器
private final Comparator<? super E> comparator;
// 修改次数
transient int modCount = 0; // non-private to simplify nested class access

(1)默认容量是11;

(2)queue,元素存储在数组中,这跟我们之前说的堆一般使用数组来存储是一致的;

(3)comparator,比较器,在优先级队列中,也有两种方式比较元素,一种是元素的自然顺序,一种是通过比较器来比较;

(4)modCount,修改次数,有这个属性表示PriorityQueue也是fast-fail的;

不知道fast-fail的,查看这篇文章的彩蛋部分:【死磕 java集合之HashSet源码分析】。

入队

入队有两个方法,add(E e)和offer(E e),两者是一致的,add(E e)也是调用的offer(E e)。

public boolean add(E e) {
return offer(e);
} public boolean offer(E e) {
// 不支持null元素
if (e == null)
throw new NullPointerException();
modCount++;
// 取size
int i = size;
// 元素个数达到最大容量了,扩容
if (i >= queue.length)
grow(i + 1);
// 元素个数加1
size = i + 1;
// 如果还没有元素
// 直接插入到数组第一个位置
// 这里跟我们之前讲堆不一样了
// java里面是从0开始的
// 我们说的堆是从1开始的
if (i == 0)
queue[0] = e;
else
// 否则,插入元素到数组size的位置,也就是最后一个元素的下一位
// 注意这里的size不是数组大小,而是元素个数
// 然后,再做自下而上的堆化
siftUp(i, e);
return true;
} private void siftUp(int k, E x) {
// 根据是否有比较器,使用不同的方法
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
} @SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
// 找到父节点的位置
// 因为元素是从0开始的,所以减1之后再除以2
int parent = (k - 1) >>> 1;
// 父节点的值
Object e = queue[parent];
// 比较插入的元素与父节点的值
// 如果比父节点大,则跳出循环
// 否则交换位置
if (key.compareTo((E) e) >= 0)
break;
// 与父节点交换位置
queue[k] = e;
// 现在插入的元素位置移到了父节点的位置
// 继续与父节点再比较
k = parent;
}
// 最后找到应该插入的位置,放入元素
queue[k] = key;
}

(1)入队不允许null元素;

(2)如果数组不够用了,先扩容;

(3)如果还没有元素,就插入下标0的位置;

(4)如果有元素了,就插入到最后一个元素往后的一个位置(实际并没有插入哈);

(5)自下而上堆化,一直往上跟父节点比较;

(6)如果比父节点小,就与父节点交换位置,直到出现比父节点大为止;

(7)由此可见,PriorityQueue是一个小顶堆。

扩容

private void grow(int minCapacity) {
// 旧容量
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
// 旧容量小于64时,容量翻倍
// 旧容量大于等于64,容量只增加旧容量的一半
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
// 检查是否溢出
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;
}

(1)当数组比较小(小于64)的时候每次扩容容量翻倍;

(2)当数组比较大的时候每次扩容只增加一半的容量;

出队

出队有两个方法,remove()和poll(),remove()也是调用的poll(),只是没有元素的时候抛出异常。

public E remove() {
// 调用poll弹出队首元素
E x = poll();
if (x != null)
// 有元素就返回弹出的元素
return x;
else
// 没有元素就抛出异常
throw new NoSuchElementException();
} @SuppressWarnings("unchecked")
public E poll() {
// 如果size为0,说明没有元素
if (size == 0)
return null;
// 弹出元素,元素个数减1
int s = --size;
modCount++;
// 队列首元素
E result = (E) queue[0];
// 队列末元素
E x = (E) queue[s];
// 将队列末元素删除
queue[s] = null;
// 如果弹出元素后还有元素
if (s != 0)
// 将队列末元素移到队列首
// 再做自上而下的堆化
siftDown(0, x);
// 返回弹出的元素
return result;
} private void siftDown(int k, E x) {
// 根据是否有比较器,选择不同的方法
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
} @SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
// 只需要比较一半就行了,因为叶子节点占了一半的元素
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
// 寻找子节点的位置,这里加1是因为元素从0号位置开始
int child = (k << 1) + 1; // assume left child is least
// 左子节点的值
Object c = queue[child];
// 右子节点的位置
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
// 左右节点取其小者
c = queue[child = right];
// 如果比子节点都小,则结束
if (key.compareTo((E) c) <= 0)
break;
// 如果比最小的子节点大,则交换位置
queue[k] = c;
// 指针移到最小子节点的位置继续往下比较
k = child;
}
// 找到正确的位置,放入元素
queue[k] = key;
}

(1)将队列首元素弹出;

(2)将队列末元素移到队列首;

(3)自上而下堆化,一直往下与最小的子节点比较;

(4)如果比最小的子节点大,就交换位置,再继续与最小的子节点比较;

(5)如果比最小的子节点小,就不用交换位置了,堆化结束;

(6)这就是堆中的删除堆顶元素;

取队首元素

取队首元素有两个方法,element()和peek(),element()也是调用的peek(),只是没取到元素时抛出异常。

public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
public E peek() {
return (size == 0) ? null : (E) queue[0];
}

(1)如果有元素就取下标0的元素;

(3)如果没有元素就返回null,element()抛出异常;

总结

(1)PriorityQueue是一个小顶堆;

(2)PriorityQueue是非线程安全的;

(3)PriorityQueue不是有序的,只有堆顶存储着最小的元素;

(4)入队就是堆的插入元素的实现;

(5)出队就是堆的删除元素的实现;

(6)还不懂堆?看一看这篇文章【拜托,面试别再问我堆(排序)了!】。

彩蛋

(1)论Queue中的那些方法?

Queue是所有队列的顶级接口,它里面定义了一批方法,它们有什么区别呢?

操作 抛出异常 返回特定值
入队 add(e) offer(e)——false
出队 remove() poll()——null
检查 element() peek()——null

(2)为什么PriorityQueue中的add(e)方法没有做异常检查呢?

因为PriorityQueue是无限增长的队列,元素不够用了会扩容,所以添加元素不会失败。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java集合之PriorityQueue源码分析的更多相关文章

  1. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  2. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  3. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  4. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

  5. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

  6. 死磕 java集合之ArrayDeque源码分析

    问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...

  7. 【死磕 Java 集合】— ConcurrentSkipListMap源码分析

    转自:http://cmsblogs.com/?p=4773 [隐藏目录] 前情提要 简介 存储结构 源码分析 主要内部类 构造方法 添加元素 添加元素举例 删除元素 删除元素举例 查找元素 查找元素 ...

  8. 死磕 java集合之LinkedList源码分析

    问题 (1)LinkedList只是一个List吗? (2)LinkedList还有其它什么特性吗? (3)LinkedList为啥经常拿出来跟ArrayList比较? (4)我为什么把LinkedL ...

  9. 死磕 java集合之ConcurrentSkipListSet源码分析——Set大汇总

    问题 (1)ConcurrentSkipListSet的底层是ConcurrentSkipListMap吗? (2)ConcurrentSkipListSet是线程安全的吗? (3)Concurren ...

随机推荐

  1. Scala编程入门---Map与Tuple

    创建Map //创建一个不可变的Map val ages = Map("Leo" -> 30,"Jen" ->25,"Jack" ...

  2. 记录一次坑爹的Python脚本抢购低价手机经历!

    无意间浏览到魅族官网,说魅族3限量100台.30号中午12点抢购.正好我爪机目前处于报废状态,就来一试手气了.11点多种,习惯性的看了下网页脚本,发现了检测是否到抢购时间,并返回抢购消息的ajax.于 ...

  3. Python 装饰器(Decorator)

    装饰器的语法为 @dec_name ,置于函数定义之前.如: import atexit @atexit.register def goodbye(): print('Goodbye!') print ...

  4. PAT1019:General Palindromic Number

    1019. General Palindromic Number (20) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN ...

  5. 【网络】TCP/IP连接三次握手

    TCP握手协议 在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接.第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确 ...

  6. Tiny4412之外部中断

    一:外部中断 在之前我们学习按键驱动的时候,我们检测按键有没有按下是通过轮循的方式(也就是我们说的死循环),这样虽然可以检测实现按键,但太浪费系统资源了,不论我们按键中断有没有发生,cpu都要一直进行 ...

  7. python 模块与包

    一.模块 1.1 什么是模块 # 什么是模块 # 一组功能的集合,就是模块,在python中一个py文件就一个模块 1.2 为什么要使用模块 # 1.可使代码的结构功能分区更清晰 # 2.可重复使用模 ...

  8. 【手记】解决EXCEL跑SQL遇“查询无法运行或数据库表无法打开...”

    报错: 解决:在语句开头指定SET NOCOUNT ON 就是这么神cao奇dan. -END-

  9. HTML学习之制作导航网页

    前言 今天用HTML写了一个网址导航,源代码如下: <html> <head> <title>网址导航</title> </head> &l ...

  10. Spring-Security-OAuth2调用微信API

    import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.an ...