LinkedList详解(含数据结构动画演示)

news2024/9/21 11:03:28

目录

  • LinkedList详解
    • 1、LinkedList的继承体系
    • 2、LinkedList的构造函数
    • 3、LinkedList的`add(E e)`方法
    • 4、LinkedList的Node节点
    • 5、双向链表的概念和Node节点的详细解释
    • 6、LinkedList的`add(E e)`方法梳理
    • 7、LinkedList的`getXXX`方法
    • 8、LinkedList的`removeXXX`方法
      • ①、removeFirst()方法 (移除LinkedList的第一个元素):
      • ②、removeLast()方法 (移除LinkedList的最后一个元素):
      • ③、remove(E e)方法 (这个方法值得深入)
      • ④、`remove(int index)`方法
    • 9、LinkedList的反向迭代器 `DescendingIterator`

LinkedList详解

LinkedList用的不多,但是其内部的双向链表实现还是值得去探讨学习的。

1、LinkedList的继承体系

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

在这里插入图片描述
可以看到和ArrayList还是有不少区别的

需要注意的两个点:

  • ①、LinkedList没有实现RandomAccess接口,意味着LinkedList不支持快速随机访问。这是由LinkedList的数据结构决定的。由于LinkedList 底层数据结构是链表,内存地址是不连续的,只能通过指针来定位,不像ArrayList底层是数组结构内存地址是连续的,可以通过数组的下标快速定位元素。所以LinkedList没有实现RandomAccess接口。

  • ②、LinkedList实现了Deque接口,说明LinkedList支持双向链表的操作,比如addFirst,addLast等方法。

2、LinkedList的构造函数

①、无参构造

public LinkedList() {
    }

②、有参构造

public LinkedList(Collection<? extends E> c) {
		// 调用无参构造函数,初始化LinkedList实例
        this();
        // 将传入的集合c中的所有元素添加到当前LinkedList实例中
        addAll(c);
    }

3、LinkedList的add(E e)方法

add(E e)方法

public boolean add(E e) {
		// 调用linkLast方法将元素e添加到链表的末尾
        linkLast(e);
        // 返回true,表示元素添加成功
        return true;
    }

linkLast(E e)方法

// 用于跟踪修改次数的计数器,主要用于迭代器的快速失败(fail-fast)机制
protected transient int modCount = 0;

// 链表中元素的数量
transient int size = 0;

// 链表的第一个节点引用
transient Node<E> first;

// 链表的最后一个节点引用
transient Node<E> last;

void linkLast(E e) {
    // 将当前链表的最后一个节点引用保存到局部变量l中
    // 这样可以方便后续对链表末尾进行操作
    final Node<E> l = last;
    
    // 创建一个新的节点,节点的前驱是l(即当前的最后一个节点),
    // 节点包含的元素是e,后继节点为null(因为新节点将成为最后一个节点)
    final Node<E> newNode = new Node<>(l, e, null);
    
    // 更新链表的最后一个节点引用为新创建的节点
    last = newNode;
    
    // 检查链表是否为空(即当前没有任何节点)
    // 如果链表为空(即last为null),那么新节点也是第一个节点
    if (l == null)
        // 将链表的第一个节点引用也更新为新创建的节点
        first = newNode;
    else
        // 如果链表不为空(即已有节点),
        // 那么将当前最后一个节点的next指向新创建的节点
        l.next = newNode;
    
    // 链表的元素数量增加1
    size++;
    
    // 修改计数器增加1
    modCount++;
}

上面的modCount在ArrayList详解的文章中已经介绍过来,这里就不再赘述。
我们需要关注LinkedList的核心 Node,可以叫节点。

4、LinkedList的Node节点

可以把每个Node节点理解为一个带有前后钩子的盒子,这个盒子里装着一个元素(item),通过前后两个钩子(prev和next)连接到前后的盒子(节点),形成一个双向链表的数据结构。

Node类是LinkedList的一个静态内部类,用于表示链表中的每个节点。
每个节点包含一个元素(item),指向前一个节点的引用(prev)和指向后一个节点的引用(next)。

private static class Node<E> {
    // 节点中存储的元素
    E item;
    
    // 指向下一个节点的引用
    Node<E> next;
    
    // 指向前一个节点的引用
    Node<E> prev;

    // Node类的构造函数,用于创建一个新节点
    // 参数prev是指向前一个节点的引用,element是当前节点中存储的元素,next是指向下一个节点的引用
    Node(Node<E> prev, E element, Node<E> next) {
        // 将传入的元素赋值给item,表示节点中存储的元素
        this.item = element;
        
        // 将传入的下一个节点引用赋值给next
        this.next = next;
        
        // 将传入的前一个节点引用赋值给prev
        this.prev = prev;
    }
}

5、双向链表的概念和Node节点的详细解释

双向链表(Doubly Linked List)是一种链表数据结构。
其中每个节点包含三个主要部分:
存储的元素(数据);
指向前一个节点的引用(前驱);
指向后一个节点的引用(后继);

这种结构允许从任意节点向前或向后遍历链表,提供了比单向链表更灵活的操作。

在双向链表中,Node节点是链表的基本组成部分。每个节点都包含存储的元素以及指向前一个节点和后一个节点的引用。

Node节点的结构如下:

  • 元素(item):节点中存储的数据,这可以是任意类型的对象。
  • 前驱节点引用(prev):指向链表中前一个节点的引用。如果当前节点是链表的第一个节点,则这个引用为null。
  • 后继节点引用(next):指向链表中下一个节点的引用。如果当前节点是链表的最后一个节点,则这个引用为null。

单向链表和双向链表的图示:
在这里插入图片描述

6、LinkedList的add(E e)方法梳理

通过空参构造新建LinkedList实例
下面分析添加 “秀逗” 、“四眼” 两个元素的过程

LinkedList<String> list = new LinkedList<>();
        list.add("秀逗");
        list.add("四眼");

执行无参构造创建 LinkedList实例

// 用于跟踪修改次数的计数器,主要用于迭代器的快速失败(fail-fast)机制
protected transient int modCount = 0;

// 链表中元素的数量
transient int size = 0;

// 链表的第一个节点引用
transient Node<E> first;

// 链表的最后一个节点引用
transient Node<E> last;
public LinkedList() {
    }

执行add方法 添加"秀逗"

public boolean add(E e) {
    // 调用linkLast方法将元素e添加到链表的末尾
    linkLast(e);
    // 始终返回true,表示元素添加成功
    return true;
}

  • 调用linkLast(E e)方法:这是将"秀逗"添加到链表末尾的具体实现方法。
void linkLast(E e) {
    // 将当前链表的最后一个节点引用保存到局部变量l中
    final Node<E> l = last;
    
    // 创建一个新的节点,节点的前驱是l(即当前的最后一个节点),
    // 节点包含的元素是e,后继节点为null(因为新节点将成为最后一个节点)
    final Node<E> newNode = new Node<>(l, e, null);
    
    // 更新链表的最后一个节点引用为新创建的节点
    last = newNode;
    
    // 检查链表是否为空(即当前没有任何节点)
    // 如果链表为空(即last为null),那么新节点也是第一个节点
    if (l == null)
        // 将链表的第一个节点引用也更新为新创建的节点
        first = newNode;
    else
        // 如果链表不为空(即已有节点),
        // 那么将当前最后一个节点的next指向新创建的节点
        l.next = newNode;
    
    // 链表的元素数量增加1
    size++;
    
    // 修改计数器增加1
    // 这个计数器在结构发生变化(例如添加或删除元素)时增加,
    // 用于实现迭代器的快速失败机制,检测并发修改
    modCount++;
}

下面把添加过程串起来

  • 1.保存当前最后一个节点的引用到局部变量 l:
    final Node<E> l = last; // 由于last在空参构造的初始化为null ,所以 l为null

  • 2.创建一个新的节点newNode:
    final Node<E> newNode = new Node<>(l, e, null);
    newNode的前驱节点为l(当前的最后一个节点) 值为null。
    newNode包含的元素为 “秀逗” 。
    newNode的后继节点为null(因为它将成为新的最后一个节点)。

  • 3.更新链表的最后一个节点引用为newNode:
    last = newNode; // 将last更新为newNode。

  • 4.检查链表是否为空:
    if (l == null) first = newNode;
    l 现在为空 ,说明这是链表中的第一个节点。
    将链表的第一个节点引用first也更新为newNode。

  • 5.增加链表的元素数量:
    将链表的大小size增加1。

  • 6.增加修改计数器modCount:
    用于实现迭代器的快速失败机制,检测并发修改。

    1. 再添加 “四眼” 这个元素
      保存当前最后一个节点的引用到局部变量 l:
      final Node<E> l = last; // 由于last在添加"秀逗"的时候已经赋值了
      所以这里 l 是有值的
  • 8.再创建一个新的节点newNode:
    final Node<E> newNode = new Node<>(l, e, null);
    newNode的前驱节点为l(当前的最后一个节点) 值为 “秀逗” 。
    newNode包含的元素为 “四眼” 。
    newNode的后继节点为null(因为它将成为新的最后一个节点)。

  • 9.再更新链表的最后一个节点引用为newNode:
    last = newNode; // 将last更新为newNode。 此时"四眼"就成了最后一个元素

  • 10.再检查链表是否为空:
    if (l == null) first = newNode; else l.next = newNode;
    l 现在为 “秀逗” ,不为空,讲 l 的下一个节点引用 next 指向 newNode 也就是 “四眼”。

这就完成了 “秀逗” , “四眼” 两个元素的 添加。

最终图示:
在这里插入图片描述

7、LinkedList的getXXX方法

LinkedList获取元素的方法有下面几个:
getFirst():获取链表的第一个元素。
getLast():获取链表的最后一个元素。
get(int index):获取链表指定位置的元素。

我在ArrayList详解里面并没有去讲解get方法,因为ArrayList中的get并没有什么好讲的。

// ArrayList中的get方法
public E get(int index) {
		// 先检查数组下标有没有越界
        rangeCheck(index);
        // 返回对应位置的元素
        return elementData(index);
    }
    
private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

但是LinkedList的getXXX方法是值得深入学一学的:
先看下最简单的两个 getFirst和getLast

public E getFirst() {
    // 获取链表的第一个节点引用,并将其保存在局部变量f中
    final Node<E> f = first;
    
    // 如果第一个节点为null,表示链表为空
    if (f == null)
        // 抛出NoSuchElementException异常,表示没有元素可以返回
        throw new NoSuchElementException();
    
    // 返回第一个节点中的元素
    return f.item;
}

public E getLast() {
    // 获取链表的最后一个节点引用,并将其保存在局部变量l中
    final Node<E> l = last;
    
    // 如果最后一个节点为null,表示链表为空
    if (l == null)
        // 抛出NoSuchElementException异常,表示没有元素可以返回
        throw new NoSuchElementException();
    
    // 返回最后一个节点中的元素
    return l.item;
}

最值得去探究的是 get(int index):获取链表指定位置的元素。

我们知道LinkedList是链表结构,无法像ArrayList那样快速随机访问。那么LinkedList想根据下标去找到某个元素就只能靠遍历了。
看下具体是怎么实现的:

public E get(int index) {
    // 检查元素索引是否合法,如果不合法会抛出 IndexOutOfBoundsException
    checkElementIndex(index);
    // 获取指定索引的节点并返回其包含的元素
    return node(index).item;
}

Node<E> node(int index) {
    // 判断索引在链表中的位置,以决定从头部还是尾部开始遍历
    // size >> 1  计算链表长度/2 向下取整  
    // 如果size是偶数 右移一位的结果就是size的一半 如果size是奇数 右移一位的结果就是size的一半再减0.5  
    
    if (index < (size >> 1)) {
        // 如果索引在前半部分,从头部开始遍历
        Node<E> x = first;
        // 通过不断访问 next 来找到指定索引的节点
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        // 如果索引在后半部分,从尾部开始遍历
        Node<E> x = last;
        // 通过不断访问 prev 来找到指定索引的节点
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

这里我做了个动画来演示查找的过程。

  • ①、从链表头部开始遍历
public static void main(String[] args){
        LinkedList<String> list = new LinkedList<>(Arrays.asList("1","2","秀逗","4","5","6","7","四眼","9","10"));
        String s = list.get(2);
        System.out.println(s);  // 秀逗
}

由于下标2 小于整个链表长度size 10右移一位 10 >> 1 = 5, 即2<5, 就从链表的头部开始遍历
在这里插入图片描述

  • ②、从链表尾部开始遍历
public static void main(String[] args){
        LinkedList<String> list = new LinkedList<>(Arrays.asList("1","2","秀逗","4","5","6","7","四眼","9","10"));
        String s = list.get(7);
        System.out.println(s);  // 四眼
}

由于下标7 大于整个链表长度size 10右移一位 10 >> 1 = 5, 即7>5, 就从链表的尾部开始遍历
在这里插入图片描述

8、LinkedList的removeXXX方法

LinkedList删除元素的方法有下面几个:

  • ①、removeFirst():删除并返回链表的第一个元素。
  • ②、removeLast():删除并返回链表的最后一个元素。
  • ③、remove(E e):删除链表中首次出现的指定元素,如果不存在该元素则返回 false。
  • ④、remove(int index):删除指定索引处的元素,并返回该元素的值。

①、removeFirst()方法 (移除LinkedList的第一个元素):

public E removeFirst() {
    // 获取链表的第一个节点,赋值给变量 f
    final Node<E> f = first;
    // 如果链表为空,即第一个节点为 null,抛出 NoSuchElementException 异常
    if (f == null)
        throw new NoSuchElementException();
    // 调用 unlinkFirst 方法,从链表中移除第一个节点
    return unlinkFirst(f);
}

private E unlinkFirst(Node<E> f) {
    // 确保 f 是第一个节点且不为 null 的断言,注释掉的是因为在实际代码中没有执行断言检查的需要
    // assert f == first && f != null;

    // 保存第一个节点的值
    final E element = f.item;
    // 保存第一个节点的下一个节点
    final Node<E> next = f.next;
    // 将第一个节点的值设为 null,帮助垃圾回收
    f.item = null;
    // 将第一个节点的 next 指针设为 null,帮助垃圾回收
    f.next = null;
    // 更新链表的头节点为原头节点的下一个节点
    first = next;
    // 如果更新后的头节点为 null,说明链表现在为空,更新尾节点为 null
    if (next == null)
        last = null;
    else
        // 如果更新后的头节点不为 null,将它的 prev 指针设为 null
        next.prev = null;
    // 链表长度减一
    size--;
    // 修改计数器增加一,用于记录链表结构的修改次数
    modCount++;
    // 返回被移除的节点的值
    return element;
}

总结一下:

  • 1.removeFirst() 方法:
    首先,获取链表的第一个节点,并检查其是否为 null。
    如果链表为空,则抛出 NoSuchElementException 异常。
    否则,调用 unlinkFirst(f) 方法从链表中移除第一个节点,并返回该节点的值。

  • 2.unlinkFirst(Node f) 方法:
    保存要移除的节点的值和它的下一个节点。
    将要移除节点的 item 和 next 设为 null,以帮助垃圾回收。
    更新链表的头节点为原头节点的下一个节点。如果新头节点为 null,说明链表现在为空,因此将尾节点也设为 null;否则,将新头节点的 prev 指针设为 null。
    减少链表的大小,并增加修改计数器。
    返回被移除节点的值。

②、removeLast()方法 (移除LinkedList的最后一个元素):

public E removeLast() {
    // 获取链表的最后一个节点,赋值给变量 l
    final Node<E> l = last;
    // 如果链表为空,即最后一个节点为 null,抛出 NoSuchElementException 异常
    if (l == null)
        throw new NoSuchElementException();
    // 调用 unlinkLast 方法,从链表中移除最后一个节点
    return unlinkLast(l);
}

private E unlinkLast(Node<E> l) {
    // 确保 l 是最后一个节点且不为 null 的断言,注释掉的是因为在实际代码中没有执行断言检查的需要
    // assert l == last && l != null;

    // 保存最后一个节点的值
    final E element = l.item;
    // 保存最后一个节点的前一个节点
    final Node<E> prev = l.prev;
    // 将最后一个节点的值设为 null,帮助垃圾回收
    l.item = null;
    // 将最后一个节点的 prev 指针设为 null,帮助垃圾回收
    l.prev = null;
    // 更新链表的尾节点为原尾节点的前一个节点
    last = prev;
    // 如果更新后的尾节点为 null,说明链表现在为空,更新头节点为 null
    if (prev == null)
        first = null;
    else
        // 如果更新后的尾节点不为 null,将它的 next 指针设为 null
        prev.next = null;
    // 链表长度减一
    size--;
    // 修改计数器增加一,用于记录链表结构的修改次数
    modCount++;
    // 返回被移除的节点的值
    return element;
}

总结一下:

  • 1.removeLast() 方法:
    首先,获取链表的最后一个节点,并检查其是否为 null。
    如果链表为空,则抛出 NoSuchElementException 异常。
    否则,调用 unlinkLast(l) 方法从链表中移除最后一个节点,并返回该节点的值。

  • 2.unlinkLast(Node l) 方法:
    保存要移除的节点的值和它的前一个节点。
    将要移除节点的 item 和 prev 设为 null,以帮助垃圾回收。
    更新链表的尾节点为原尾节点的前一个节点。如果新尾节点为 null,说明链表现在为空,因此将头节点也设为 null;否则,将新尾节点的 next 指针设为 null。
    减少链表的大小,并增加修改计数器。
    返回被移除节点的值。

③、remove(E e)方法 (这个方法值得深入)

public boolean remove(Object o) {
    // 如果要移除的元素为 null
    if (o == null) {
        // 从链表的第一个节点开始遍历
        for (Node<E> x = first; x != null; x = x.next) {
            // 如果当前节点的值为 null
            if (x.item == null) {
                // 调用 unlink 方法移除当前节点
                unlink(x);
                // 返回 true 表示成功移除元素
                return true;
            }
        }
    } else {
        // 如果要移除的元素不为 null
        // 从链表的第一个节点开始遍历
        for (Node<E> x = first; x != null; x = x.next) {
            // 如果当前节点的值与要移除的元素相等
            if (o.equals(x.item)) {
                // 调用 unlink 方法移除当前节点
                unlink(x);
                // 返回 true 表示成功移除元素
                return true;
            }
        }
    }
    // 如果遍历整个链表没有找到要移除的元素,返回 false
    return false;
}

E unlink(Node<E> x) {
    // 确保 x 不为 null 的断言,注释掉的是因为在实际代码中没有执行断言检查的需要
    // assert x != null;

    // 保存要移除节点的值
    final E element = x.item;
    // 保存要移除节点的下一个节点
    final Node<E> next = x.next;
    // 保存要移除节点的前一个节点
    final Node<E> prev = x.prev;

    // 如果要移除的节点是第一个节点
    if (prev == null) {
        // 更新链表的头节点为要移除节点的下一个节点
        first = next;
    } else {
        // 否则,将前一个节点的 next 指向要移除节点的下一个节点
        prev.next = next;
        // 将要移除节点的 prev 指针设为 null,帮助垃圾回收
        x.prev = null;
    }

    // 如果要移除的节点是最后一个节点
    if (next == null) {
        // 更新链表的尾节点为要移除节点的前一个节点
        last = prev;
    } else {
        // 否则,将下一个节点的 prev 指向要移除节点的前一个节点
        next.prev = prev;
        // 将要移除节点的 next 指针设为 null,帮助垃圾回收
        x.next = null;
    }

    // 将要移除节点的值设为 null,帮助垃圾回收
    x.item = null;
    // 链表长度减一
    size--;
    // 修改计数器增加一,用于记录链表结构的修改次数
    modCount++;
    // 返回被移除节点的值
    return element;
}

总结一下:
remove(Object o) 方法:
该方法用于从链表中移除第一个与给定元素 o 相等的节点。
如果 o 为 null,则遍历链表,找到第一个节点值为 null 的节点,并调用 unlink(x) 方法移除该节点,返回 true。
如果 o 不为 null,则遍历链表,找到第一个节点值与 o 相等的节点,并调用 unlink(x) 方法移除该节点,返回 true。
如果遍历完整个链表没有找到与 o 相等的节点,返回 false。

unlink(Node<E> x) 方法:
该方法用于从链表中移除指定的节点 x。
保存要移除节点的值、下一个节点和前一个节点。
如果要移除的节点是第一个节点,则更新链表的头节点为下一个节点;否则,将前一个节点的 next 指针指向下一个节点,并将要移除节点 的 prev 指针设为 null。
如果要移除的节点是最后一个节点,则更新链表的尾节点为前一个节点;否则,将下一个节点的 prev 指针指向前一个节点,并将要移除节点的 next 指针设为 null。
将要移除节点的值设为 null,以帮助垃圾回收。
链表的长度减一,并增加修改计数器。
返回被移除节点的值。

这里还是做个动画帮助理解 双向链表移除某个数据的过程:
在这里插入图片描述
可以看到最后
在这里插入图片描述
秀逗的pre 和 next 都被置为了null。
这样做可以确保该节点与链表完全断开连接,从而有助于垃圾回收机制回收该节点。

④、remove(int index)方法

这个方法 实际上在上面的分析中都提到了:
比如checkElementIndex、unlink、node方法在上面都有。
所以这个只给出方法注释 就不总结画图了。

public E remove(int index) {
    // 检查给定的索引是否在链表的有效范围内
    checkElementIndex(index);
    // 找到给定索引处的节点并移除它,返回被移除节点的值
    return unlink(node(index));
}

private void checkElementIndex(int index) {
    // 如果给定的索引不是有效的元素索引,抛出 IndexOutOfBoundsException 异常
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

Node<E> node(int index) {
    // 确保给定的索引是有效的元素索引的断言,注释掉的是因为在实际代码中没有执行断言检查的需要
    // assert isElementIndex(index);

    // 如果给定的索引在前半部分
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 从头节点开始遍历,找到索引处的节点
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        // 如果给定的索引在后半部分
        Node<E> x = last;
        // 从尾节点开始反向遍历,找到索引处的节点
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

E unlink(Node<E> x) {
    // 确保 x 不为 null 的断言,注释掉的是因为在实际代码中没有执行断言检查的需要
    // assert x != null;

    // 保存要移除节点的值
    final E element = x.item;
    // 保存要移除节点的下一个节点
    final Node<E> next = x.next;
    // 保存要移除节点的前一个节点
    final Node<E> prev = x.prev;

    // 如果要移除的节点是第一个节点
    if (prev == null) {
        // 更新链表的头节点为要移除节点的下一个节点
        first = next;
    } else {
        // 否则,将前一个节点的 next 指向要移除节点的下一个节点
        prev.next = next;
        // 将要移除节点的 prev 指针设为 null,帮助垃圾回收
        x.prev = null;
    }

    // 如果要移除的节点是最后一个节点
    if (next == null) {
        // 更新链表的尾节点为要移除节点的前一个节点
        last = prev;
    } else {
        // 否则,将下一个节点的 prev 指向要移除节点的前一个节点
        next.prev = prev;
        // 将要移除节点的 next 指针设为 null,帮助垃圾回收
        x.next = null;
    }

    // 将要移除节点的值设为 null,帮助垃圾回收
    x.item = null;
    // 链表长度减一
    size--;
    // 修改计数器增加一,用于记录链表结构的修改次数
    modCount++;
    // 返回被移除节点的值
    return element;
}

最后还有个clear方法 也很有意思
也来看下 这可比 ArrayList的 clear方法麻烦多了

public void clear() {
    // 清除所有节点之间的链接是“非必要”的,但这样做有以下好处:
    // - 如果被丢弃的节点跨越多个垃圾分代,有助于分代的垃圾回收 (generational GC)
    // - 即使存在可达的迭代器,也能确保释放内存

    // 从链表的第一个节点开始
    for (Node<E> x = first; x != null; ) {
        // 保存当前节点的下一个节点
        Node<E> next = x.next;
        // 将当前节点的值设为 null,帮助垃圾回收
        x.item = null;
        // 将当前节点的 next 指针设为 null,帮助垃圾回收
        x.next = null;
        // 将当前节点的 prev 指针设为 null,帮助垃圾回收
        x.prev = null;
        // 移动到下一个节点
        x = next;
    }
    // 将链表的头节点和尾节点设为 null,表示链表为空
    first = last = null;
    // 将链表的大小设为 0
    size = 0;
    // 修改计数器增加一,用于记录链表结构的修改次数
    modCount++;
}

9、LinkedList的反向迭代器 DescendingIterator

DescendingIterator 可以从最后一个元素往前迭代。

public static void main(String[] args) throws Exception {
        LinkedList<String> list = new LinkedList<>(Arrays.asList("1","2","秀逗","4","5","6","7","四眼","9","10"));

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.print(iterator.next() + " ");
        }

        System.out.println("\n=============================");

        Iterator<String> descIterator = list.descendingIterator();
        while (descIterator.hasNext()){
            System.out.print(descIterator.next() + " ");
        }
    }

执行结果:

1 2 秀逗 4 5 6 7 四眼 9 10 
=============================
10 9 四眼 7 6 5 4 秀逗 2 1 

看下DescendingIterator 的实现:

// LinkedList的内部类,实现从尾部到头部的迭代器
private class DescendingIterator implements Iterator<E> {
    // 使用ListItr类的实例实现迭代器功能
    private final ListItr itr = new ListItr(size());

    // 检查是否有下一个元素(实际上是前一个元素,因为我们是反向迭代)
    public boolean hasNext() {
        // 调用ListItr的hasPrevious方法,检查是否有前一个元素
        return itr.hasPrevious();
    }

    // 获取下一个元素(实际上是前一个元素,因为我们是反向迭代)
    public E next() {
        // 调用ListItr的previous方法,返回前一个元素
        return itr.previous();
    }

    // 移除当前元素
    public void remove() {
        // 调用ListItr的remove方法,移除当前元素
        itr.remove();
    }
}

所以DescendingIterator 本质上 就是对ListItr 的包装 主要是实现细节都在 ListItr中
再看下 ListItr 的细节:
注意这个ListItr 是LinkedList的内部类 , 而不是 AbstractList抽象类里面的 内部类 ListItr (集合API内部类的命名就不能好好区分一下吗…)

// LinkedList的内部类,实现双向列表迭代器
private class ListItr implements ListIterator<E> {
    // 最近返回的节点
    private Node<E> lastReturned;
    // 下一个节点
    private Node<E> next;
    // 下一个节点的索引
    private int nextIndex;
    // 预期的修改计数,用于检测并发修改
    private int expectedModCount = modCount;

    // 构造函数,根据给定的索引初始化迭代器
    ListItr(int index) {
        // 如果索引等于列表大小,next为null
        next = (index == size) ? null : node(index);
        // 初始化下一个节点的索引
        nextIndex = index;
    }

    // 检查是否有下一个元素
    public boolean hasNext() {
        return nextIndex < size;
    }

    // 获取下一个元素
    public E next() {
        // 检查是否有并发修改
        checkForComodification();
        if (!hasNext())
            throw new NoSuchElementException();

        // 设置最后返回的节点为当前的下一个节点
        lastReturned = next;
        // 移动到下一个节点
        next = next.next;
        // 增加下一个节点的索引
        nextIndex++;
        // 返回最后返回节点的值
        return lastReturned.item;
    }

    // 检查是否有前一个元素
    public boolean hasPrevious() {
        return nextIndex > 0;
    }

    // 获取前一个元素
    public E previous() {
        // 检查是否有并发修改
        checkForComodification();
        if (!hasPrevious())
            throw new NoSuchElementException();

        // 如果next为null,说明当前在列表的末尾
        // 否则移动到前一个节点
        lastReturned = next = (next == null) ? last : next.prev;
        // 减少下一个节点的索引
        nextIndex--;
        // 返回最后返回节点的值
        return lastReturned.item;
    }

    // 获取下一个元素的索引
    public int nextIndex() {
        return nextIndex;
    }

    // 获取前一个元素的索引
    public int previousIndex() {
        return nextIndex - 1;
    }

    // 移除当前元素
    public void remove() {
        // 检查是否有并发修改
        checkForComodification();
        if (lastReturned == null)
            throw new IllegalStateException();

        // 获取最后返回节点的下一个节点
        Node<E> lastNext = lastReturned.next;
        // 从链表中取消链接最后返回的节点
        unlink(lastReturned);
        if (next == lastReturned)
            next = lastNext;
        else
            nextIndex--;
        // 重置最后返回的节点
        lastReturned = null;
        // 更新预期的修改计数
        expectedModCount = modCount;
    }

    // 更新当前元素
    public void set(E e) {
        if (lastReturned == null)
            throw new IllegalStateException();
        checkForComodification();
        lastReturned.item = e;
    }

    // 添加元素
    public void add(E e) {
        checkForComodification();
        lastReturned = null;
        if (next == null)
            linkLast(e);
        else
            linkBefore(e, next);
        nextIndex++;
        expectedModCount = modCount;
    }

    // 检查是否有并发修改的方法
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

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

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

相关文章

vue中插槽的本质

定义slotCompoent.vue 组件 <template><slot></slot><slot nameslot1></slot><slot name"slot2" msg"hello"></slot> </template>使用组件&#xff1a; <slotComponent><p>默认的</p>…

电脑上的瑞士军刀

一、简介 1、一款专为 Windows 操作系统设计的桌面管理工具&#xff0c;它具备保存和恢复桌面图标位置的功能&#xff0c;使用户能够在各种情况下&#xff0c;如分辨率变动、系统更新或其他原因导致的图标位置混乱后&#xff0c;快速恢复到熟悉的工作环境。它还拥有诸多实用功能…

1.nginx介绍

介绍 是一个高性能的http和反向代理服务器。 特点 占用内存少&#xff0c;并发能力强。 nginx专为性能优化而开发&#xff0c;性能是其最重要的考量&#xff0c;实现上非常注重效率&#xff0c;能经受高负载的考验&#xff0c;有报告表明能支持高达50,000个并发连接数。 基…

李廉洋:6.10黄金原油非农之后,今日行情走势分析策略。

黄金消息面分析&#xff1a;即美联储不会在短期内以降息的方式出手纾困。该报告确实将首次降息的可能性推迟到了12月&#xff0c;但股市的反应不像多数交易商预期的那样。股市非但没有崩盘&#xff0c;反而随着交易员逢低买入而大幅反弹。很明显&#xff0c;市场完全专注于盈利…

基于STM32F030设计的多点温度采集系统(BC26+OneNet)

一、项目背景 随着物联网技术的迅猛发展&#xff0c;越来越多的智能设备应运而生&#xff0c;而温度采集系统是其中重要的一类。在现代工业和家庭生活中&#xff0c;温度对于生产、居住和储存等过程的控制有着非常重要的作用。因此&#xff0c;准确地采集环境温度数据并进行处…

网络安全领域六大顶级会议介绍:含会议介绍、会议地址及会议时间和截稿日期

引言&#xff1a; 从事网络安全工作&#xff0c;以下六个顶会必须要知道&#xff0c;很多安全的前沿技术都会在如下会议中产生与公开&#xff0c;如下会议发表论文大部分可以公开下载。这些会议不仅是学术研究人员展示最新研究成果的平台&#xff0c;也是行业专家进行面对面交流…

vscode copilot git commit 生成效果太差,用其他模型替换

问题 众所周知&#xff0c;copilot git commit 就像在随机生成 git commit 这种较为复杂的内容还是交给大模型做比较合适 方法 刚好&#xff0c;gitlens 最近开发了 AI commit的功能&#xff0c;其提供配置url api可以实现自定义模型 gitlens 只有3种模型可用&#xff1a…

SpringCloud-面试篇(二十四)

&#xff08;1&#xff09;Nacos如何支撑数十万服务注册的压力 小型企业来讲nacos压力没有那么大&#xff0c;但是想阿里&#xff0c;服务的数量可能会达到数万&#xff0c;那麽多的服务。当服务原来越多时&#xff0c;除了服务注册以外&#xff0c;还有服务的定时更新&#x…

【数据分享】《中国投资领域统计年鉴》1950-2022(中国固定资产投资统计年鉴)

​最近老有同学过来询问《中国投资领域统计年鉴》、《中国固定资产投资统计年鉴》这两本年年鉴的关系以及怎么获取这两本本年鉴。今天就在这里给大家分享一下这两本年鉴的具体情况。 《中国投资领域统计年鉴》是一部全面反映中国固定资产投资情况的权威资料。本书收集了全国、…

张霖浩在娱乐“名利场”玩出“修罗场”的贵族范儿

众所周知娱乐圈是个大型“名利场”&#xff01;近日&#xff0c;2025年北京广播电视台春晚发布会现场&#xff0c;众大咖汇聚&#xff0c;妆容、装扮、穿搭&#xff0c;更是争奇斗艳、八仙过海各显神通。同时&#xff0c;也揭露出娱乐圈当下穿搭界”修罗场”的残酷现实。在出彩…

刷代码随想录有感(99):动态规划——使用最小花费爬楼梯

题干&#xff1a; 代码&#xff1a; class Solution { public:int minCostClimbingStairs(vector<int>& cost) {vector<int>dp(cost.size() 1);dp[0] 0;dp[1] 0;for(int i 2; i < cost.size(); i){dp[i] min(dp[i - 1] cost[i - 1], dp[i - 2] cost…

MySQL快速入门(极简)

SQL 介绍及 MySQL 安装 一、实验简介 本课程为实验楼提供的 MySQL 实验教程&#xff0c;所有的步骤都在实验楼在线实验环境中完成&#xff0c;学习中请按照实验步骤依次操作。 本课程为 SQL 基本语法及 MySQL 基本操作的实验&#xff0c;理论内容较少&#xff0c;动手实践多…

C# BindingSource 未完BindingNavigator

数据绑定导航事件数据验证自定义示例示例总结 在 C#中&#xff0c; BindingSource 是一个非常有用的控件&#xff0c;它提供了数据绑定的基础设施。 BindingSource 允许开发者将数据源&#xff08;如数据库、集合、对象等&#xff09;与用户界面控件&#xff08;如文本框、下…

Fedora的远程桌面

要在 Fedora 40 上开启远程桌面功能。 首先&#xff0c;要确保已安装 gnome-remote-desktop 和 vino 包。 这些软件包通常默认安装在 Fedora 的 GNOME 桌面环境中。 可以按照以下步骤操作&#xff1a; 1、判断电脑是否安装了 gnome-remote-desktop 和 vino 包: tomfedora:…

51单片机独立按键控制LED灯,按键按一次亮,再按一次灭

1、功能描述 独立按键控制LED灯&#xff0c;按键按一次亮&#xff0c;再按一次灭 2、实验原理 轻触按键:相当于是一种电子开关&#xff0c;按下时开关接通&#xff0c;松开时开关断开&#xff0c;实现原理是通过轻触按键内部的金属弹片受力弹动米实现接通和断开&#xff1b;…

demo xshell (程序替换 工作目录 内建命令)

1.程序替换 在学习完一些列的进程替换接口之后我们大概就能知道&#xff0c;我们的环境变量以及命令行参数是如何传递给子进程的&#xff0c;这些参数是我们在调用进程替换时就传给了子进程的数据。 那么如果我们自己要实现一个简单的命令行解释器&#xff0c;我们是不是首先…

6.全开源源码---小红书卡片-跳转微信-自动回复跳转卡片-商品卡片-发私信-发群聊-安全导流不封号-企业号白号都可以用

现在用我们的方法&#xff0c;可以规避违规风险&#xff0c;又可以丝滑引流&#xff0c;因为会以笔记的形式发给客户&#xff0c;点击之后直接跳微信&#xff0c;我们来看看演示效果吧&#xff08;没有风险提示&#xff09; 无论是引流还是销售产品都会事半功倍。

tkinter用按钮实现工具栏

tkinter用按钮实现工具栏 效果代码 使用 Python 的 Tkinter 库&#xff0c;我们可以轻松创建一个包含按钮的工具栏。本文将介绍如何在 Tkinter 中创建一个 简单的工具栏&#xff0c;并演示如何添加功能按钮。 效果 代码 import tkinter as tk from tkinter import ttk, filed…

Kafka集成flume

1.flume作为生产者集成Kafka kafka作为flume的sink&#xff0c;扮演消费者角色 1.1 flume配置文件 vim $kafka/jobs/flume-kafka.conf # agent a1.sources r1 a1.sinks k1 a1.channels c1 c2# Describe/configure the source a1.sources.r1.type TAILDIR #记录最后监控文件…