数据结构-Queue系列之ArrayDeque(一)

这段时间暂时不写关于各种框架的博客了。因为自己是非科班出生,所以想的还是先从根基抓起,目前在学大学的计算机基础课,计算机网络,操作系统那些比较偏理论,而数据结构和算法有很大的关系,所以单单做一些数据结构方面的笔记。
首先就来看看队列,queue。

最最最基本的概念:

队列是一种特殊的线性表,是一种先进先出(FIFO)的数据结构。它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。

关键词:FIFO,作为先进先出的一种数据结构,可以较为方便的按插入顺序来获取元素。

总所周知,Queue在java也只是一个接口,肯定的嘛,就上面那一段抽象的定义,不整成个接口还咋地?Queue的实现类有很多,笔者以java8为例,如下图,有十多种Queue的实现类。其中不少与线程并发有关,作为Queue系列的第一篇,我们就来看看ArrayDeque,因为不涉及并发安全,所以相对简单一些。
在这里插入图片描述

ArrayDeque的继承图

首先看看ArrayDeque实现了哪些接口与父类:
在这里插入图片描述

源码部分研究

源码部分有几处非常关键,即使不研究这个类,也可以学习其写法,我第一次看源码看到虎躯一震的好吗,其优雅,其精简,差点让我在办公室直接升天:

源码:找到大于某个数的最小2的次幂的最佳写法

这段代码意义:
规定最小值MIN_INITIAL_CAPACITY = 8,如果入参小于8,数组大小就定义成8;如果大于等于8,就找到到大于入参的2的次幂中最小的一个。
先不看下面代码,你觉得找到大于入参的最小2的次幂,这个算法该怎么写?
然后你停下coding,来看看下面这段代码,你就会像我一样惊叹:

private static final int MIN_INITIAL_CAPACITY = 8;

// ******  Array allocation and resizing utilities ******

private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}

这一通右移是啥操作?假如我们传入了16,二进制10000,逐步分析下:

1.initialCapacity |= (initialCapacity >>> 1)
右移1位作|操作,10000->01000,‘或’ 操作后11000

2.initialCapacity |= (initialCapacity >>> 2)
接上一步,右移2位作|操作,11000->00110,‘或’ 操作后11110

3.initialCapacity |= (initialCapacity >>> 4)
接上一步,右移4位作|操作,11110->00001,‘或’ 操作后 11111

……

后面就两步都是11111 | 00000,结果就是 11111

4.initialCapacity++
二进制数11111,+1之后100000,转换成十进制32

结论:这些’或’ 操作,最终得到了大于入参的2的次幂中最小的一个。

问题:为什么是从1右移到16,?
答:我认为是因为int数最大32位,除去符号位1位,剩下数字位31位,从1+2+4+。。+16恰好是31

当初看这一段,我还去补了一下原码,反码,补码的知识(见上一篇),真的太佩服作者了。

源码:确保增减数组索引不越界

再看看插入操作:

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;    //关键
    if (head == tail)
        doubleCapacity();
}

elements[head = (head - 1) & (elements.length - 1)] 是什么鬼?
我先说结论:其实这里就是让原head前的那一个数组位置,变为新的head(因为是插入到队列最前端嘛)
所以有一个head-1,其实就是head的索引之前的那一个索引位
但是问题来了,如果原来head是0呢?未必把新元素插到索引为-1的位置上?
吃我数组越界警告!!
所以这里有个按位与操作,(head - 1) & (elements.length - 1)的结果就是,如果为head-1为正,则返回head-1,如果为负,则到最后一位。相当惊艳有没有!如果你拿笔用二进制推敲一遍是如何达到这个结果的,那你会更惊艳,恭喜你,找到了程序之美~

addLast,同样的意思,感兴趣可以自己分析~

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}
源码:扩容
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;    //左移,等价乘2,依然保持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 = a;
    head = 0;
    tail = n;
}

图示一目了然,我就感觉这个交叉移位帅爆了好不好
在这里插入图片描述

部分参考下列博客,如果想继续深挖,可以看看:
https://segmentfault.com/a/1190000017480142?utm_source=tag-newest

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章