Java中级编程大师班<第一篇:初识数据结构与算法-链表(4)>

在上一期,我们深入探讨了数组数据结构以及其在编程中的广泛应用,包括了 HashMap、HashTable、ConcurrentHashMap、HashSet、LinkedHashMap 等数据结构。这期,我们将开始介绍链表数据结构以及与之相关的扩展内容。链表是一种基础而强大的数据结构,掌握链表的原理和应用将为您的编程技能增添新的维度。接下来的内容将帮助您更深入地理解链表,以及如何在实际项目中灵活应用它们。

链表的基本特性

链表(Linked List)是一种常见的线性数据结构,具有以下基本特性:

  1. 链式结构:链表是由节点(Node)组成的,每个节点包含两部分:数据域和指针域。数据域用于存储数据,指针域用于指向下一个节点,从而将节点串联在一起,形成链式结构。

  2. 动态大小:链表的大小可以动态增加或减少,不像数组一样需要预先分配固定大小的空间。这使得链表更加灵活,能够有效地处理不定数量的元素。

  3. 插入和删除效率高:由于链表的动态性,插入和删除元素的效率通常较高。只需改变节点之间的指针,而不需要像数组一样涉及元素的搬迁。

  4. 不连续的内存分配:链表中的节点可以分布在内存的不同位置,它们通过指针连接在一起,这与数组内存中连续的存储方式不同。

  5. 无需预分配内存空间:链表不需要预先分配连续的内存空间,因此不会出现数组中的“溢出”问题,但会引入一些额外的空间开销用于存储指针。

  6. 有单向链表和双向链表:链表可以是单向的,其中每个节点只有一个指向下一个节点的指针,也可以是双向的,其中每个节点同时具有指向下一个节点和上一个节点的指针,使得双向链表在某些操作中更加高效。

  7. 不支持随机访问:链表不支持像数组那样的随机访问,要访问链表中的某个元素,需要从头节点开始按顺序遍历。

  8. 常见操作:链表的常见操作包括在头部插入节点、在尾部插入节点、在指定位置插入节点、删除节点、查找节点等。

链表是计算机科学中重要且常用的数据结构,特别适用于需要频繁的插入和删除操作的场景,例如实现栈、队列、高级语言中的垃圾回收器等。在学习链表时,通常会深入研究单向链表、双向链表、循环链表等不同类型的链表以及它们的应用。

示例代码

下面是一个示例代码,演示如何创建一个简单的单向链表以及基本的链表操作,包括插入、删除和遍历节点:

class Node {
    int data;
    Node next;

    public Node(int data) {
        this.data = data;
        this.next = null;
    }
}

class LinkedList {
    Node head;

    public LinkedList() {
        head = null;
    }

    // 插入节点到链表尾部
    public void append(int data) {
        Node newNode = new Node(data);
        if (head == null) {
            head = newNode;
            return;
        }
        Node current = head;
        while (current.next != null) {
            current = current.next;
        }
        current.next = newNode;
    }

    // 删除节点
    public void delete(int data) {
        if (head == null) {
            return;
        }
        if (head.data == data) {
            head = head.next;
            return;
        }
        Node current = head;
        while (current.next != null && current.next.data != data) {
            current = current.next;
        }
        if (current.next != null) {
            current.next = current.next.next;
        }
    }

    // 遍历链表并打印节点值
    public void printList() {
        Node current = head;
        while (current != null) {
            System.out.print(current.data + " ");
            current = current.next;
        }
        System.out.println();
    }
}

public class Main {
    public static void main(String[] args) {
        LinkedList list = new LinkedList();
        list.append(1);
        list.append(2);
        list.append(3);

        System.out.println("原始链表:");
        list.printList();

        list.delete(2);

        System.out.println("删除节点后的链表:");
        list.printList();
    }
}

上述代码创建了一个简单的单向链表,包括插入、删除和遍历操作。在这个示例中,我们首先创建了一个链表对象 list,然后向链表中追加了三个节点。接着,我们删除了节点值为 2 的节点,并打印了链表的最终状态。

链表的使用场景

链表是一种灵活的数据结构,适用于多种实际开发场景。以下是一些链表的常见使用场景:

  1. 实现栈和队列:链表可用于实现栈(先进后出)和队列(先进先出)这两种重要的数据结构。在栈中,节点从链表的头部添加和删除;而在队列中,节点从链表的尾部添加,从头部删除。

  2. 缓存实现:链表可用于实现简单的缓存结构,例如 LRU(最近最少使用)缓存策略。当缓存达到容量限制时,最久未使用的数据可以从链表头部删除。

  3. 编辑器的撤销功能:许多文本编辑器和图形设计工具使用链表来实现撤销(undo)和重做(redo)功能。每个编辑操作被保存为一个节点,用户可以通过链表的前进和后退来查看和还原编辑历史。

  4. 实现高级数据结构:链表是许多高级数据结构的基础,如跳表、图、树(例如二叉搜索树的底层实现 AVL 树)等。链表的灵活性使其成为构建这些复杂数据结构的有力工具。

  5. 表达无限数据流:在处理无限数据流时,链表可以很好地表示数据的有限窗口。只需保留链表中的有限数量的节点,以适应可用内存。

  6. 实现符号表:链表可用于实现符号表,其中每个节点包含键值对。这在编译器、解析器和数据库中有广泛的应用。

  7. 任务调度器:链表可用于管理和调度任务队列,例如操作系统中的任务调度器或异步编程中的任务队列。

  8. 多项式运算:链表可以用来表示多项式,其中每个节点包含一个项的系数和指数。这在数学和科学计算中很常见。

  9. 游戏开发:链表可以用于游戏中的对象管理,例如管理敌人、子弹、动画等游戏元素。

需要注意的是,虽然链表在某些场景中非常有用,但在其他场景中,数组或其他数据结构可能更适合。选择合适的数据结构取决于具体的问题和性能需求。链表的主要优势在于插入和删除操作的效率,但它在随机访问和空间占用方面可能不如数组。因此,在选择数据结构时,需要综合考虑问题的特性和需求。

主要知识点讲解

链表是计算机科学中重要的数据结构,有许多与之相关的主要知识点。以下是关于链表的主要知识点的讲解:

  1. 节点(Node)
    • 节点(Node)通常用于构建链表等数据结构。
    • 链表的基本构建块是节点。每个节点包含两部分:数据域和指针域。
    • 数据域用于存储数据,可以是整数、字符串、对象等。
    • 指针域用于指向下一个节点(单向链表)或同时指向下一个节点和前一个节点(双向链表)。

以下是一个示例代码,展示了一个简单的节点类的定义和用法:

public class Node<T> {
    private T data;
    private Node<T> next;

    public Node(T data) {
        this.data = data;
        this.next = null;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public Node<T> getNext() {
        return next;
    }

    public void setNext(Node<T> next) {
        this.next = next;
    }
}

在上面的示例中,我们定义了一个泛型的 Node 类,包含以下成员变量和方法:

  • data:用于存储节点的数据。
  • next:指向下一个节点的引用。

使用示例:

public class Main {
    public static void main(String[] args) {
        // 创建节点
        Node<Integer> node1 = new Node<>(10);
        Node<Integer> node2 = new Node<>(20);
        Node<Integer> node3 = new Node<>(30);

        // 构建链表:node1 -> node2 -> node3
        node1.setNext(node2);
        node2.setNext(node3);

        // 遍历链表并打印数据
        Node<Integer> current = node1;
        while (current != null) {
            System.out.println(current.getData());
            current = current.getNext();
        }
    }
}

在上述示例中,我们创建了三个整数类型的节点,并使用 setNext 方法将它们链接在一起,形成一个简单的链表。然后,我们遍历链表并打印每个节点的数据。这是一个基本的节点示例,用于构建链表等数据结构。

  1. 单向链表
    • 单向链表中,每个节点只有一个指针域,指向下一个节点。
    • 链表的头节点用于标识链表的起始位置。

以下是一个简单的单向链表(Singly Linked List)的示例代码:

public class SinglyLinkedList<T> {
    private Node<T> head;
    
    public SinglyLinkedList() {
        this.head = null;
    }
    
    // 添加元素到链表的末尾
    public void append(T data) {
        Node<T> newNode = new Node<>(data);
        if (head == null) {
            head = newNode;
        } else {
            Node<T> current = head;
            while (current.getNext() != null) {
                current = current.getNext();
            }
            current.setNext(newNode);
        }
    }
    
    // 删除指定元素
    public void delete(T data) {
        if (head == null) {
            return;
        }
        if (head.getData().equals(data)) {
            head = head.getNext();
            return;
        }
        Node<T> current = head;
        while (current.getNext() != null && !current.getNext().getData().equals(data)) {
            current = current.getNext();
        }
        if (current.getNext() != null) {
            current.setNext(current.getNext().getNext());
        }
    }
    
    // 打印链表元素
    public void print() {
        Node<T> current = head;
        while (current != null) {
            System.out.print(current.getData() + " -> ");
            current = current.getNext();
        }
        System.out.println("null");
    }
}

在上述代码中,我们定义了一个 SinglyLinkedList 类,表示单向链表。该链表包括以下方法:

  • append(data):将元素添加到链表的末尾。
  • delete(data):删除指定元素。
  • print():打印链表元素。

节点(Node)的定义与之前的示例相同。

使用示例:

public class Main {
    public static void main(String[] args) {
        SinglyLinkedList<Integer> list = new SinglyLinkedList<>();
        
        list.append(1);
        list.append(2);
        list.append(3);
        
        list.print(); // 输出: 1 -> 2 -> 3 -> null
        
        list.delete(2);
        
        list.print(); // 输出: 1 -> 3 -> null
    }
}

在示例中,我们创建了一个整数类型的单向链表,并演示了如何向链表中添加元素、删除元素以及打印链表的操作。这是一个简单的单向链表示例,用于基本的链表操作。

  1. 双向链表
    • 双向链表中,每个节点同时包含两个指针域,一个指向下一个节点,另一个指向前一个节点。
    • 双向链表可以更容易地实现某些操作,如反向遍历和在节点之间的插入和删除。

以下是一个简单的双向链表(Doubly Linked List)的示例代码:

public class DoublyLinkedList<T> {
    private Node<T> head;
    private Node<T> tail;
    
    public DoublyLinkedList() {
        this.head = null;
        this.tail = null;
    }
    
    // 在链表末尾添加元素
    public void append(T data) {
        Node<T> newNode = new Node<>(data);
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            newNode.setPrev(tail);
            tail.setNext(newNode);
            tail = newNode;
        }
    }
    
    // 在链表开头添加元素
    public void prepend(T data) {
        Node<T> newNode = new Node<>(data);
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            newNode.setNext(head);
            head.setPrev(newNode);
            head = newNode;
        }
    }
    
    // 删除指定元素
    public void delete(T data) {
        Node<T> current = head;
        while (current != null) {
            if (current.getData().equals(data)) {
                if (current.getPrev() != null) {
                    current.getPrev().setNext(current.getNext());
                } else {
                    head = current.getNext();
                }
                if (current.getNext() != null) {
                    current.getNext().setPrev(current.getPrev());
                } else {
                    tail = current.getPrev();
                }
                return;
            }
            current = current.getNext();
        }
    }
    
    // 打印链表元素
    public void print() {
        Node<T> current = head;
        while (current != null) {
            System.out.print(current.getData() + " <-> ");
            current = current.getNext();
        }
        System.out.println("null");
    }
}

在上述代码中,我们定义了一个 DoublyLinkedList 类,表示双向链表。该链表包括以下方法:

  • append(data):在链表末尾添加元素。
  • prepend(data):在链表开头添加元素。
  • delete(data):删除指定元素。
  • print():打印链表元素。

节点(Node)的定义包括 prevnext 引用,用于指向前一个节点和后一个节点。

使用示例:

public class Main {
    public static void main(String[] args) {
        DoublyLinkedList<Integer> list = new DoublyLinkedList<>();
        
        list.append(1);
        list.append(2);
        list.append(3);
        
        list.print(); // 输出: 1 <-> 2 <-> 3 <-> null
        
        list.prepend(0);
        
        list.print(); // 输出: 0 <-> 1 <-> 2 <-> 3 <-> null
        
        list.delete(2);
        
        list.print(); // 输出: 0 <-> 1 <-> 3 <-> null
    }
}

在示例中,我们创建了一个整数类型的双向链表,并演示了如何向链表中添加元素、删除元素以及打印链表的操作。这是一个简单的双向链表示例,用于基本的链表操作。

  1. 循环链表
    • 循环链表是一种特殊的链表,其中最后一个节点的指针指向链表的头节点,形成一个循环结构。
    • 循环链表可以用于模拟循环队列或其他需要循环访问的场景。

以下是一个简单的循环链表(Circular Linked List)的示例代码:

public class CircularLinkedList<T> {
    private Node<T> head;
    private Node<T> tail;
    
    public CircularLinkedList() {
        this.head = null;
        this.tail = null;
    }
    
    // 在链表末尾添加元素
    public void append(T data) {
        Node<T> newNode = new Node<>(data);
        if (head == null) {
            head = newNode;
            tail = newNode;
            newNode.setNext(newNode); // 将尾节点的 next 指向自身,形成循环
        } else {
            newNode.setNext(head); // 新节点的 next 指向头节点
            tail.setNext(newNode); // 尾节点的 next 指向新节点
            tail = newNode; // 更新尾节点
        }
    }
    
    // 删除指定元素
    public void delete(T data) {
        if (head == null) {
            return;
        }
        if (head.getData().equals(data)) {
            if (head.getNext() == head) { // 如果链表中只有一个节点
                head = null;
                tail = null;
            } else {
                tail.setNext(head.getNext()); // 更新尾节点的 next
                head = head.getNext(); // 更新头节点
            }
            return;
        }
        Node<T> current = head;
        while (current.getNext() != head) {
            if (current.getNext().getData().equals(data)) {
                current.setNext(current.getNext().getNext());
                return;
            }
            current = current.getNext();
        }
    }
    
    // 打印链表元素
    public void print() {
        if (head == null) {
            System.out.println("null");
            return;
        }
        Node<T> current = head;
        do {
            System.out.print(current.getData() + " -> ");
            current = current.getNext();
        } while (current != head);
        System.out.println(" (head)");
    }
}

在上述代码中,我们定义了一个 CircularLinkedList 类,表示循环链表。该链表包括以下方法:

  • append(data):在链表末尾添加元素,同时确保尾节点的 next 指向头节点,形成循环。
  • delete(data):删除指定元素,需要特别处理头节点的删除和链表中只有一个节点的情况。
  • print():打印链表元素,从头节点开始遍历,并在头节点处标记 “(head)”。

节点(Node)的定义与之前的示例相同。

使用示例:

public class Main {
    public static void main(String[] args) {
        CircularLinkedList<Integer> list = new CircularLinkedList<>();
        
        list.append(1);
        list.append(2);
        list.append(3);
        
        list.print(); // 输出: 1 -> 2 -> 3 ->  (head)
        
        list.delete(2);
        
        list.print(); // 输出: 1 -> 3 ->  (head)
    }
}

在示例中,我们创建了一个整数类型的循环链表,并演示了如何向链表中添加元素、删除元素以及打印链表的操作。这是一个简单的循环链表示例,用于基本的链表操作。

  1. 链表的插入和删除

    • 链表的插入操作可以在指定位置或链表头部插入新节点。
    • 链表的删除操作可以删除指定位置或值匹配的节点。
  2. 链表的遍历

    • 遍历链表是通过迭代依次访问链表中的每个节点,通常使用循环实现。
    • 遍历过程中可以对节点进行查找、修改或其他操作。
  3. 时间复杂度分析

    • 链表的插入和删除操作通常具有 O(1) 的时间复杂度,因为只需要修改节点的指针。
    • 链表的查找操作通常具有 O(n) 的时间复杂度,因为需要遍历整个链表。
  4. 链表的优点和缺点

    • 优点包括高效的插入和删除操作、动态大小和灵活性。
    • 缺点包括随机访问效率低、占用额外的内存空间用于存储指针。
  5. 应用场景

    • 链表广泛用于栈、队列、缓存、编辑器的撤销功能、任务调度器、符号表、多项式运算等应用中。
  6. 复杂链表结构

    • 除了基本的单向、双向和循环链表,还存在复杂的链表结构,如跳表(Skip List)和各种树结构的底层实现。

了解这些主要知识点将帮助您深入理解链表的原理、应用和性能特征,从而更好地应用它们解决实际问题。链表是数据结构领域的基础,也是许多高级数据结构的构建模块。

链表的应用扩展

Java中有一些数据类型和类可以被看作是链表的扩展,它们提供了更多的功能和特性,例如双向遍历、随机访问、自动扩容等。以下是一些常见的链表扩展类型:

  1. ArrayList
    • ArrayList 是 Java 中的一个动态数组实现。它允许在数组末尾高效地添加和删除元素,并且支持随机访问,因此在某些场景下可以被视为链表的扩展。
    • 与链表不同,ArrayList 需要预先分配一定的内存空间,并会在需要时自动扩容。

以下是 ArrayList 的核心代码讲解,涵盖了初始化、添加元素、删除元素、扩容等关键操作。

首先,让我们看一下 ArrayList 的基本属性和构造方法:

// ArrayList 类的定义
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 存储元素的数组
    transient Object[] elementData;

    // ArrayList 的大小,即元素的个数
    private int size;

    // 构造方法
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
        }
    }
    
    // ...
}

在上述代码中,ArrayList 使用一个 Object 类型的数组 elementData 来存储元素。默认情况下,elementData 初始化为空数组,容量为 DEFAULT_CAPACITY(默认为 10)。size 属性表示当前 ArrayList 中的元素个数。

接下来,让我们看一下 ArrayList 的添加元素操作,包括 add 方法和扩容:

// 添加元素到 ArrayList
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}

// 确保容量足够,如果不够则进行扩容
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 如果需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 扩容操作
private void grow(int minCapacity) {
    // 原容量
    int oldCapacity = elementData.length;
    // 新容量,扩容为原来的 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新容量仍小于所需容量,新容量则为所需容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 复制元素到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

在上述代码中,add 方法用于将元素添加到 ArrayList 的末尾。如果添加元素后,元素个数超过了当前数组的容量,就会触发扩容操作。扩容操作会创建一个新的数组,并将元素从旧数组复制到新数组中,然后将新数组赋给 elementData

最后,让我们看一下 ArrayList 的删除元素操作,包括 remove 方法:

// 删除指定索引位置的元素
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    // 计算需要移动的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0) {
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    }
    elementData[--size] = null; // 清空最后一个元素,协助垃圾回收
    return oldValue;
}

// 辅助方法,检查索引是否越界
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 辅助方法,获取指定索引位置的元素
E elementData(int index) {
    return (E) elementData[index];
}

remove 方法用于删除指定索引位置的元素。首先,它会检查索引是否越界,然后使用 System.arraycopy 将后面的元素向前移动,覆盖被删除的元素。最后,将末尾的元素置为 null,以协助垃圾回收。

这些是 ArrayList 的核心操作,它们确保了 ArrayList 的高效添加

、删除和扩容能力。ArrayList 是 Java 集合框架中常用的数据结构之一,适用于大多数情况下的动态数组需求。

  1. LinkedList
    • LinkedList 是 Java 中的双向链表实现。它支持在链表头部和尾部高效地添加和删除元素,同时也支持随机访问,但访问效率相对较低。
    • LinkedList 的双向特性允许双向遍历。

以下是 LinkedList 的核心代码讲解,涵盖了初始化、添加元素、删除元素等关键操作。

首先,让我们看一下 LinkedList 的基本属性和构造方法:

// LinkedList 类的定义
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    // 链表的首节点
    transient Node<E> first;

    // 链表的尾节点
    transient Node<E> last;

    // 链表的大小,即元素的个数
    transient int size = 0;

    // 构造方法
    public LinkedList() {
    }

    // ...
}

在上述代码中,LinkedList 使用 Node 类来表示链表的节点。firstlast 分别表示链表的首节点和尾节点。size 属性表示当前链表中的元素个数。

接下来,让我们看一下 LinkedList 的添加元素操作,包括 addFirstaddLastadd 方法:

// 在链表头部添加元素
public void addFirst(E e) {
    linkFirst(e);
}

// 在链表尾部添加元素
public void addLast(E e) {
    linkLast(e);
}

// 在链表尾部添加元素,等效于 addLast
public boolean add(E e) {
    linkLast(e);
    return true;
}

// 在链表的指定位置添加元素
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

上述代码中,addFirst 方法用于在链表头部添加元素,addLast 方法用于在链表尾部添加元素,而 add 方法用于在链表尾部添加元素,并返回 trueadd 方法还支持在链表的指定位置添加元素。

接下来,让我们看一下 LinkedList 的删除元素操作,包括 removeFirstremoveLastremove 方法:

// 删除链表头部的元素
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

// 删除链表尾部的元素
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

// 删除链表中指定的元素
public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

// ...

上述代码中,removeFirst 方法用于删除链表头部的元素,removeLast 方法用于删除链表尾部的元素。remove 方法用于删除链表中指定的元素。删除操作会调用 unlink 方法,将节点从链表中断开。

最后,让我们看一下 LinkedList 的遍历操作,包括 get 方法和迭代器:

// 获取链表中指定索引位置的元素
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

// 返回一个迭代器,支持从前往后遍历
public Iterator<E> iterator() {
    return new ListItr(0);
}

// 返回一个迭代器,支持从后往前遍历
public Iterator<E> descendingIterator() {
    return new DescendingIterator(size());
}

// ...

get 方法用于获取链表中指定索引位置的元素,迭代器支持从前往后和从后往前遍历链表。

这些是 LinkedList 的核心操作,它们确保了链表的高效添加和删除能力。LinkedList 适用于需要频繁在链表头部或尾部进行添加和删除操作的场景,但在需要随机访问元素的情况下效率相对较低。

  1. Vector
    • VectorArrayList 类似,也是一个动态数组实现,但它是线程安全的。它提供了与 ArrayList 相似的操作,并且支持自动扩容。
    • 由于线程安全的特性,Vector 在多线程环境中使用,但通常会因为性能问题而被 ArrayListLinkedList 替代。

以下是 Vector 的核心代码讲解,涵盖了初始化、添加元素、删除元素、扩容等关键操作。

首先,让我们看一下 Vector 的基本属性和构造方法:

// Vector 类的定义
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 存储元素的数组
    protected Object[] elementData;

    // Vector 的大小,即元素的个数
    protected int elementCount;

    // 构造方法
    public Vector() {
        this(DEFAULT_CAPACITY);
    }

    public Vector(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
        elementData = new Object[initialCapacity];
    }
    
    // ...
}

在上述代码中,Vector 使用一个 Object 类型的数组 elementData 来存储元素。默认情况下,elementData 初始化为指定的容量,容量不足时会触发扩容操作。elementCount 属性表示当前 Vector 中的元素个数。

接下来,让我们看一下 Vector 的添加元素操作,包括 addElementadd 方法:

// 向 Vector 添加元素
public synchronized void addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = obj;
}

// 向 Vector 尾部添加元素
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

// 辅助方法,确保容量足够
private void ensureCapacityHelper(int minCapacity) {
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 扩容操作
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0)
                  ? capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

上述代码中,addElement 方法用于向 Vector 添加元素,add 方法用于向 Vector 尾部添加元素。如果添加元素后,元素个数超过了当前数组的容量,就会触发扩容操作。扩容操作会创建一个新的数组,并将元素从旧数组复制到新数组中,然后将新数组赋给 elementData

接下来,让我们看一下 Vector 的删除元素操作,包括 removeElement 方法:

// 删除指定元素
public synchronized boolean removeElement(Object obj) {
    modCount++;
    int i = indexOf(obj);
    if (i >= 0) {
        removeElementAt(i);
        return true;
    }
    return false;
}

// 删除指定索引位置的元素
public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
    }
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    elementCount--;
    elementData[elementCount] = null;
}

removeElement 方法用于删除指定元素,而 removeElementAt 方法用于删除指定索引位置的元素。删除操作会调用 System.arraycopy 将后面的元素向前移动,覆盖被删除的元素。

最后,让我们看一下 Vector 的遍历操作,包括 get 方法和迭代器:

// 获取指定索引位置的元素
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

// 返回一个迭代器,支持从前往后遍历
public Iterator<E> iterator() {
    return listIterator(0);
}

// 返回一个列表迭代器,支持从前往后遍历
public synchronized ListIterator<E> listIterator(int index) {
    if (index < 0 || index > elementCount)
        throw new IndexOutOfBoundsException("Index: "+index);

    return new ListItr(index);
}

// ...

get 方法用于获取指定索引位置的元素,迭代器支持从前往后遍历 Vector

这些是 Vector 的核心操作,它们确保了 Vector 的线程安全和高效添加、删除、扩容能力。Vector 在多线程环境中使用,但通常会因为性能问题而被 ArrayListLinkedList 替代。

  1. Deque
    • Deque 是 Java 中的双端队列(Double-ended Queue)接口,它提供了在队列的两端进行添加和删除操作的能力。LinkedList 实现了 Deque 接口,因此可以用作双向队列。
    • Deque 可以用于模拟栈(先进后出)和队列(先进先出)以及其他需要双向操作的数据结构。

Deque(双端队列)是 Java 中的一种队列数据结构,它可以在队列的两端添加和删除元素,支持先进先出(FIFO)和后进先出(LIFO)等多种操作。Deque 接口有多个实现类,其中最常用的是 ArrayDequeLinkedList。以下是 Deque 的核心代码讲解,涵盖了初始化、添加元素、删除元素等关键操作。

首先,让我们看一下 Deque 的基本属性和构造方法,以及 ArrayDeque 作为一个实现的示例:

// Deque 接口的定义
public interface Deque<E> extends Queue<E> {
    // 添加元素到队列的头部
    void addFirst(E e);

    // 添加元素到队列的尾部
    void addLast(E e);

    // 移除并返回队列头部的元素
    E removeFirst();

    // 移除并返回队列尾部的元素
    E removeLast();

    // ...
}

// ArrayDeque 类的定义
public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, java.io.Serializable {
    // 存储元素的数组
    transient Object[] elements;

    // 队列的头部索引
    transient int head;

    // 队列的尾部索引
    transient int tail;

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

    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }

    // ...
}

在上述代码中,Deque 接口定义了双端队列的基本操作,包括在头部和尾部添加元素、在头部和尾部删除元素等。ArrayDequeDeque 接口的一个实现,使用一个数组 elements 来存储元素。headtail 分别表示队列的头部和尾部索引。

接下来,让我们看一下 Deque 的添加元素操作,包括 addFirstaddLastoffer 方法:

// 添加元素到队列的头部
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

// 添加元素到队列的尾部
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

// 在队列的尾部添加元素,返回是否成功
public boolean offer(E e) {
    return addLast(e);
}

addFirst 方法用于添加元素到队列的头部,addLast 方法用于添加元素到队列的尾部,而 offer 方法则是在尾部添加元素并返回是否成功。在添加元素时,如果队列已满,就会触发扩容操作。

接下来,让我们看一下 Deque 的删除元素操作,包括 removeFirstremoveLastpoll 方法:

// 移除并返回队列头部的元素
public E removeFirst() {
    E x = pollFirst();
    if (x == null)
        throw new NoSuchElementException();
    return x;
}

// 移除并返回队列尾部的元素
public E removeLast() {
    E x = pollLast();
    if (x == null)
        throw new NoSuchElementException();
    return x;
}

// 移除并返回队列头部的元素,返回 null 如果队列为空
public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // 如果头部元素为空,返回 null
    if (result == null)
        return null;
    // 释放头部元素
    elements[h] = null;
    head = (h + 1) & (elements.length - 1);
    return result;
}

// ...

removeFirst 方法用于移除并返回队列头部的元素,removeLast 方法用于移除并返回队列尾部的元素,而 pollFirst 方法则是移除并返回队列头部的元素,如果队列为空则返回 null

这些是 Deque 的核心操作,它们确保了双端队列的高效添加和删除能力。不同的 Deque 实现类可能有不同的性能特性和用途,开发人员可以根据实际需求选择合适的实现类。

  1. CopyOnWriteArrayList
    • CopyOnWriteArrayList 是 Java 中的线程安全的列表实现,它在写操作时进行复制,以保证线程安全。这意味着写操作会创建一个新的底层数组,因此写操作的效率较低。
    • 适用于读多写少的场景,例如读取频繁但很少修改的数据。

CopyOnWriteArrayList 是 一个线程安全的动态数组实现,它通过在修改操作时创建一个副本(copy)来保证线程安全,因此适用于多线程环境下的读多写少的场景。以下是 CopyOnWriteArrayList 的核心代码讲解,涵盖了初始化、添加元素、删除元素等关键操作。

首先,让我们看一下 CopyOnWriteArrayList 的基本属性和构造方法:

// CopyOnWriteArrayList 类的定义
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 存储元素的数组
    private transient volatile Object[] array;

    // 构造方法
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    public CopyOnWriteArrayList(Collection<? extends E> c) {
        setArray(c.toArray());
    }

    // ...
}

在上述代码中,CopyOnWriteArrayList 使用一个 Object 类型的数组 array 来存储元素。在默认构造方法中,array 初始化为空数组。在带有 Collection 参数的构造方法中,array 初始化为 Collection 中的元素数组。

接下来,让我们看一下 CopyOnWriteArrayList 的添加元素操作,包括 addaddAllset 方法:

// 向列表尾部添加元素
public boolean add(E e) {
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    }
}

// 向列表尾部添加元素集合
public boolean addAll(Collection<? extends E> c) {
    if (c.isEmpty())
        return false;

    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + c.size());
        int added = 0;
        for (E e : c) {
            newElements[len + added] = e;
            added++;
        }
        setArray(newElements);
        return added > 0;
    }
}

// 将指定位置的元素设置为新元素
public E set(int index, E element) {
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        if (index < 0 || index >= len)
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len);
        Object oldValue = elements[index];
        if (oldValue != element) {
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        }
        return oldValue;
    }
}

上述代码中,add 方法用于向列表尾部添加元素,addAll 方法用于向列表尾部添加元素集合,而 set 方法则是将指定位置的元素设置为新元素。这些添加和修改操作都在同步块内完成,确保了线程安全。

接下来,让我们看一下 CopyOnWriteArrayList 的删除元素操作,包括 removeremoveAll 方法:

// 删除指定元素
public boolean remove(Object o) {
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        int index = indexOf(o, elements, 0, len);
        if (index < 0)
            return false;
        int numMoved = len - index - 1;
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index, numMoved);
            setArray(newElements);
        }
        return true;
    }
}

// 删除列表中包含在指定集合中的元素
public boolean removeAll(Collection<?> c) {
    if (c.isEmpty())
        return false;

    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        int newlen = 0;
        Object[] tmp = new Object[len];
        for (int i = 0; i < len; i++) {
            if (!c.contains(elements[i]))
                tmp[newlen++] = elements[i];
        }
        if (newlen == len)
            return false;
        setArray(Arrays.copyOf(tmp, newlen));
        return true;
    }
}

remove 方法用于删除指定元素,removeAll 方法用于删除列表中包含在指定集合中的元素。删除操作同样在同步块内完成。

这些是 CopyOnWriteArrayList 的核心操作,它们确保了在多线程环境下的线程安全性。需要注意的是,CopyOnWriteArrayList 的读操作不需要加锁,因此适用于读多写少的场景,但在写入操作较频繁的情况下,性能可能会受到影响。

注意事项

以下是针对不同的列表实现(ArrayListLinkedListVectorDequeCopyOnWriteArrayList)的使用注意事项:

ArrayList:
  1. 动态数组ArrayList 是基于动态数组实现的,因此在添加元素时要注意动态扩容带来的性能开销。如果知道列表的大致大小,可以在初始化时指定初始容量,减少扩容次数。

  2. 随机访问快ArrayList 支持常数时间内的随机访问,但在中间插入或删除元素时会导致后续元素的位移,性能较低。适用于频繁读取但较少修改的场景。

  3. 线程安全ArrayList 不是线程安全的,如果在多线程环境中使用,需要采取额外的同步措施,或者考虑使用线程安全的替代实现。

LinkedList:
  1. 双向链表LinkedList 是基于双向链表实现的,支持高效的插入和删除操作,但随机访问性能较差。适用于频繁插入和删除元素的场景。

  2. 迭代器:在遍历 LinkedList 时,建议使用迭代器而不是索引。索引访问可能导致遍历性能变得很差。

  3. 内存占用LinkedList 每个节点都需要额外的内存空间存储前后节点的引用,因此在大量数据情况下可能占用较多内存。

Vector:
  1. 线程安全Vector 是线程安全的,但在多线程环境中,如果需要高性能,可能需要考虑更轻量级的同步机制。

  2. 扩容开销VectorArrayList 类似,需要进行动态扩容,因此在大量元素插入时,要注意频繁的扩容开销。

  3. 性能:由于同步机制,Vector 的性能可能低于 ArrayList,除非需要线程安全性,否则不建议使用。

Deque:
  1. 双端队列Deque 支持在队列头部和尾部进行高效的添加和删除操作,是一种灵活的数据结构。

  2. 多种实现:Java 提供了多种 Deque 的实现,如 ArrayDequeLinkedList,可以根据具体需求选择合适的实现。

  3. 线程安全Deque 不是线程安全的,如果需要在多线程环境中使用,需要采取适当的同步措施。

CopyOnWriteArrayList:
  1. 线程安全CopyOnWriteArrayList 是线程安全的,适用于读多写少的场景。在写入操作较频繁的情况下,性能可能不如其他非线程安全的列表。

  2. 内存占用:由于写操作会复制整个数组,因此可能会消耗较多的内存。不适合存储大量数据。

  3. 不支持修改操作:在 CopyOnWriteArrayList 上执行修改操作(如 addremove)会抛出 UnsupportedOperationException 异常。只能通过复制一个新的列表来进行修改。

根据实际需求和性能要求,选择合适的列表实现非常重要。不同的列表实现有不同的适用场景和性能特点,需要根据具体情况进行选择。

应用场景

例如电商场景下,可以根据不同的业务需求选择合适的数据结构,以下是一些示例:

  1. ArrayList

    • 应用场景:商品列表展示。
    • 示例:在电商网站的商品列表页面中,通常会使用 ArrayList 存储商品信息,以支持快速的随机访问和排序操作。
  2. LinkedList

    • 应用场景:购物车管理。
    • 示例:购物车通常需要频繁地添加、删除商品,因此使用 LinkedList 可以更高效地执行这些操作,同时支持迭代浏览购物车中的商品。
  3. Vector

    • 应用场景:遗留系统的订单管理。
    • 示例:在维护遗留的订单管理系统时,可能需要继续使用 Vector,以保持与原有代码的兼容性。
  4. Deque

    • 应用场景:订单处理系统。
    • 示例:订单处理系统可能需要实现一个订单队列,支持高效的订单添加和处理。使用 Deque 可以在队列的头部和尾部进行快速插入和删除。
  5. CopyOnWriteArrayList

    • 应用场景:商品库存管理。
    • 示例:在管理商品库存时,可能需要使用 CopyOnWriteArrayList 来保证多线程环境下的读取一致性,以防止商品库存数据不一致的问题。

需要注意的是,以上示例仅为参考,实际的业务场景可能更加复杂,需要根据具体情况权衡性能、并发性和代码复杂性,选择合适的数据结构。不同的业务模块可能会使用不同的数据结构来满足其需求。

这些数据类型和类提供了不同的性能和线程安全特性,我们可以根据具体的需求选择合适的数据结构。链表适用于需要高效插入和删除操作的情况,而动态数组(如 ArrayList)适用于需要快速随机访问的情况。同时,双端队列和线程安全的实现可以满足特定的多线程需求。

结语

好的,本期我们的链表数据类型的讲解就到此结束。链表是一种非常重要的数据结构,它为我们提供了灵活性和效率,特别是在插入和删除元素时。希望您能够充分理解链表的概念和操作,以便在编程中更好地应用它们。

在下期,我们将继续深入探讨栈(Stack)数据结构。栈是一种基本的数据结构,它遵循后进先出(Last-In-First-Out,LIFO)的原则,常用于解决许多计算机科学和编程问题。我们将详细介绍栈的工作原理、常见操作以及如何在实际编程中使用它。

谢谢您的关注,下期敬请期待!如果在本期的内容中发现了错误或异常,或者您有任何疑问或建议,请随时留言或私信与我联系。您的反馈对于改进和完善内容非常重要,我将非常感激您的指正。期待与您继续分享有关编程和计算机科学的知识,祝您一切顺利!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值