【Java 数据结构及算法实战】系列 014:Java队列08——数组实现的双端队列ArrayDeque

简介: 【Java 数据结构及算法实战】系列 014:Java队列08——数组实现的双端队列ArrayDeque

ArrayDeque是基于数组实现的无界双端队列。ArrayDeque中的数组没有容量限制,它们能根据需要增长以支持使用。需要注意的是ArrayDeque不是线程安全的,因此在没有外部同步的情况下,它们不支持多线程并发访问。

ArrayDeque用作栈时可能比Stack更快,用作队列时可能比LinkedList更快。

ArrayDeque禁止插入空元素。

ArrayDeque及其迭代器实现了Collection和Iterator接口的所有可选方法。

ArrayDeque是Java Collections Framework的一个成员。

1.   ArrayDeque的声明

ArrayDeque的接口和继承关系如下

publicclass ArrayDeque<E> extends AbstractCollection<E>

     implements Deque<E>, Cloneable, Serializable

  …

}

完整的接口继承关系如下图所示。

image.gif编辑

从上述代码可以看出,ArrayDeque既实现了java.util.Deque<E> 、java.lang.Cloneable、java.io.Serializable接口,又继承了java.util.AbstractCollection<E>。

2.   ArrayDeque的成员变量和构造函数

以下是ArrayDeque的构造函数和成员变量。

// 元素数组

   transient Object[] elements;

// 队列头索引

   transientint head;

// 队列尾索引

   transientint tail;

// 数组最大容量

   privatestaticfinalintMAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

public ArrayDeque() {

       elements = new Object[16 + 1];

   }

   public ArrayDeque(int numElements) {

       elements =

           new Object[(numElements < 1) ? 1 :

                      (numElements == Integer.MAX_VALUE) ? Integer.MAX_VALUE :

                      numElements + 1];

   }

   public ArrayDeque(Collection<? extends E> c) {

       this(c.size());

       copyElements(c);

}

从上述代码可以看出,构造函数有3种。构造函数中的参数含义如下

l  numElements用于设置队列中内部数组的元素总数。如果没有指定,则会使用默认元素总数16。需要注意的是,实际数组的大小,是numElements+1。

l  c用于设置最初包含给定集合的元素,按集合迭代器的遍历顺序添加

类成员elements是一个数组,用于存储队列中的元素。head和tail分别表示队头索引和队尾索引。

思考:为什么实际数组的实际数组的大小,是numElements+1?

3.   ArrayDeque的核心方法

以下对ArrayDeque常用核心方法的实现原理进行解释。

3.1.     addLast(e)

执行addLast(e)方法后有两种结果

l  队列未达到容量时,直接插入,没有返回值

l  队列达到容量时,先扩容,再插入,没有返回值

ArrayDeque的addLast(e)方法源码如下:

   publicvoid addLast(E e) {

       if (e == null)  // 判空

           thrownew NullPointerException();

       final Object[] es = elements;

       es[tail] = e;

       if (head == (tail = inc(tail, es.length)))

           grow(1);  // 扩容

   }

从上面代码可以看出,addLast(e)方法会先进行判空处理,而后再将元素插入。如果插入前判断容量不够,则会执行grow()方法进行扩容。

grow()方法源码如下:

privatevoid grow(int needed) {

       // overflow-conscious code

       finalint oldCapacity = elements.length;

       int newCapacity;

       // Double capacity if small; else grow by 50%

       int jump = (oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1);

       if (jump < needed

           || (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE > 0)

           newCapacity = newCapacity(needed, jump);

       final Object[] es = elements = Arrays.copyOf(elements, newCapacity);

       // Exceptionally, here tail == head needs to be disambiguated

       if (tail < head || (tail == head && es[head] != null)) {

           // wrap around; slide first leg forward to end of array

           int newSpace = newCapacity - oldCapacity;

           System.arraycopy(es, head,

                            es, head + newSpace,

                            oldCapacity - head);

           for (int i = head, to = (head += newSpace); i < to; i++)

               es[i] = null;

       }

   }

   /** Capacity calculation for edge conditions, especially overflow. */

   privateint newCapacity(int needed, int jump) {

       finalint oldCapacity = elements.length, minCapacity;

       if ((minCapacity = oldCapacity + needed) - MAX_ARRAY_SIZE > 0) {

           if (minCapacity < 0)

               thrownew IllegalStateException("Sorry, deque too big");

           return Integer.MAX_VALUE;

       }

       if (needed > jump)

           return minCapacity;

       return (oldCapacity + jump - MAX_ARRAY_SIZE < 0)

           ? oldCapacity + jump

           : MAX_ARRAY_SIZE;

   }

3.2.     offerLast(e)

执行offerLast(e)方法后有两种结果

l  队列未达到容量时,返回 true

l  队列达到容量时,先扩容,再返回 true

ArrayDeque的offerLast(e)方法源码如下:

publicboolean offerLast(E e) {

       addLast(e);

       returntrue;

   }

从上面代码可以看出,执行offerLast(e)方法直接调用的是addLast(e)

3.3.     addLast(e)

执行addFirst(e)方法后有两种结果

l  队列未达到容量时,直接插入,没有返回值

l  队列达到容量时,先扩容,再插入,没有返回值

ArrayDeque的addFirst(e)方法源码如下:

   publicvoid addFirst(E e) {

       if (e == null)  // 判空

           thrownew NullPointerException();

       final Object[] es = elements;

       es[head = dec(head, es.length)] = e;

       if (head == tail)

           grow(1);  // 扩容

   }

从上面代码可以看出,addFirst(e)方法会先进行判空处理,而后再将元素插入。如果插入前判断容量不够,则会执行grow()方法进行扩容。

3.4.     pollFirst()

执行pollFirst()方法后有两种结果:

l  队列不为空时,返回队首值并移除

l  队列为空时,返回 null

ArrayDeque的pollFirst()方法源码如下:

public E pollFirst() {

       final Object[] es;

       finalint h;

       E e = elementAt(es = elements, h = head);

       if (e != null) {

           es[h] = null;

           head = inc(h, es.length);

       }

       return e;

}

从上面代码可以看出,执行pollFirst()方法时,分为以下几个步骤:

l  先取队列的队首元素。

l  如果队首元素不存在,直接返回null。

l  如果队首元素存在,则返回该元素同时移除元素。

3.5.     removeFirst()

执行removeFirst()方法后有两种结果:

l  队列不为空时,返回队首值并移除

l  队列为空时,抛出异常

ArrayDeque的removeFirst()方法源码如下:

public E removeFirst() {

       E e = pollFirst();

       if (e == null)

           thrownew NoSuchElementException();

       return e;

}

从上面代码可以看出,removeFirst()方法直接调用了pollFirst()方法。如果pollFirst()方法返回结果为null,则抛出NoSuchElementException异常。

pollFirst()方法此处不再赘述。

3.6.     peekFirst()

执行peekFirst()方法后有两种结果:

l  队列不为空时,返回队首值但不移除

l  队列为空时,返回null

peekFirst()方法源码如下:

public E peekFirst() {

       returnelementAt(elements, head);

}

staticfinal <E> E elementAt(Object[] es, int i) {

       return (E) es[i];

}

从上面代码可以看出,peekFirst()方法比较简单,直接就是获取了数组里面的索引为head的元素。

3.7.     getFirst()

执行getFirst()方法后有两种结果:

l  队列不为空时,返回队首值但不移除

l  队列为空时,抛出异常

getFirst()方法源码如下:

public E getFirst() {

       E e = elementAt(elements, head);

       if (e == null)

           thrownew NoSuchElementException();

       return e;

}

从上面代码可以看出,执行getFirst()方法时,先是获取了数组里面的索引为head的元素。如果结果是null,则抛出NoSuchElementException异常。

4.   ArrayDeque的单元测试

ArrayDeque的单元测试如下:

package com.waylau.java.demo.datastructure;

importstatic org.junit.jupiter.api.Assertions.assertEquals;

importstatic org.junit.jupiter.api.Assertions.assertNull;

importstatic org.junit.jupiter.api.Assertions.assertThrows;

importstatic org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayDeque;

import java.util.Deque;

import java.util.NoSuchElementException;

import org.junit.jupiter.api.Test;

/**

* ArrayDeque Tests

*

* @since 1.0.0 2020年5月3日

* @author <a href="https://waylau.com">Way Lau</a>

*/

class ArrayDequeTests {

   @Test

   void testAddLast() {

       // 初始化队列

       Deque<String> queue = new ArrayDeque<String>(3);

       // 测试队列未满时,直接插入没有返回值;

       queue.addLast("Java");

       // 测试队列满则扩容

       queue.addLast("C");

       queue.addLast("Python");

       queue.addLast("C++"); // 扩容

   }

   @Test

   void testOfferLast() {

       // 初始化队列

       Deque<String> queue = new ArrayDeque<String>(3);

       // 测试队列未满时,返回 true

       boolean resultNotFull = queue.offerLast("Java");

       assertTrue(resultNotFull);

       // 测试队列达到容量时,会自动扩容

       queue.offerLast("C");

       queue.offerLast("Python");

       boolean resultFull = queue.offerLast("C++"); // 扩容

       assertTrue(resultFull);

   }

   @Test

   void testAddFirst() {

       // 初始化队列

       Deque<String> queue = new ArrayDeque<String>(3);

       // 测试队列未满时,直接插入没有返回值;

       queue.addFirst("Java");

       // 测试队列满则扩容

       queue.addFirst("C");

       queue.addFirst("Python");

       queue.addFirst("C++"); // 扩容

   }

   @Test

   void testPollFirst() throws InterruptedException {

       // 初始化队列

       Deque<String> queue = new ArrayDeque<String>(3);

       // 测试队列为空时,返回 null

       String resultEmpty = queue.pollFirst();

       assertNull(resultEmpty);

       // 测试队列不为空时,返回队首值并移除

       queue.addLast("Java");

       queue.addLast("C");

       queue.addLast("Python");

       String resultNotEmpty = queue.pollFirst();

       assertEquals("Java", resultNotEmpty);

   }

   @Test

   void testRemoveFirst() throws InterruptedException {

       // 初始化队列

       Deque<String> queue = new ArrayDeque<String>(3);

       // 测试队列为空时,抛出异常

       Throwable excpetion = assertThrows(NoSuchElementException.class, () -> {

           queue.removeFirst();// 抛异常

       });

       assertEquals(null, excpetion.getMessage());

       // 测试队列不为空时,返回队首值并移除

       queue.addLast("Java");

       queue.addLast("C");

       queue.addLast("Python");

       String resultNotEmpty = queue.removeFirst();

       assertEquals("Java", resultNotEmpty);

   }

   @Test

   void testPeekFirst() throws InterruptedException {

       // 初始化队列

       Deque<String> queue = new ArrayDeque<String>(3);

       // 测试队列不为空时,返回队首值并但不移除

       queue.add("Java");

       queue.add("C");

       queue.add("Python");

       String resultNotEmpty = queue.peekFirst();

       assertEquals("Java", resultNotEmpty);

       resultNotEmpty = queue.peekFirst();

       assertEquals("Java", resultNotEmpty);

       resultNotEmpty = queue.peekFirst();

       assertEquals("Java", resultNotEmpty);

       // 测试队列为空时,返回null

       queue.clear();

       String resultEmpty = queue.peek();

       assertNull(resultEmpty);

   }

   @Test

   void testGetFirst() throws InterruptedException {

       // 初始化队列

       Deque<String> queue = new ArrayDeque<String>(3);

       // 测试队列不为空时,返回队首值并但不移除

       queue.add("Java");

       queue.add("C");

       queue.add("Python");

       String resultNotEmpty = queue.getFirst();

       assertEquals("Java", resultNotEmpty);

       resultNotEmpty = queue.getFirst();

       assertEquals("Java", resultNotEmpty);

       resultNotEmpty = queue.getFirst();

       assertEquals("Java", resultNotEmpty);

       // 测试队列为空时,抛出异常

       queue.clear();

       Throwable excpetion = assertThrows(NoSuchElementException.class, () -> {

           queue.getFirst();// 抛异常

       });

       assertEquals(null, excpetion.getMessage());

   }

}

5.    ArrayDeque的应用案例:工作窃取

双端队列的一个经典使用场景就是工作窃取。ForkJoinPool线程池就利用了双端队列支持工作窃取。

线程池中每个线程都有一个互不影响的任务队列(双端队列),线程每次都从自己的任务队列的队头中取出一个任务来运行;如果某个线程对应的队列已空并且处于空闲状态,而其他线程的队列中还有任务需要处理但是该线程处于工作状态,那么空闲的线程可以从其他线程的队列的队尾取一个任务来帮忙运行 —— 感觉就像是空闲的线程去偷人家的任务来运行一样,所以叫 “工作窃取”。这是保证LB的一个重要思路。

6.   参考引用

本系列归档至《Java 数据结构及算法实战》:https://github.com/waylau/java-data-structures-and-algorithms-in-action

《数据结构和算法基础(Java 语言实现)》(柳伟卫著,北京大学出版社出版):https://item.jd.com/13014179.html

 

目录
相关文章
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
564 3
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
682 1
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
1141 1
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
385 11
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
1114 77
|
8月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
417 1
|
8月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
388 1
|
9月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案