ArrayDeque集合源码分析

ArrayDeque集合源码分析

文章目录

  • ArrayDeque集合源码分析
  • 一、字段分析
  • 二、构造函数分析
  • 方法、方法分析
  • 四、总结


在这里插入图片描述

  • 实现了 Deque,说面该数据结构一定是个双端队列,我们知道 LinkedList 也是双端队列,并且是用双向链表 存储结构的。而 ArrayDeque 则是使用 环形数组 来实现双端队列。各有优缺点。
  • 实现代码比较少,难点在于使用了很多位运算进行了优化,来学习下ArrayDueue如何使用位运算代替 运算符的从而实现优化的。

一、字段分析

//用来存储元素的数组,这个数组可循环利用,有效数据范围为 [head, tail)注意tail 取不到
//也会出现tail < head 的情况,即前面的数据被废弃了,且tail到已经过了数组尾部了,然后跳到开始位置
//开始循环利用前面废弃的位置。
transient Object[] elements;
//指向头部有效数据
transient int head;
//指向尾部有效数据 + 1,或者理解为下一个添加元素的位置。整个过程,head 和 tail 都是不断变化的,head ~ tail - 1 范围内为有效数据。
//并不是tail 到达数组尾部即 elements.lengt - 1 就会扩容的。因为他可能复用前面可能发生废弃数组位置。
transient int tail;
//默认初始容量
private static final int MIN_INITIAL_CAPACITY = 8;

二、构造函数分析

//调用无参构造函数,直接创建一个容量为 16 的数组作为初始数组。
public ArrayDeque() {elements = new Object[16];}//给定初始容量构建ArrayDeque,但并不是给 numElements 值为多少,就会创建多大的数组作为初始数组。
//给是判断并计算 最接近numElements的值,且为 2的幂次方的数作为初始容量。
public ArrayDeque(int numElements) {//分配容量allocateElements(numElements);}private void allocateElements(int numElements) {//确定初始容量并构建数组,赋值给实际存储元素的 elements 数组elements = new Object[calculateSize(numElements)];}//用来计算最终的容量的,计算结果为最接近 numElements 的数,且为 2的幂次方。
//比如 numElements = 15,则计算结果为 16(2^4)
//那么为什么一定要为 2的幂次方呢?为了在做取余运算时用位运算代替运算符运算,从而提高效率。
private static int calculateSize(int numElements) {//拿到默认最小的初始值,值为8int initialCapacity = MIN_INITIAL_CAPACITY;// Find the best power of two to hold elements.// Tests "<=" because arrays aren't kept full.//传入的 参数 numElements 比默认最小值还小,则直接使用默认的最小值为初始容量。,否则进行//计算,得到最接近 numElements ,且为 2 的次方的数。if (numElements >= initialCapacity) {initialCapacity = numElements;//为什么这样的计算能够得到 最接近 numElements ,且为 2 的幂次方的数呢?//首先列举下2的幂次方的二进制表示,来看下规律//0001  : 2^0//0010  :  2^1//0100	: 2^3//1000  : 2^4//上面例子可以看出,如果想要是 2的幂次方,那么整个二进制表示只能有一个1,其他的//1应该被消除,如果数中有不止一个1,那么最接近的应该是最高位的1前进一位,//且其他未都是0,该数就是最接近,且为2的幂次方。   //消除1的方法有所有位 都为1 在 + 1,则得到结果就有一个1,//如0111 + 1 =》 1000//我们知道一个数的最高位那一定是1了,其他位可能有1或0,而这个算法就是将其位//全部转化为1,之后再 + 1就是答案。//我们知道 Integer一共是4个字节,即32 位。//如果极限状态第31位是1(32位为1位负数,不会走当前逻辑,会给默认的最小初始容量)//我们需要将31位右边所有位都转化为1,我换一种说话,将右边连续的三十一位都转化为1。//将右边连续的2位转化为1initialCapacity |= (initialCapacity >>>  1);//将右边连续的4位转化为1initialCapacity |= (initialCapacity >>>  2);//将右边连续的8位转化为1initialCapacity |= (initialCapacity >>>  4);//将右边连续的16位转化为1initialCapacity |= (initialCapacity >>>  8);//将右边连续的32位转化为1,都能将连续的32位都转为1,你少于32的难道还不能转化为1吗。initialCapacity |= (initialCapacity >>> 16);//都转为1的基础上+1,就得到大于且最接近当前数,且为2的幂次方的数。initialCapacity++;//可能计算结果为负数,比如第31位为1,计算后变成最后一位(即符号位)变为1,成了负数//所以Good luck allocating 2 ^ 30 elements 注释也给出了我们最大容量就是2^30if (initialCapacity < 0)   // Too many elements, must back offinitialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements}return initialCapacity;}public ArrayDeque(Collection<? extends E> c) {//判断是否需要扩容allocateElements(c.size());//将集合c中所有元素添加到ArrayDeque中addAll(c);}//将集合c中所有元素添加到ArrayDeque中
public boolean addAll(Collection<? extends E> c) {boolean modified = false;//循环遍历添加集合c中的元素for (E e : c)if (add(e))modified = true;return modified;}//添加元素e
public boolean add(E e) {//添加元素到尾部addLast(e);return true;}//添加元素到尾部,即tail位置
public void addLast(E e) {if (e == null)throw new NullPointerException();//呼应我前面介绍tail的,tail 为下一个添加元素的位置,所以直接在该位置添加。 elements[tail] = e;//判断队列是否已满了,其实效果就是 tail + 1 后是否和 head 重合,即 tail + 1 == head ???//如何理解这段代码呢,就要结合我上面说的 为什么 elements 的数组长度设计为 2的幂次方有关了!!//正因为 elements.length 是2的幂次方,那么它 - 1 后二进制的最高位(该位置记为@)由 1 -> 0,而所有的低位变为1//在和tail 做 & 运算时,就会将tail二进制 @ 位及更高位全部转化为0,而@后面的全部转化为1,//相当于 tail % elements 做取余运算。//所以将elements.length设为2的幂次方好处就是:在判断tail 与 head 的关系是,//可用二进制运算取余代替 运算符取余运算,提高效率。//name 有一个疑问了,为什么不直接用 tail + 1 == head ? 不就行了吗??算都不用算不是更快吗??//因为要考虑越界的情况!!elements 是一个可循环使用的环形数组,比如 tail = elements.lenth - 1,tail + 1后越界了//所以要做 & 运算!!if ( (tail = (tail + 1) & (elements.length - 1)) == head)//tail 与 head重合,需要扩容了doubleCapacity();}private void doubleCapacity() {//断言, head != tail 就会抛出异常。也可体现出,ArrayDequeue 当 head == tail 时,进行扩容。 assert head == tail;//记录头结点int p = head;//当前数组容量int n = elements.length;//head 到 elements.length - 1 ,共有多少元素 (p,elements.length - 1]int r = n - p; // number of elements to the right of p//将数组的 容量扩容两倍int newCapacity = n << 1;//越界爬出异常if (newCapacity < 0)throw new IllegalStateException("Sorry, deque too big");//使用扩容后的容量创建新的数组    Object[] a = new Object[newCapacity];//相当于将 老数组 head 到 数组末尾的数据,复制到新数组中,复制到新数组下标0开始位置,长度就是r//你可以理解为head位起始位置,从head一直复制到tail,依次复制给新数组,从0 开始的位置。System.arraycopy(elements, p, a, 0, r);//将老数组o 位置开始以此复制给新数组。//这两部给个过程就明白了//				// 1. element[0, 1, 2, 3, 4, 5]   => newElements[3,4,5,0,0,0,0,0,0,0];//                 head// 2. element[0, 1, 2, 3, 4, 5]   => newElements[3,4,5,0,1,2,0,0,0,0];//                 headSystem.arraycopy(elements, 0, a, r, p);//elements 指向新的数组elements = a;//head 指向索引 0head = 0;//tail 指向最后一个位置的下一位,即下一个添加进ArrayDequeue元素的位置。tail = n;}

方法、方法分析

  • 添加元素方法。

//添加元素到尾部,即tail位置
public void addLast(E e) {if (e == null)throw new NullPointerException();//呼应我前面介绍tail的,tail 为下一个添加元素的位置,所以直接在该位置添加。 elements[tail] = e;//判断队列是否已满了,其实效果就是 tail + 1 后是否和 head 重合,即 tail + 1 == head ???//如何理解这段代码呢,就要结合我上面说的 为什么 elements 的数组长度设计为 2的幂次方有关了!!//正因为 elements.length 是2的幂次方,那么它 - 1 后二进制的最高位(该位置记为@)由 1 -> 0,而所有的低位变为1//在和tail 做 & 运算时,就会将tail二进制 @ 位及更高位全部转化为0,而@后面的全部转化为1,//相当于 tail % elements 做取余运算。//所以将elements.length设为2的幂次方一个好处就是:在判断tail 与 head 的关系是,//可用二进制运算取余代替 运算符取余运算,提高效率。//name 有一个疑问了,为什么不直接用 tail + 1 == head ? 不就行了吗??算都不用算不是更快吗??//因为要考虑越界的情况!!elements 是一个可循环使用的环形数组,比如 tail = elements.lenth - 1,tail + 1后越界了//所以要做 & 运算!!if ( (tail = (tail + 1) & (elements.length - 1)) == head)//tail 与 head重合,需要扩容了doubleCapacity();}//向 head - 1 处添加元素
public void addFirst(E e) {if (e == null)throw new NullPointerException();//取余运算,同时防止了越界。将head - 1 处复制eelements[head = (head - 1) & (elements.length - 1)] = e;//判断是否扩容if (head == tail)//扩容,扩容方法看构造方法里介绍了doubleCapacity();}
//向 tail 位置添加元素
public void addLast(E e) {if (e == null)throw new NullPointerException();//直接给tail 位置赋值elements[tail] = e;//取余元素,判断是否扩容if ( (tail = (tail + 1) & (elements.length - 1)) == head)//扩容doubleCapacity();}//向 tail 处添加元素    
public boolean offerLast(E e) {addLast(e);return true;}//向 head - 1 处添加元素
public boolean offerFirst(E e) {addFirst(e);return true;}//向tail 处添加元素
public boolean add(E e) {addLast(e);return true;}
//向tail 处添加元素
public boolean offer(E e) {return offerLast(e);}
//向 head - 1处添加元素public void push(E e) {addFirst(e);}
  • 获取元素方法。
//弹出 head 位置元素
public E pollFirst() {int h = head;@SuppressWarnings("unchecked")E result = (E) elements[h];// Element is null if deque emptyif (result == null)return null;//弹出后将head位置设为nullelements[h] = null;     // Must null out slot//获取head的下一个位置,取余运算防止越界。head = (h + 1) & (elements.length - 1);//返回结果return result;}//弹出 tail - 1 处元素
public E pollLast() {//获取元素位置,取余防止越界int t = (tail - 1) & (elements.length - 1);@SuppressWarnings("unchecked")E result = (E) elements[t];if (result == null)return null;//弹出后,该位置设为null    elements[t] = null;//新的tailtail = t;return result;}//获取head位置元素,但不弹出
public E getFirst() {@SuppressWarnings("unchecked")E result = (E) elements[head];if (result == null)throw new NoSuchElementException();return result;}//获取tail-1位置处元素,但不弹出
public E getLast() {@SuppressWarnings("unchecked")E result = (E) elements[(tail - 1) & (elements.length - 1)];if (result == null)throw new NoSuchElementException();return result;}//获取head位置元素,但不弹出
public E peekFirst() {// elements[head] is null if deque emptyreturn (E) elements[head];}//获取tail-1位置处元素,但不弹出
public E peekLast() {return (E) elements[(tail - 1) & (elements.length - 1)];}//弹出 head 位置元素
public E poll() {return pollFirst();}//获取head位置元素,但不弹出
public E element() {return getFirst();}//获取head位置元素,但不弹出
public E peek() {return peekFirst();}//弹出 head 位置元素
public E pop() {return removeFirst();}
  • 移除元素方法:
//删除 head 处元素
public E removeFirst() {//删除 head 处元素E x = pollFirst();if (x == null)throw new NoSuchElementException();//返回被删除的元素   return x;}//删除 tail - 1 处的元素
public E removeLast() {删除 tail - 1 处的元素E x = pollLast();if (x == null)throw new NoSuchElementException();//返回被删除的元素    return x;}//删除 head 处元素
public E pollFirst() {//记录head位置int h = head;@SuppressWarnings("unchecked")//记录head处元素,用于返回E result = (E) elements[h];// Element is null if deque emptyif (result == null)return null;//将head处元素设为空    elements[h] = null;     // Must null out slot//计算新的head,取余元素防止越界head = (h + 1) & (elements.length - 1);return result;}//删除 tail - 1 处的元素
public E pollLast() {//获取 tail -1 位置下标,取余运算,防止越界int t = (tail - 1) & (elements.length - 1);@SuppressWarnings("unchecked")//获取删除元素位置E result = (E) elements[t];if (result == null)return null;//tail - 1位置被置空    elements[t] = null;//更新tail位置tail = t;return result;}//删除head处元素
public E remove() {return removeFirst();}//删除head出元素
public E poll() {return pollFirst();}//删除head处元素
public E pop() {return removeFirst();}
  • 扩容方法:在介绍构造函数时,一并介绍了。

  • 迭代器:

private class DeqIterator implements Iterator<E> {/*** Index of element to be returned by subsequent call to next.*///用于记录迭代得到下一个元素的下标位置。初始值当时是head了。private int cursor = head;/*** Tail recorded at construction (also in remove), to stop* iterator and also to check for comodification.*///用于记录迭代的中止位置private int fence = tail;/*** Index of element returned by most recent call to next.* Reset to -1 if element is deleted by a call to remove.*///用于记录最后一次迭代得到的元素位置,迭代还未开始,当然给-1了。private int lastRet = -1;//判断是否还有元素可以迭代public boolean hasNext() {return cursor != fence;}//迭代获取下一个元素public E next() {//无元素了,报错if (cursor == fence)throw new NoSuchElementException();@SuppressWarnings("unchecked")//获取cursor处的位置元素E result = (E) elements[cursor];// This check doesn't catch all possible comodifications,// but does catch the ones that corrupt traversal//判断迭代过程中集合是否被修改过。和 modCount作用相同if (tail != fence || result == null)throw new ConcurrentModificationException();//记录最后一次迭代得到元素的位置    lastRet = cursor;//获取下一次迭代元素位置下标,位运算,防止越界cursor = (cursor + 1) & (elements.length - 1);return result;}//迭代删除,删除的是最后以此迭代得到的元素public void remove() {//以此还被迭代呢,删除失败if (lastRet < 0)throw new IllegalStateException();//删除元素if (delete(lastRet)) { // if left-shifted, undo increment in next()cursor = (cursor - 1) & (elements.length - 1);fence = tail;}//上一次迭代的元素被删除了,所以即为-1。所以迭代过程中不能连续remove。lastRet = -1;}}

四、总结

  • 使用的是可循环使用的双指针数组来存储结构。可以有效的减少扩容次数,并且提高资源利用率。不支持存储null元素。
  • 是线程不安全的。
  • 可当做链表,栈,队列使用。在头部和尾部插入或者删除元素,时间复杂度为 O(1),但是在扩容的时候需要批量移动元素,其时间复杂度为 O(n)。
  • 扩容的时候,将数组长度扩容为原来的 2 倍,即 n << 1。

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

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

相关文章

哪些狗粮比较适合幼年犬?

亲爱的朋友&#x1f44b;&#xff0c;你为家中的幼年犬挑选狗粮可真是个贴心的主人呢&#xff01;选择适合幼年犬的狗粮&#xff0c;确实是个需要仔细考虑的问题。幼年犬处于生长发育的关键期&#xff0c;所以狗粮的营养成分和口感都非常重要。 &#x1f436; 在选择狗粮时&…

排序算法全景:从基础到高级的Java实现

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

CentOS系统上安装Redis操作教程

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

Pulsar 社区周报 | No.2024.03.08 Pulsar-Spark Connector 助力实时计算

关于 Apache Pulsar Apache Pulsar 是 Apache 软件基金会顶级项目&#xff0c;是下一代云原生分布式消息流平台&#xff0c;集消息、存储、轻量化函数式计算为一体&#xff0c;采用计算与存储分离架构设计&#xff0c;支持多租户、持久化存储、多机房跨区域数据复制&#xff0c…

每日一练:LeeCode-209、长度最小的子数组【滑动窗口+双指针】

每日一练&#xff1a;LeeCode-209、长度最小的子数组【滑动窗口双指针】 思路暴⼒解法滑动窗口 本文是力扣 每日一练&#xff1a;LeeCode-209、长度最小的子数组【滑动窗口双指针】 学习与理解过程&#xff0c;本文仅做学习之用&#xff0c;对本题感兴趣的小伙伴可以出门左拐 L…

基于php的用户登录实现(v2版)(持续迭代)

目录 版本说明 数据库连接 登录页面&#xff1a;login.html 登录处理实现&#xff1a;login.php 用户欢迎页面&#xff1a;welcome.php 密码修改页面&#xff1a;change_password.html 修改执行&#xff1a;change_password.php 用户注册页面&#xff1a;register.html …

远程连接Linux系统

图形化、命令行 对于操作系统的使用&#xff0c;有2种使用形式&#xff1a; 图形化页面使用操作系统 图形化&#xff1a;使用操作系统提供的图形化页面&#xff0c;以获得图形化反馈的形式去使用操作系统。 以命令的形式使用操作系统 命令行&#xff1a;使用操作系统提供的各…

React-路由导航

1.声明式路由导航 1.1概念 说明&#xff1a;声明式导航是指通过在模版中通过<Link/>组件描述出要跳转到哪里去&#xff0c;比如后台管理系统的左侧菜单通常使用这种方式进行。 import {Link} from "react-router-dom" const Login()>{return (<div>…

物联网云原生云边协同

文章目录 一、物联网平台设计1.物联网平台设计2.物联网平台实现 二、部署环境1.节点配置2.版本信息 三、物联网平台部署1.部署 Kubernetes 集群2.部署 KubeEdge3.部署 ThingsBoard 集群4.部署 ThingsBoard Edge4.1.创建 Edge 实例4.2.部署 PostgreSQL4.3.创建数据库4.4.部署 Th…

在Blender中清理由Instant-NGP等几何学习技术生成的网格

使用布尔运算: 创建一个大的立方体或其他简单几何体包裹住全部网格。使用布尔修改器对两个网格进行“差集”运算。这将移除超出包裹体之外的多余网格部分。 手动选择并删除: 进入编辑模式&#xff08;按Tab键&#xff09;。按A键取消选择所有顶点。按B键并拖动以选择您想要删除…

11. 搭建较通用的GoWeb开发脚手架

文章目录 导言一、加载配置二、初始化日志三、初始化MySQL连接四、初始化Redis连接五、初始化gin框架内置的校验器使用的翻译器六、注册路由七、 启动服务八、测试运行九&#xff1a;注意事项 代码地址&#xff1a;https://gitee.com/lymgoforIT/bluebell 导言 有了前述知识的…

最简单的基于 FFmpeg 的内存读写的例子:内存转码器

最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器 最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器正文源程序结果工程文件下载参考链接 最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器 参考雷霄骅博士的文章&#xff0c;链接&#xf…

Chrome中如何导出和导入书签

导出书签 如下图所示&#xff1a; 右上角三点->书签和清单->书签管理器->右上角三点->导出书签 然后你选择保存地址即可。打开后如下&#xff1a; 导入书签 如下图所示&#xff1a; 右上角三点->书签和清单->导入书签和设置->选择以前导出的书签&…

【Node.js】-闲聊:前端框架发展史

前端框架的发展史是一个不断演进和创新的过程&#xff0c;旨在提高开发效率、优化用户体验&#xff0c;并推动前端技术的不断发展。以下是前端框架发展的主要阶段和关键里程碑&#xff1a; 早期阶段&#xff1a; 在这个阶段&#xff0c;前端主要由HTML、CSS和JavaScript等基础技…

LLM Drift(漂移), Prompt Drift Cascading(级联)

原文地址&#xff1a;LLM Drift, Prompt Drift & Cascading 提示链接可以手动或自动执行&#xff1b;手动需要通过 GUI 链构建工具手工制作链。自治代理在执行时利用可用的工具动态创建链。这两种方法都容易受到级联、LLM 和即时漂移的影响。 2024 年 2 月 23 日 在讨论大型…

什么是自动化测试?什么情况下使用?

什么是自动化测试? 自动化测试是指把以人为驱动的测试行为转化为机器执行的过程。实际上自动化测试往往通过一些测试工具或框架&#xff0c;编写自动化测试脚本&#xff0c;来模拟手工测试过程。比如说&#xff0c;在项目迭代过程中&#xff0c;持续的回归测试是一项非常枯燥…

如何在Mapbox GL中处理大的GEOJSON文件

Mapbox GL可以将 GeoJSON 数据由客户端(Web 浏览器或移动设备)即时转换为 Mapbox 矢量切片进行显示和处理。本文的目的是教大家如何有效加载和渲染大型 GeoJSON 源,并优化渲染显示速度,增强用户体验,减少客户端卡顿问题。本文以Mapbox 为例,至于其它框架原理大致相同,可…

【HarmonyOS】ArkTS-枚举类型

枚举类型 枚举类型是一种特殊的数据类型&#xff0c;约定变量只能在一组数据范围内选择值 定义枚举类型 定义枚举类型&#xff08;常量列表&#xff09; enum 枚举名 { 常量1 值, 常量2 值,......}enum ThemeColor {Red #ff0f29,Orange #ff7100,Green #30b30e}使用枚举…

报错Importing ArkTS files to JS and TS files is not allowed. <etsLint>

ts文件并不支持导入ets文件&#xff0c;为了方便开发应用卡片&#xff0c;entryformAbility创建的时候默认是ts文件&#xff0c;这里只需要把ts文件改成ets便可以轻松的导入所需要的ets即可 我创建了一个鸿蒙开发的交流群&#xff0c;喜欢的鸿蒙朋友可以扫码或者写群号&#xf…

微服务自动化管理初步认识与使用

目录 一、ETCD 1.1、ETCD简介 对于实施工程师&#xff1a; 1.2、特点 1.3. 使用场景 1.4、 关键字 1.5 工作原理 二、ETCD的安装 2.1、下载路径 2.2、介绍 2.3、具体操作 安装服务端 安装etcd客户端 测试 三、ETCD使用 3.1、前奏具体操作 3.2、 常用操作 一、ET…