• java数据结构之双端队列ArrayDeque


    这篇文章介绍java的数据结构之双端队列之ArrayDeque

    1、ArrayDeque

    ArrayDeque是一个双端数组,既可以当做栈使用,也可以当做队列使用。

    ArrayDeque有两个指针head和tail分别指向数据开头和数据结尾,当你从一端进行数据的插入和弹出时,它可以当做栈使用;当你从一端进行数据的插入,从另一端进行数据的弹出时,它可以当做队列使用;当然也可以从两端同时进行数据的插入和弹出,这种情况就比较灵活了,具体在使用在什么场景下,目前还不太清楚,带我仔细研究后再加说明。

    本篇文章从源代码的角度讨论ArrayDeque的数据结构,以及数据的插入和弹出操作。

    2、ArrayDeque的初始化

    (1)、当你不指定ArrayDeque数组的长度时,默认长度是16

    (2)、当你指定ArrayDeque数组的长度时,代码会将数组长度转化为2的整次幂。比如如果你传的长度是18,则会转化为比18大但是2的整次幂的数32,也就是实际数组的长度为32。

    注:数组长度不能小于8,如果指定的数组长度小于8,则按8进行处理。

    构造函数如下:

    1. //默认长度16
    2. public ArrayDeque() {
    3. elements = new Object[16];
    4. }
    5. //如果指定长度,会调用allocateElements进行长度的归一化
    6. public ArrayDeque(int numElements) {
    7. allocateElements(numElements);
    8. }
    9. private void allocateElements(int numElements) {
    10. elements = new Object[calculateSize(numElements)];
    11. }
    12. private static int calculateSize(int numElements) {
    13. int initialCapacity = MIN_INITIAL_CAPACITY;
    14. // Find the best power of two to hold elements.
    15. // Tests "<=" because arrays aren't kept full.
    16. if (numElements >= initialCapacity) {
    17. initialCapacity = numElements;
    18. initialCapacity |= (initialCapacity >>> 1);
    19. initialCapacity |= (initialCapacity >>> 2);
    20. initialCapacity |= (initialCapacity >>> 4);
    21. initialCapacity |= (initialCapacity >>> 8);
    22. initialCapacity |= (initialCapacity >>> 16);
    23. initialCapacity++;
    24. if (initialCapacity < 0) // Too many elements, must back off
    25. initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    26. }
    27. return initialCapacity;
    28. }

    初始化之后,ArrayDeque的初始结构如下:

    初始化结构展示

    3、数据的插入

    (1)、插入数据有两个方法,addFirst(E e) 和 addLast(E e),其中

            addFirst(E e):操作head指针,从头部插入(逻辑上)

            addLast(E e):操作tail指针,从尾部插入(逻辑上)

    3.1 addFirst(0):

     addFirst(E e)源码解释:

    1. public void addFirst(E e) {
    2. if (e == null)
    3. throw new NullPointerException();
    4. elements[head = (head - 1) & (elements.length - 1)] = e;
    5. if (head == tail)
    6. doubleCapacity();
    7. }

    说明1:代码中(head - 1) & (elements.length - 1)的作用:

    进行(head - 1) & (elements.length - 1)计算之后才赋予head的原因是防止head指针的下标小于0

    为了防止这种情况发生(假如数组长度elements.length = 16):

    (1)、当head = 0时,head - 1 = -1,数组的下标是不能小于0的,为了防止这种情况发生,将 -1 与 15进行与运算,得到的结果是15。也就是说当head下标小于0时,将head指向到数组的最大处。

    3.2 addLast(1)

     addLast(E e)源码解释:

    1. public void addLast(E e) {
    2. if (e == null)
    3. throw new NullPointerException();
    4. elements[tail] = e;
    5. if ( (tail = (tail + 1) & (elements.length - 1)) == head)
    6. doubleCapacity();
    7. }

    说明2:代码中(tail + 1) & (elements.length - 1)的作用:

    进行(tail + 1) & (elements.length - 1)计算之后才赋予tail的原因是防止tail指针的下标大于数组的最大下标

    为了防止这种情况发生(假如数组长度elements.length = 16):

    (1)、当tail= 15时,tail + 1 = 16,数组的下标是不能大于16的,为了防止这种情况发生,将 16 与 15进行与运算,得到的结果是0。也就是说当tail下标大于数组的最大下标时,将tail指向到数组下标为0处。

    4、获取数据

     为了更好的说明,我们在head指针插入偶数,tail指针插入奇数,假如我们的数据结构现在如下:

    获取数据有4个方法:

    peekFirst():

    操作head指针,获取head指针处的数据,但不移动head指针(只获取数据,不删除数据)

    操作之后数据结构如下:

    pollFirst(): 

    操作head指针,获取head指针处的数据,移动head指针(获取数据,并从数组中删除数据)

    操作之后数据结构如下:

    peekLast():

    操作tail指针,获取tail指针处的数据,但不移动tail指针(只获取数据,不删除数据)

    操作之后数据结构如下:

    pollLast():

    操作tail指针,获取tail指针处的数据,移动tail指针(获取数据,并从数组中删除数据)

    操作之后数据结构如下:

     5、数组扩展

     当head = tail时,表示此时数组已经塞满,需要进行扩展,扩展代码在doubleCapacity()方法体中,代码如下:

    1. private void doubleCapacity() {
    2. assert head == tail;
    3. int p = head;
    4. int n = elements.length;
    5. int r = n - p; // number of elements to the right of p
    6. int newCapacity = n << 1; //作者注:数组长度扩展一倍
    7. if (newCapacity < 0)
    8. throw new IllegalStateException("Sorry, deque too big");
    9. Object[] a = new Object[newCapacity];
    10. System.arraycopy(elements, p, a, 0, r); //作者注:将原数组head到末尾的数据拷贝到新数组从0下标下标位置
    11. System.arraycopy(elements, 0, a, r, p); //作者注:将原数组0到tail的数据拷贝到新数组。
    12. elements = a;
    13. head = 0;
    14. tail = n;
    15. }

    下面通过直观的数据结构表现这部分代码是如何扩展数组的。

    假设原来的数组长度是16,已经塞满,如下:

     扩展之后的数组长度是32,数据拷贝之后形成的结构如下:

     6、ArrayDeque使用场景

    1、作者是在追踪Flink的barrier的处理过程中,看到了ArrayDeque的使用,在Flink中,barrier的添加和删除相当于是使用了ArrayDeque的队列功能,从tail添加,从head删除。

    2、ArrayDeque 作为队列使用时性能比 LinkedList 好,作为栈使用时性能比Stack好。

  • 相关阅读:
    PAT 1042 Shuffling Machine
    代理模式:静态代理与动态代理(JDK、CGLIB、javassist动态代理)
    kafka_2.10启动Kafka broker
    用于一型糖尿病血糖调节的无模型iPID控制器
    阐述以下方法 @classmethod, @staticmethod, @property?
    Babylonjs学习笔记(六)——贴图的使用
    【Java】接口 interface
    Linux命令--定时运行程序(脚本)--方法/实例
    工程化:Loader 介绍
    NR 物理层编码: CRC循环冗余码
  • 原文地址:https://blog.csdn.net/chenzhiang1/article/details/126709941