二十四、剖析 ArrayDeque

news2024/12/24 7:01:35

文章目录

  • 剖析 ArrayDeque
    • 3.1 循环数组
    • 3.2 构造方法
    • 3.3 从尾部添加 addLast(E)
    • 3.4 从头部添加 addFirst(E)
    • 3.5 从头部和尾部删除
    • 3.6 查看长度 size()
    • 3.7 检查给定元素是否存在
    • 3.8 toArray
    • 3.9 ArrayDeque 特点分析

剖析 ArrayDeque

本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记

LinkedList实现了队列接口Queue和双端队列接口DequeJava容器类中还有一个双端队列的实现类ArrayDeque,它是基于数组实现的。我们知道,一般而言,由于需要移动元素,数组的插入和删除效率比较低,但ArrayDeque的效率却非常高,它是怎么实现的呢?

ArrayDeque有如下构造方法:

    /**
     * Constructs an empty array deque with an initial capacity
     * sufficient to hold 16 elements.
     */
    public ArrayDeque()

    /**
     * Constructs an empty array deque with an initial capacity
     * sufficient to hold the specified number of elements.
     *
     * @param numElements lower bound on initial capacity of the deque
     */
    public ArrayDeque(int numElements)

    /**
     * Constructs a deque containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.  (The first element returned by the collection's
     * iterator becomes the first element, or <i>front</i> of the
     * deque.)
     *
     * @param c the collection whose elements are to be placed into the deque
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayDeque(Collection<? extends E> c)

ArrayDeque实现了Deque接口,同LinkedList一样,它的队列长度也是没有限制的, Deque扩展了Queue,有队列的所有方法,还可以看作栈,有栈的基本方法push/pop/peek,还有明确的操作两端的方法如addFirst/removeLast等,具体用法与LinkedList一节介绍的类似,重要的是它实现的原理。

ArrayDeque内部主要有如下实例变量:

    /**
     * The array in which the elements of the deque are stored.
     * All array cells not holding deque elements are always null.
     * The array always has at least one null slot (at tail).
     */
    transient Object[] elements;

    /**
     * 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 0 <= head < elements.length 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));
     * elements[tail] is always null.
     */
    transient int tail;

elements就是存储元素的数组。ArrayDeque的高效来源于headtail这两个变量,它们使得物理上简单的从头到尾的数组变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。接下来,我们学习一下循环数组。

3.1 循环数组

对于一般数组,比如arr,第一个元素为arr[0],最后一个为arr[arr.length-1]。但对于ArrayDeque中的数组,它是一个逻辑上的循环数组,所谓循环是指元素到数组尾之后可以接着从数组头开始,数组的长度、第一个和最后一个元素都与headtail这两个变量有关,根据headtail值之间的大小关系,可以具体分为四种情况:

  1. 如果headtail相同,则数组为空,长度为 0 0 0
  2. 如果tail大于head,则第一个元素为elements[head],最后一个为elements[tail-1],长度为tail-head,元素索引从headtail-1
  3. 如果tail小于head,且为0,则第一个元素为elements[head],最后一个为elements [elements.length-1],元素索引从headelements.length-1
  4. 如果tail小于head,且大于0,则会形成循环,第一个元素为elements[head],最后一个是elements[tail-1],元素索引从headelements.length-1,然后再从0tail-1

上面情况,根据图示查看温习一遍。

在这里插入图片描述

3.2 构造方法

默认构造方法的代码为:

    public ArrayDeque() {
        elements = new Object[16 + 1];
    }

分配了一个长度为 16 16 16​的数组。如果有参数numElements,代码为:

    public ArrayDeque(int numElements) {
        elements =
            new Object[(numElements < 1) ? 1 :
                       (numElements == Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                       numElements + 1];
    }

看最后一个构造方法:

    /**
     * Constructs a deque containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.  (The first element returned by the collection's
     * iterator becomes the first element, or <i>front</i> of the
     * deque.)
     *
     * @param c the collection whose elements are to be placed into the deque
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayDeque(Collection<? extends E> c) {
        this(c.size());
        copyElements(c);
    }

先调用ArrayDeque(int numElements)构造器,初始化elements属性,再调用copyElements函数,代码如下:

    private void copyElements(Collection<? extends E> c) {
        c.forEach(this::addLast);
    }

遍历容器c,调用addLast函数。

3.3 从尾部添加 addLast(E)

addLast(E)代码如下:

    public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        final Object[] es = elements;
        es[tail] = e;
        if (head == (tail = inc(tail, es.length)))
            grow(1);
    }


首先将元素添加到tail处(es[tail] = e),然后tail指向下一个位置,调用inc(int i, int modulus)函数确定tail的下一位置,代码如下:

    /**
     * Circularly increments i, mod modulus.
     * Precondition and postcondition: 0 <= i < modulus.
     */
    static final int inc(int i, int modulus) {
        if (++i >= modulus) i = 0;
        return i;
    }

tail先加一,这时,分两种情况,如果tail大于elements的长度,那么tail的值重置为 0 0 0,否则很简单,直接加一即可。

确认tail后,通过head==tail判断数组是否满员,如果满员,则调用grow函数扩容,处理数组满员的情况,否则完成整个在尾部添加元素动作。grow函数代码如下所示:

/**
 * Increases the capacity of this deque by at least the given amount.
 *
 * @param needed the required minimum extra capacity; must be positive
 */
private void grow(int needed) {
    // overflow-conscious code
    final int 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;
    }
}

grow传入一个needed(扩张需求的数量)参数计算新容量(newCapacity)。计算newCapacity的过程中,先计算jump值,计算公式:(oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1),如果原来的长度低于 64 64 64,那么数组翻倍,否则增加 50 % 50\% 50%。得到jump值后,如果需求大于供给,即:jump < needed或者增加jump后数组的长度大于允许的最大数组长度(MAX_ARRAY_SIZE),那么调用newCapacity(int needed, int jump)函数重新计算newCapacity值,代码如下所示:

    /** Capacity calculation for edge conditions, especially overflow. */
    private int newCapacity(int needed, int jump) {
        final int oldCapacity = elements.length, minCapacity;
        if ((minCapacity = oldCapacity + needed) - MAX_ARRAY_SIZE > 0) {
            if (minCapacity < 0)
                throw new 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;
    }

代码中,先计算允许的最小容量,计算公式:minCapacity = oldCapacity + needed,即:原来的容量加上本次申请的容量。接下来需要分五种情况讨论:

  1. minCapacity > Integer.MAX_VALUE,即minCapacity溢出,那么抛出IllegalStateException("Sorry, deque too big")异常;
  2. MAX_ARRAY_SIZE < minCapacity <= Integer.MAX_VALUE,返回Integer.MAX_VALUE作为newCapacity值;
  3. minCapacity<=MAX_ARRAY_SIZE and needed > jump,返回minCapacity作为newCapacity值;
  4. minCapacity<=MAX_ARRAY_SIZE and needed <= jump and oldCapacity + jump<MAX_ARRAY_SIZE,返回oldCapacity + jump作为newCapacity值;
  5. minCapacity<=MAX_ARRAY_SIZE and needed <= jump and oldCapacity + jump >= MAX_ARRAY_SIZE,返回MAX_ARRAY_SIZE作为newCapacity值;

确认完newCapacity后,分配一个newCapacity长度的新数组es,并将elementData数组复制给es,随后将head右边的元素[head, oldCapacity),复制到[head+newSpace, newCapacity),再将[head, head+newSpace)置为null,最后更新head=head+newSpace

我们来看一个例子,例子如下图所示,添加元素 9 9 9
在这里插入图片描述

3.4 从头部添加 addFirst(E)

    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        final Object[] es = elements;
        es[head = dec(head, es.length)] = e;
        if (head == tail)
            grow(1);
    }
    static final int dec(int i, int modulus) {
        if (--i < 0) i = modulus - 1;
        return i;
    }

对比addLast(E)代码,发现两者非常相像,一个是更新tail值,一个是更新head,更新完后,后面的代码完全一样,都是先判断head==tail,如果成立,再调用grow(1),这里不重复分析。

3.5 从头部和尾部删除

从头部删除removeFirst方法的代码为:

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeFirst() {
        E e = pollFirst();
        if (e == null)
            throw new NoSuchElementException();
        return e;
    }

可知,removeFirst调用的函数是pollFirst,代码如下:

    public E pollFirst() {
        final Object[] es;
        final int h;
        E e = elementAt(es = elements, h = head);
        if (e != null) {
            es[h] = null;
            head = inc(h, es.length);
        }
        return e;
    }

代码比较简单,将原头部位置置为null,然后head置为下一个位置,下一个位置=原来head+1,如果head+1>=es.length,那么head=0,否则head=head+1

从尾部删除removeLast()调用了pollLast()函数,代码如下:

    /**
     * @throws NoSuchElementException {@inheritDoc}
     */
    public E removeLast() {
        E e = pollLast();
        if (e == null)
            throw new NoSuchElementException();
        return e;
    }

    public E pollLast() {
        final Object[] es;
        final int t;
        E e = elementAt(es = elements, t = dec(tail, es.length));
        if (e != null)
            es[tail = t] = null;
        return e;
    }
    static final int dec(int i, int modulus) {
        if (--i < 0) i = modulus - 1;
        return i;
    }

代码比较简单,将原尾部位置置为null,然后tail置为上一个位置,上一个位置=原来的tail-1,如果tail-1<0,那么tail=es.length-1,否则tail=tail-1

3.6 查看长度 size()

ArrayDeque没有单独的字段维护长度,其size方法的代码如下:

    public int size() {
        return sub(tail, head, elements.length);
    }
    static final int sub(int i, int j, int modulus) {
        if ((i -= j) < 0) i += modulus;
        return i;
    }

通过该方法即可计算出size

3.7 检查给定元素是否存在

contains方法的代码为:

    public boolean contains(Object o) {
        if (o != null) {
            final Object[] es = elements;
            for (int i = head, end = tail, to = (i <= end) ? end : es.length;
                 ; i = 0, to = end) {
                for (; i < to; i++)
                    if (o.equals(es[i]))
                        return true;
                if (to == end) break;
            }
        }
        return false;
    }

如果head<=tail,那么直接遍历[head,tail),判断给定元素是否存在;否则,遍历 [ h e a d ,   e s . l e n g t h )   ⋃   [ 0 , t a i l ) [head,\ es.length)\ \bigcup\ [0,tail) [head, es.length)  [0,tail),判断给定的元素是否存在。

3.8 toArray

toArray方法toArray方法的代码为:

    public Object[] toArray() {
        return toArray(Object[].class);
    }

    private <T> T[] toArray(Class<T[]> klazz) {
        final Object[] es = elements;
        final T[] a;
        final int head = this.head, tail = this.tail, end;
        if ((end = tail + ((head <= tail) ? 0 : es.length)) >= 0) {
            // Uses null extension feature of copyOfRange
            a = Arrays.copyOfRange(es, head, end, klazz);
        } else {
            // integer overflow!
            a = Arrays.copyOfRange(es, 0, end - head, klazz);
            System.arraycopy(es, head, a, 0, es.length - head);
        }
        if (end != tail)
            System.arraycopy(es, 0, a, es.length - head, tail);
        return a;
    }

contains一样,如果head<=tail,那么直接复制[head,tail)索引元素;否则,复制 [ h e a d ,   e s . l e n g t h )   ⋃   [ 0 , t a i l ) [head,\ es.length)\ \bigcup\ [0,tail) [head, es.length)  [0,tail)索引元素。

3.9 ArrayDeque 特点分析

ArrayDeque内部维护一个动态扩展的循环数组,通过headtail变量维护数组的开始和结尾。ArrayDeque实现了双端队列,内部使用循环数组实现,这决定了它有如下特点。

  1. 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加 N N N个元素的效率为 O ( N ) O(N) O(N)
  2. 根据元素内容查找和删除的效率比较低,为 O ( N ) O(N) O(N)
  3. ArrayListLinkedList不同,没有索引位置的概念,不能根据索引位置进行操作。

ArrayDequeLinkedList都实现了Deque接口,应该用哪一个呢?如果只需要Deque接口,从两端进行操作,一般而言,ArrayDeque效率更高一些,应该被优先使用;如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该选LinkedList


  1. 马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎

  2. 尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1487200.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity UGUI之Slider基本了解

在Unity中&#xff0c;Slider&#xff08;滑动条&#xff09;是一种常用的用户界面控件之一&#xff0c;允许用户通过拖动滑块来选择一个数值。常常应用于调节数值&#xff08;如调节音量、亮度、游戏难度等&#xff09;、设置选项等。 以下是Slider的基本信息和用法: 1、创建…

Neoverse CSS N3:实现市场领先能效的最快途径

区分老的架构 从云到边缘&#xff0c;Arm Neoverse 提供无与伦比的性能、效率、设计灵活性和 TCO 优势&#xff0c;正在颠覆传统基础设施芯片。 我们看到云和超大规模服务运营商正在推动更高的计算密度。随着 128 核心 CPU 设计上市&#xff08;Microsoft Cobalt、阿里巴巴 Y…

工作微信统一管理(还带监管功能)

1.会话页面(可统一管理多个微信号、聚合聊天、手动搜索添加好友、通过验证请求、查看好友的朋友圈等) 2.聊天历史(可查看 所有聊天记录&#xff0c;包括手机.上撤回、删除的消息) 3.群发助手(可以一 -次群发多个好友和群&#xff0c;还可以选择定时发送&#xff0c;目前还在内测…

postman传参与返回值切换为左右显示的操作

目录 第一步 点击“Settings”&#xff0c;在下拉框选择“Settings” 第二步 在默认打开的General页面&#xff0c;参照下图改动两处 第一步 点击“Settings”&#xff0c;在下拉框选择“Settings” 第二步 在默认打开的General页面&#xff0c;参照下图改动两处 附上修改后…

Mysql深入学习 基础篇 Ss.02 详解四类SQL语句

我亲爱的对手&#xff0c;亦敌亦友&#xff0c;但我同样希望你能成功&#xff0c;与我一起&#xff0c;站在人生的山顶上 ——24.3.1 一、DDL 数据定义语言 1.DDL —— 数据库操作 查询 查询所有数据库 show databases; 查询当前数据库 select database(); 创建 create databa…

Linux:kubernetes(k8s)部署CNI网络插件(4)

在上一章进行了node加入master Linux&#xff1a;kubernetes&#xff08;k8s&#xff09;node节点加入master主节点&#xff08;3&#xff09;-CSDN博客https://blog.csdn.net/w14768855/article/details/136420447?spm1001.2014.3001.5501 但是他们显示还是没准备好 看一下…

linux安全--DNS欺骗,钓鱼网站搭建

目录 一&#xff0c;实验准备 首先让client能上网 1&#xff09;实现全网互通&#xff0c;实现全网互通过程请看 2&#xff09;SNAT源地址转换 3&#xff09;部署DHCP服务 4)配置DHCP服务 5&#xff09;启动服务 6&#xff09;安装DNS服务 7&#xff09;DNS配置 8)启动DNS…

数据结构c版(3)——排序算法

本章我们来学习一下数据结构的排序算法&#xff01; 目录 1.排序的概念及其运用 1.1排序的概念 1.2 常见的排序算法 2.常见排序算法的实现 2.1 插入排序 2.1.1基本思想&#xff1a; 2.1.2直接插入排序&#xff1a; 2.1.3 希尔排序( 缩小增量排序 ) 2.2 选择排序 2.2…

Java两周半速成之路(第九天)

一.Object类 1.概述&#xff1a; Object类&#xff1a;是java中所有的类共同的父类 1、观察包所属后发现&#xff0c;Object类属于java.lang包下的类&#xff0c;今后使用的时候&#xff0c;不需要进行导包 2.构造方法 Object() 无参构造方法 3.Object类的成员方法 (1)…

C语言数组作为函数参数

有两种情形&#xff1b; 一种是数组元素作为函数实参&#xff1b;一种是数组名作为函数参数&#xff1b; 新建一个VC6单文档工程&#xff1b; void printshz(int , CDC* , int , int ); double getav(int a[5]); ...... void CShzcshView::OnDraw(CDC* pDC) {CShzcshDoc* pDo…

pycharm 自定义TODO类注释以及其高亮颜色

大体介绍 使用自定义TODO是为了方便看&#xff0c;并且快速定位到位置 上面是为了进行标记&#xff0c;下面是让哪些标记可以过滤掉&#xff08;自定义过滤规则&#xff09;&#xff0c;从而在pycharm下面的TODO可以显示并过滤 如何设置&#xff1f; Setting-Preferences-Ed…

华为配置攻击检测功能示例

配置攻击检测功能示例 组网图形 图1 配置攻击检测功能示例组网图 业务需求组网需求数据规划配置思路配置注意事项操作步骤配置文件 业务需求 企业用户通过WLAN接入网络&#xff0c;以满足移动办公的最基本需求。且在覆盖区域内移动发生漫游时&#xff0c;不影响用户的业务使用。…

AutoSar PWM配置详解

背景 芯片:AURIX TC3xx 软件:Vector DaVinci CFG(简称达芬奇) 目标:配置AURIX TC3xx的P34.4脚为30HZ的PWM输出 配置过程 1.AUTOSAR架构 下图显示了PWM在AUTOSAR架构中的位置&#xff0c;在MCAL区。 2.Port模块配置 主要配置Port的输出模式与输出类型。 查看手册 配置P…

2024.3.1 网络编程

1.思维导图 2.TCP机械臂测试 程序代码&#xff1a; #include <myhead.h> #define SER_IP "192.168.125.254" //服务器端IP #define SER_PORT 8888 //服务器端端口号#define CLI_IP "192.168.199.131" //客户端IP …

一二三文档管理系统整体介绍

系统简介 企事业单位一站式文档管理系统&#xff0c;让组织内文档管理有序&#xff0c;协作高效、安全可控。 本应用完全开源&#xff0c;开源协议为MIT。 本应用基于一二三应用开发平台构建&#xff0c;该平台完全开源&#xff0c;开源协议为MIT&#xff0c;传送门。 系统特…

VMware 虚拟机安装windows 10操作系统

先提前准备好镜像文件 1.创建新的虚拟机 2.选择自定义&#xff0c;然后下一步 v Windows 建议选择2G以上&#xff0c;下一步 选择网络地址转换&#xff08;NAT&#xff09;&#xff0c;下一步 这里可按自己的需求来分区&#xff0c;也可以安装好后再分区 选择立即重启&#xff…

【心理】程序人生之情绪与压力篇,附心理学相关证书备考指南(心理学312统考,心理治疗师,中科院心理咨询师,家庭教育指导师,企业培训证书)

程序员生活指南&#xff08;情绪与压力篇&#xff09;之 【心理】程序人生之情绪与压力专项&#xff0c;附心理学相关证书备考指南&#xff08;心理学312统考&#xff0c;心理治疗师&#xff0c;中科院心理咨询师&#xff0c;家庭教育指导师&#xff0c;企业培训证书&#xff0…

3.3日学习打卡----初学Redis(一)

3.3日学习打卡 目录&#xff1a; 3.3日学习打卡NoSQL为什么要用NoSQL什么是NoSQL?NoSQL的四大分类关系型数据库和非关系型数据及其区别NoSQL经典应用 RedisRedis是什么?Linux下安装RedisDocker下安装Redis基本知识 NoSQL 为什么要用NoSQL 单机Mysql的美好年代 在90年代&…

【排序】基于快速选择算法的快排实现

目录 一、常用排序算法比较 二、快速选择算法 快速选择 图解快速选择 三、代码实现 一、常用排序算法比较 排序 时间复杂度 空间复杂度 稳定性 插入排序 O(n) O(1) 稳定 希尔排序 O(nlogn)-O(n)取决于增量序列 O(1) 不稳定 选择排序 O(n) O(1) 不稳定 冒泡…