Java 集合 - List 接口

1.List 接口介绍

在 Java 中,java.util.List 接口是 Java 集合框架中的一个接口,它继承自 Collection 接口,是单列集合的一个重要分支。List 接口的常见实现类包括 ArrayListLinkedListVector

List 接口特点如下:

  • 有序性List 中的元素是按照插入顺序排序的,因此可以很容易地遍历 List 中的元素。对于 ArrayList,底层采用数组来存储元素,因此可以进行随机访问;对于 LinkedList,底层采用链表来存储元素,因此只能进行顺序访问。
  • 可重复性List 中允许出现重复元素。这对于保存一组数据中可能存在重复数据的场景非常有用。
  • 可变性List 中的元素可以随时被修改、添加和删除。对于 ArrayList,修改和删除元素的效率较高,但添加元素的效率较低;对于 LinkedList,添加和删除元素的效率较高,但是随机访问元素的效率较低。

注意:List 集合关心元素是否有序,而不关心是否重复。

2.List 接口常用 API

List 作为 Collection 集合的子接口,不但继承了 Collection 接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法。

方法签名 描述
void add(int index, E element) 在指定的索引位置插入指定的元素到列表中
boolean addAll(int index, Collection<? extends E> c) 从指定索引位置开始将指定集合中的所有元素插入到列表中
E get(int index) 返回指定索引位置的元素
int indexOf(Object o) 返回列表中首次出现指定元素的索引,如果不存在返回 -1
int lastIndexOf(Object o) 返回列表中最后一次出现指定元素的索引,如果不存在返回 -1
E remove(int index) 移除指定索引位置的元素,并将其返回
E set(int index, E element) 将指定索引位置的元素替换为指定元素,并返回被替换的元素
List<E> subList(int fromIndex, int toIndex) 返回列表中指定范围的子列表

3.ListIterator 迭代器

List 集合额外提供了 listIterator()listIterator(int index) 两个方法,皆返回一个 ListIterator 列表迭代器对象,用于迭代列表中的元素。

这两个方法的主要区别在于它们开始迭代的位置:

  1. listIterator(): 这个方法返回一个 ListIterator,并且开始在列表的头部,即索引位置 0 开始遍历。如果我们调用 next(),将获取列表的第一个元素。
  2. listIterator(int index): 这个方法也返回一个 ListIterator,但是它从指定的索引位置开始遍历。也就是说,如果我们传入的参数是 n,那么迭代器将从列表的第 n 个元素开始。此方法可用于从列表中间开始迭代。

ListIterator 扩展了 Iterator 接口,特别用于 List 接口的集合类。除了提供正常的迭代功能之外,ListIterator 还提供额外的功能,让我们可以在列表中向前和向后移动。此外,ListIterator 也允许我们在迭代过程中添加和设置元素,这在 Iterator 接口中是不可能的。

下面是 ListIterator 接口中的方法:

方法签名 描述
boolean hasNext() 如果列表迭代器有更多的元素(以正向遍历列表时),则返回 true。
E next() 返回列表中的下一个元素。
boolean hasPrevious() 如果列表迭代器有更多的元素(以反向遍历列表时),则返回 true。
E previous() 返回列表中的上一个元素。
int nextIndex() 返回下一个元素的索引,如果列表迭代器位于列表的末尾,则返回列表的大小。
int previousIndex() 返回前一个元素的索引,如果列表迭代器位于列表的开始,则返回-1。
void remove() 从列表中移除最后一个访问过的元素(可选操作)。
void set(E e) 替换列表中最后访问过的元素(可选操作)。
void add(E e) 在列表中的当前位置插入指定的元素(可选操作)。

这些方法提供了强大的操作列表的功能,允许我们在遍历列表的同时修改列表。下面是一个简单的例子,展示了如何使用 ListIterator

public class ListIteratorDemo {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        list.add("java");
        list.add("c++");
        list.add("python");
        list.add("c");

        System.out.println("正向遍历");
        ListIterator<String> listIterator = list.listIterator();
        while (listIterator.hasNext()) {
    
    
            // 获取下一个元素的索引
            int nextIndex = listIterator.nextIndex();
            // 获取下一个元素
            String next = listIterator.next();
            System.out.println("索引:" + nextIndex + ",元素:" + next);
        }

        System.out.println("反向遍历");
        ListIterator<String> listIterator1 = list.listIterator(list.size());
        while (listIterator1.hasPrevious()) {
    
    
            // 获取上一个元素的索引
            int previousIndex = listIterator1.previousIndex();
            // 获取上一个元素
            String previous = listIterator1.previous();
            System.out.println("索引:" + previousIndex + ",元素:" + previous);
        }

        System.out.println("在遍历过程中修改元素");
        ListIterator<String> listIterator2 = list.listIterator();
        while (listIterator2.hasNext()) {
    
    
            // 获取下一个元素的索引
            int nextIndex = listIterator2.nextIndex();
            // 获取下一个元素
            String next = listIterator2.next();
            System.out.println("索引:" + nextIndex + ",元素:" + next);
            if (next.equals("c++")) {
    
    
                // 修改元素
                listIterator2.set("C#");
            }
        }

        System.out.println("在遍历过程中添加元素");
        ListIterator<String> listIterator3 = list.listIterator();
        while (listIterator3.hasNext()) {
    
    
            // 获取下一个元素的索引
            int nextIndex = listIterator3.nextIndex();
            // 获取下一个元素
            String next = listIterator3.next();
            System.out.println("索引:" + nextIndex + ",元素:" + next);
            if (next.equals("c++")) {
    
    
                // 添加元素(添加到当前元素的前面)
                listIterator3.add("C#");
            }
        }

        System.out.println("在遍历过程中删除元素");
        ListIterator<String> listIterator4 = list.listIterator();
        while (listIterator4.hasNext()) {
    
    
            // 获取下一个元素的索引
            int nextIndex = listIterator4.nextIndex();
            // 获取下一个元素
            String next = listIterator4.next();
            System.out.println("索引:" + nextIndex + ",元素:" + next);
            if (next.equals("c")) {
    
    
                // 删除元素
                listIterator4.remove();
            }
        }
    }
}

4.ArrayList - 动态数组

4.1 ArrayList 概述

在 Java 中,动态数组由 ArrayList 类实现,它是 List 接口的一部分。其类图如下:

动态数组的主要特点如下:

逻辑结构特点:

  1. 有序: ArrayList 中的元素有固定的插入顺序,也就是说,你可以通过索引来访问列表中的任何位置的元素。
  2. 可重复: ArrayList 可以包含重复的元素。即同一对象可以多次出现在列表中。
  3. 动态大小: ArrayList 可以在运行时动态地改变其大小。你可以向其添加和删除元素,它会自动调整其大小。

物理结构特点:

  1. 基于数组实现: 在内部,ArrayList 使用一个数组来存储元素。当 ArrayList 的大小超过当前数组的大小时,ArrayList 会创建一个新的数组,将所有元素复制到新的数组中,然后丢弃旧的数组。这种自动管理数组大小的过程使得 ArrayList 成为一个动态数组。
  2. 时间复杂度: 对于 ArrayList 的随机访问(例如 get()set() 操作),时间复杂度为 O(1)。这是因为它是基于数组的,因此可以直接通过索引访问元素。然而,添加和删除元素(特别是在列表的中间)可能需要移动元素,这将使时间复杂度升到 O(n)。
  3. 空间效率: 因为 ArrayList 需要额外的空间来存储元素,所以它不是最空间有效的数据结构。特别是当数组需要扩展时,需要创建新的数组,这会占用更多的空间。

4.2 手撸动态数组

了解了动态数组的特点后,我们也可以自定义自己的动态数组,如下是一个简单的例子:

public class MyArrayList<E> implements Iterable<E> {
    
    
    // 容器大小(这里为了能够存储不同类型的数据,所以使用Object类型)
    private Object[] elementData;

    // 容器中实际存储的元素个数
    private int size;

    // 默认容器大小(如果用户没有指定容器大小,则使用默认容器大小)
    private static final int DEFAULT_CAPACITY = 10;

    // 无参构造方法
    public MyArrayList() {
    
    
        // 初始化容器大小(使用默认容器大小)
        elementData = new Object[DEFAULT_CAPACITY];
    }

    // 有参构造方法
    public MyArrayList(int capacity) {
    
    
        // 初始化容器大小(使用用户指定的容器大小)
        elementData = new Object[capacity];
    }

    // 添加元素
    public void add(Object obj) {
    
    
        // 判断是否需要扩容
        ensureCapacity();
        // 将元素添加到数组中(size表示数组中实际存储的元素个数,添加元素后,size需要加1)
        elementData[size++] = obj;
    }

    // 判断是否需要扩容
    private void ensureCapacity() {
    
    
        // 判断容器是否已满
        if (size == elementData.length) {
    
    
            // 扩容(扩容为原来的1.5倍)
            Object[] newArray = new Object[size + size / 2];
            // 将原来数组中的元素复制到新数组中(原数组,原数组的起始位置,新数组,新数组的起始位置,复制的长度)
            System.arraycopy(elementData, 0, newArray, 0, elementData.length);
            // 将新数组赋值给原数组
            elementData = newArray;
        }
    }

    // 在指定位置添加元素
    public void add(int index, E value) {
    
    
        // 判断索引是否合法
        checkIndex(index);
        // 判断是否需要扩容
        ensureCapacity();
        // 将指定位置及其后面的元素向后移动一位(原数组,原数组的起始位置,新数组,新数组的起始位置,复制的长度)
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        // 将元素添加到指定位置
        elementData[index] = value;
        // 元素个数加1
        size++;
    }

    // 检查索引是否合法
    private void checkIndex(int index) {
    
    
        // 判断索引是否合法
        if (index < 0 || index >= size) {
    
    
            throw new IndexOutOfBoundsException("索引不合法:" + index);
        }
    }

    // 删除指定位置的元素
    public void remove(int index) {
    
    
        // 判断索引是否合法
        checkIndex(index);
        // 将指定位置及其后面的元素向前移动一位(原数组,原数组的起始位置,新数组,新数组的起始位置,复制的长度)
        System.arraycopy(elementData, index + 1, elementData, index, size - index - 1);
        // 元素个数减1
        size--;
    }

    // 删除指定元素
    public void remove(E value) {
    
    
        // 判断指定元素是否存在
        if (contains(value)) {
    
    
            // 获取指定元素的索引
            int index = indexOf(value);
            // 删除指定位置的元素
            remove(index);
        }
    }

    // 判断指定元素是否存在
    public boolean contains(E value) {
    
    
        // 判断指定元素是否存在
        return indexOf(value) != -1;
    }

    // 获取指定元素的索引
    public int indexOf(E value) {
    
    
        // 判断指定元素是否存在
        if (value == null) {
    
    
            // 遍历数组
            for (int i = 0; i < size; i++) {
    
    
                // 判断指定元素是否存在
                if (elementData[i] == null) {
    
    
                    return i;
                }
            }
        } else {
    
    
            // 遍历数组
            for (int i = 0; i < size; i++) {
    
    
                // 判断指定元素是否存在
                if (value.equals(elementData[i])) {
    
    
                    return i;
                }
            }
        }
        return -1;
    }

    // 修改指定位置的元素
    public void set(int index, E obj) {
    
    
        // 判断索引是否合法
        checkIndex(index);
        // 修改指定位置的元素
        elementData[index] = obj;
    }

    // 使用新元素替换旧元素
    public void replace(E oldObj, E newObj) {
    
    
        // 获取旧元素的索引
        int index = indexOf(oldObj);
        // 判断旧元素是否存在
        if (index != -1) {
    
    
            // 修改指定位置的元素
            set(index, newObj);
        }
    }

    // 获取指定位置的元素
    public Object get(int index) {
    
    
        // 判断索引是否合法
        checkIndex(index);
        // 获取指定位置的元素
        return elementData[index];
    }

    // 获取容器中实际存储的元素个数
    public int size() {
    
    
        return size;
    }

    // 判断容器是否为空
    public boolean isEmpty() {
    
    
        return size == 0;
    }

    // 清空容器
    public void clear() {
    
    
        // 将数组中的元素置为null
        for (int i = 0; i < size; i++) {
    
    
            elementData[i] = null;
        }
        // 元素个数置为0
        size = 0;
    }

    // 重写toString方法
    @Override
    public Iterator<E> iterator() {
    
    
        return new Itr();
    }

    // 自定义迭代器
    private class Itr implements Iterator<E> {
    
    
        // 定义游标(用于记录遍历到的位置)
        private int cursor;

        // 重写hasNext方法
        @Override
        public boolean hasNext() {
    
    
            // 判断是否有下一个元素(如果游标等于元素个数,表示没有下一个元素)
            return cursor != size;
        }

        // 重写next方法
        @Override
        public E next() {
    
    
            // 获取下一个元素(游标加1,表示下一个元素的索引)
            return (E) elementData[cursor++];
        }

        // 重写remove方法
        @Override
        public void remove() {
    
    
            // 删除上一个元素(游标减1,表示上一个元素的索引)
            MyArrayList.this.remove(--cursor);
        }
    }
}

对上述自定义动态数组进行简单测试:

public class MyArrayListTest {
    
    
    public static void main(String[] args) {
    
    
        // 创建集合对象
        MyArrayList<String> list = new MyArrayList<>();

        // 添加元素
        list.add("java");
        list.add("c++");
        list.add("python");
        list.add("c");
        list.add(null); // 可以添加null元素

        System.out.println("----- 测试获取集合大小 -----");
        System.out.println("集合的长度:" + list.size());

        // 遍历集合
        System.out.println("----- 遍历集合 ------");
        for (String s : list) {
    
    
            System.out.println(s);
        }

        // 删除元素
        System.out.println("----- 删除元素 ------");
        list.remove(0);

        // 遍历集合
        for (String s : list) {
    
    
            System.out.println(s);
        }

        // 获取指定索引处的元素
        System.out.println("----- 获取指定索引处的元素 -----");
        System.out.println(list.get(0));

        // 修改指定索引处的元素
        System.out.println("----- 修改指定索引处的元素 ------");
        list.set(0, "C#");

        // 遍历集合
        for (String s : list) {
    
    
            System.out.println(s);
        }

        // 判断集合中是否包含指定元素
        System.out.println("----- 判断集合中是否包含指定元素 -----");
        System.out.println(list.contains("C#"));

        // 清空集合
        System.out.println("----- 清空集合 -----");
        list.clear();

        // 判断集合是否为空
        System.out.println(list.isEmpty());

        // 遍历集合
        for (String s : list) {
    
    
            System.out.println(s);
        }
    }
}

测试结果:

----- 测试获取集合大小 -----
集合的长度:5
----- 遍历集合 ------
java
c++
python
c
null
----- 删除元素 ------
c++
python
c
null
----- 获取指定索引处的元素 -----
c++
----- 修改指定索引处的元素 ------
C#
python
c
null
----- 判断集合中是否包含指定元素 -----
true
----- 清空集合 -----
true

小结:

ArrayList 适用于需要快速随机访问的情况,但如果你需要频繁地在列表的中间添加或删除元素,可能需要考虑其他的数据结构,例如 LinkedList

5.Vector - 动态数组

Java 的 List 接口的实现类中有两个动态数组的实现,上面的 ArrayList 便是其中一个,还有一个就是 Vector

类图如下:

ArrayListVector 都是 Java 中实现动态数组的类,它们的底层物理结构都是数组(动态数组),都属于 java.util 包下的 List 接口的实现。然而,尽管它们在功能上有许多相似之处,但在实现细节和使用方式上还是存在一些关键的区别。

  1. 线程安全: 这是 ArrayListVector 之间最重要的区别。Vector 是同步的,也就是线程安全的。这意味着在多线程环境下,只有一个线程可以访问 Vector 的实例的任何方法。这样可以防止数据的不一致性和数据冲突。而 ArrayList 则不是线程安全的,它的方法在多线程环境下不会进行同步。因此,在需要考虑线程安全的情况下,应该使用 Vector;而在不需要考虑线程安全,或者自行管理线程安全的情况下,ArrayList 可能会有更好的性能。

  2. 性能: ArrayList 通常比 Vector 快,因为 Vector 需要花费额外的时间来同步其方法。如果你不需要关心线程安全,那么使用 ArrayList 会有更高的性能。

  3. 容量增长: 当需要增加 ArrayListVector 的大小时,两者的处理方式不同。ArrayList 默认(没有指定初始化容量)情况下,每次扩容会增长为原来的50%,即增长为原来大小的 1.5 倍。而 Vector 默认情况下每次扩容增长为原来的100%,即增长为原来大小的 2 倍。当然,这些都是默认行为,开发者可以在创建 ArrayListVector 实例时指定容量增长的大小。

    注意:ArrayList 在 JDK1.6 及之前的版本默认初始化容量为 10,JDK1.7 之后的版本 ArrayList 初始化为长度为 0 的空数组,之后在添加第一个元素时,再创建长度为 10 的数组。

  4. 遗留类: Vector 是 Java 早期版本中的一部分,因此被视为遗留类。新的代码通常应该使用 ArrayList,除非有特定的需求需要使用 Vector

除了上面的结果特点外,我们还需要注意:由于 Vector 是 Java 早期版本中的一部分,支持 Enumeration 迭代器。然而,这个迭代器并不支持所谓的 “快速失败” 行为。“快速失败” 是一个错误检测机制,当有其他线程修改了集合的结构(通过迭代器自身的 removeadd 方法之外的任何其他方式),“快速失败” 迭代器将抛出 ConcurrentModificationException

在 Java 的 Vector 类中,除了 Enumeration,我们还可以使用 IteratorListIterator 迭代器,这两种迭代器都支持 “快速失败”。 这意味着,如果在创建迭代器后,有其他线程通过非迭代器操作(如 add, remove 等方法)修改了 Vector,那么当你下次调用迭代器的 next, previous, remove, 或 add 方法时,它将抛出 ConcurrentModificationException

这种 “快速失败” 行为可以防止在并发修改的情况下发生不确定的行为,而不是冒着在未来某个不确定的时间点发生不可预知的问题。然而,值得注意的是,“快速失败” 是一种尽力而为的行为,并不能在所有情况下保证检测出并发修改,因为它主要是用于检测 bug,而不是作为并发操作的同步机制。

6.LinkedList - 双向链表

Java 中的 LinkedList 类是一个双链表的实现。通过使用双链表,LinkedList 可以高效地在列表的开始和结束添加或删除元素。另外,LinkedList 还实现了 Deque 接口,这意味着它可以被用作双端队列(即可以从列表的两端插入和删除元素)。

6.1 链表概述

链表是一种常见的数据结构,它由一系列的节点(Node)组成,每个节点包含数据和指向下一个节点的指针。与数组相比,链表在内存中的分布不需要连续,它的节点可以分散在内存的任何地方。

  • 逻辑结构特点:链表的逻辑结构非常直观。它由一系列节点组成,每个节点都有一个指向下一个节点的指针。最后一个节点(称为尾节点)的指针指向 null,表示链表的结束。链表的起始节点被称为头节点。
  • 物理结构特点:在物理上,链表的节点可以被存储在内存的任何地方。链表的每个节点通常都包含两个部分:一个是存储数据的部分,另一个是指向下一个节点的指针。因此,通过链表的头节点,我们可以遍历整个链表。
  • 存储特点:链表的主要特点是其动态的存储管理方式。你可以很容易地向链表中添加和删除节点,而不需要移动其他节点。这是因为添加或删除节点只涉及到更改一些指针,而不需要移动节点本身。

下面是三种常见的链式存储结构:

  1. 单链表(Singly Linked List): 单链表中的每个节点只包含一个指向下一个节点的指针。这使得单链表只能从头节点开始,按照链表的方向向前遍历。最后一个节点(也被称为尾节点)的指针指向 null,表示链表的结束。单链表通常被用于实现栈、队列等数据结构。

  2. 双链表(Doubly Linked List): 双链表中的每个节点包含两个指针,一个指向下一个节点,另一个指向前一个节点。这意味着你可以从任何一个节点开始,并向前或向后遍历链表。双链表允许双向遍历,这在某些情况下提供了更高的效率,例如在进行大量的插入和删除操作时。

  3. 循环单链表(Circular Singly Linked List): 在循环单链表中,最后一个节点的指针不再指向 null,而是指向头节点,形成了一个闭环。这使得链表可以从任何节点开始,并无限次地遍历链表。因此,循环单链表可以被用于实现需要循环处理数据的算法或数据结构,例如环形队列。

  4. 循环双链表(Circular Doubly Linked List): 循环双链表是双链表和循环单链表的结合。它的头节点和尾节点是相连的,形成了一个闭环。并且每个节点都有两个指针,一个指向前一个节点,另一个指向后一个节点。这样,我们可以从任何节点开始,并向前或向后无限次地遍历链表。和循环单链表一样,循环双链表适合于需要循环处理数据的算法或数据结构。

6.2 手撸双链表

下面同样通过手撸一个简单的双链表来更好的理解上面的概念:

import java.util.Iterator;

/**
 * 自定义双向链表
 * @author: JavGo
 * @description: TODO
 * @date: 2023/5/26 11:55
 */
public class MyLinkedList<E> implements Iterable<E> {
    
    

    // 定义头节点(哨兵节点)
    private Node head;

    // 定义尾节点(哨兵节点)
    private Node tail;

    // 定义链表的长度(不包含头节点和尾节点)
    private int size;

    // 无参构造方法
    public MyLinkedList() {
    
    
        // 初始化头节点(将头节点的上一个节点和下一个节点都指向自己)
        head = new Node(null, null, null);
        // 初始化尾节点(将尾节点的上一个节点和下一个节点都指向自己)
        tail = new Node(null, head, null);
        // 头节点的下一个节点指向尾节点(将头节点和尾节点连接起来)
        head.next = tail;
    }

    // 添加元素
    public void add(E data) {
    
    
        // 在尾节点的前一个节点添加元素(尾节点的前一个节点就是链表的最后一个节点)
        addBefore(data, tail);
    }

    // 在尾节点的前一个节点添加元素
    private void addBefore(E data, Node node) {
    
    
        // 创建新节点(新节点的上一个节点是尾节点的前一个节点,新节点的下一个节点是尾节点)
        Node newNode = new Node(data, node.prev, node);
        // 将新节点赋值给尾节点的前一个节点的下一个节点
        node.prev.next = newNode;
        // 将新节点赋值给尾节点的前一个节点
        node.prev = newNode;
        // 链表长度加1
        size++;
    }

    // 在指定位置添加元素
    public void add(int index, E data) {
    
    
        // 判断索引是否合法
        checkIndex(index);
        // 获取指定位置的节点
        Node node = getNode(index);
        // 在指定位置的节点的前一个节点添加元素
        addBefore(data, node);
    }

    // 判断索引是否合法
    private void checkIndex(int index) {
    
    
        // 判断索引是否合法
        if (index < 0 || index > size) {
    
    
            throw new IndexOutOfBoundsException("索引不合法");
        }
    }

    // 获取指定位置的节点
    private Node getNode(int index) {
    
    
        // 判断索引是否合法
        checkIndex(index);
        // 判断索引是否小于链表长度的一半
        if (index < size / 2) {
    
    
            // 从头节点开始遍历
            Node node = head.next;
            // 遍历索引 index 次,拿到指定位置的节点
            for (int i = 0; i < index; i++) {
    
    
                // 获取下一个节点
                node = node.next;
            }
            // 返回指定位置的节点
            return node;
        } else {
    
     // 索引大于等于链表长度的一半
            // 从尾节点开始遍历
            Node node = tail.prev;
            // 遍历(size - index)次
            for (int i = 0; i < size - index; i++) {
    
    
                // 获取上一个节点
                node = node.prev;
            }
            // 返回指定位置的节点
            return node;
        }
    }

    // 获取指定位置的元素
    public E get(int index) {
    
    
        // 获取指定位置的节点
        Node node = getNode(index);
        // 返回指定位置的节点的数据
        return node.data;
    }

    // 修改指定位置的元素
    public void set(int index, E data) {
    
    
        // 获取指定位置的节点
        Node node = getNode(index);
        // 将指定位置的节点的数据修改为新的数据
        node.data = data;
    }

    // 删除指定位置的元素
    public void remove(int index) {
    
    
        // 获取指定位置的节点
        Node node = getNode(index);
        // 将指定位置的节点的上一个节点的下一个节点指向指定位置的节点的下一个节点
        node.prev.next = node.next;
        // 将指定位置的节点的下一个节点的上一个节点指向指定位置的节点的上一个节点
        node.next.prev = node.prev;
        // 将指定位置的节点的上一个节点和下一个节点都置为null
        node.prev = null;
        node.next = null;
        // 链表长度减1
        size--;
    }

    // 删除指定元素
    public void remove(E data) {
    
    
        // 获取头节点的下一个节点
        Node node = head.next;
        // 遍历链表
        while (node != tail) {
    
    
            // 判断当前节点的数据是否和指定数据相等
            if (node.data.equals(data)) {
    
    
                // 将当前节点的上一个节点的下一个节点指向当前节点的下一个节点
                node.prev.next = node.next;
                // 将当前节点的下一个节点的上一个节点指向当前节点的上一个节点
                node.next.prev = node.prev;
                // 将当前节点的上一个节点和下一个节点都置为null
                node.prev = null;
                node.next = null;
                // 链表长度减1
                size--;
                // 结束循环
                break;
            }
            // 获取下一个节点
            node = node.next;
        }
    }

    // 获取链表的长度
    public int size() {
    
    
        return size;
    }

    // 判断链表是否为空
    public boolean isEmpty() {
    
    
        return size == 0;
    }

    // 清空链表
    public void clear() {
    
    
        // 获取头节点的下一个节点
        Node node = head.next;
        // 遍历链表
        while (node != tail) {
    
    
            // 获取下一个节点
            Node temp = node.next;
            // 将当前节点的上一个节点和下一个节点都置为null
            node.prev = null;
            node.next = null;
            // 将当前节点的下一个节点赋值给当前节点
            node = temp;
        }
        // 将头节点的下一个节点指向尾节点
        head.next = tail;
        // 将尾节点的上一个节点指向头节点
        tail.prev = head;
        // 链表长度置为0
        size = 0;
    }
    
    // 判断链表是否包含指定元素
    public boolean contains(E data) {
    
    
        // 获取头节点的下一个节点
        Node node = head.next;
        // 遍历链表
        while (node != tail) {
    
    
            // 判断当前节点的数据是否和指定数据相等
            if (node.data.equals(data)) {
    
    
                return true;
            }
            // 获取下一个节点
            node = node.next;
        }
        return false;
    }

    // 节点
    private class Node {
    
    
        // 存储的数据
        private E data;
        // 上一个节点(前驱)
        private Node prev;
        // 下一个节点(后继)
        private Node next;

        // 有参构造方法
        public Node(E data, Node prev, Node next) {
    
    
            this.data = data;
            this.prev = prev;
            this.next = next;
        }
    }

    @Override
    public Iterator<E> iterator() {
    
    
        return new Itr();
    }

    private class Itr implements Iterator<E>{
    
    
        // 当前节点
        private Node node = head.next;

        @Override
        public boolean hasNext() {
    
    
            // 判断当前节点是否为尾节点
            return node != null && node != tail;
        }

        @Override
        public E next() {
    
    
            // 获取当前节点的数据
            E value = node.data;
            // 获取下一个节点
            node = node.next;
            // 返回当前节点的数据
            return value;
        }
    }
}

下面进行简单测试:

public class MyLinkedListTest {
    
    
    public static void main(String[] args) {
    
    
        MyLinkedList<String> list = new MyLinkedList<>();
        // 添加元素
        list.add("java");
        list.add("c++");
        list.add("python");
        list.add("c");

        // 遍历集合
        System.out.println("----- 遍历集合 ------");
        for (String s : list) {
    
    
            System.out.println(s);
        }

        // 删除元素
        System.out.println("----- 删除元素 ------");
        list.remove(0);

        // 遍历集合
        for (String s : list) {
    
    
            System.out.println(s);
        }

        // 获取指定索引处的元素
        System.out.println("----- 获取指定索引处的元素 -----");
        System.out.println(list.get(0));

        // 修改指定索引处的元素
        System.out.println("----- 修改指定索引处的元素 ------");
        list.set(0, "C#");

        // 遍历集合
        for (String s : list) {
    
    
            System.out.println(s);
        }

        // 判断集合中是否包含指定元素
        System.out.println("----- 判断集合中是否包含指定元素 -----");
        System.out.println(list.contains("C#"));

        // 清空集合
        System.out.println("----- 清空集合 -----");
        list.clear();

        // 判断集合是否为空
        System.out.println(list.isEmpty());
    }
}

输出结果:

----- 遍历集合 ------
java
c++
python
c
----- 删除元素 ------
c++
python
c
----- 获取指定索引处的元素 -----
c++
----- 修改指定索引处的元素 ------
C#
python
c
----- 判断集合中是否包含指定元素 -----
true
----- 清空集合 -----
true

6.3 链表与动态数组的区别

  1. 内存分配: 动态数组在内存中是连续的,而链表的元素(节点)可以在内存中任意位置,通过指针链接在一起。这使得链表在插入和删除元素时更为灵活,因为它不需要移动其他元素。而动态数组在添加或删除元素时可能需要重新分配内存或移动元素。
  2. 访问速度: 动态数组可以提供快速的随机访问(即直接通过索引访问),访问速度是 O(1)。而链表则需要从头节点开始,依次遍历链表直到找到需要的元素,访问速度是 O(n)。
  3. 插入和删除: 链表在插入和删除元素时更为高效。在链表中,添加或删除元素只需更改一些指针,时间复杂度为 O(1),前提是你已经有了要操作元素的引用。而动态数组则可能需要移动元素或重新分配内存,时间复杂度可能高达 O(n)。
  4. 空间使用: 动态数组通常更加节省空间效率,因为它只需要存储元素本身。而链表则需要额外的空间来存储指向下一个元素(在双链表中还包括前一个元素)的指针。
  5. 线性遍历: 由于链表的节点分布在内存的各个地方,相比之下,动态数组的连续内存布局使得其在进行线性遍历时能够更好地利用 CPU 缓存,提高性能。

7.Stack - 栈

栈(Stack)是一种特殊的线性数据结构,它遵循先进后出(FILO:First In Last Out)或者后进先出(LIFO:Last In First Out)的原则。这意味着在栈中,最后加入的元素总是被首先移除,最先加入的元素总是被最后移除。栈常常被用于实现深度优先搜索(DFS)、函数调用堆栈等。

值得注意的是,栈是一个逻辑概念,它的物理实现可以有多种方式。常见的物理实现包括数组(称为顺序栈)和链表(称为链式栈或链表栈)。在 Java 核心类库中,有多种方式来实现栈结构,最常见的包括 StackLinkedList 类。

  • Stack 类是一个顺序栈的实现,它是 Vector 类的子类。Stack 提供了 push(压栈)、pop(弹栈)、peek(查看栈顶元素,不弹出) 等方法,使得我们可以方便地对栈进行操作。然而,由于 Stack 类是线程安全的,所以它的性能可能会略低于非线程安全的实现。

  • LinkedList 类可以被用作链式栈的实现。LinkedList 实现了 Deque 接口,这意味着我们可以使用 pushpoppeek 等方法对链表进行操作,使其表现得像一个栈。与 Stack 类不同,LinkedList不是线程安全的,所以在没有并发访问的情况下,它可能会提供更好的性能。

下面以 Stack 为例,进行简单使用示例:

public class StackTest {
    
    
    public static void main(String[] args) {
    
    
        // 创建栈对象
        Stack<String> stack = new Stack<>();

        // 入栈
        stack.push("java");
        stack.push("c++");
        stack.push("python");
        stack.push("c");

        // 遍历栈
        System.out.println("----- 遍历栈 -----");
        for (String s : stack) {
    
    
            System.out.println(s);
        }

        // 出栈
        System.out.println("----- 出栈 -----");
        stack.pop();

        // 遍历栈
        for (String s : stack) {
    
    
            System.out.println(s);
        }

        // 获取栈顶元素
        System.out.println("----- 获取栈顶元素 -----");
        System.out.println(stack.peek());
    }
}

运行结果如下:

----- 遍历栈 -----
java
c++
python
c
----- 出栈 -----
java
c++
python
----- 获取栈顶元素 -----
python

对应图解如下:

8.总结

下面是一个简单的总结表格:

特性 ArrayList Vector LinkedList Stack
元素的顺序 插入顺序 插入顺序 插入顺序 LIFO(先进后出)
允许 null
线程安全
性能 中等 中等
基于 动态数组 动态数组 双链表 Vector
特殊功能 双端操作 出栈入栈

猜你喜欢

转载自blog.csdn.net/ly1347889755/article/details/130905216