Java 集合框架:Java 中的双端队列 ArrayDeque 的实现

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 019 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

在 Java 编程中,集合框架提供了一系列强大的数据结构来处理各种常见的数据存储和操作需求。其中,双端队列(Deque, Double-ended Queue)是一种灵活的数据结构,它允许在队列的两端进行元素的插入和移除。ArrayDeque 是 Java 集合框架中的一个重要实现,提供了高效的双端操作功能,兼具队列和栈的特性。

ArrayDeque 通过循环数组来实现队列操作,这种设计使得它在执行插入和删除操作时具有卓越的性能。与传统的 LinkedList 相比,ArrayDeque 在内存使用和性能上具有显著优势,尤其是在频繁进行头部和尾部操作时。它避免了 LinkedList 中节点的频繁分配和回收,并且通过数组的循环使用来最大限度地减少了空间浪费。

本文将详细介绍 ArrayDeque 的实现细节,包括其内部数据结构、核心方法的工作原理以及性能优化策略。我们将探讨 ArrayDeque 如何高效地支持双端操作,以及在实际开发中如何利用这一数据结构来优化应用程序的性能和资源使用。通过对 ArrayDeque 的深入分析,读者将能够更好地理解双端队列的运作机制,并在实际项目中充分利用这一强大的数据结构。


文章目录

      • 1、ArrayDeque 概述
        • 1.1、ArrayDeque 介绍
        • 1.2、ArrayDeque 特点
        • 1.3、ArrayDeque 用法
      • 2、ArrayDeque 底层实现
        • 2.1、ArrayDeque 数据结构
        • 2.2、插入操作
        • 2.3、推出操作
      • 3、ArrayDeque 的使用
        • 3.1、ArrayDeque 使用示例
        • 3.2、ArrayDeque 主要方法


1、ArrayDeque 概述

1.1、ArrayDeque 介绍

Deque 接口表示一个双端队列(Double Ended Queue),允许在队列的首尾两端操作,所以既能实现队列行为,也能实现栈行为。Deque 常用的两种实现 ArrayDeque 和 LinkedList。

ArrayDeque 是 Java 集合中双端队列的数组实现,可以从两端进行插入或删除操作,当需要使用栈时,Java 已不推荐使用 Stack,而是推荐使用更高效的ArrayDeque,当需要使用队列时也可以使用 ArrayDeque。

1.2、ArrayDeque 特点

ArrayDeque 有着如下的主要特性:

  • 双端队列: ArrayDeque 允许在两端(头部和尾部)进行高效的插入和删除操作;
  • 可变大小数组: ArrayDeque 使用一个可变大小的数组来存储元素,当数组满了时,会自动扩展其容量;
  • 无容量限制: 与 ArrayList 类似,ArrayDeque 没有固定的容量限制,可以根据需要动态扩展;
  • 高效操作: 在头部和尾部进行的插入和删除操作通常比 LinkedList 更高效,因为它没有涉及节点的额外开销。
1.3、ArrayDeque 用法

ArrayDeque 典型用法如下:

  1. 作为栈 (Stack): 可以使用 ArrayDeque 作为栈来使用,因为它提供了 pushpop 方法;
  2. 作为队列 (Queue): 也可以将 ArrayDeque 用作队列,因为它提供了 offerpoll 等方法。

例子:

import java.util.ArrayDeque;
import java.util.Deque;public class ArrayDequeExample {public static void main(String[] args) {// 创建一个 ArrayDeque 实例Deque<String> deque = new ArrayDeque<>();// 添加元素deque.add("Element 1");deque.addFirst("Element 2");deque.addLast("Element 3");// 访问元素System.out.println("Head: " + deque.peek());System.out.println("Tail: " + deque.peekLast());// 删除元素System.out.println("Removed Head: " + deque.removeFirst());System.out.println("Removed Tail: " + deque.removeLast());// 栈操作deque.push("Stack Element 1");deque.push("Stack Element 2");System.out.println("Popped from stack: " + deque.pop());}
}

2、ArrayDeque 底层实现

2.1、ArrayDeque 数据结构

ArrayDeque 是 Java 集合框架中双端队列 (Deque) 的一种实现,基于可变大小的数组来存储元素。其数据结构设计使得在队列两端进行插入和删除操作非常高效:

  • 基础数组:ArrayDeque 内部使用一个数组来存储元素。这个数组是可变大小的,默认容量为 16。当元素数量超过当前容量时,数组会自动扩展;
  • 头部和尾部指针: ArrayDeque 通过两个指针来追踪队列的头部和尾部。这两个指针分别称为 headtail。初始情况下,headtail 都指向数组的第一个位置。
public class ArrayDeque<E> extends AbstractCollection<E>implements Deque<E>, Cloneable, Serializable
{/*** 用于存储双端队列元素的数组。* 双端队列的容量是该数组的长度,这个长度总是2的幂。* 除了在某个addX方法内瞬时变满之外,这个数组永远不会变满,* 一旦变满(参见doubleCapacity方法),就会立即调整大小,从而避免* 头指针和尾指针绕回并相等。我们还保证所有不持有双端队列元素的数组单元总是为null。*/transient Object[] elements; // 非private以简化嵌套类访问/*** 双端队列头部元素的索引(即remove()或pop()将要移除的元素的索引);* 如果双端队列为空,则为等于tail的任意数。*/transient int head;/*** 下一个元素将被添加到双端队列尾部的索引(通过addLast(E)、add(E)或push(E))。*/transient int tail;/*** 用于新创建的双端队列的最小容量。* 必须是2的幂。*/private static final int MIN_INITIAL_CAPACITY = 8;public ArrayDeque() {elements = new Object[16];}public ArrayDeque(int numElements) {// 调用 allocateElements 方法分配数组空间// 传入的 numElements 参数决定了初始容量allocateElements(numElements);}public ArrayDeque(Collection<? extends E> c) {// 调用 allocateElements 方法分配数组空间// 传入集合 c 的大小决定了初始容量allocateElements(c.size());// 将集合 c 中的所有元素添加到当前双端队列中addAll(c);}private static int calculateSize(int numElements) {// 初始容量设置为最小初始容量int initialCapacity = MIN_INITIAL_CAPACITY;// 如果 numElements 大于或等于初始容量,则计算需要的容量if (numElements >= initialCapacity) {// 将 initialCapacity 设置为 numElements 的值initialCapacity = numElements;// 通过位运算将容量扩展到大于或等于 numElements 的最小2的幂次方initialCapacity |= (initialCapacity >>> 1);initialCapacity |= (initialCapacity >>> 2);initialCapacity |= (initialCapacity >>> 4);initialCapacity |= (initialCapacity >>> 8);initialCapacity |= (initialCapacity >>> 16);// 将容量扩大到下一个2的幂次方initialCapacity++;// 如果计算出的容量小于0,说明容量过大,避免超出数组的最大容量if (initialCapacity < 0)// 将容量减半,以避免分配过大的内存initialCapacity >>>= 1;}// 返回计算得到的容量return initialCapacity;}private void allocateElements(int numElements) {// 根据 calculateSize 方法计算的容量来创建数组elements = new Object[calculateSize(numElements)];}// 省略其他方法和实现细节...
}

详细解读:

  1. elements:这个数组用于存储 ArrayDeque 中的所有元素。数组的长度始终是 2 的幂,确保了高效的取模操作(通过位运算实现)。除了在某些方法中瞬时变满之外,数组永远不会变满。当数组变满时,ArrayDeque 会立即调用 doubleCapacity 方法来扩展容量,以避免头尾指针相等的情况;
  2. head:这是一个指向双端队列头部的索引,即最先添加的元素的索引。remove()pop() 方法将会移除该索引处的元素。当双端队列为空时,这个索引将等于 tail
  3. tail:这是一个指向双端队列尾部的索引,即下一个元素将要添加到的索引。addLast(E)add(E)push(E) 方法会在这个索引处添加元素;
  4. MIN_INITIAL_CAPACITY:这是 ArrayDeque 初始容量的最小值,必须是2的幂。这个值定义了新创建的 ArrayDeque 的初始容量,确保在容量扩展前有足够的空间来存储元素;
  5. 默认构造器会将 elements 初始化为一个长度为 16 的数组,另外两个构造器将根据参数的长度来初始化,最终会调用calculateSize方法计算初始长度。

这里需要注意一点,用 elements 存储的是双端队列,headtail 参数表示双端队列的头和尾的索引,但并不意味着 head 值永远小于 tail,当 tail 移动至数组末尾,但队列长度小于数组时(意味着此时 head大 于0),再向队列末尾添加数据将会使 tail 移动至数组的开头。

假设下图为当前的 elements 数组,黄色方块表示其中有数据:

img

当我们向队列末尾添加一个元素时,数组变成了下面这样:

img

目前都是按照我们预期发展的,现在我们来调用poll方法移除队列头部的一个元素,此时elements变成了下图:

img

这个时候因为我们移除了队列的头部元素,数组的开头已经空下来了一个位置,这时候再向队列末尾追加一个元素。

img

可以看到,此时 headtail 的右侧,ArrayDeque 为了不浪费数组空间进行了这样的设计,也不难理解。

2.2、插入操作

addFirst(E e)addLast(E e) 两个方法分别在头部和尾部添加元素,并在必要时调用 doubleCapacity() 方法扩展容量。

public class ArrayDeque<E> extends AbstractCollection<E>implements Deque<E>, Cloneable, Serializable
{// 省略其他方法和实现细节.../*** 将指定的元素插入到此双端队列的前面。** @param e 要添加的元素* @throws NullPointerException 如果指定的元素为 null*/public void addFirst(E e) {if (e == null)throw new NullPointerException();// 计算新的 head 索引,并将元素 e 插入到新的 head 位置elements[head = (head - 1) & (elements.length - 1)] = e;// 如果 head 与 tail 相等,表示数组已满,调用 doubleCapacity 方法扩展容量if (head == tail)doubleCapacity();}/*** 将指定的元素插入到此双端队列的末尾。** <p>此方法等效于 {@link #add}.** @param e 要添加的元素* @throws NullPointerException 如果指定的元素为 null*/public void addLast(E e) {if (e == null)throw new NullPointerException();// 将元素 e 插入到 tail 位置elements[tail] = e;// 计算新的 tail 索引,并进行循环数组操作if ((tail = (tail + 1) & (elements.length - 1)) == head)doubleCapacity();}/*** 扩展此双端队列的容量。仅在满时调用,即当 head 和 tail* 环绕相等时。*/private void doubleCapacity() {assert head == tail;int p = head;int n = elements.length;int r = n - p; // p右侧的元素个数int newCapacity = n << 1; // 新容量为当前容量的两倍if (newCapacity < 0)throw new IllegalStateException("对不起,双端队列太大了");Object[] a = new Object[newCapacity];// 将元素从原数组复制到新数组System.arraycopy(elements, p, a, 0, r);System.arraycopy(elements, 0, a, r, p);elements = a;// 更新 head 和 tail 指针head = 0;tail = n;}// 省略其他方法和实现细节...
}
2.3、推出操作

ArrayDequepollremove 相关方法通过循环数组实现高效的双端操作。pollFirst()pollLast() 方法尝试从队列的前端或末尾移除并返回元素,如果队列为空则返回 null;而 removeFirst()removeLast() 方法在移除元素时会检查队列是否为空,若为空则抛出 NoSuchElementException 异常。这些方法通过更新数组的 headtail 指针来维护队列状态,并在移除元素后将相应的数组槽置为空,以便进行垃圾回收。

public class ArrayDeque<E> extends AbstractCollection<E>implements Deque<E>, Cloneable, Serializable
{// 省略其他方法和实现细节.../*** 从队列的前面移除并返回第一个元素。** @throws NoSuchElementException 如果队列为空*/public E removeFirst() {// 调用 pollFirst 方法尝试移除并返回第一个元素E x = pollFirst();// 如果 pollFirst 返回 null,则抛出 NoSuchElementException 异常if (x == null)throw new NoSuchElementException();return x;}/*** 从队列的末尾移除并返回最后一个元素。** @throws NoSuchElementException 如果队列为空*/public E removeLast() {// 调用 pollLast 方法尝试移除并返回最后一个元素E x = pollLast();// 如果 pollLast 返回 null,则抛出 NoSuchElementException 异常if (x == null)throw new NoSuchElementException();return x;}public E pollFirst() {int h = head;@SuppressWarnings("unchecked")// 获取并强制转换头部的元素E result = (E) elements[h];// 如果头部的元素为 null,表示队列为空,直接返回 nullif (result == null)return null;// 置空头部的元素槽elements[h] = null;// 更新 head 索引,移到下一个位置head = (h + 1) & (elements.length - 1);return result;}public E pollLast() {int t = (tail - 1) & (elements.length - 1);@SuppressWarnings("unchecked")// 获取并强制转换尾部的元素E result = (E) elements[t];// 如果尾部的元素为 null,表示队列为空,直接返回 nullif (result == null)return null;// 置空尾部的元素槽elements[t] = null;// 更新 tail 索引,移到前一个位置tail = t;return result;}// 省略其他方法和实现细节...
}

3、ArrayDeque 的使用

3.1、ArrayDeque 使用示例

ArrayDeque 使用循环数组来存储元素,当在数组头部添加元素时,如果当前头部指针在数组的起始位置(索引为 0),新元素将被插入到数组的最后一位。这是通过循环数组的特性实现的,确保在队列的两端进行操作时能够高效利用数组的空间。

假设我们有一个初始的 ArrayDeque,数组的初始状态如下:

[ 1, null, null, null, null, null, null, null ]  // head = 0, tail = 1

数组索引 0 位置有元素 1,head 指向 0,tail 指向 1。

现在,如果我们在队列前面添加一个元素 2,那么新元素将被插入到数组的最后一位(索引7),并且 head 指针将会更新:

deque.addFirst(2);

此时,数组和指针的状态如下:

[ 1, null, null, null, null, null, null, 2 ]  // head = 7, tail = 1

是的,ArrayDeque 使用循环数组来存储元素,当在数组头部添加元素时,如果当前头部指针在数组的起始位置(索引为 0),新元素将被插入到数组的最后一位。这是通过循环数组的特性实现的,确保在队列的两端进行操作时能够高效利用数组的空间。

现在,如果我们在队列前面添加一个元素2,那么新元素将被插入到数组的最后一位(索引7),并且 head 指针将会更新,下面是一个具体的代码示例:

import java.util.ArrayDeque;public class ArrayDequeExample {public static void main(String[] args) {ArrayDeque<Integer> deque = new ArrayDeque<>(8);// 初始化添加一个元素deque.add(1);// 打印初始状态System.out.println("Initial deque: " + deque);// 在队列前面添加一个元素deque.addFirst(2);// 打印添加后的状态System.out.println("After adding to the front: " + deque);}
}

输出结果:

Initial deque: [1]
After adding to the front: [2, 1]
3.2、ArrayDeque 主要方法

ArrayDeque 主要方法:

  • add(E e): 将元素添加到队列尾部。
  • addFirst(E e): 将元素添加到队列头部。
  • addLast(E e): 将元素添加到队列尾部。
  • offer(E e): 尝试将元素添加到队列尾部。
  • offerFirst(E e): 尝试将元素添加到队列头部。
  • offerLast(E e): 尝试将元素添加到队列尾部。
  • remove(): 移除并返回队列头部的元素。
  • removeFirst(): 移除并返回队列头部的元素。
  • removeLast(): 移除并返回队列尾部的元素。
  • poll(): 检索并移除队列头部的元素,如果队列为空,则返回 null
  • pollFirst(): 检索并移除队列头部的元素,如果队列为空,则返回 null
  • pollLast(): 检索并移除队列尾部的元素,如果队列为空,则返回 null
  • peek(): 检索但不移除队列头部的元素,如果队列为空,则返回 null
  • peekFirst(): 检索但不移除队列头部的元素,如果队列为空,则返回 null
  • peekLast(): 检索但不移除队列尾部的元素,如果队列为空,则返回 null
  • push(E e): 将元素推送到栈顶。
  • pop(): 从栈顶弹出元素并返回。

ArrayDeque 是一个非常灵活且高效的双端队列实现,在需要频繁操作队列两端的场景下,ArrayDeque 是一个很好的选择。

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

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

相关文章

共享模型之无锁

一、问题提出 1.1 需求描述 有如下的需求&#xff0c;需要保证 account.withdraw() 取款方法的线程安全&#xff0c;代码如下&#xff1a; interface Account {// 获取余额Integer getBalance();// 取款void withdraw(Integer amount);/*** 方法内会启动 1000 个线程&#xf…

GraphPad prism处理cck-8获得ic50

C组为空白对照组&#xff0c;a组为dmso对照组&#xff0c;b组为细胞加药组&#xff0c;八个梯度的药物浓度 一、数据转化 首先&#xff0c;打开软件&#xff0c;选项中选择x的第一项&#xff0c;y的第二项&#xff0c;单一药物浓度设定了几个孔就选几 把自己的药物浓度直接复制…

ubuntu22安装拼音输入法

专栏总目录 一、安装命令&#xff1a; sudo apt update sudo apt install fcitx sudo apt install fcitx-pinyin 二、切换输入法

游戏常用运行库安装包 Game Runtime Libraries Package

游戏常用运行库安装包&#xff08;Game Runtime Libraries Package&#xff09;是一个整合了多种游戏所需运行库的安装程序&#xff0c;旨在帮助玩家和开发者解决游戏无法正常运行的问题。该安装包支持从Windows XP到Windows 11的系统&#xff0c;并且具备自动检测系统并推荐合…

代码随想录训练第二十七天|LeetCode56.合并区间、LeetCode738.单调递增的数字、LeetCode968.监控二叉树

文章目录 56.合并区间思路 738.单调递增的数字思路 968.监控二叉树思路确定遍历顺序如何隔两个节点放一个摄像头 56.合并区间 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一…

Step-DPO 论文——数学大语言模型理解

论文题目&#xff1a;STEP-DPO: STEP-WISE PREFERENCE OPTIMIZATION FOR LONG-CHAIN REASONING OF LLMS 翻译为中文就是&#xff1a;“LLMs长链推理的逐步偏好优化” 论文由港中文贾佳亚团队推出&#xff0c;基于推理步骤的大模型优化策略&#xff0c;能够像老师教学生一样优…

String 和StringBuilder字符串操作快慢的举例比较

System.currentTimeMillis(); //当前时间与1970年1月1日午夜UTC之间的毫秒差。public class HelloWorld {public static void main(String[] args) {String s1 "";StringBuilder s2 new StringBuilder("");long time System.currentTimeMillis();long s…

git命令学习分享

分布式版本控制系统&#xff0c;本地仓库和远程仓库相互独立。 使用repository仓库进行控制&#xff0c;可以对里面的文件进行跟踪&#xff0c;复原。 git config --global --list&#xff1a;查看git配置列表 cd ** &#xff1a;进入** cd .. &#xff1a;退回上一级 echo…

AI Agent项目探索与实践记录

AI Agent项目探索与实践记录 1. 概述2. 总体结构2.1 记忆模块2.2 模型服务模块2.2.1 LLM服务2.2.2 retrieval服务2.2.3 rerank服务 2.3 Agent系统2.3.1 Planner2.3.2 Code/SQL Generator2.3.3 Code Executor2.3.4 Responser2.3.5 Round Compressor2.3.6 New Turn Discriminator…

基于Llama Index构建RAG应用(Datawhale AI 夏令营)

前言 Hello&#xff0c;大家好&#xff0c;我是GISer Liu&#x1f601;&#xff0c;一名热爱AI技术的GIS开发者&#xff0c;本文参与活动是2024 DataWhale AI夏令营&#xff1b;&#x1f632; 在本文中作者将通过&#xff1a; Gradio、Streamlit和LlamaIndex介绍 LlamaIndex 构…

全局 loading

好久不见&#xff01; 做项目中一直想用一个统一的 loading 状态控制全部的接口加载&#xff0c;但是一直不知道怎么处理&#xff0c;最近脑子突然灵光了一下想到了一个办法。 首先设置一个全局的 loading 状态&#xff0c;优先想到的就是 Pinia 然后因为页面会有很多接口会…

数据结构——栈(链式结构)

一、栈的链式存储结构 如果一个栈存在频繁进栈和出栈操作&#xff0c;可以考虑链式结构。 栈的链式存储结构是指使用链表来实现栈这种数据结构。在链式存储结构中&#xff0c;栈的每个元素被封装成一个节点&#xff0c;节点之间通过指针相连&#xff0c;形成一个链表。栈顶元…

Linux下开放指定端口

比如需要开放82端口&#xff1a; #查询是否开通 firewall-cmd --query-port82/tcp#开放端口82 firewall-cmd --zonepublic --add-port82/tcp --permanent#重新加载防火墙 firewall-cmd --reload

java学习--代码块

package com.block.test01; class Main{public static void main(String[] args) {Block block new Block("你好&#xff0c;李焕英");new Block("你好",12,24);} } public class Block {String name;int begin_time;int end_time; //如果在调用构造器时都…

SwiftUI 5.0(iOS 17)滚动视图的滚动目标行为(Target Behavior)解惑和实战

概览 在 SwiftUI 的开发过程中我们常说&#xff1a;“屏幕不够&#xff0c;滚动来凑”。可见滚动视图对于超长内容的呈现有着多么秉轴持钧的重要作用。 这不&#xff0c;从 SwiftUI 5.0&#xff08;iOS 17&#xff09;开始苹果又为滚动视图增加了全新的功能。但是官方的示例可…

Linux----Mplayer音视频库的移植

想要播放视频音乐就得移植相关库到板子上 Mplayer移植需要依赖以下源文件&#xff1a;(从官网获取或者网上) 1、zlib-1.2.3.tar.gz &#xff1a;通用的内存空间的压缩库。 2、libpng-1.2.57.tar.gz :png格式图片的压缩或解压库 3、Jpegsrc.v9b.tar.gz : jpeg格式图片的压…

数据结构day3

一、思维导图 二、顺序表实现学生管理系统 //头文件 #ifndef TEST_H #define TEST_H #define MAX_SIZE 100//定义学生类型 typedef struct {char name[20]; //姓名int age; //年龄double score; //分数 }datatype;//定义班级类型 typedef struct {datatype student[MAX…

CDGA数据治理:突破卡点堵点,解决确权难、流通交易难问题

随着大数据时代的来临&#xff0c;数据已成为推动社会进步和经济发展的重要力量。然而&#xff0c;数据治理中的卡点堵点问题&#xff0c;特别是确权难、流通交易难&#xff0c;正成为制约数据要素市场健康发展的瓶颈。本文将探讨这些问题&#xff0c;并提出相应的解决方案。 确…

uniapp写登陆|微信小程序登录和微信h5登录使用同一个页面

文章目录 导文微信小程序登录先写一个样式代码实现详细解释&#xff1a; 微信h5登录先写一个样式代码实现1. checkWeChatCode()2. getWeChatCode()页面获取登陆后的code 导文 微信小程序登录怎么实现&#xff1f; 微信h5登录怎么实现&#xff1f; 用uniapp写同一个页面&#xf…

CloudCampus的三种部署模式

CloudCampus的三种部署模式 本地部署 客户购买控制器 自己运营 软件永久license sns &#xff0c;将软件补丁、软件升级&#xff08;含升级版本的新特性&#xff09;、远程支持等打包在一起组成SnS年费 msp自建云部署 msp 购买控制器 msp运营 …