1. 概述

数据结构是任何编程语言的核心组成部分。Java 在 Collection<T> 接口下提供了大部分常用数据结构。虽然 Map 也被视为 Java 集合的一部分,但它并未实现该接口。

本教程将聚焦链表数据结构,重点讨论单链表中删除最后一个元素的操作。

2. 单链表 vs 双链表

先明确两种链表的核心差异。它们的名称已经揭示了关键区别:

双链表:每个节点同时保存前驱节点后继节点的引用(头尾节点除外)
双链表结构

单链表:结构更简单,每个节点仅包含后继节点的引用
单链表结构

这种设计带来了典型的权衡:

  • 内存占用:单链表每个节点只需维护一个引用,更省内存
  • 操作灵活性:双链表支持反向遍历,在搜索、插入、删除等操作上更具优势
    ⚠️ 单链表在反向操作时会遇到明显瓶颈

3. 双链表删除最后一个元素

由于双链表节点包含前驱引用,删除操作非常简单。以 Java 标准库的 LinkedList<T> 为例,其节点结构如下:

class Node<E> {
    E item;
    LinkedList.Node<E> next;
    LinkedList.Node<E> prev;

    Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

通过 prev 引用,删除操作只需几行代码即可完成(时间复杂度 O(1)):
双链表删除末节点

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    E element = l.item;
    Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // 帮助GC回收
    last = prev;
    if (prev == null) {
        first = null;
    } else {
        prev.next = null;
    }
    size--;
    modCount++;
    return element;
}

4. 单链表删除最后一个元素

单链表的主要难点在于:无法直接获取倒数第二个节点。其节点结构仅包含后继引用:

public static class Node<T>  {
    private T element;
    private Node<T> next;

    public Node(T element) {
        this.element = element;
    }
}

这导致我们必须从头遍历整个链表来定位倒数第二个节点:

public void removeLast() {
    if (isEmpty()) {
        return;
    } else if (size() == 1) {
        tail = null;
        head = null;
    } else {
        Node<S> secondToLast = null;
        Node<S> last = head;
        while (last.next != null) {
            secondToLast = last;
            last = last.next;
        }
        secondToLast.next = null;
    }
    --size;
}

❌ 这种实现的时间复杂度为 O(n),在队列等高频操作场景下性能堪忧。
优化方案:在链表类中额外维护 secondToLast 引用:

public class SinglyLinkedList<S> {
    private int size;
    private Node<S> head = null;
    private Node<S> tail = null;
    private Node<S> secondToLast = null; // 新增引用
    
    // 其他方法...
}

虽然无法解决遍历问题,但至少能让 removeLast() 操作达到 O(1) 时间复杂度。

5. 结论

数据结构没有绝对的好坏之分,只有适用场景的差异。关键点总结:

特性 单链表 双链表
内存占用 ✅ 更低(每个节点1个引用) ❌ 更高(每个节点2个引用)
删除末节点 ❌ O(n) 需遍历 ✅ O(1) 直接操作
反向操作 ❌ 不支持 ✅ 天然支持

💡 核心建议:深入理解数据结构的底层实现,才能根据实际需求选择最合适的工具。单链表适合内存敏感场景,双链表则在需要频繁双向操作时更具优势。


原始标题:Removing the Last Node in a Linked List | Baeldung