java集合类型源码解析之PriorityQueue
本来第二篇想解析一下LinkedList,不过扫了一下源码后,觉得LinkedList的实现比较简单,没有什么意思,于是移步PriorityQueue。
PriorityQueue通过数组实现了一个堆数据结构(相当于一棵完全二叉树),元素的优先级可以通过一个Comparator来计算,如果不指定Comparator,那么元素类型应该实现Comparable接口。最终compare得出的最小元素,放在堆的根部。
成员变量
public class PriorityQueue<E> extends AbstractQueue<E> {
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
}
PriorityQueue的成员变量和ArrayList高度类似:
- queue 元素存储空间数组,代表一棵完全二叉树,索引位置n的节点的左右子树分别为(2n+1)和(2n+2);
- size 元素数量
- modCount 队列修改追踪标记
- comparator 优先级比较器,可以为null
堆算法
PriorityQueue最重要的部分就是维护堆的几个方法,它基本实现了《算法导论》介绍的堆算法。
1、插入元素siftDown
假定k位置的左右子树都是堆,siftDown方法把元素x插入位置k,然后对这个子堆进行调整,保证以k为根的子树也是堆。视comparator是否存在,siftDown使用siftDownUsingComparator或siftDownComparable,它们只是比较元素的方式不一样,结构是完全一致的,这里就只解读siftDownComparable。
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) {
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;
}
- 先计算一个half位置,因为这个位置之后就是叶节点,不需要继续往下;
- 计算k的左右子节点,找出compare值最小的节点;
- 如果就是x,那么k就是插入位置;
- 如果是子节点,子节点value上移,k指向子节点位置
- 继续尝试向下探索
2、插入元素siftUp
往k位置插入值x,siftUp采用的方式是往树根方向移动,将祖先节点compare值比较大的往下交换。
siftup的用途要少一点。
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) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
- 如果k>0,那么k不是树根,就可以继续往上探索;
- 找到k的父节点parent,比较x和parent的值
- 如果x>=parent,说明x放在k,以parent为根是一个子堆;
- 否则将parent放入k位置,k指向parent,继续探索
3、建堆
@SuppressWarnings("unchecked")
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
从中间索引位置开始(往后的都是叶节点),往树根方向遍历,对每个位置调用siftDown。
- i的初始值,由于只有一层叶子节点,所以满足
左右子树都是子堆的条件,siftDown使得,以i为树根的节点也是子堆; - 当i变小时,由于k>i的子树已经是堆,那么必然i的左右子树都是堆,所以siftDown使得,以i为树根的节点也是子堆;
- 最终siftDown(0,queue[0])使得整棵树成为一个堆。
offer和poll
现在可以来看看queue最常见的两个操作:offer和poll。
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;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
offer先把一个对象放入队列的末尾,前面的空间检查及增长和ArrayList极为类似。
i就是插入位置,如果i==0,queue是空的,插入就完事了;否则通过siftUp来插入,因为i是叶节点,所以可以认为i子树是一个子堆,siftup保证e会往树根方向,找到一个合适的位置,使整棵树保持了堆的特性。
public E poll() {
if (size == 0)
return null;
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;
}
poll取出树根元素作为返回值,然后将最后一个元素取出来,通过siftDown插入到树根位置,前面讲过siftDown的性质,这个操作使得整棵树仍然保持堆特性。
remove操作
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
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);
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
这个private的removeAt操作比较有意思,它执行的操作是删除第i个节点(存储空间的第i个,而不是优先级的第i个,这块容易引起人的误解,所以没有类似的public方法)。
- 如果删除的是最后一个元素,直接置为null即可;
- 否则把最后一个元素取出来,插入到i位置
- 插入的过程是先siftDown,如果siftDown的最终位置就是i,那么说明move比i的子树元素都小,此时再尝试一下siftUp;否则siftUp是不需要执行的;
- 当siftUp执行的结果是末尾元素,被移动到了i之前,那么返回这个元素,其他情况都返回null
这个返回值也是出人意料,不是返回删除的元素,而是在保持堆特性的过程中,如果有尾部元素被移动到i之前的位置,就返回它。这纯粹是为了帮助PriorityQueue的迭代器实现,下一节马上解释。
迭代器
首先要明确一点,PriorityQueue的迭代器并不按优先级顺序来遍历元素,主要就是按存储顺序来遍历,先看迭代器的成员
private final class Itr implements Iterator<E> {
private int cursor = 0;
private int lastRet = -1;
private int expectedModCount = modCount;
private ArrayDeque<E> forgetMeNot = null;
private E lastRetElt = null;
}
cursor、lastRet、expectedModCount的作用和ArrayList的迭代器完全一致;但是多出来的forgetMeNot和lastRetElt让人有点莫民奇妙。
再看看remove方法的实现:
public void remove() {
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
if (lastRet != -1) {
E moved = PriorityQueue.this.removeAt(lastRet);
lastRet = -1;
if (moved == null)
cursor--;
else {
if (forgetMeNot == null)
forgetMeNot = new ArrayDeque<>();
forgetMeNot.add(moved);
}
} else if (lastRetElt != null) {
PriorityQueue.this.removeEq(lastRetElt);
lastRetElt = null;
} else {
throw new IllegalStateException();
}
expectedModCount = modCount;
}
如果lastRet有效,那么调用PriorityQueued.removeAt(lastRet)来删除元素,通过上一节我们知道,removeAt方法可能导致某个元素从末尾被移动到lastRet前面,这样的话,迭代器就会丢失这个元素。为了解决这个问题,迭代器把这个元素放到了一个临时ArrayDeque里面。
这样如果lastRet没有指向有效的元素,那么有可能正在遍历ArrayDeque里面的元素,此时通过lastRetElt来指向。
再看next方法就很容易明白了
public E next() {
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
if (cursor < size)
return (E) queue[lastRet = cursor++];
if (forgetMeNot != null) {
lastRet = -1;
lastRetElt = forgetMeNot.poll();
if (lastRetElt != null)
return lastRetElt;
}
throw new NoSuchElementException();
}
先顺着cursor遍历,再把forgetMeNot里面的元素遍历一遍。
java集合类型源码解析之PriorityQueue的更多相关文章
- java集合类型源码解析之ArrayList
前言 作为一个老码农,不仅要谈架构.谈并发,也不能忘记最基础的语言和数据结构,因此特开辟这个系列的文章,争取每个月写1~2篇关于java基础知识的文章,以温故而知新. 如无特别之处,这个系列文章所使用 ...
- Java集合-ArrayList源码解析-JDK1.8
◆ ArrayList简介 ◆ ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAcc ...
- Java集合---LinkedList源码解析
一.源码解析1. LinkedList类定义2.LinkedList数据结构原理3.私有属性4.构造方法5.元素添加add()及原理6.删除数据remove()7.数据获取get()8.数据复制clo ...
- java集合-HashSet源码解析
HashSet 无序集合类 实现了Set接口 内部通过HashMap实现 // HashSet public class HashSet<E> extends AbstractSet< ...
- java集合-HashMap源码解析
HashMap 键值对集合 实现原理: HashMap 是基于数组 + 链表实现的. 通过hash值计算 数组索引,将键值对存到该数组中. 如果多个元素hash值相同,通过链表关联,再头部插入新添加的 ...
- java基础类型源码解析之String
差点忘了最常用的String类型,我们对String的大多数方法都已经很熟了,这里就挑几个平时不会直接接触的点来解析一下. 先来看看它的成员变量 public final class String { ...
- java基础类型源码解析之HashMap
终于来到比较复杂的HashMap,由于内部的变量,内部类,方法都比较多,没法像ArrayList那样直接平铺开来说,因此准备从几个具体的角度来切入. 桶结构 HashMap的每个存储位置,又叫做一个桶 ...
- 【java集合框架源码剖析系列】java源码剖析之TreeSet
本博客将从源码的角度带领大家学习TreeSet相关的知识. 一TreeSet类的定义: public class TreeSet<E> extends AbstractSet<E&g ...
- 【java集合框架源码剖析系列】java源码剖析之ArrayList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...
随机推荐
- Tomcat安装及环境配置
欢迎任何形式的转载,但请务必注明出处. 本章内容 安装 环境变量入口 三个系统变量配置 测试安装配置是否成功 安装之前请安装jdk并进行环境配置(点击进入jdk教程) 一.安装 点击进入官网下载 二. ...
- 【robotframework】robotframework基本使用
一.创建项目 1.创建测试项目 选择菜单栏 file----->new Project Name 输入项目名称:Type 选择 Directory. 2.创建测试套件 右键点击“测试项目”选择 ...
- Typora数学公式
LaTeX编辑数学公式基本语法元素 LaTeX中的数学模式有两种形式: inline 和 display. 前者是指在正文插入行间数学公式,后者独立排列,可以有或没有编号. 行间公式(inline) ...
- SpringBoot下,@WebFilter配置获取日志
CREATE TABLE [dbo].[SWEBSERVICELOG]( [WLG_ID] [varchar](100) NOT NULL, [WLG_SESSIONID] [varchar](100 ...
- 5.Kafka消费者-从Kafka读取数据(转)
http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/14/kafka-consumer.ht ...
- aiops相关
AIOPS的能力框架 AIOps平台能力体系 AIOps 常见应用场景 按照时间来分 AIOPS实施的关键技术 1.数据采集(硬件,业务指标等) 2.数据预处理(特征工程) 3.数据可视化 4.数据存 ...
- canvas圆形进度条(逆时针)
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8& ...
- 0、Python学习路线
阶段一.Python语言(熟练掌握Python多线程并发编程技术,可以编写爬虫程序和语音识别软件.) 1.1 基础语法 1.1.1 python概述 1.1.2 数据的存储 1.1.3 ...
- 哇!吐槽!oh shit
一个jsp写了5000行,我尼玛醉了,看晕了-2017年10月12日10:19:40
- pygame游戏图像绘制及精灵用法
1精灵文件 plane_sprites.py import pygame class GameSprite(pygame.sprite.Sprite): """飞机大战游 ...