说一下 ArrayDeque 和 LinkedList 的区别?

news2025/8/9 23:40:39

大家好,我是小彭。

在上一篇文章里,我们聊到了基于链表的 Queue 和 Stack 实现 —— LinkedList。那么 Java 中有没有基于数组的 Queue 和 Stack 实现呢?今天我们就来聊聊这个话题。


小彭的 Android 交流群 02 群已经建立啦,扫描文末二维码进入~


思维导图:


1. 回顾 LinkedList

在数据结构上,LinkedList 不仅实现了与 ArrayList 相同的 List 接口,还实现了 Deque 接口,而我们今天要讲的 ArrayDeque 就是实现于 Deque 接口的动态数组。

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

1.1 Queue 接口

Queue 的 API 可以分为 2 类,区别在于方法的拒绝策略上:

  • 抛异常:
    • 向空队列取数据,会抛出 NoSuchElementException 异常;
    • 向容量满的队列加数据,会抛出 IllegalStateException 异常。
  • 返回特殊值:
    • 向空队列取数据,会返回 null;
    • 向容量满的队列加数据,会返回 false。
拒绝策略抛异常返回特殊值
入队(队尾)add(e)offer(e)
出队(队头)remove()poll()
观察(队头)element()peek()

1.2 Deque 接口(继承于 Queue 接口)

Java 没有提供标准的栈接口(很好奇为什么不提供),而是放在 Deque 接口中:

拒绝策略抛异常等价于
入栈push(e)addFirst(e)
出栈pop()removeFirst()
观察(栈顶)peek()peekFirst()

除了标准的队列和栈行为,Deque 接口还提供了 12 个在两端操作的方法:

拒绝策略抛异常返回值
增加addFirst(e)/ addLast(e)offerFirst(e)/ offerLast(e)
删除removeFirst()/ removeLast()pollFirst()/ pollLast()
观察getFirst()/ getLast()peekFirst()/ peekLast()

2. ArrayDeque 的特点

2.1 说一下 ArrayDeque 的特点

  • 1、ArrayDeque 是基于动态数组实现的 Deque 双端队列,内部封装了扩容和数据搬运的逻辑;
  • 2、ArrayDeque 的数组容量保证是 2 的整数幂;
  • 3、ArrayDeque 不是线程安全的;
  • 4、ArrayDeque 不支持 null 元素;
  • 5、ArrayDeque 虽然入栈和入队有可能会触发扩容,但从均摊分析上看依然是 O(1) 时间复杂度;

2.2 说一下 ArrayDeque 和 LinkedList 的区别?

  • 1、数据结构: 在数据结构上,ArrayDeque 和 LinkedList 都实现了 Java Deque 双端队列接口。但 ArrayDeque 没有实现了 Java List 列表接口,所以不具备根据索引位置操作的行为;

  • 2、线程安全: ArrayDeque 和 LinkedList 都不考虑线程同步,不保证线程安全;

  • 3、底层实现: 在底层实现上,ArrayDeque 是基于动态数组的,而 LinkedList 是基于双向链表的。

    • 在遍历速度上: ArrayDeque 是一块连续内存空间,基于局部性原理能够更好地命中 CPU 缓存行,而 LinkedList 是离散的内存空间对缓存行不友好;

    • 在操作速度上: ArrayDeque 和 LinkedList 的栈和队列行为都是 O(1) 时间复杂度,ArrayDeque 的入栈和入队有可能会触发扩容,但从均摊分析上看依然是 O(1) 时间复杂度;

    • 额外内存消耗上: ArrayDeque 在数组的头指针和尾指针外部有闲置空间,而 LinkedList 在节点上增加了前驱和后继指针。


3. 如何使用数组实现栈和队列?

我们知道栈和队列都是 “操作受限” 的线性表:栈是 LIFO,限制在表的一端入栈和出栈。而队列是 FIFO,限制在表的一端入队,在另一端出队。栈和队列既可以用数组实现,也可以用链表实现:

  • 数组:用数组实现时叫作顺序栈和顺序队列;
  • 链表:用链表实现时叫作链式栈和链式队列。

3.1 为什么 ArrayList 不适合实现队列?

在上一篇文章里,我们提到了 LinkedList 的多面人生,它即作为 List 的链式表,又作为 Queue 的链式队列,又作为 “Stack” 的链式栈,功能很全面。相较之下,ArrayList 却只作为实现了 List 的顺序表,为什么呢?

这是因为在数组上同时实现 List 和 Queue 时,无法平衡这两个行为的性能矛盾。具体来说:ArrayList 不允许底层数据有空洞,所有的有效数据都会 “压缩” 到底层数组的首部。所以,使用 ArrayList 开发栈的结构或许合适,可以在数组的尾部操作数据。但使用 ArrayList 开发队列就不合适,因为在数组的首部入队或出队需要搬运数据。

3.2 使用数组实现栈结构

使用数组实现栈相对容易,因为栈结构的操作被限制在数组的一端,所以我们可以选择数组的尾部作为栈顶,并且使用一个 top 指针记录栈顶位置:

  • 栈空: top == 0;
  • 栈满: top == n;
  • 入栈: 将数据添加到栈顶位置,均摊时间复杂度是 O(1);
  • 出栈: 将栈顶位置移除,时间复杂度是 O(1);

对于出栈而言,时间复杂度总是 O(1),但是对于入栈而言,却不一定。因为当数组的空间不足(top == n)时,就需要扩容和搬运数据来容纳新的数据。此时,时间复杂度就从 O(1) 退化到 O(n)。

对于这种大部分操作时间复杂度很低,只有个别情况下时间复杂度会退化,而且这些操作之间存在很强烈的顺序关系的情况,就很适合用 “均摊时间复杂度分析” 了:

假设我们的扩容策略是将数组扩大到旧数组的 2 倍,用均摊分析法:

  • 1、对于一个大小为 K 的空数组,在前 K - 1 次入栈操作中,时间复杂度都是 O(1);
  • 2、在第 K 次入栈中,由于数组容量不足,所以我们将数组扩大为 2K,并且搬运 K 个数据,时间复杂度退化为 O(K);
  • 3、对于一个大小为 2K 的数组,在接下来的 K - 1 次入栈操作中,时间复杂度都是 O(1);
  • 4、在第 2K 次入栈中,由于数组容量不足,所以我们将数组扩大为 4K,并且搬运 2K 个数据,时间复杂度再次退化为 O(K);
  • 5、依此类推。

可以看到,在每次搬运 K 个次数后,随后的 K - 1 次入栈操作就只是简单的 O(1) 操作,K 次入栈操作涉及到 K 个数据搬运和 K 次赋值操作。那我们从整体看,如果把复杂度较高的 1 次入栈操作的耗时,均摊到其他复杂度较低的操作上,就等于说 1 次入栈操作只需要搬运 1 个数据和 1 次赋值操作,所以入栈的均摊时间复杂度就是 O(1)。

入栈的均摊时间复杂度分析

3.3 使用数组实现队列结构

使用数组实现队列相对复杂,我们需要一个队头指针和一个队尾指针:

  • 队空: head == tail;
  • 队满: tail == n(并不是真的满,只是无法填充新数据);
  • 入队: 将数据添加到队尾位置,均摊时间复杂度是 O(1);
  • 出队: 将队头位置移除,时间复杂度是 O(1)。

对于出队而言,时间复杂度总是 O(1)。对于入队而言,当 tail == n 时,就需要扩容和搬运数据来容纳新的数据,我们用均摊分析法得出均摊时间复杂度依然是 O(1),就不重复了。

但是我们发现,栈的 top == n 表示栈空间不足,扩容没有问题,而队列的 tail == n 却不一定表示队列空间不足。因为入队和出队发生在不同方向,有可能出现 tail == n 但队头依然有非常多剩余空间的情况。此时,扩容显得没有必要。

扩容显得没有必要

那么,怎么避免没有必要的扩容和数据搬移呢?—— 循环数组。

我们在逻辑上将数组的首尾相连,当 tail == n 时,如果数组头部还有空闲位置,我们就把 tail 指针调整到数组头部,在数组头部添加数据。我们下面要分析的 ArrayDeque 数据结构,就是采用了循环数组的实现。

使用循环数组后,队列空和队列满的判断条件会发生变化:

  • 队列空: head == tail;
  • 队列满: (tail + 1)%size == head,如果 size 是 2 的整数幂,还可以用位运算判断:(tail + 1) & (size - 1) == head。

理解了使用数组实现栈和队列的思路后,下面再来分析 ArrayDeque 的实现原理,就显得游刃有余了。


4. ArrayDeque 源码分析

这一节,我们来分析 ArrayDeque 中主要流程的源码。

4.1 ArrayDeque 的属性

  • ArrayDeque 底层是一个 Object 数组;
  • ArrayDeque 用 headtail 指针指向数组的 “队头位置” 和 “队尾位置”,需要注意 tail 队尾指针实际上是指向队尾最后一个有效元素的下一位。

ArrayDeque 的属性很好理解的,不出意外的话又有小朋友出来举手提问了:

  • 🙋🏻‍♀️疑问 1: 为什么字段都不声明 private 关键字?(回答过多少次了,把手给我放下)
  • 🙋🏻‍♀️疑问 2: 为什么字段都声明 transient 关键字?(回答过多少次了,把手给我放下)
  • 🙋🏻‍♀️疑问 3: 为什么 elements 字段不声明为泛型类型 E?(回答过多少次了,把手给我放下)
  • 🙋🏻‍♀️疑问 4:为什么没有看到 ArrayList 类似的 MAX_ARRAY_SIZE 最大容量限制?

这个问题我们在分析源码的过程中回答。有了 ArrayList 的分析基础,问题少了很多,ArrayDeque 真香。

public class ArrayDeque<E> extends AbstractCollection<E>
		implements Deque<E>, Cloneable, Serializable
{
    // 疑问 1:为什么字段都声明 private 关键字?
    // 疑问 2:为什么字段都声明 transient 关键字?
    // 疑问 3:为什么 elements 字段不声明为泛型类型 E?
    // 疑问 4:为什么没有看到 ArrayList 类似的 MAX_ARRAY_SIZE 最大容量限制?

    // 底层数组
    transient Object[] elements; // non-private to simplify nested class access

    // 队头指针
    transient int head;

    // 队尾指针
    transient int tail;

    // new ArrayDeque(0) 最小初始容量
    private static final int MIN_INITIAL_CAPACITY = 8;

    // 尾指针 - 头指针
    public int size() {
        return (tail - head) & (elements.length - 1);
    }
}

4.2 ArrayDeque 的构造方法

ArrayDeque 有 3 个构造函数:

  • 1、无参构造方法: 初始化容量为 16 的数组;
  • 2、带初始容量的构造方法: 如果初始容量小于 8 (MIN_INITIAL_CAPACITY),则初始化数组大小为 8。如果初始容量大于 8,则计算 “最近的 2 的整数幂” 作为初始大小。例如 numElements 为 8,则初始化容量为 16。numElements 为 19,则初始化容量为 32;
  • 3、带集合的构造方法: 用相同的方法创建初始容量为 2 的整数幂的数组,并调用 addAll 逐个添加元素。

ArrayDeque.java

// 无参构造方法
public ArrayDeque() {
    elements = new Object[16];
}

// 带初始容量的构造方法
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

// 带集合的构造方法
public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    // 疑问 5:为什么带集合的构造方法不使用 Arrays 工具整体复制,而是逐个添加?
    addAll(c);
}

private void allocateElements(int numElements) {
    elements = new Object[calculateSize(numElements)];
}

// 求离 numElements 最近的 2 的整数幂,即 nextPow2 问题
// 疑问 6:为什么 ArrayDeque 要求容量是 2 的整数幂?
// 疑问 7:calculateSize() 的函数体解释一下?
private static int calculateSize(int numElements) {
    // 如果 numElements 小于 8(MIN_INITIAL_CAPACITY),则返回 8
    int initialCapacity = MIN_INITIAL_CAPACITY;
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        // 五轮无符号右移和或运算后,变成最高有效位开始后面都是 1
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        // 加 1 进位,得到最近 2 的整数幂
        initialCapacity++;
			
        // 变成负数(高位是 1000,低位都是 0000)
        if (initialCapacity < 0)
            // 右移 1 位,取 2^30 为初始容量
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}

AbstractCollection.java

// 逐个添加元素
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

小朋友总有太多的问号,举手提问 🙋🏻‍♀️

  • 🙋🏻‍♀️疑问 5:为什么带集合的构造方法不使用 Arrays 工具整体复制,而是逐个添加?

因为 ArrayDeque 禁止存储 null 元素,所以需要逐个判断元素是否为 null 值后才添加。

  • 🙋🏻‍♀️疑问 6:为什么 ArrayDeque 要求数组容量是 2 的整数幂?

在循环数组中需要使用取余运算计算游标指针循环后的位置,例如 (tail + 1) % size,而如果数组的尺寸 size 是 2 的整数幂,那么就可以将取余运算替换为位运算,例如 (tail + 1) & (size - 1) ,不管被除数是正负结果都是正数。 不仅将取余运算替换为位运算,而且减少了一次取绝对值运算,提高了索引的计算效率。

size     = 0 0 0 1 0 0 0 0 0 0     // 2^n 的补码
size - 1 = 0 0 0 0 1 1 1 1 1 1     // 2^n - 1 的补码
-1       = 1 1 1 1 1 1 1 1 1 1     // -1 的补码
0        = 0 0 0 0 0 0 0 0 0 0     // 0 的补码

// 尾指针的循环:
1、如果 tail + 1 <= size - 1,则 (tail + 1) & (size - 1) 后保持不变
2、如果 tail + 1 == size,则 size & (size - 1)0
// 头指针的循环
1、如果 head - 1 >= 0,则(head - 1) & (size - 1) 后保持不变
2、如果 head - 1 == -1,则 -1 & (size - 1) 后为 size - 1
  • 🙋🏻‍♀️疑问 7:calculateSize() 的函数体解释一下?

calculateSize() 是求离 numElements 最近的 2 的整数幂,即 nextPow2 问题。

  • 1、首先,如果 numElements 小于 8(MIN_INITIAL_CAPACITY),则直接返回 8;
  • 2、否则执行 nextPow2 运算,经过五轮无符号右移和或运算,将 numElements 转换为从最高位开始后面都是 1 的数。再执行 +1 运算,就求出了最近的 2 的整数幂(最高有效位是 1,低位都是 0);
  • 3、当 numElements 在 2^30 到 2^ 31-1 之间(即最高位是 0100 的数),计算后得到的 nextPow2 就是负数(最高位是 1000,低位都是 0),此时就需要右移一位,取 2^30 为初始容量。
n = 0 0 0 0 1 x x x x x     // n
n = 0 0 0 0 1 1 x x x x     // n |= n >>> 1;
n = 0 0 0 0 1 1 1 1 x x     // n |= n >>> 2;
n = 0 0 0 0 1 1 1 1 1 1     // n |= n >>> 4;
n = 0 0 0 0 1 1 1 1 1 1     // n |= n >>> 8;(这一步对 n 没有影响了)
n = 0 0 0 0 1 1 1 1 1 1     // n |= n >>> 16;(这一步对 n 没有影响了)
n = 0 0 0 1 0 0 0 0 0 0     // n ++(进位,得到最近 2 的整数幂)

4.3 ArrayDeque 的添加和扩容方法

ArrayDeque 可以在数组的两端添加元素,不支持在数组的中间添加元素:

  • 在队头添加: 在 head 指针的上一个位置赋值,如果数组越界则循环到数组尾部;
  • 在队尾添加: 在 tail 指针的位置赋值,并将 tail 指针指向下一个位置,如果数组越界则循环到数组头部。
public void addLast(E e) {
    // 疑问:为什么 ArrayDeque 不支持添加 null 元素
    if (e == null)
        throw new NullPointerException();
    // tail 指针本身就是指向下一个位置,所以直接填充
    elements[tail] = e;
    // 修改 tail 指针到下一个位置,并通过取余操作循环到数组头部
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

public void addFirst(E e) {
    // 疑问 8:为什么 ArrayDeque 禁止存储 null 元素?
    if (e == null)
        throw new NullPointerException();
    // 修改 head 指针到前一个位置,并通过取余操作循环到数组尾部
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

不出意外小朋友又要出来举手提问了🙋🏻‍♀️

  • 🙋🏻‍♀️疑问 8:为什么 ArrayDeque 禁止存储 null 元素?

其实在 Deque 接口上并不严格禁止存储 null 元素,但是会强烈建议 Deque 的实现不提供存储 null 值的能力。因为 null 通常会作为一个特殊值来判断队列是否为空。

Deque javadoc

While Deque implementations are not strictly required to prohibit the insertion of null elements, they are strongly encouraged to do so. Users of any Deque implementations that do allow null elements are strongly encouraged not to take advantage of the ability to insert nulls. This is so because null is used as a special return value by various methods to indicated that the deque is empty.
public E pollFirst() {
    int h = head;
    E result = (E) elements[h];
    // 队列为空
    if (result == null)
        return null;
    elements[h] = null;     // Must null out slot
    head = (h + 1) & (elements.length - 1);
    return result;
}

在每次添加元素后,如果队头指针和队尾指针相遇,说明数组空间已满,此时就需要扩容操作。ArrayDeque 会将新数组的容量扩大到旧数组的 2 倍 ,由于旧数组的容量也是 2 的整数幂,因此乘以 2 之后依然是 2 的整数幂。

搬运数据的过程就是把 head 头指针到数组末尾的元素拷贝到数组头部,而剩下的从数组头部到尾指针的元素则衔接到后面,使得所有元素规整地排列在数组的头部:

// 扩容操作
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    // 队头指针到数组末尾的元素个数
    int r = n - p;
    // 容量翻倍
    int newCapacity = n << 1;
    // 容量变成负数
    if (newCapacity < 0)
        // ArrayList 扩容整型溢出时会抛出 OutOfMemoryError
        // ArrayDeque 扩容整型溢出时会抛出 IllegalStateException
        // 看了一下,发现这两个容器出自不同作者,不能统一下吗哈哈
        throw new IllegalStateException("Sorry, deque too big");
    // 创建新数组
    Object[] a = new Object[newCapacity];
    // 将队头指针到数组末尾的元素拷贝到数组头部
    System.arraycopy(elements, p, a, 0, r);
    // 拷贝剩下的从数组头部到尾指针的元素
    System.arraycopy(elements, 0, a, r, p);
    // 指向新数组
    elements = a;
    // 重置头尾指针
    head = 0;
    tail = n;
}

扩容操作

  • 🙋🏻‍♀️疑问 4:为什么没有看到 ArrayList 类似的 MAX_ARRAY_SIZE 最大容量限制?

现在我们可以回答这个疑问了, 网上也有资料说 ArrayDeque 没有容量限制,最坑的是代码注释也这么说:“Array deques have no capacity restrictions”。 显然不是这么一回事。 第一,数组的容量显示是被虚拟机固化的,不可能无限容量。第二,从 doubleCapacity() 函数可以看出, 最大容量值是 2^30(高位 0100,低位都是0), 如果超过这个数,在 doubleCapacity() 扩容的时候就会抛出异常了。

4.4 ArrayDeque 的获取和移除方法

ArrayDeque 可以在数组的两端移除元素,不支持在数组的中间移除元素:

  • 在队头移除: 在 head 指针的位置获取,再将 head 指向上一个位置,如果数组越界则循环到数组尾部;
  • 在队尾移除: 在 tail 指针的下一个位置获取,如果数组越界则循环到数组头部。
public E pollFirst() {
    // head 指针本身就是指向队头元素,所以直接获取
    int h = head;
    E result = (E) elements[h];
    if (result == null)
        return null;
    elements[h] = null;
    // 修改 head 指针
    head = (h + 1) & (elements.length - 1);
    return result;
}

public E pollLast() {
    // tail 指针本身是指向队尾元素的下一个位置,所以需要返回上一个位置
    int t = (tail - 1) & (elements.length - 1);
    E result = (E) elements[t];
    if (result == null)
        return null;
    elements[t] = null;
    tail = t;
    return result;
}

4.5 ArrayDeque 的迭代器

Java 的 foreach 是语法糖,本质上也是采用 iterator 的方式。ArrayDeque 提供了 2 个迭代器:

  • iterator():DeqIterator(): 正向迭代器
  • ListIterator DescendingIterator(): 反向迭代器

ArrayDeque 迭代器同样有 fail-fast 机制,不给过ArrayDeque 并没有使用类似 ArrayList 类似的 modCount 检查并发修改,而是通过头尾指针的位置和元素检查并发修改。这个方法不一定能保证检测到所有的并发修改情况,例如无法检查先移除了尾部元素,又马上添加了一个尾部元素的情况。

private class DeqIterator implements Iterator<E> {

    private int cursor = head;

    private int fence = tail;

    private int lastRet = -1;

    public boolean hasNext() {
        return cursor != fence;
    }

    public E next() {
        if (cursor == fence) throw new NoSuchElementException();
        E result = (E) elements[cursor];
        // 无法检测到所有并发修改的情况
        if (tail != fence || result == null)
            throw new ConcurrentModificationException();
        lastRet = cursor;
        cursor = (cursor + 1) & (elements.length - 1);
        return result;
    }
    ...
}

4.6 ArrayDeque 的序列化过程

ArrayDeque 重写了 JDK 序列化的逻辑,只把 elements 数组中有效元素的部分序列化,而不会序列化整个数组。

// 序列化过程
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
    s.defaultWriteObject();
    // 写入数组长度
    s.writeInt(size());
    // 写入从 head 指针到 tail 指针之间的有效元素
    int mask = elements.length - 1;
    for (int i = head; i != tail; i = (i + 1) & mask)
        s.writeObject(elements[i]);
}

// 反序列化过程
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    // 读取数组长度
    int size = s.readInt();
    // 计算最近的 2 的整数幂
    int capacity = calculateSize(size);
    // 分配数组
    SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
    allocateElements(size);
    // 设置头尾指针
    head = 0;
    tail = size;
    // 将数据规整到数组下标 0 位置
    for (int i = 0; i < size; i++)
        elements[i] = s.readObject();
}

4.7 ArrayDeque 的 clone() 过程

ArrayDeque 中的 elements 数组是引用类型,因此在 clone() 中需要实现深拷贝,否则原对象与克隆对象会相互影响:

public ArrayDeque<E> clone() {
    try {
        @SuppressWarnings("unchecked")
        ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
        result.elements = Arrays.copyOf(elements, elements.length);
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

5. 总结

  • 1、ArrayDeque 是基于动态数组的线性表,具备 Queue 和 Stack 的行为,但不具备 List 的行为;

  • 2、ArrayDeque 的数组容量是 2 的整数幂,在扩容时容量会翻倍,且不支持 null 元素;

  • 3、ArrayDeque 和 LinkedList 的栈和队列行为都是 O(1) 时间复杂度,ArrayDeque 的入栈和入队有可能会触发扩容,但从均摊分析上看依然是 O(1) 时间复杂度;

  • 4、ArrayDeque 是一块连续内存空间,基于局部性原理能够更好地命中 CPU 缓存行,而 LinkedList 是离散的内存空间对缓存行不友好;

  • 5、ArrayDeque 和 LinkedList 都不考虑线程同步,不保证线程安全。


参考资料

  • 数据结构与算法之美(第 4、8、9 讲) —— 王争 著,极客时间 出品
  • 17 张图带你深度剖析 ArrayDeque(JDK双端队列)源码 —— 一无是处的研究僧
  • Java 容器源码分析之 Deque 与 ArrayDeque —— jrthe42 著
  • Why null values are not allowed in ArrayDeque? —— stack overflow

小彭的 Android 交流群 02 群

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

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

相关文章

APP到底有没有权限-恶意拷贝删除照片

作者&#xff1a;黑蛋 近期发生了一件比较恶劣的事情&#xff0c;某客户在某物上面买了一件东西&#xff0c;但是这个东西是假货&#xff0c;所以客户致电某物人工客服&#xff0c;并进行了录音&#xff0c;这时候某物试图通过自身的客户端软件&#xff0c;去删除客户手机上的…

DFP 数据转发协议应用实例 7.使用 DLS1x 与 VSxxx 设备的 LoRA 匹配

DFP 数据转发协议应用实例 7.使用 DLS1x 与 VSxxx 设备的 LoRA 匹配 DFP 是什么&#xff1f; 稳控科技编写的一套数据转发规则&#xff0c; 取自“自由转发协议 FFP&#xff08;Free Forward Protocol&#xff09;” &#xff0c;或者 DFP&#xff08;DoubleF Protocol&#x…

维格云单点登录SSO入门教程

功能简介 无代码维格云单点登录功能支持用户通过配置,将无代码维格云的帐号体系,和提供标准OAuth2.0认证服务系统、LDAP的帐号体系统一起来。 两种单点登录方式的配置见子文档:OAuth 2.0LDAP,本文只描述通用功能:全局单点登录、退出登录后跳转指定页面。 注:本功能为A…

ArcGIS综合制图教程,简单上手!

目的 1、了解专题地图组成的各个要素&#xff1b; 2、掌握ArcGIS编辑专题图的方法和步骤&#xff1b; 实习内容 使用ArcGIS生成1&#xff1a;200万比例尺的浙江省县级行政区划图&#xff0c;并输出成像文件。 实习步骤 一、将ArcGIS切换到Layout视图&#xff0c;并调整页面…

HTML、CSS学习笔记小结

目录 1&#xff0c;HTML 1.1 简单介绍 1.2 快速入门 1.3 基础标签 代码演示&#xff1a; 1.4 图片、音频、视频标签 代码演示&#xff1a; 1.5 超链接标签 代码演示&#xff1a; 1.6 列表标签 代码演示&#xff1a; 1.7 表格标签 代码演示&#xff1a; 1.8 布局标…

【LeetCode与《代码随想录》】哈希表篇:做题笔记与总结-JavaScript版

文章目录代码随想录主要题目242. 有效的字母异位词349. 两个数组的交集202. 快乐数1. 两数之和&#xff08;经典哈希&#xff09;454. 四数相加 II15. 三数之和&#xff08;双指针&#xff09;18. 四数之和&#xff08;双指针&#xff09;相关题目383. 赎金信49. 字母异位词分组…

【构建ML驱动的应用程序】第 9 章 :选择部署选项

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

Redis基础命令(set类型)交集并集差集

目录 概述: 特征&#xff1a; Set常见命令&#xff1a; 1.Sadd key number..&#xff1a;向set中添加一个或多个元素 2.Srem key number...&#xff1a;移除set中指定的元素 3.Scard key&#xff1a;返回set中元素的个数 4.Sismember key member&#xff1a;判断一个元素…

liunx中如何启动redis

连上服务器之后&#xff0c;这个“~”’波浪符号应该是在root目录下&#xff0c;我们需要到这个目录的外面&#xff0c;根目录 root目录 目录的外面&#xff0c;根目录 cd .. 变为"/"符号即可 然后再找到redis的安装目录&#xff0c;也可以在xftp中找&#xff0c;可视…

【LeetCode每日一题】——90.子集 II

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 数组 二【题目难度】 中等 三【题目编号】 90.子集 II 四【题目描述】 给你一个整数数组 num…

有没有把语音转为文字的软件?这几个转换软件你值得收藏

我们在日常的工作和生活中&#xff0c;应该经常会遇到需要将音频转换成文字的情况吧。相信大部分的小伙伴都会选择直接使用转换软件进行音频转文字的操作&#xff0c;但在使用的过程中就会发现&#xff0c;有些软件会在使用次数、音频时长上面有所限制&#xff0c;导致我们会转…

【文件格式_XML_HTML_】XML、HTML文件

HTML xml what&#xff1f; XML是被设计用来存储数据、携带数据和交换数据的&#xff0c;通过XML&#xff0c;可以在不兼容的系统之间交换数据&#xff0c;利用XML&#xff0c;纯文本文件可以用来存储数据。在不使用XML时&#xff0c;HTML用于显示数据&#xff0c;数据必须存…

python面向对象(上)

python面向对象上命名规则对象的创建创建类对象创建实例对象修改增加类属性构造方法构造实例方法不带变量带变量构造方法小例子构造类方法构造静态方法运算符的重载比较运算符重载字符串重载索引或切片重载索引或切片重载检查成员重载重载小结持续更新中~~~~~~~~先上个例子&…

莫名其妙: conda使用过程各种错误及总结

conda真的很烦呢&#xff0c;有啥好用的&#xff0c;要不是准备学习一下虚拟环境&#xff0c;我才不折腾呢&#xff0c;浪费好多时间&#xff0c;(&#xff1b;′⌒) 不要挂代理&#xff0c;我怎么知道为什么&#xff0c;和pip一样&#xff0c;不过后来不知道为什么pip很流畅c…

知识蒸馏(Knowledge Distillation)

0.Introduction 知识蒸馏&#xff08;Knowledge Distillation&#xff0c;简记为 KD&#xff09;是一种经典的模型压缩方法&#xff0c;核心思想是通过引导轻量化的学生模型“模仿”性能更好、结构更复杂的教师模型&#xff08;或多模型的 ensemble&#xff09;&#xff0c;在…

第3章:中文本文向量化——代码详解

1.单条语句的向量化 根据不同的向量化&#xff0c;对“中华女子学院&#xff1a;本科层次仅1专业招男生”这句话进行向量化 1.1 One-hot方法 # one-hot代码 import jieba import os import numpy as npstopwords open(./data/哈工大停用词表.txt).read().split("\n&qu…

物联网安全年报暴露情况分析

暴露情况分析 在去年研究的基础上&#xff0c;我们今年对 UPnP 协议的暴露情况进行持续关注。如无特殊说明&#xff0c;本章中的 统计数据基于全球单轮次的测绘&#xff08;2019 年 10 月&#xff09;。本节我们将对 SSDP 与 SOAP服务的暴露情况进行 分析&#xff0c;4.4.2 节…

图床项目架构与设计

分布式文件服务系统架构与设计 该项目利用FastDFS分布式文件系统以及集群等技术提供统一的接口对外提供文件存储和访问服务&#xff0c;使用户能够通过网络访问服务端的文件资源&#xff0c;满足海量数据存储的需求 架构 项目采用B/S模型&#xff0c;服务端与浏览器进行交互&a…

论文阅读-Detecting and Recovering Sequential DeepFake Manipulation(SeqFakeFormer)

一、论文信息 论文名称&#xff1a;Detecting and Recovering Sequential DeepFake Manipulation&#xff08;篡改序列检测还原&#xff09; 论文链接: https://arxiv.org/pdf/2207.02204.pdf 项目主页: https://rshaojimmy.github.io/Projects/SeqDeepFake GitHub: https:…

Linux 最近学习总结

Linux 最近学习总结一.基础命令一.基础命令 基础命令就像是锤子、螺丝刀、锯等木匠常用到的工具&#xff0c;也是我们每天都会用到的命令 1.ls 列出当前所在目录的内容 2.ls 文件夹名 列出其他文件夹内容 3.ls ~ 其中~代表home目录 4.ls ~/Desktop/Java电子书/*.pdf 使用…