Java容器解析系列(9) PrioriyQueue详解
PriorityQueue:优先级队列;
在介绍该类之前,我们需要先了解一种数据结构——堆,在有些书上也直接称之为优先队列;
堆(Heap)是是具有下列性质的完全二叉树:每个结点的值都 >= 其左右子结点的值,称为最大堆(Max Heap,或称大顶堆);每个结点的值都 <= 其左右子结点的值,称为最小堆(Min Heap,或称小顶堆);
Note:上述定义摘自《大话数据结构》,其定义并不是很准确,堆有很多种,完全二叉树只是其一种实现方式,也是最常见的一种实现方式。在《数据结构与算法分析艺术中》有言:当堆这个词不加修饰地使用时一般指该数据结构的二叉堆实现方式。(也即这里所说的完全二叉树的实现方式)。
完全二叉树的最简单的实现方式是数组实现:
对于数组中任意位置i上的元素,其左儿子在位置2i上,右儿子在左儿子的右边,位置为2i+1,它的父结点位于i/2位置上。
Note1:上述假设数组第0个位置不存储堆元素(作为一个哨兵),第1个位置的元素为根结点;
如果第0个元素存储堆元素,那么左儿子应该为2i+1,右儿子为2(i+1),根结点为位置为0的元素;
PriorityQueue中的数组第0个位置存储堆元素。
综合堆的定义和完全二叉树的表示方式,可以用如下方式表示一个二叉堆:
arr[i] <= arr[2i],arr[i] <= arr[2i+1] (最小堆)
或者
arr[i] >= arr[2i],arr[i] >= arr[2i+1] (最大堆)
其中 1 <= i <= n / 2
Note:同Note1
java的PriorityQueue的内部实现原理就是一个数组表示的最小堆,对于一个最小堆,其最重要的3个操作如下:
Insert:插入一个元素
DeleteMin:删除最小元素
BuildHeap:已知n个元素,构建一个堆
关于上述3中操作,这里我做一些举例,为了形象的说明这些操作的步骤,贴上《数据结构与算法分析》这本树上的一些内容图片。
Insert:
插入一个新元素,先在这个堆的最后插入一个空穴,然后将这个元素与空穴的父结点进行比较,如果比父结点小,那么将用父结点的元素移动到这个空穴中,并将空穴上移;如果比父结点大,此时空穴的位置即为插入的元素应该在的位置,直接用插入的元素填到这个空穴即可。这个空穴上移的过程称之为上滤,对应于PriorityQueue总的siftup().
下图比较好地解释了这个过程。
DeleteMin:
最小的元素,也就是根节点的额元素,直接取出。这是根这个位置形成一个空穴,最后一个元素因为堆的大小减1,需要移动其到新的位置。这是将其余根的较小的子节点进行比较,如果比子节点大,将较小的子节点移动到空穴中,并将空穴下移到这个较小的子节点处。如果比这个较小的子节点小,那么当前空穴即为该元素的最终位置。这个空穴下移的过程称为下滤,对应于PriorityQueue总的siftdown().
下图比较好地解释了这个过程。
BuildHeap:
BuildHeap有两种实现方式:
- 将所有的元素逐个Insert到堆中,因为插入一个元素的时间复杂度为O(logn),插入n个元素的时间复杂度为O(nlogn)
- 将所有元素赋值到堆中,在将这些元素从下往上逐步调成一个完整的堆,可以通过数学方法算出,这种实现方式的时间复杂度为O(n)
因为时间复杂地更低,通常使用第2种方式。
下图比较好地解释了这个过程。
关于堆的基础知识已经介绍完了,java的PriorityQueue的内部实现原理就是一个数组表示的最小堆,我们来看源码:
/**
* 一个基于堆数据结构的优先级队列;
* 优先级队列中的元素按照其自然序列排序或指定的Comparator对象进行排序,具体使用的排序方式取决于调用的构造方法;
* 不允许null元素;
* 如果不使用Comparator,那么元素本身必须实现Comparable接口的;
* 优先队列的头部元素为排序后的最小元素,如果最小元素有多个,可能是其中的任意一个;
* 自动扩容;
* 非线程安全;
* 入队和出队的时间复杂度为O(log(n));
* 删除指定元素removeAt(index)和查找指定元素的时间复杂度为O(n);
* iterator()返回的Iterator对象不保证对优先队列进行遍历的顺序(这个在源码注释中再细讲),推荐使用Arrays.sort(pq.toArray())}进行顺序遍历;
* @since 1.5
*/
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {
private static final long serialVersionUID = -7720805057305804111L;
// 默认初始化大小:11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 极限大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 表示一个最小堆(或称优先队列);
// 节点queue[n]的两个子节点为queue[2*n]和queue[2*(n+1)];
// 根节点为queue[0],也就是排序出来的最小值;
// 本队列按照指定的comparator排序,如果comparator为null,按元素的自然顺序排序;
// 这里所谓的自然排序是指类型本身实现了Comparable接口;
private transient Object[] queue;
private int size = 0;
// 指定的comparator,如果为null,按元素的自然顺序排序;
private final Comparator<? super E> comparator;
private transient int modCount = 0;
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
// SortedSet是排序好的,copy一份数据建堆即可
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
} else if (c instanceof PriorityQueue<?>) {
// 既然是以指定的集合本身就是一个优先队列,那么就直接复制元素就行了,不用再调整成一个最小堆
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
} else {
this.comparator = null;
// 堆的BuildHeap的完整流程,集合中的元素可能本身并没有排序好,除了复制元素之外,还要将所有的元素调整成一个最小堆
initFromCollection(c);
}
}
@SuppressWarnings("unchecked")
public PriorityQueue(PriorityQueue<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initFromPriorityQueue(c);
}
@SuppressWarnings("unchecked")
public PriorityQueue(SortedSet<? extends E> c) {
this.comparator = (Comparator<? super E>) c.comparator();
initElementsFromCollection(c);
}
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
if (c.getClass() == PriorityQueue.class) {
this.queue = c.toArray();
this.size = c.size();
} else {
initFromCollection(c);
}
}
// 将Collection中的元素全部克隆到队列中
private void initElementsFromCollection(Collection<? extends E> c) {
Object[] a = c.toArray();
// If c.toArray incorrectly doesn't return Object[], copy it.
if (a.getClass() != Object[].class)
a = Arrays.copyOf(a, a.length, Object[].class);
int len = a.length;
if (len == 1 || this.comparator != null)
for (int i = 0; i < len; i++)
if (a[i] == null)
throw new NullPointerException();
this.queue = a;
this.size = a.length;
}
// BuildHeap有两种实现方式:
// 1. 将所有的元素逐个Insert到堆中,因为插入一个元素的时间复杂度为O(logn),插入n个元素的时间复杂度为O(nlogn)
// 2. 将所有元素赋值到堆中,在将这些元素从下往上逐步调成一个完整的堆,可以通过数学方法算出,这种实现方式的时间复杂度为O(n)
// 该方法使用的是第2种实现方式
private void initFromCollection(Collection<? extends E> c) {
initElementsFromCollection(c);
// 将队列中的数据调整为一个优先队列(最小堆)
heapify();
}
// 扩容
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 容量 < 64的时候,扩容后2倍 + 2(这里为什么+2,应该是考虑优先级初始化时的大小可能为1,这里+2,让容量很小时,增长速度更快)
// 否则扩容后1.5倍
int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
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;
}
public boolean add(E e) {
return offer(e);
}
// 堆内添加指定元素,也就是堆的Insert操作
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
// 原来元素个数为0
if (i == 0)
queue[0] = e;
else
// 添加了新元素,当前空的地方(空穴)就是当前堆的最后一个位置,待找到具体位置的元素为添加的这个元素
// 这个过程为上滤
siftUp(i, e);
return true;
}
// 直接返回根节点元素即可
public E peek() {
if (size == 0)
return null;
return (E) queue[0];
}
// 顺序查找,时间复杂度为O(n),通过equals()判断元素相等
private int indexOf(Object o) {
if (o != null) {
for (int i = 0; i < size; i++)
if (o.equals(queue[i]))
return i;
}
return -1;
}
// 删除指定元素,时间复杂度为O(n),通过equals()判断元素相等
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
// 移除指定元素,时间复杂度为O(n),使用 == 来判断元素相等
boolean removeEq(Object o) {
for (int i = 0; i < size; i++) {
if (o == queue[i]) {
removeAt(i);
return true;
}
}
return false;
}
public boolean contains(Object o) {
return indexOf(o) != -1;
}
public Object[] toArray() {
return Arrays.copyOf(queue, size);
}
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(queue, size, a.getClass());
System.arraycopy(queue, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
public Iterator<E> iterator() {
return new Itr();
}
private final class Itr implements Iterator<E> {
private int cursor = 0;
private int lastRet = -1;
private E lastRetElt = null;
private int expectedModCount = modCount;
private ArrayDeque<E> forgetMeNot = null;
public boolean hasNext() {
return cursor < size || (forgetMeNot != null && !forgetMeNot.isEmpty());
}
public E next() {
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
if (cursor < size)
// 默认顺序遍历数组
return (E) queue[lastRet = cursor++];
if (forgetMeNot != null) {
// 顺序遍历数组到了最后,开始遍历之前未被遍历到的元素,也就是forgetMeNot双端队列中的元素
lastRet = -1;
lastRetElt = forgetMeNot.poll();
if (lastRetElt != null)
return lastRetElt;
}
throw new NoSuchElementException();
}
// 删除指定位置的元素,可能存在上滤的操作,导致之后需要遍历的元素被移动至之前已经遍历过的一个位置,
// 将这样的元素移至forgetMeNot双端队列中
public void remove() {
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
if (lastRet != -1) {
// 还在顺序遍历数组的过程
E moved = PriorityQueue.this.removeAt(lastRet);
lastRet = -1;
// 在removeAt()中没有siftup操作,这里直接将cursor回退即可(之后没有遍历的被移动元素被移动到了这个位置)
if (moved == null)
cursor--;
else {
// 在removeAt()中有siftup操作,将这个未被遍历的元素移动到forgetMeNot双端队列中,
// 备后续数组遍历完成后,再遍历这个双端队列中的元素
if (forgetMeNot == null)
forgetMeNot = new ArrayDeque<>();
forgetMeNot.add(moved);
}
} else if (lastRetElt != null) {
// 顺序遍历数组已经完成了,接下来就是处理forgetMeNot双端队列中的元素了
PriorityQueue.this.removeEq(lastRetElt);
lastRetElt = null;
} else {
throw new IllegalStateException();
}
expectedModCount = modCount;
}
}
public int size() {
return size;
}
public void clear() {
modCount++;
for (int i = 0; i < size; i++)
queue[i] = null;
size = 0;
}
// 取出根节点的元素,将剩下的元素调整成堆
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
// 第0个元素就是根节点
E result = (E) queue[0];
E x = (E) queue[s];
// help GC
queue[s] = null;
if (s != 0)
// 因为取出一个元素,空穴位置为0,堆变小
// 待找到具体位置的元素为删除前的堆最后一个元素
// 这个过程为下滤
siftDown(0, x);
return result;
}
// 一般情况下,返回null,如果存在上滤操作,返回上滤的元素
private E removeAt(int i) {
assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved);
// 这里为什么会有上滤,可以参考
// [In Java Priority Queue implementation remove at method, why it does a sift up after a sift down?]
// (https://stackoverflow.com/questions/38696556/in-java-priority-queue-implementation-remove-at-method-why-it-does-a-sift-up-af)
// 这里也是iterator()返回的Iterator对象不保证对优先队列进行遍历的顺序的原因
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
// 上滤,k为当前空穴位置,x为待找到具体位置的元素
// sift:筛选;过滤
private void siftUp(int k, E x) {
// 按照设置的排序规则进行上滤
// 如果设置了comparator,以comparator为准
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
// 这个实现与siftUpUsingComparator其实一样
private void siftUpComparable(int k, E x) {
// 如果没有comparator,那么元素本身就要实现Comparable接口
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
// 空穴的父结点位置
int parent = (k - 1) >>> 1;
Object e = queue[parent];
// x比空穴父结点大,当前空穴就是x的位置
if (comparator.compare(x, (E) e) >= 0)
break;
// x比该元素小,当前空穴被其父结点代替
queue[k] = e;
// 空穴上移
k = parent;
}
queue[k] = x;
}
// 下滤,k为当前空穴位置,x为待找到具体位置的元素
private void siftDown(int k, E x) {
// 按照设置的排序规则进行下滤(这个过程中空穴下移)
// 如果设置了comparator,以comparator为准
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
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) {
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;
}
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
// 左结点位置
int child = (k << 1) + 1;
Object c = queue[child];
// 右结点位置
int right = child + 1;
// 从左右两个子结点中选出一个较小的
if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
// 如果x比较小的元素更小,x的位置就为当前空穴位置
if (comparator.compare(x, (E) c) <= 0)
break;
// 这个较小的元素填到当前空穴位置
queue[k] = c;
// 空穴下移
k = child;
}
queue[k] = x;
}
// 已经有了数据,但是其中的数据是无序的,将其调整为堆,使用这种方式进行调整时间复杂度为O(n)
private void heapify() {
// 从高度为1的层开始,对每一个结点,将其本身和左右子堆调整为一个更大的堆(添加了当前结点)
for (int i = (size >>> 1) - 1; i >= 0; i--)
// 空穴位置为当前结点位置,待找到具体位置的元素为当前结点
siftDown(i, (E) queue[i]);
}
public Comparator<? super E> comparator() {
return comparator;
}
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
s.writeInt(Math.max(2, size + 1));
for (int i = 0; i < size; i++)
s.writeObject(queue[i]);
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
s.readInt();
queue = new Object[size];
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
heapify();
}
}
Java容器解析系列(9) PrioriyQueue详解的更多相关文章
- Java容器解析系列(11) HashMap 详解
本篇我们来介绍一个最常用的Map结构--HashMap 关于HashMap,关于其基本原理,网上对其进行讲解的博客非常多,且很多都写的比较好,所以.... 这里直接贴上地址: 关于hash算法: Ha ...
- Java容器解析系列(13) WeakHashMap详解
关于WeakHashMap其实没有太多可说的,其与HashMap大致相同,区别就在于: 对每个key的引用方式为弱引用; 关于java4种引用方式,参考java Reference 网上很多说 弱引用 ...
- Java容器解析系列(7) ArrayDeque 详解
ArrayDeque,从名字上就可以看出来,其是通过数组实现的双端队列,我们先来看其源码: /** 有自动扩容机制; 不是线程安全的; 不允许添加null; 作为栈使用时比java.util.Stac ...
- Java容器解析系列(14) IdentityHashMap详解
IdentityHashMap,使用什么的跟HashMap相同,主要不同点在于: 数据结构:使用一个数组table来存储 key:value,table[2k] 为key, table[2k + 1] ...
- Java容器解析系列(17) LruCache详解
在之前讲LinkedHashMap的时候,我们说起可以用来实现LRU(least recent used)算法,接下来我看一下其中的一个具体实现-----android sdk 中的LruCache. ...
- Java容器解析系列(12) LinkedHashMap 详解
LinkedHashMap继承自HashMap,除了提供HashMap的功能外,LinkedHashMap还是维护一个双向链表(实际为带头结点的双向循环链表),持有所有的键值对的引用: 这个双向链表定 ...
- Java容器解析系列(0) 开篇
最近刚好学习完成数据结构与算法相关内容: Data-Structures-and-Algorithm-Analysis 想结合Java中的容器类加深一下理解,因为之前对Java的容器类理解不是很深刻, ...
- Java容器解析系列(10) Map AbstractMap 详解
前面介绍了List和Queue相关源码,这篇开始,我们先来学习一种java集合中的除Collection外的另一个分支------Map,这一分支的类图结构如下: 这里为什么不先介绍Set相关:因为很 ...
- Java容器解析系列(6) Queue Deque AbstractQueue 详解
首先我们来看一下Queue接口: /** * @since 1.5 */ public interface Queue<E> extends Collection<E> { / ...
随机推荐
- 【搬运工】修改mysql数据库的时区
转载:https://blog.csdn.net/huangyuehong914/article/details/81742039 --------------------- ------------ ...
- MySQL单表多次查询和多表联合查询,哪个效率高?
很多高性能的应用都会对关联查询进行分解. 简单地,可以对每个表进行一次单表查询,然后将结果在应用程序中进行关联.例如,下面这个查询: select * from tag join tag_post o ...
- Laravel Not Found Exceptions 取数据的一个小技巧
从 model 中取数据的时候, 用下面的方法, 而不是简单的 find(1), 或 first(), 这样如果刚好要查询的数据没有, 就会返回比较友好的 404 页面: $model = App\F ...
- Hadoop之搭建完全分布式运行模式
一.过程分析 1.准备3台客户机(关闭防火墙.修改静态ip.主机名称) 2.安装JDK 3.配置环境变量 4.安装Hadoop 5.配置集群 6.单点启动 7.配置ssh免密登录 8.群起并测试集群 ...
- Mybatis日志
[DEBUG] 2018-05-20 09:56:36,404(19404) --> [main] org.slf4j.impl.JCLLoggerAdapter.trace(JCLLogger ...
- 基于redis的分布式锁(转)
基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...
- js 空语句
不写就行了 ){}
- C# Selenium 破解腾讯滑动验证
什么是Selenium? WebDriver是主流Web应用自动化测试框架,具有清晰面向对象 API,能以最佳的方式与浏览器进行交互. 支持的浏览器: Mozilla Firefox Google C ...
- 给web请求加遮罩动画
效果预览: css: /*#fountainG{ position:relative; margin:10% auto; width:240px; height:29px }*/ #fountainG ...
- [python]Git
Git 修改默认编辑器 git config –global core.editor vim 提交发生变化得文件 # 提交新文件(new)和被修改(modified)文件,不包括被删除(deleted ...