ArrayDeque详解(含动画演示)

目录

  • ArrayDeque详解
    • 1、 ArrayDeque的继承体系
    • 2、Queue和Deque接口的区别
    • 3、 ArrayDeque的数据结构
    • 4、ArrayDeque的构造方法
    • 5、 ArrayDeque的`addFirst`方法
    • 6、 ArrayDeque的`addLast`方法
    • 7、 ArrayDeque的如何利用head和tail索引实现环形数组
    • 8、 ArrayDeque的`doubleCapacity`方法(扩容)
    • 9、 ArrayDeque的其他方法`pollFirst、pollLast、peekFirst、peekLast、delete`
      • `pollFirst`方法
      • `pollLast`方法
      • `peekFirst`方法
      • `peekLast`方法
    • 10、 ArrayDeque实现栈

ArrayDeque详解

ArrayDeque是JDK中对双端队列的一种实现。

1、 ArrayDeque的继承体系

可以看到ArrayDeque实现了Collection、Deque,说明是单值集合,且具有双端队列的功能。

小知识点:
Queue 读作 /kjuː/,发音类似于“ cue ”。
Deque 读作 /deɪk/ 或 /dɛk/,发音接近于“deck ”。

在这里插入图片描述

2、Queue和Deque接口的区别

  • Queue:
    单端队列的抽象,遵循先进先出(FIFO, First In First Out)原则。元素从队尾(rear)加入,从队头(front)移除。
    定义了一组操作队列的规则。
    比如: boolean add(E e); boolean offer(E e); E remove(); E poll(); E element(); E peek();
    单端队列图示:
    在这里插入图片描述

  • Deque(双端队列,Double Ended Queue):
    是Queue的子接口,不仅支持FIFO操作,还支持后进先出(LIFO,类似栈的行为)以及其他灵活的插入和删除操作。元素可以从两端(前端和后端)进行插入和删除。
    也定义了一组操作双端的规则。
    比如: void addFirst(E e); void addLast(E e); E removeFirst(); E removeLast(); 等等
    双端队列图示:
    在这里插入图片描述

应用场景:
Queue常用于需要按顺序处理元素的场景,如任务调度、消息队列等。
Deque因其双端操作的灵活性,适用于更广泛的场景,如作为栈使用(后进先出)、实现撤销/重做功能、以及任何需要在两端高效插入和删除的场合。

3、 ArrayDeque的数据结构

先看下ArrayDeque类的属性注释:

可以看出ArrayDeque底层使用Object[]数组来存储元素。

// 使用可变数组实现的双端队列ArrayDeque
public class ArrayDeque<E> {// 用于存储队列元素的数组,transient表示序列化时不包括该字段transient Object[] elements; // 非私有化以简化嵌套类的访问// 队列头部的位置索引transient int head;// 队列尾部的位置索引transient int tail;// 如果自定义容量小于8  则使用8作为最小容量初始化数组private static final int MIN_INITIAL_CAPACITY = 8;// 构造函数和其他方法实现...
}

4、ArrayDeque的构造方法

  • ①、无参构造
public ArrayDeque() {// 初始化一个默认容量为16的数组elements = new Object[16];
}

如果使用无参构造,初始容量就是16。

  • ②、带参构造1,接收一个自定义容量
public ArrayDeque(int numElements) {// 调用 allocateElements 方法分配数组空间allocateElements(numElements);
}private void allocateElements(int numElements) {// 初始化初始容量为 8int initialCapacity = MIN_INITIAL_CAPACITY;// 如果 numElements 大于初始容量,则找到最合适的2的幂次方作为容量if (numElements >= initialCapacity) {initialCapacity = numElements;// 下面的代码通过位运算将 initialCapacity 调整为大于 numElements 的最小2的幂次方initialCapacity |= (initialCapacity >>>  1);initialCapacity |= (initialCapacity >>>  2);initialCapacity |= (initialCapacity >>>  4);initialCapacity |= (initialCapacity >>>  8);initialCapacity |= (initialCapacity >>> 16);initialCapacity++;// 检查是否溢出,如果溢出,则将 initialCapacity 右移一位// 当 initialCapacity 超过某个极限时,处理起来会非常困难甚至是不可能的。// 通过右移一位,代码试图回退到一个可以实际处理的大小(大约 2^30),这是一种防止极端情况下内存分配失败的保护措施。if (initialCapacity < 0)initialCapacity >>>= 1; // Good luck allocating 2 ^ 30 elements}// 创建一个指定容量的数组elements = new Object[initialCapacity];
}

// Good luck allocating 2 ^ 30 elements
这个注释比较有意思,就像是你写了一个方法给别人用,你发现别人传过来数值大的离谱,然后你对这种情况做了点处理让这个数值小点,变成了 2^30。 实际上这也是个大的离谱的数值。 你于是在代码上面写了个注释,祝你好运,God bless you!
大概就是这个意思。 说明这个作者比较幽默呀,哈哈~
@author Josh Bloch and Doug Lea 可以看到作者是 Josh Bloch(《Effective Java》的作者) 和 Doug Lea(JUC并发框架的作者) 两位都是大神级的人物~
我猜 // Good luck allocating 2 ^ 30 elements 应该是 Josh Bloch写的注释,如果你看过《Effective Java》可能也会猜是他了 ~

  • ③、带参构造2,接收一个集合
public ArrayDeque(Collection<? extends E> c) {// 调用 allocateElements 方法分配数组空间,容量为集合的大小allocateElements(c.size());// 将集合中的所有元素添加到 ArrayDeque 中addAll(c);
}public boolean addAll(Collection<? extends E> c) {boolean modified = false;// 遍历集合中的每一个元素for (E e : c) {// 将元素添加到 ArrayDeque 中,如果成功添加则修改标志if (add(e))modified = true;}// 返回是否有元素被添加return modified;
}

5、 ArrayDeque的addFirst方法

将元素添加到队列头部

public void addFirst(E e) {// 检查传入的元素是否为 null,如果是则抛出 NullPointerExceptionif (e == null)throw new NullPointerException();// 计算新的头部索引// 1. head - 1 表示向前移动一个位置。// 2. (head - 1) & (elements.length - 1) 表示取模运算,以保持环形数组的特性。// 使用按位与运算 (&) 来代替取模运算 (%) 是因为这在性能上更高效,// 前提是数组的长度必须是 2 的幂,这也是 ArrayDeque 的设计约束。head = (head - 1) & (elements.length - 1);// 将元素存放在新的头部位置elements[head] = e;// 如果头部和尾部相遇,说明数组已满,需要扩容if (head == tail)doubleCapacity();
}

6、 ArrayDeque的addLast方法

将元素添加到队列尾部

public void addLast(E e) {// 检查传入的元素是否为 null,如果是则抛出 NullPointerExceptionif (e == null)throw new NullPointerException();// 将元素存放在当前的尾部位置elements[tail] = e;// 计算新的尾部索引// 1. tail + 1 表示向后移动一个位置。// 2. (tail + 1) & (elements.length - 1) 表示取模运算,以保持环形数组的特性。// 使用按位与运算 (&) 来代替取模运算 (%) 是因为这在性能上更高效,// 前提是数组的长度必须是 2 的幂,这也是 ArrayDeque 的设计约束。tail = (tail + 1) & (elements.length - 1);// 如果尾部和头部相遇,说明数组已满,需要扩容if (tail == head)doubleCapacity();
}

7、 ArrayDeque的如何利用head和tail索引实现环形数组

主要看head和tail是怎么计算的: 默认下标 head和tail都是0
head

head = (head - 1) & (elements.length - 1);
// 将元素存放在新的头部位置
elements[head] = e;

tail

elements[tail] = e;
tail = (tail + 1) & (elements.length - 1);

可以看到head是先计算,再利用计算过的下标 去给数组赋值
tail则是先用计算前的下标给数组赋值后,再计算新的下标。

addFirst的过程:
当head默认是0。开始的时候,先减一,再和数组长度减一求与,这个操作就相当于,head下标和数组长度求余,再往左移动一位。
比如一开始 head = 0,数组长度是8, 那head = (0-1)&(8-1)=7, 那么第一个添加到队列头部的元素就在整个数组的最后一个位置。
依次类推第二个添加到队列头部的元素就在整个数组的倒数第二个位置。

addLast的过程:
当tail默认是0开始的时候,开始的时候,直接添加元素到数组的第一个位置,然后再计算tail(此时计算的位置是下一次要添加到位列末尾的位置),tail = (0+ 1) & (8 - 1) = 1,也就是第二次要添加到队列末尾的元素放在数组的第二个位置。
依次类推。

这个时候再把上面的动画拿过来看下更清晰点:
在这里插入图片描述

假如ArrayDeque的容量是8,依次向队头添加一个元素,再向队尾添加一个元素,直到 tail= head
在这里插入图片描述
此时正好数组被填满了,ArrayDeque就需要扩容了。
下面我们看下ArrayDeque是如何扩容的。

8、 ArrayDeque的doubleCapacity方法(扩容)

如果比较熟悉Collection类型集合的读者,一定很清楚,涉及到扩容就少不了数组复制 System.arraycopy
下面这段代码的核心是将现有的元素复制到一个新的更大的数组中,以便有足够的空间添加新的元素。

private void doubleCapacity() {assert head == tail;  // 确保在扩容时,队列是满的,即 head == tailint p = head;  // 记录当前 head 的位置int n = elements.length;  // 获取当前数组的长度int r = n - p;  // 计算从 head 到数组末尾的元素数量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);  // 将 head 到数组末尾的元素复制到新数组的开头System.arraycopy(elements, 0, a, r, p);  // 将数组开头到 head 的元素复制到新数组后续位置elements = a;  // 更新 elements 引用,指向新的数组head = 0;  // 重置 head 为新数组的开头tail = n;  // tail 更新为原数组的长度,表示队列元素的结束位置
}

注释已经非常详细了,我们再写一段代码来验证下。

我们利用反射获取ArrayDeque的数组,数组长度,head,tail的值。

import java.lang.reflect.Field;
import java.util.ArrayDeque;public class TestA {public static void main(String[] args) throws Exception {ArrayDeque<String> deque = new ArrayDeque<>(1); // 容量小于8就默认是8deque.addFirst("队头1");deque.addFirst("队头2");deque.addFirst("队头3");deque.addFirst("队头4");deque.addLast("队尾1");deque.addLast("队尾2");deque.addLast("队尾3");deque.addLast("队尾4");Class<? extends ArrayDeque> aClass = deque.getClass();// 通过反射获取 elements 数组Field elementsField = aClass.getDeclaredField("elements");elementsField.setAccessible(true);Object[] elements = (Object[]) elementsField.get(deque);// 获取数组长度int length = elements.length;// 通过反射获取 headField headField = aClass.getDeclaredField("head");headField.setAccessible(true);int head = (int) headField.get(deque);// 通过反射获取 tailField tailField = aClass.getDeclaredField("tail");tailField.setAccessible(true);int tail = (int) tailField.get(deque);// 打印数组、数组长度、head 和 tailSystem.out.println("Array elements: ");for (Object element : elements) {System.out.print(element + " ");}System.out.println();System.out.println("Array length: " + length);System.out.println("Head: " + head);System.out.println("Tail: " + tail);}
}

运行结果:

Array elements: 
队头4 队头3 队头2 队头1 队尾1 队尾2 队尾3 队尾4 null null null null null null null null 
Array length: 16
Head: 0
Tail: 8

从结果可以看到 我们把队列添加第八个元素的时候正好填满了队列,tail=head ,触发扩容,扩容后的情况看运行结果就很清晰了。

我们再看下扩容前的情况,即队尾4还没添加之前的数组情况:

队尾1 队尾2 队尾3 null 队头4 队头3 队头2 队头1 
Array length: 8
Head: 4
Tail: 3

这样对比看就十分清晰了。
再画个动画加深印象:
在这里插入图片描述

9、 ArrayDeque的其他方法pollFirst、pollLast、peekFirst、peekLast、delete

pollFirst方法

pollFirst 方法用于从队列头部移除并返回第一个元素,如果队列为空则返回 null。

public E pollFirst() {int h = head;  // 获取头部索引@SuppressWarnings("unchecked")E result = (E) elements[h];  // 获取头部元素// 如果队列为空,返回 nullif (result == null)return null;elements[h] = null;  // 将头部元素置为空// 更新头部索引,使用按位与操作来处理环形缓冲区head = (h + 1) & (elements.length - 1);return result;  // 返回移除的元素
}

pollLast方法

pollLast 方法用于从队列尾部移除并返回最后一个元素,如果队列为空则返回 null。

public E pollLast() {// 获取尾部索引,使用按位与操作来处理环形缓冲区int t = (tail - 1) & (elements.length - 1);@SuppressWarnings("unchecked")E result = (E) elements[t];  // 获取尾部元素// 如果队列为空,返回 nullif (result == null)return null;elements[t] = null;  // 将尾部元素置为空tail = t;  // 更新尾部索引return result;  // 返回移除的元素
}

peekFirst方法

peekFirst 方法用于返回队列头部的第一个元素,但不移除它。如果队列为空,则返回 null。

public E peekFirst() {// 如果队列为空,elements[head] 为 nullreturn (E) elements[head];  // 返回头部元素
}

peekLast方法

peekLast 方法用于返回队列尾部的最后一个元素,但不移除它。如果队列为空,则返回 null。

public E peekLast() {// 使用按位与操作来处理环形缓冲区,返回尾部元素return (E) elements[(tail - 1) & (elements.length - 1)];
}

10、 ArrayDeque实现栈

在Stack详解那篇文章已经写过了,这里直接搬过来。

class MyStack<T>{// 使用 ArrayDeque 作为底层数据结构来存储栈元素private ArrayDeque<T> arrayDeque;// 构造方法,初始化 ArrayDequepublic MyStack() {this.arrayDeque = new ArrayDeque<T>();}// 将元素压入栈顶,使用 addFirst 方法将元素添加到双端队列的头部public void push(T elementData){arrayDeque.addFirst(elementData);}public void peek(){arrayDeque.getFirst();}public T pop(){return arrayDeque.removeFirst();}public int size(){return arrayDeque.size();}public boolean isEmpty(){return arrayDeque.isEmpty();}@Overridepublic String toString() {StringBuilder builder = new StringBuilder();// 这里不需要反序迭代  因为我们的push是往头部添加元素 直接正序迭代就是出栈顺序Iterator<T> iterator = arrayDeque.iterator();while (iterator.hasNext()){builder.append(iterator.next().toString()).append("  ");}return builder.toString();}
}

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

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

相关文章

20. mediasoup服务器的布署与使用

Mediasoup Demo部署 架构服务分析 服务端提供3个服务&#xff1a; 1.www服务&#xff0c;浏览器通过访问服务器目录获取客户端代码&#xff0c;通过V8引擎&#xff0c;启动底层WebRTC 2.nodejs提供websocket服务和http服务&#xff0c;用于信令交互 3.Mediasoup C提供的流媒体…

Java中的内存泄漏问题解析与应对

Java中的内存泄漏问题解析与应对 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在Java应用开发中&#xff0c;内存泄漏是一个常见但又十分棘手的问题。它会导…

逆向学习Windows篇:通过编写函数处理菜单消息

本节课在线学习视频&#xff08;网盘地址&#xff0c;保存后即可免费观看&#xff09;&#xff1a; ​​https://pan.quark.cn/s/27ab8558281e​​ 在Windows应用程序开发中&#xff0c;菜单是用户界面的重要组成部分&#xff0c;它提供了用户与应用程序交互的途径。处理菜单…

BL104应用在智慧零售多协议采集监控远程实时查看

在智慧零售领域&#xff0c;如今的市场竞争日益激烈&#xff0c;传统的零售模式已经难以满足消费者对服务和体验的高需求。智能化技术的引入&#xff0c;尤其是基于物联网的解决方案&#xff0c;成为提升零售业务效率和服务质量的关键。钡铼BL104 Modbus转MQTT网关作为一种先进…

Nginx负载均衡之反向代理缓存服务器配置

Nginx 代理功能根据应用方式的不同分为正向代理和反向代理&#xff0c;Nginx 开源版本的正向代理功能并不完整&#xff0c;不支持 HTTP 的 CONNECT 方法&#xff0c;所以 HTTPS 的正向代理功能通常是使用第三方模块来实现的。 Nginx 的 HTTPS 正向代理使用最多的第三方…

同时使用磁吸充电器和Lightning时,iPhone充电速度会变快吗?

在智能手机的世界里&#xff0c;续航能力一直是用户关注的焦点。苹果公司以其创新的MagSafe技术和传统的Lightning接口&#xff0c;为iPhone用户提供了多样化的充电解决方案。 然而&#xff0c;当这两种技术同时使用时&#xff0c;它们能否带来更快的充电速度&#xff1f;本文…

力扣(2024.06.19)

1. 42——接雨水 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 标签&#xff1a;数组&#xff0c;双指针 代码&#xff1a; class Solution:def trap(self, height: List[int]) -> int:max_left…

Talk|新加坡国立大学贾鑫宇:适用于高自由度机器人的运动控制器

本期为TechBeat人工智能社区第600期线上Talk。 北京时间6月13日(周四)20:00&#xff0c;新加坡国立大学博士生—贾鑫宇的Talk已经准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “适用于高自由度机器人的运动控制器”&#xff0c;向大家系统地介绍了如何通…

千脑计划:模拟人类大脑皮层,开启AI新纪元

随着科技的飞速发展&#xff0c;人工智能已成为当今时代的热门话题。然而&#xff0c;目前主流的深度神经网络虽然取得了显著成就&#xff0c;但也面临着能耗高、稳定性差等问题。为了解决这些挑战&#xff0c;一项名为“千脑计划”的宏伟项目应运而生&#xff0c;旨在通过模仿…

实数系和复数系-习题

出去有明确的相反的说明以外&#xff0c;本习题中所提到的数&#xff0c;都理解为实数 1.如果 r ( r ≠ 0 ) r\left( r\neq 0 \right) r(r0)是有理数而 x x x是无理数&#xff0c;证明 r x r x rx及 r x rx rx是无理数 证明&#xff1a; 假设 r x r x rx是有理数&#x…

数据结构:4.1.1二叉搜素树及查找

静态查找&#xff1a;要找的集合的元素是不动的&#xff0c;主要是find操作&#xff0c;没有delete操作 动态查找&#xff1a;要查找的集合会经常发生插入删除的操作 静态查找的一个很好的方法就是二分查找 把数据直接放在树上 结点右子树的值>结点的值>结点左子树的…

nRF Connect固件升级 OTA DFU Library for Mac and iOS, compatible with nRF5x SoCs

参考链接&#xff1a; NordicSemiconductor/IOS-DFU-Library - github

学习使用js和jquery修改css路径,实现html页面主题切换功能

学习使用js和jquery修改css路径&#xff0c;实现html页面主题切换功能 效果图html代码jquery切换css关键代码js切换css关键代码 效果图 html代码 <!DOCTYPE html> <html> <head><meta charset"utf-8"><title>修改css路径</title&g…

段式存储底层原理

段式存储管理&#xff08;Segmented Storage&#xff09;是一种内存管理技术&#xff0c;它允许程序以逻辑段&#xff08;Segment&#xff09;为单位来组织数据和代码&#xff0c;而不是像页式存储那样以固定大小的页来组织。段式存储提供了更大的灵活性&#xff0c;因为段可以…

openGauss学习笔记-299 openGauss AI特性-AI4DB数据库自治运维-DBMind的AI子功能-SQLdiag慢SQL发现

文章目录 openGauss学习笔记-299 openGauss AI特性-AI4DB数据库自治运维-DBMind的AI子功能-SQLdiag慢SQL发现299.1 概述299.2 使用指导299.2.1 前提条件299.2.2 SQL流水采集方法299.2.3 操作步骤299.2.4 使用方法示例299.3 获取帮助299.4 命令参考299.5 常见问题处理openGauss学…

几种常见的排序算法及其特性

当谈到排序算法时&#xff0c;有许多经典的算法被广泛应用。以下是几种常见的排序算法及其特性&#xff1a; 冒泡排序&#xff08;Bubble Sort&#xff09; 特性&#xff1a;通过重复地遍历要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果他们的顺序错误就把他们交…

Hive分区和分桶

分区&#xff1a; 根据某一列进行进行划分存储&#xff0c;常用的有时间分区&#xff1b; 查询数据时只需要扫描特定的分区数据&#xff0c;不需要全盘扫描&#xff0c;节省时间, 方便数据归档和清理 创建分区表 create table table_name( col1 int, col2 string ) partition …

管道保温的介绍

通风空调管道及各种水管的保温材料主要有&#xff1a;聚氨酯泡沫塑料保温、高级橡塑保温、酚醛泡沫塑料保温等。现对以上材料的特性、适用范围、施工要点等进行介绍&#xff0c;以供各位借鉴。 01 常用的绝热材料 1、聚氨酯泡沫塑料保温 该材料用于直埋管段的保温。在工程中…

Centos-Php-Nginx

安装Nginx&#xff08;如果尚未安装&#xff09;&#xff1a; sudo yum install nginx启动Nginx服务&#xff1a; sudo systemctl start nginx设置Nginx开机自启&#xff08;可选&#xff09;&#xff1a; sudo systemctl enable nginx安装PHP和PHP-FPM&#xff1a; sudo yum i…

网络安全:入侵检测系统的原理与应用

文章目录 网络安全&#xff1a;入侵检测系统的原理与应用引言入侵检测系统简介IDS的工作原理IDS的重要性结语 网络安全&#xff1a;入侵检测系统的原理与应用 引言 在我们的网络安全系列文章中&#xff0c;我们已经涵盖了从SQL注入到端点保护的多个主题。本篇文章将探讨入侵检…