Skip to content

链表

链表是一种基础的数据结构,其特点是节点通过指针串联,不支持随机访问。面试中链表题目通常考察指针操作的细节,如反转、合并、环检测等。掌握虚拟头节点、快慢指针、多指针联动等技巧,能让你在链表题中游刃有余。


Q1: 反转链表 「🟢 初级」

题目描述:给你单链表的头节点 head,请你反转链表,并返回反转后的链表。

考察点:链表指针操作、递归与迭代、虚拟头节点。

解题思路

  • 方法1:迭代法,三指针 prev/curr/next 逐个反转(时间O(n),空间O(1))- 最优解
  • 方法2:递归法,先递归到尾部再逐层反转(时间O(n),空间O(n)栈空间)

代码实现

java
// 迭代法
public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

复杂度分析:时间复杂度 O(n),遍历一次链表;空间复杂度 O(1),仅使用常数额外指针。

变体扩展

  • 变体1:反转链表II(反转指定区间)
  • 变体2:K个一组翻转链表
  • 变体3:两两交换链表中的节点
  • 变体4:回文链表

Q2: 合并两个有序链表 「🟢 初级」

题目描述:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

考察点:链表的归并操作、虚拟头节点技巧、递归思路。

解题思路

  • 方法1:迭代法,使用虚拟头节点串联(时间O(n+m),空间O(1))- 最优解
  • 方法2:递归法(时间O(n+m),空间O(n+m)栈空间)

代码实现

java
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    ListNode dummy = new ListNode(-1);
    ListNode curr = dummy;
    while (list1 != null && list2 != null) {
        if (list1.val < list2.val) {
            curr.next = list1;
            list1 = list1.next;
        } else {
            curr.next = list2;
            list2 = list2.next;
        }
        curr = curr.next;
    }
    curr.next = (list1 != null) ? list1 : list2;
    return dummy.next;
}

复杂度分析:时间复杂度 O(n+m),最多遍历两个链表各一次;空间复杂度 O(1),仅使用虚拟头节点。

变体扩展

  • 变体1:合并k个升序链表
  • 变体2:合并两个有序数组
  • 变体3:排序链表(归并排序思路)

Q3: 环形链表 「🟢 初级」

题目描述:给你一个链表的头节点 head,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。

考察点:快慢指针(Floyd判圈算法)、哈希表。

解题思路

  • 方法1:哈希表,存储已访问节点(时间O(n),空间O(n))
  • 方法2:快慢指针,fast走两步,slow走一步,相遇则有环(时间O(n),空间O(1))- 最优解

代码实现

java
public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) return false;
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

复杂度分析:时间复杂度 O(n),有环时fast在环内追上slow最多需要n步;空间复杂度 O(1)。

变体扩展

  • 变体1:环形链表II(找到环的入口)
  • 变体2:快乐数
  • 变体3:寻找重复数
  • 变体4:相交链表

Q4: 环形链表II 「🟡 中级」

题目描述:给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,则返回 null。不允许修改给定的链表。

考察点:快慢指针数学推导、环的入口定位。

解题思路

  • 方法1:哈希表(时间O(n),空间O(n))
  • 方法2:快慢指针 + 数学推导,相遇后一个指针从头开始,两指针同速前进,再次相遇即为入口(时间O(n),空间O(1))- 最优解

代码实现

java
public ListNode detectCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            ListNode ptr = head;
            while (ptr != slow) {
                ptr = ptr.next;
                slow = slow.next;
            }
            return ptr;
        }
    }
    return null;
}

复杂度分析:时间复杂度 O(n),快慢指针相遇后再走到入口不超过n步;空间复杂度 O(1)。

变体扩展

  • 变体1:环形链表(判断是否有环)
  • 变体2:寻找重复数
  • 变体3:快乐数

Q5: 链表中倒数第k个节点 「🟢 初级」

题目描述:输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

考察点:快慢指针、一次遍历技巧。

解题思路

  • 方法1:两次遍历,先求长度再找第n-k+1个节点(时间O(n),空间O(1))
  • 方法2:快慢指针,fast先走k步,然后同速前进,fast到末尾时slow即为倒数第k个(时间O(n),空间O(1))- 最优解

代码实现

java
public ListNode getKthFromEnd(ListNode head, int k) {
    ListNode fast = head;
    ListNode slow = head;
    for (int i = 0; i < k; i++) {
        if (fast == null) return null;
        fast = fast.next;
    }
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

复杂度分析:时间复杂度 O(n),一次遍历;空间复杂度 O(1)。

变体扩展

  • 变体1:删除链表的倒数第N个节点
  • 变体2:旋转链表
  • 变体3:中间节点(快慢指针变体)

Q6: 两两交换链表中的节点 「🟡 中级」

题目描述:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)。

考察点:多指针操作、递归与迭代、虚拟头节点。

解题思路

  • 方法1:迭代法,使用虚拟头节点和多指针(时间O(n),空间O(1))- 最优解
  • 方法2:递归法(时间O(n),空间O(n)栈空间)

代码实现

java
public ListNode swapPairs(ListNode head) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode prev = dummy;
    while (prev.next != null && prev.next.next != null) {
        ListNode first = prev.next;
        ListNode second = prev.next.next;
        first.next = second.next;
        second.next = first;
        prev.next = second;
        prev = first;
    }
    return dummy.next;
}

复杂度分析:时间复杂度 O(n),每个节点被访问一次;空间复杂度 O(1)。

变体扩展

  • 变体1:K个一组翻转链表
  • 变体2:反转链表
  • 变体3:重排链表

Q7: 重排链表 「🟡 中级」

题目描述:给定一个单链表 L 的头节点 head,单链表 L 表示为:L0 → L1 → … → Ln-1 → Ln。请将其重新排列后变为:L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …。不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

考察点:链表综合题(找中点 + 反转 + 合并)、快慢指针。

解题思路

  • 方法1:数组存储 + 双指针重建(时间O(n),空间O(n))
  • 方法2:快慢指针找中点 → 反转后半段 → 交替合并(时间O(n),空间O(1))- 最优解

代码实现

java
public void reorderList(ListNode head) {
    if (head == null || head.next == null) return;
    // 1. 快慢指针找中点
    ListNode slow = head, fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    // 2. 反转后半段
    ListNode second = reverse(slow.next);
    slow.next = null;
    // 3. 交替合并
    ListNode first = head;
    while (second != null) {
        ListNode next1 = first.next;
        ListNode next2 = second.next;
        first.next = second;
        second.next = next1;
        first = next1;
        second = next2;
    }
}

private ListNode reverse(ListNode head) {
    ListNode prev = null;
    while (head != null) {
        ListNode next = head.next;
        head.next = prev;
        prev = head;
        head = next;
    }
    return prev;
}

复杂度分析:时间复杂度 O(n),三次线性遍历;空间复杂度 O(1),原地操作。

变体扩展

  • 变体1:回文链表
  • 变体2:K个一组翻转链表
  • 变体3:排序链表

Q8: LRU缓存机制 「🔴 高级」

题目描述:请你设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value;如果不存在,则向缓存中插入该组 key-value。如果插入操作导致关键字数量超过 capacity,则应该逐出最久未使用的关键字。 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

考察点:哈希表 + 双向链表、数据结构设计、最近最少使用策略。

解题思路

  • 方法:哈希表 + 双向链表。哈希表实现O(1)查找,双向链表维护使用顺序(时间O(1) get/put,空间O(capacity))- 最优解

代码实现

java
class LRUCache {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    
    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;
    
    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) return -1;
        moveToHead(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            DLinkedNode newNode = new DLinkedNode(key, value);
            cache.put(key, newNode);
            addToHead(newNode);
            size++;
            if (size > capacity) {
                DLinkedNode tail = removeTail();
                cache.remove(tail.key);
                size--;
            }
        } else {
            node.value = value;
            moveToHead(node);
        }
    }
    
    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    
    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }
    
    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

复杂度分析:get 和 put 操作时间复杂度均为 O(1);空间复杂度 O(capacity),哈希表和双向链表各存 capacity 个节点。

变体扩展

  • 变体1:LFU缓存
  • 变体2:设计循环双端队列
  • 变体3:全 O(1) 的数据结构