Interview

链表

链表

介绍

链表,一个节点指向下一个节点。遍历靠 node = node.next 一步步走。在算法题中,链表也是树形结构的基础。

一句话直觉:链表题 = 玩指针。核心就三个工具:dummy node(统一边界)、快慢指针(找位置)、画图追踪(别在脑子里模拟)。

ArrayList vs LinkedList — 必考题

这可能是 Java 面试中最常问的数据结构对比:

ArrayListLinkedList
底层动态数组双向链表
随机访问 get(i)O(1)O(n)
头/尾插入删除O(n) 头 / O(1) 尾O(1)
内存连续内存,cache 友好每个节点有 prev/next 指针开销
适用随机访问多,尾部追加频繁头/中间插入删除

判断原则:绝大多数场景用 ArrayList(cache 友好 + O(1) 随机访问)。LinkedList 只在频繁在头部插入删除时才考虑。

// Definition for singly-linked list
public class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

模式识别

当你看到这些信号:

  • “原地修改”、“O(1) 空间”、“不能新建链表” → 操作指针,别复制值
  • “找中点”、“找倒数第 n 个”、“检测环” → 快慢指针
  • “删除/插入头节点可能不同” → 哑节点 (Dummy Node)
  • “合并”、“反转” → 画图追踪指针,别在脑子里模拟

核心模式

遍历条件

遍历链表时有三种常见循环条件,选错会导致 NPE 或漏掉最后一个节点:

条件停止位置使用场景
while (node != null)停在最后一个节点 +1 (null)只用当前节点的值
while (node.next != null)停在最后一个节点需要访问下一个节点的值
node = node.next—前进一步

哑节点 (Dummy Node)

在链表头前加一个虚拟节点,统一处理边界(头节点被删除、空链表等场景)。几乎所有链表题都能用 dummy node 简化代码。

快慢指针 (Fast & Slow Pointers)

  • 找中点:快指针每次走两步,慢指针走一步。快指针到末尾时,慢指针恰好在中间
  • 找倒数第 n 个:快指针先走 n 步,然后两个指针同步走;快指针到底时,慢指针指向倒数第 n+1 个

代码挑战

1. Remove Nth Node From End

问题:删除链表倒数第 n 个节点,返回头节点。

思路:快慢指针。快指针先走 n 步,然后同步走到底。慢指针停在倒数第 n+1 位,直接删除下一个节点。

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode fast = dummy;
    ListNode slow = dummy;

    // move fast n steps ahead
    for (int i = 0; i < n; i++) {
        fast = fast.next;
    }

    // move both until fast reaches the last node
    while (fast.next != null) {
        fast = fast.next;
        slow = slow.next;
    }

    // delete the nth node from end
    slow.next = slow.next.next;

    return dummy.next;
}

2. Merge Two Sorted Lists

问题:合并两个有序链表,返回新的有序链表。

迭代解法:用 dummy node 统一边界,逐一比较两个链表的当前节点,接到结果链表上。

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;

    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            curr.next = l1;
            l1 = l1.next;
        } else {
            curr.next = l2;
            l2 = l2.next;
        }
        curr = curr.next;
    }

    // attach remaining nodes
    curr.next = (l1 != null) ? l1 : l2;
    return dummy.next;
}

递归解法:利用链表本身的递归结构 — 合并两个链表 = 选较小的头 + 合并剩余部分。

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;

    if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
}

关键技巧

  • Dummy node 是链表题的万能钥匙 — 避免处理头节点被删的特殊情况
  • 比较两个链表时注意三种情况:null、l1 > l2、l1 < l2
  • 快慢指针两招:找中点(fast 两步 slow 一步),找倒数第 n 个(fast 先行 n 步)
  • 链表天然适合递归 — 每个节点本身就是一个”更小的链表”

参考资料