Java数据结构之Deque

  • Post author:
  • Post category:java




引题

进入本文之前,我们先看一段代码

 Deque<Integer> que = new ArrayDeque<>();

上面的代码是否感觉到熟悉?前面的

接口Deque

有没有感觉与

Queue–队列

看起来有点像,都是

que

结尾。后面的

ArrayDeque

看着眼熟?是不是跟

ArrayList

有点像。但这个接口描述的到底是怎样的数据结构呢?我们现在就去源码里一探究竟吧。



Deque接口分析

我们按住ctrl+左键点进去看看

Deque接口

的源码就能明白这个类的作用是什么了?



Deque的注释

先看接口上面的

注释

,这里就不放英文了,我直接上一段百度翻译的原文。

支持两端元素插入和移除的线性集合。
deque是“双端队列”的缩写,通常发音为“deck”。
大多数Deque实现对它们可能包含的元素数量没有固定的限制,
但是这个接口支持容量限制的Deque以及那些没有固定大小限制的Deque。

这个接口定义了访问deque容器两端元素的方法。
提供了插入、删除和检查元素的方法。
这些方法以两种形式存在:一个在操作失败时抛出异常,
另一个返回特殊值(null或false,取决于操作)。
插入操作的后一种形式是专门为使用容量受限的Deque实现而设计的;
在大多数实现中,插入操作不会失败。

抓住关键词:

支持两端元素插入和移除的线性集合



双端队列



包含的元素数量没有固定的限制



访问deque容器两端元素的方法

然后接着往下看注释:

在这里插入图片描述

表示该接口中的12个方法,其实从

方法名称

中我们就可以看出,这个数据结构可以从

两端

进行

插入、取出、删除数据



与Queue的联系

该接口扩展了Queue接口。
当deque被用作队列时,会产生FIFO (First-In-First-Out)行为。
在deque容器的末尾添加元素,并从开头删除元素。

也就是说

Deque



Queue



子类

,可以把它当作队列来使用。

但还记得上面的关键词吗?

支持两端元素插入和移除的线性集合



也就是

Deque

可以

在两边都操作元素

:新增、删除、访问元素等。



Queque


只能在队首

对元素进行操作。



还在使用Stack?你OUT啦!

Deque也可以用作LIFO(后进先出)堆栈。
这个接口应该优先于旧的Stack类使用。
当一个队列被用作堆栈时,元素从队列的开始被推入和弹出
。堆栈方法完全等同于Deque方法。

所以以后需要使用Stack的情况时候,要记得优先使用

Deque

哦。



peek方法更方便

注意,当一个deque用作队列或堆栈时,peek方法同样工作良好;
在这两种情况下,元素都是从队列的开头开始绘制的。

虽然有

get

方法获取元素,但官方表示:

peek方法

,你值得拥有!



与List的不同

与List接口不同,该接口不支持对元素的索引访问。



与null说goodbye

虽然不严格要求Deque实现禁止空元素的插入,但是强烈建议这样做。
强烈建议任何允许null元素的Deque实现的用户不要利用插入null的能力。
这是因为null被各种方法用作特殊的返回值,以指示deque为空。

因为

null

被各种方法

作为特殊的返回值

,所以建议不要存储null值。



子类ArrayDeque.class分析

Deque接口的实现类很多,这里就不一一分析了,以下只分析其中的的一个子类

ArrayDeque



基本结构



官方的代码

    /**
     * The array in which the elements of the deque are stored.
     * The capacity of the deque is the length of this array, which is
     * always a power of two. The array is never allowed to become
     * full, except transiently within an addX method where it is
     * resized (see doubleCapacity) immediately upon becoming full,
     * thus avoiding head and tail wrapping around to equal each
     * other.  We also guarantee that all array cells not holding
     * deque elements are always null.
     */
    transient Object[] elements; // non-private to simplify nested class access

    /**
     * The index of the element at the head of the deque (which is the
     * element that would be removed by remove() or pop()); or an
     * arbitrary number equal to tail if the deque is empty.
     */
    transient int head;

    /**
     * The index at which the next element would be added to the tail
     * of the deque (via addLast(E), add(E), or push(E)).
     */
    transient int tail;

    /**
     * The minimum capacity that we'll use for a newly created deque.
     * Must be a power of 2.
     */
    private static final int MIN_INITIAL_CAPACITY = 8;

上述代码的注释中已经将各个属性的作用描述的很清楚了,这里就直接翻译下。

/*
	存储deque容器元素的数组。deque的容量是数组的长度,总是2的幂。
	数组永远不允许被填满,除非在一个addX方法中,数组在被填满后立即被调整大小
	(参见doubleCapacity),从而避免头尾绕成相等。
	我们还保证所有不包含deque元素的数组单元格始终为空。
*/
	transient Object[] elements;//非私有以简化嵌套类访问

/*
	deque容器头部元素的索引(即将被remove()或pop()移除的元素);或者如果deque为空,则等于tail的任意数。 -- elements的头部
*/
    transient int head;
/*
	下一个元素将被添加到deque容器尾部的索引(通过addLast(E), add(E),
	或push(E))。 -- 可以理解为elements的末端
*/
    transient int tail;
    /*
    我们将用于新创建deque的最小容量。一定是2的幂。 最小的初始化容量
    */
	private static final int MIN_INITIAL_CAPACITY = 8;


值得注意的是

transient

关键字是指,该数据

只能在内存中使用

,而

无法保存到本地

的文件中,这文章比较详细:

Java中transient关键字的详细总结

.



图解数据存储过程

从下图初始化的Deque结构中很明显可以看出为什么我们不能插入null。因为实例后各个元素的

默认值就是null

。从而插入null的话会造成以下情况:

下次在该位置插入元素时无法确定该位置是否存在元素



在这里插入图片描述

从尾部插入两个元素,注意头(head)尾(tail)指针的位置

在这里插入图片描述

如果从头部插入数据,注意头(head)尾(tail)指针的位置

在这里插入图片描述


注意head和tail指向的地方是否有数据存储



简单思考 1

如果

先后从头尾

分别插入

一个

数据,请问插

入第几个数据时扩容



假设当head = tail 时扩容

)?答案可以评论区留言哦。



部分代码的分析



关于初始容量

初始容量默认为

8

,如果小于8则设为8,大于8则取大于该数的2次幂的整数.

private void allocateElements(int numElements) {
        int initialCapacity = MIN_INITIAL_CAPACITY;
		//默认最小为8 如果大于8则取大于该数的2次幂的整数
        if (numElements >= initialCapacity) {
            initialCapacity = numElements;
            // |= 做一个向右移位后的或运算
            // >>>n 无符号右移n位 高位用0补齐
            // |= 就是 initialCapacity  = initialCapacity |  (initialCapacity >>>  1)
            initialCapacity |= (initialCapacity >>>  1);
            initialCapacity |= (initialCapacity >>>  2);
            initialCapacity |= (initialCapacity >>>  4);
            initialCapacity |= (initialCapacity >>>  8);
            initialCapacity |= (initialCapacity >>> 16);
            initialCapacity++;

            if (initialCapacity < 0) 
                initialCapacity >>>= 1;//直接给你分配 2 ^ 30 容量的元素 一般都直接爆炸
        }
        //小于8直接设置为8
        elements = new Object[initialCapacity];
    }



关于扩容

直接扩展为

双倍容量

    private void doubleCapacity() {
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p;
        int newCapacity = n << 1;//直接移位变成两倍
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity]; //新的对象数组
        System.arraycopy(elements, p, a, 0, r);//写入 p - n 位置的数据到新的对象数组中(不包含第n号元素)
        System.arraycopy(elements, 0, a, r, p); //写入 0 - p 位置的数据到新数组中 (不包含第p号元素)
        elements = a;//替换对象数组的地址
        head = 0;
        tail = n;
    }

System.arraycopy(elements, 0, a, r, p);

这一行代码的意思就是:从

elements

数组的

p号位置开始



复制r个元素(包含p)

,到a数组中,从

0

开始。



代码中的&运算

以在队列首部增加元素方法

addFirst

为例,源码如下

public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        //先计算head 再存值
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
        	//扩容
            doubleCapacity();
}

注意以下代码

 elements[head = (head - 1) & (elements.length - 1)] = e;
 //上面的代码可以看成以下两行
 head = (head - 1) & (elements.length - 1);
 elements[head] = e;

上述代码作用:将head往前移动一位,

用来确定该插入元素存放的位置

head =  (head - 1) & (elements.length - 1)

上述的



(&)操作,可以理解为一个

取余



与取余不同的是当

head – 1的结果为负数

时,如当head = 0时,上面的赋值语句为:

head = -1 & (elements.length – 1)

,结果是-1。

而&运算的结果是:

head = (elements.length – 1)



即&操作,使得head的取值永远在[0 , elements.length – 1]中



而且&操作由于是二进制操作,所以相较于取余

运行速度更快,效率更高

与之相对的

addLast

方法

public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        //先存值 
        elements[tail] = e;
        //再计算tail
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        	//扩容
            doubleCapacity();
}

从这里我们很明显可以看出,两个方法的区别在于:

addFirst是

先计算出head的位置

,然后

存放元素

,head指向的地方是

存储了元素的

而addLast是

先存储元素

,再计算

tail的值

,tail指向的地方为

null



简单思考2

如果将

addLast

方法的执行顺序改为:

先计算tail值,再存储元素

。按照

简单思考 1

,请问插入第几个元素时扩容?并且

扩容时

数组内部

是否有元素为null

?答案可以评论区留言哦。



线程并不安全

我们可以看到,ArrayDeque中的方法都没有加

synchronized

的关键字,故而其是

线程不安全

的。



版权声明:本文为qq_44717657原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。