【由浅入深理解java集合】(四)——集合 Queue
今天我们来介绍下集合Queue中的几个重要的实现类。关于集合Queue中的内容就比较少了。主要是针对队列这种数据结构的使用来介绍Queue中的实现类。
Queue用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
这种结构就如同我们生活中的排队一样。
下面我们就来介绍Queue中的一个重要的实现类PriorityQueue。
PriorityQueue
PriorityQueue保存队列元素的顺序不是按加入队列的顺序,而是按队列元素的大小进行重新排序。因此当调用peek()或pool()方法取出队列中头部的元素时,并不是取出最先进入队列的元素,而是取出队列中的最小的元素。
PriorityQueue的排序方式
PriorityQueue中的元素可以默认自然排序(也就是数字默认是小的在队列头,字符串则按字典序排列)或者通过提供的Comparator(比较器)在队列实例化时指定的排序方式。关于自然排序与Comparator(比较器)可以参考我在介绍集合Set时的讲解。
注意:队列的头是按指定排序方式的最小元素。如果多个元素都是最小值,则头是其中一个元素——选择方法是任意的。
注意:当PriorityQueue中没有指定Comparator时,加入PriorityQueue的元素必须实现了Comparable接口(即元素是可比较的),否则会导致 ClassCastException。
下面具体写个例子来展示PriorityQueue中的排序方式:
PriorityQueue<Integer> qi = new PriorityQueue<Integer>();
qi.add(5);
qi.add(2);
qi.add(1);
qi.add(10);
qi.add(3);
while (!qi.isEmpty()){
System.out.print(qi.poll() + ",");
}
System.out.println();
//采用降序排列的方式,越小的越排在队尾
Comparator<Integer> cmp = new Comparator<Integer>() {
public int compare(Integer e1, Integer e2) {
return e2 - e1;
}
};
PriorityQueue<Integer> q2 = new PriorityQueue<Integer>(5,cmp);
q2.add(2);
q2.add(8);
q2.add(9);
q2.add(1);
while (!q2.isEmpty()){
System.out.print(q2.poll() + ",");
}
输出结果:
1,2,3,5,10,
9,8,2,1,
由此可以看出,默认情况下PriorityQueue采用自然排序。指定Comparator的情况下,PriorityQueue采用指定的排序方式。
PriorityQueue的方法
PriorityQueue实现了Queue接口,下面列举出PriorityQueue的方法。
PriorityQueue的本质
PriorityQueue 本质也是一个动态数组,在这一方面与ArrayList是一致的。
PriorityQueue调用默认的构造方法时,使用默认的初始容量(DEFAULT_INITIAL_CAPACITY=11
)创建一个 PriorityQueue,并根据其自然顺序来排序其元素(使用加入其中的集合元素实现的Comparable)。
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
当使用指定容量的构造方法时,使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序来排序其元素(使用加入其中的集合元素实现的Comparable)。
public PriorityQueue(int initialCapacity) {
this(initialCapacity, null);
}
当使用指定的初始容量创建一个 PriorityQueue,并根据指定的比较器comparator来排序其元素。
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
从第三个构造方法可以看出,内部维护了一个动态数组。当添加元素到集合时,会先检查数组是否还有余量,有余量则把新元素加入集合,没余量则调用 grow()
方法增加容量,然后调用siftUp
将新加入的元素排序插入对应位置。
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;
}
除此之外,还要注意:
①PriorityQueue不是线程安全的。如果多个线程中的任意线程从结构上修改了列表, 则这些线程不应同时访问 PriorityQueue 实例,这时请使用线程安全的PriorityBlockingQueue 类。
②不允许插入 null 元素。
③PriorityQueue实现插入方法(offer、poll、remove() 和 add 方法) 的时间复杂度是O(log(n)) ;实现 remove(Object) 和 contains(Object) 方法的时间复杂度是O(n) ;实现检索方法(peek、element 和 size)的时间复杂度是O(1)。所以在遍历时,若不需要删除元素,则以peek的方式遍历每个元素。
④方法iterator()中提供的迭代器并不保证以有序的方式遍历优PriorityQueue中的元素。
Dueue接口与ArrayDeque实现类
Dueue接口
Deque接口是Queue接口的子接口,它代表一个双端队列。LinkedList也实现了Deque接口,所以也可以被当作双端队列使用。也可以看到前面对LinkedList的介绍来理解Deque接口。
因此Deque接口增加了一些关于双端队列操作的方法。
void addFirst(E e):将指定元素插入此列表的开头。
void addLast(E e): 将指定元素添加到此列表的结尾。
E getFirst(E e): 返回此列表的第一个元素。
E getLast(E e): 返回此列表的最后一个元素。
boolean offerFirst(E e): 在此列表的开头插入指定的元素。
boolean offerLast(E e): 在此列表末尾插入指定的元素。
E peekFirst(E e): 获取但不移除此列表的第一个元素;如果此列表为空,则返回 null。
E peekLast(E e): 获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。
E pollFirst(E e): 获取并移除此列表的第一个元素;如果此列表为空,则返回 null。
E pollLast(E e): 获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。
E removeFirst(E e): 移除并返回此列表的第一个元素。
boolean removeFirstOccurrence(Objcet o): 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
E removeLast(E e): 移除并返回此列表的最后一个元素。
boolean removeLastOccurrence(Objcet o): 从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。
从上面方法中可以看出,Deque不仅可以当成双端队列使用,而且可以被当成栈来使用,因为该类里还包含了pop(出栈)、push(入栈)两个方法。
Deque与Queue、Stack的关系
当 Deque 当做 Queue队列使用时(FIFO),添加元素是添加到队尾,删除时删除的是头部元素。从 Queue 接口继承的方法对应Deque 的方法如图所示:
Deque 也能当Stack栈用(LIFO)。这时入栈、出栈元素都是在 双端队列的头部 进行。Deque 中和Stack对应的方法如图所示:
注意:Stack过于古老,并且实现地非常不好,因此现在基本已经不用了,可以直接用Deque来代替Stack进行栈操作。
ArrayDeque
顾名思义,就是用数组实现的Deque;既然是底层是数组那肯定也可以指定其capacity,也可以不指定,默认长度是16,然后根据添加的元素的个数,动态扩展。ArrayDeque由于是两端队列,所以其顺序是按照元素插入数组中对应位置产生的(下面会具体说明)。
由于本身数据结构的限制,ArrayDeque没有像ArrayList中的trimToSize方法可以为自己瘦身。ArrayDeque的使用方法就是上面的Deque的使用方法,基本没有对Deque拓展什么方法。
ArrayDeque的本质
循环数组
ArrayDeque为了满足可以同时在数组两端插入或删除元素的需求,其内部的动态数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。
ArrayDeque维护了两个变量,表示ArrayDeque的头和尾
transient int head;
transient int tail;
当向头部插入元素时,head下标减一然后插入元素。而 tail表示的索引为当前末尾元素表示的索引值加一。若当向尾部插入元素时,直接向tail表示的位置插入,然后tail再减一。
具体以下面的图片为例解释。
在上图中:左边图表示从头部插入了4个元素,尾部插入了2个。初始的时候,head=0,tail=0。当从头部插入元素5,head-1,由于数组是循环数组,则移动到数组的最后位置插入5。当从头部插入元素34,head-1然后再对应位置插入。下面以此类推,最后在头部插入4个元素。当在尾部插入12时,直接在0的位置插入,然后tail=tail+1=1,当从尾部插入7时,直接在1的位置插入,然后tail = tail +1=2。最后队列中的输出顺序是8,3,34,5, 12, 7。
把数组看成一个首尾相接的圆形数组更好理解循环数组的含义。
下面具体看看ArrayDeque怎么把循环数组实际应用的?
addFirst(E e)
为例来研究
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
当加入元素时,先看是否为空(ArrayDeque不可以存取null元素,因为系统根据某个位置是否为null来判断元素的存在)。然后head-1插入元素。head = (head - 1) & (elements.length - 1)
很好的解决了下标越界的问题。这段代码相当于取模,同时解决了head为负值的情况。因为elements.length必需是2的指数倍(代码中有具体操作),elements - 1就是二进制低位全1,跟head - 1相与之后就起到了取模的作用。如果head - 1为负数,其实只可能是-1,当为-1时,和elements.length - 1进行与操作,这时结果为elements.length - 1。其他情况则不变,等于它本身。
当插入元素后,在进行判断是否还有余量。因为tail总是指向下一个可插入的空位,也就意味着elements数组至少有一个空位,所以插入元素的时候不用考虑空间问题。
下面再说说扩容函数doubleCapacity(),其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示:
图中我们看到,复制分两次进行,第一次复制head右边的元素,第二次复制head左边的元素。
//doubleCapacity()
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // head右边元素的个数
int newCapacity = n << 1;//原空间的2倍
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分
System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分
elements = (E[])a;
head = 0;
tail = n;
}
由此,我们便理解了ArrayDeque循环数组添加以及扩容的过程,其他操作类似。
注意:ArrayDeque不是线程安全的。 当作为栈使用时,性能比Stack好;当作为队列使用时,性能比LinkedList好。
以上就是关于集合Queue的介绍。
由浅入深理解java集合(一)——集合框架 Collction、Map
由浅入深理解java集合(二)——集合 Set
由浅入深理解java集合(三)——集合 List
由浅入深理解java集合(五)——集合 Map
由浅入深理解java集合(六)——集合增删改查的细节、性能及选择推荐(待更新)
【由浅入深理解java集合】(四)——集合 Queue的更多相关文章
- 【由浅入深理解java集合】(五)——集合 Map
前面已经介绍完了Collection接口下的集合实现类,今天我们来介绍Map接口下的两个重要的集合实现类HashMap,TreeMap.关于Map的一些通用介绍,可以参考第一篇文章.由于Map与Lis ...
- 【由浅入深理解java集合】(三)——集合 List
第一篇文章中介绍了List集合的一些通用知识.本篇文章将集中介绍List集合相比Collection接口增加的一些重要功能以及List集合的两个重要子类ArrayList及LinkedList. 一. ...
- 【由浅入深理解java集合】(二)——集合 Set
上一篇文章介绍了Set集合的通用知识.Set集合中包含了三个比较重要的实现类:HashSet.TreeSet和EnumSet.本篇文章将重点介绍这三个类. 一.HashSet类 HashSet简介 H ...
- 【由浅入深理解java集合】(一)——集合框架 Collction、Map
本篇文章主要对java集合的框架进行介绍,使大家对java集合的整体框架有个了解.具体介绍了Collection接口,Map接口以及Collection接口的三个子接口Set,List,Queue. ...
- 万字长文深入理解java中的集合-附PDF下载
目录 1. 前言 2. List 2.1 fail-safe fail-fast知多少 2.1.1 Fail-fast Iterator 2.1.2 Fail-fast 的原理 2.1.3 Fail- ...
- 由浅入深理解Java线程池及线程池的如何使用
前言 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担.线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory ...
- 深入理解Java虚拟机(四)——HotSpot垃圾收集器详解
垃圾收集器 新生代收集器 1.Serial收集器 特点: 单线程工作,收集的时候就会停止其他所有工作线程,用户不可知不可控,会使得用户界面出现停顿. 简单高效,是所有收集器中额外内存消耗最少的. 没有 ...
- 由浅入深理解----java反射技术
java反射机制详解 java反射机制是在运行状态下,对任意一个类可以获取该类的属性和方法,对任意一个对象可以调用其属性和方法.这种动态的获取信息和调用对象的方法的功能称为java的反射机制 clas ...
- 理解java集合——集合框架 Collection、Map
1.概述: @white Java集合就像一种容器,可以把多个对象(实际上是对象的引用,但习惯上都称对象)"丢进"该容器中. 2.Java集合大致可以分4类: @white Set ...
随机推荐
- Photoshop调出外景婚片蓝色小清新艺术效果
春季婚纱旺季来了,好多童鞋给我抱怨说客片太难转色了,春天的小清新感都转不了,其实并不难,运用好互补色来进行加减色,能很快调整好照片的偏色,互补色也可称为对比色,后期调色的加也可称为减,如加蓝=减黄.加 ...
- javascript高级排序算法之快速排序(快排)
javascript高级排序算法之快速排序(快排)我们之前讨论了javascript基本排序算法 冒泡排序 选择排序 插入排序 简单复习: 冒泡排序: 比较相邻的两个元素,如果前一个比后一个大,则交换 ...
- mysql的时区错误问题: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one
问题:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone.. ...
- [模板] 回文树/回文自动机 && BZOJ3676:[Apio2014]回文串
回文树/回文自动机 放链接: 回文树或者回文自动机,及相关例题 - F.W.Nietzsche - 博客园 状态数的线性证明 并没有看懂上面的证明,所以自己脑补了一个... 引理: 每一个回文串都是字 ...
- element-ui和npm、webpack、vue-cli搭建Vue项目
一.element-ui的简单使用 1.安装 1. npm 安装 推荐使用 npm 的方式安装,它能更好地和 webpack 打包工具配合使用. npm i element-ui -S 2. CDN ...
- saltstack之自动化运维
引入 简介 saltstack是由thomas Hatch于2011年创建的一个开源项目,设计初衷是为了实现一个快速的远程执行系统. 早期运维人员会根据自己的生产环境来写特定脚本完成大量重复性工作,这 ...
- ☆ [POI2007] ZAP-Queries 「莫比乌斯反演」
题目类型:莫比乌斯反演 传送门:>Here< 题意:求有多少对正整数对\((a,b)\),满足\(0<a<A\),\(0<b<B\),\(gcd(a,b)=d\) ...
- Codeforces 1082C Multi-Subject Competition(前缀+思维)
题目链接:Multi-Subject Competition 题意:给定n名选手,每名选手都有唯一选择的科目si和对应的能力水平.并且给定科目数量为m.求选定若干个科目,并且每个科目参与选手数量相同的 ...
- 内网ntp时间同步配置
选择局域网中的一台机器作为ntp服务器,在ntp server上安装并启动ntpd客户端上要关闭ntpd,安装ntpdateCentOS7上这两个软件都是自带的,只需根据需要打开或者关闭.注意客户端机 ...
- 测试框架httpclent 2.配置优化方法
优化就是为了使代码看起来更简便,如果代码里面的每一个请求都写一次url,那么整体代码看起来很乱,而且一旦某个服务器的端口号或者域名有变动,那么所有的url都需要改变,成本太大.为了让代码看起来更简便, ...