Java 集合 - List 接口

news2024/11/28 9:29:36

文章目录

    • 1.List 接口介绍
    • 2.List 接口常用 API
    • 3.ListIterator 迭代器
    • 4.ArrayList - 动态数组
      • 4.1 ArrayList 概述
      • 4.2 手撸动态数组
    • 5.Vector - 动态数组
    • 6.LinkedList - 双向链表
      • 6.1 链表概述
      • 6.2 手撸双链表
      • 6.3 链表与动态数组的区别
    • 7.Stack - 栈
    • 8.总结

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.总结

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

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

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

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

相关文章

Linux:查看主机运行状态的一系列命令:top、df、iostat、sar

Linux&#xff1a;查看主机运行状态的一系列命令&#xff1a;top、df、iostat、sar 命令top监控系统资源&#xff1a; 使用top(回车)命令后&#xff0c;整个控制台会变成任务管理器的形式&#xff1a; 退出可以使用&#xff1a;ctrlc 或 q 第一行补充&#xff1a;表示正在执行的…

高程复习 欧几里得算法和扩展欧几里得算法考试前冲刺简约版

gcd(m,n)gcd(n,m%n) gcd欧几里得算法标准代码求最大公约数 #include <iostream>using namespace std;typedef long long LL; LL gcd(int a,int b) {if(b0)return a;return gcd(b,a%b); } int main() {LL a,b;cin>>a>>b;cout<<gcd(a,b)<<endl;re…

Linux基础:文件权限详细说明(全)

一、前提 我们要知道&#xff0c;Linux系统&#xff0c;一切皆文件的含义。 对于Linux来说&#xff0c;一切皆文件。 我们常涉及到的概念是目录和文件。 权限主要有三种&#xff1a;r(读)w(写)x(执行)。 二、正文 1、修改文件或者目录所属用户和所属组 chown [用户名[:组名…

规则网络构建

规则网络构建 文章目录 规则网络构建[toc]1 规则网络定义2 规则网络的构建3 代码实现 1 规则网络定义 常见规则网络包包括全局耦合网络、最近邻耦合网络和星型耦合网络&#xff0c;三种规则网络定义如下&#xff1a; (1)全局耦合网络&#xff1a;任意两个节点均存在连边的网络…

云原生 HTAP -- PolarDB-IMCI:A Cloud-Native HATP Database

文章目录 0 背景1 IMCI 架构1.1 架构演进的背景1.2 基本架构1.2 基本使用1.4 列索引存储 设计1.5 RW-RO 的数据同步实现1.5.1 CALS1.5.2 2P-COFFER 1.6 计算引擎实现1.7 性能 近期除了本职工作之外想要再跟进一下业界做讨论以及落地的事情&#xff0c;扩宽一下视野&#xff0c;…

算法7.从暴力递归到动态规划0

算法|7.从暴力递归到动态规划0 1.汉诺塔 题意&#xff1a;打印n层汉诺塔从最左边移动到最右边的全部过程 解题思路&#xff1a; 把字母抛掉&#xff0c;变成左中右三个盘子多个盘子能一下到吗&#xff1f;不能&#xff0c;把上边的拿走&#xff0c;最下边的才能放到指位置(…

java汉字转拼音pinyin4j-2.5.0.jar用法

要先下载哦&#xff0c; pinyin4j下载链接 可能会出现Cannot resolve symbol ‘net’&#xff0c;找到上面文件的下载路径&#xff0c;IDEA中File->Project Structure -> Modules->Dependencies import java.util.*; import net.sourceforge.pinyin4j.PinyinHelper;…

算法基础学习笔记——⑬质数\约数

✨博主&#xff1a;命运之光 ✨专栏&#xff1a;算法基础学习 目录 ✨质数 &#x1f353;&#xff08;1&#xff09;质数的判定——试除法 &#x1f353;&#xff08;2&#xff09;分解质因数——试除法 ✨约数 &#x1f353;&#xff08;1&#xff09;试除法求一个数的所…

算法基础学习笔记——⑪拓扑排序\最短路

✨博主&#xff1a;命运之光 ✨专栏&#xff1a;算法基础学习 目录 ✨拓扑排序 &#x1f353;朴素dijkstra算法&#xff1a; &#x1f353;堆优化版dijkstra : &#x1f353;Bellman-Ford算法 &#x1f353;spfa 算法&#xff08;队列优化的Bellman-Ford算法&#xff09; …

操作系统(2.8)--线程的实现

目录 线程的实现方式 1.内核支持线程(KST) 2.用户级线程(ULT) 3.组合方式 线程的实现 1.内核支持线程的实现 2.用户级线程的实现 线程的创建和终止 线程的实现方式 1.内核支持线程(KST) 内核支持线程&#xff0c;与进程相同&#xff0c;是在内核的支持下运行的&#x…

二叉树及其相关题目相关的功能的实现

前言&#xff1a;前面我们简单提及了二叉树的相关初级知识和顺序实现二叉树的相关操作详解&#xff0c;并且由完全二叉树延伸到了堆的相关知识&#xff0c;具体详见二叉树初阶和堆的详解&#xff0c;今天&#xff0c;我们展开二叉树的相关 的链式实现操作和经常考察的二叉树的相…

2023 华为 Datacom-HCIE 真题题库 07--含解析

多项选择题 1.[试题编号&#xff1a;190187] &#xff08;多选题&#xff09;如图所示的拓扑采用了VXLAN分布式网关&#xff0c;SW1上的VBDIF10配置了&#xff1a;arp-proxy local enable命令&#xff0c;则以下描述中正确的有哪些项&#xff1f; A、SW1收到PC1发往PC2的报文&…

【搭建私人图床】使用LightPicture开源搭建图片管理系统并远程访问

文章目录 1.前言2. Lightpicture网站搭建2.1. Lightpicture下载和安装2.2. Lightpicture网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1.前言 现在的手机越来越先进&#xff0c;功能也越来越多&#xff0c;而手机…

C#,码海拾贝(22)——“全选主元高斯-约当消去法“求解“线性方程组“的C#源代码,《C#数值计算算法编程》源代码升级改进版

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 求解线性方程组的类 LEquations /// 原作 周长发 /// 改编 深度混淆 /// </summary> public static partial class LEquations { /// <summary> …

天天被开发怼?4个方法区分bug前后端归属,我再也不背锅了!

“开发都这么不友善吗&#xff1f;” 有朋友跟我说&#xff0c;刚上岗经常分不清bug是前端还是后端&#xff0c;一直需要开发帮忙重新指派&#xff0c;甚至还会被开发拿来吐槽.... 其实不是开发态度不好&#xff0c;而是对于前后端分离的应用&#xff0c;既需要进行功能测试&am…

什么是网络安全?如何让普通人简单的了解网络安全

一、介绍网络安全 可以介绍一下河南郑州的网络安全科技馆。网络安全科技馆设置个人安全、政企安全、社会安全、综合竞技四个主展区&#xff0c;帮大家普及网络安全知识。首先&#xff0c;可以从个人安全展区开始游览&#xff0c;了解我们身边的网络安全&#xff0c;原来网络安…

调幅波解调-二极管峰值包络检波器【Multisim】【高频电子线路】

目录 一、实验目的与要求 二、实验仪器 三、实验内容与测试结果 1.观测输入、输出波形&#xff0c;估算检波效率&#xff08;D1接法不同&#xff0c;分别观测&#xff09; 2.观察惰性失真波形(C1100nF&#xff0c;其他参数保持不变) 3.观测负峰切割失真(ma0.8&#xff0c…

【目标检测实验系列】AutoDL线上GPU服务器租用流程以及如何用Pycharm软件远程连接服务器进行模型训练 (以Pycharm远程训练Yolov5项目为例子 超详细)

目录 1. 文章主要内容2. 租用AutoDL服务器详细教程2.1 注册AutoDL账号&#xff0c;并申请学生认证(学生认证有优惠&#xff0c;如果不是学生可以忽略此点)2.2 算力市场选择GPU&#xff0c;并选择初始化配置环境2.3 控制台参数解析&#xff0c;并使用相关参数登录Xftp(Windows与…

Spring Boot如何实现OAuth2授权?

Spring Boot如何实现OAuth2授权&#xff1f; OAuth2是一种授权框架&#xff0c;用于授权第三方应用程序访问受保护的资源。在Web应用程序中&#xff0c;OAuth2通常用于授权用户访问受保护的API。 在本文中&#xff0c;我们将介绍如何使用Spring Boot实现OAuth2授权。我们将使…

5-python的Number类型

内容提要 主要介绍python中的Number类型&#xff1a; python的类型转换&#xff0c;oct()、hex()、bin()函数的使用。 python的整数表示&#xff1a;十进制、二进制、八进制、十六进制。&#xff08;*&#xff0c;0b&#xff0c;0o&#xff0c;0x&#xff09; python中ASCII码…