跳转至

2- 链表(Linked List)

链表是一种动态数据结构,提供了灵活的方式来存储和操作数据。与数组相比,链表具有动态分配内存的优势,可以在运行时动态地添加和删除元素,无需在内存中连续存储。这种特性使得链表在添加和删除元素时更为灵活,但在访问元素时可能较慢,因为需要从头开始遍历整个链表。

- Node类

在Java中实现链表前,首先需要定义一个节点类(Node class)。节点是链表的基本单位,每个节点至少包含两个成员:存储的数据和指向列表中下一个节点的引用。

public class Node<T> {
    T data;
    Node<T> next;
    Node<T> prev; // 双向链表使用

    public Node(T data) {
        this.data = data;
        this.next = null;
        this.prev = null; // 双向链表初始化前驱节点
    }
}

- 单向链表(Singly Linked List)

单向链表的每个节点只包含一个指向下一个节点的链接,这简化了结构,但在执行某些操作时可能需要更多的遍历。单向链表 是一种链表,其中每个节点只包含指向下一个节点的引用。它的结构简单,适合大多数基本操作。单向链表的基本字段如下:

public class SinglyLinkedList<T> {
    private Node<T> head;
    private Node<T> tail; // 如果有尾部指针

    public SinglyLinkedList() {
        this.head = null;
    }
    /* 方法 */
}

- 双向链表(Doubly Linked List)

双向链表的每个节点除了指向下一个节点的链接外,还包含一个指向前一个节点的链接。这增加了存储需求,但可以使得在链表中向前和向后遍历变得更加高效。双向链表与单向链表 的区别在于每个节点包含两个引用:一个指向下一个节点,另一个指向前一个节点。这种结构允许双向遍历,提高了灵活性。双向链表的基本字段如下:

public class DoublyLinkedList<T> {
    private Node<T> head;
    private Node<T> tail; // 如果有尾部指针

    public DoublyLinkedList() {
        this.head = null;
        this.tail = null;
    }
    /* 方法 */
}

链表操作的实现细节(非官方,根据自己随便的思路写的)

- 插入头部(Inserting at Head)

在单向链表或双向链表的头部插入一个新的节点是非常高效的操作,是O(1)的时间复杂度,因为直接访问了头部指针。

public void insertAtHead(T data) {
    Node<T> newNode = new Node<>(data);
    newNode.next = head;
    if (head != null) {
        head.prev = newNode; // 双向链表需要处理
    }
    head = newNode;
}

- 删除头部(Removing at Head)

删除链表头部的节点也是一个时间复杂度为O(1)的操作。

public void removeFromHead() {
    if (head != null) {
        head = head.next;
        if (head != null) {
            head.prev = null; // 双向链表需要处理
        }
    }
}

- 插入尾部(Inserting at Tail)

插入尾部需要遍历整个链表来找到最后一个节点,因此时间复杂度为O(n),除非持有尾部节点的引用。

插入尾部和删除尾部的操作可以更高效地实现,如果维护了尾部指针,则插入尾部的复杂度也可以是O(1)。

public void insertAtTail(T data) {
    Node<T> newNode = new Node<>(data);
    if (head == null) {
        head = newNode;
    } else {
        Node<T> last = head;
        while (last.next != null) {
            last = last.next;
        }
        last.next = newNode;
        newNode.prev = last; // 双向链表需要处理
    }
}

- 删除尾部(Removing at Tail)

在没有尾部指针的情况下,删除尾部节点效率低下(O(n)),因为需要找到倒数第二个节点。

public void removeFromTail() {
    if (head == null || head.next == null) {
        head = null;
    } else {
        Node<T> last = head;
        while (last.next.next != null) {
            last = last.next;
        }
        last.next = null; // 断开连接
        if (last.next != null) {
            last.next.prev = null; // 双向链表需要处理
        }
    }
}

在链表内部插入节点

在链表两个相邻节点之间插入节点,或在两个不相邻节点之间更换其中的 节点/子链表 的操作如下:

public void insertBetween(Node<T> prevNode, Node<T> nextNode, T data) {
   if (prevNode == null || nextNode == null) {
      /* 可将 prevNode.next 赋给 nextNode 或将 nextNode.prev 赋给 prevNode,并在此检查情况,更改 if 的条件,调用插入尾部或头部的方法 */
      throw new IllegalArgumentException("Previous and next nodes cannot be null");
    }

    Node<T> newNode = new Node<>(data);
    newNode.next = nextNode;
    newNode.prev = prevNode; // 双向链表需要处理

    prevNode.next = newNode;
    nextNode.prev = newNode; // 双向链表需要处理
}

注意事项

  • 如果 prevNode 在 nextNode 的后面,会有特殊的效果

  • 参数检查:确保prevNodenextNode不为空。

  • 正确链接:确保新节点的next指向nextNodeprev指向prevNode,同时更新prevNodenextnextNodeprev

通过这种方式,可以在链表的两个节点之间高效地插入新的节点。

在链表内部删除节点

public void removeBetween(Node<T> prevNode, Node<T> nextNode) {
    if (prevNode == null || nextNode == null || prevNode.next != nextNode) {
       /* 可在此检查删除头部或尾部的情况,更改 if 的条件,调用删除尾部或头部的方法 */
       throw new IllegalArgumentException("Invalid nodes provided");
    }

    Node<T> nodeToRemove = prevNode.next;

    prevNode.next = nextNode;
    nextNode.prev = prevNode; // 双向链表需要处理

    // 清理待删除节点的链接
    nodeToRemove.next = null;
    nodeToRemove.prev = null; // 双向链表需要处理
}

删除步骤

  • 如果 prevNode 在 nextNode 的后面,会有特殊的效果

  • 检查节点有效性:确保prevNodenextNode不是null,并且prevNode.next应该指向nextNode

  • 调整链接prevNode.next指向nextNodenextNode.prev指向prevNode

  • 清理删除节点:将nodeToRemovenextprev设为null,以帮助垃圾回收。