链表技巧总结:双指针的七大应用场景

raw/articles/2026/05/链表技巧总结

侧重:双指针技巧在单链表算法中的7大应用场景,每种技巧对应1-2道经典LeetCode题目

一句话总结

本文系统总结了单链表算法中双指针技巧的7种核心应用,覆盖合并、分解、查找、环检测、交点判断等常见场景,每种技巧都配有Java实现和LeetCode对应题目。

核心要点

1. 合并两个有序链表

对应题目:LeetCode 21 / 力扣 21

核心思路:类似"拉拉链",用两个指针分别遍历两个链表,每次取较小节点接入结果链表。

关键技巧:使用**虚拟头结点(dummy)**简化边界处理——当需要创造一条新链表时,虚拟头结点可以避免处理空指针的情况。

ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(-1), p = dummy;
    ListNode p1 = l1, p2 = l2;
    while (p1 != null && p2 != null) {
        if (p1.val > p2.val) {
            p.next = p2;
            p2 = p2.next;
        } else {
            p.next = p1;
            p1 = p1.next;
        }
        p = p.next;
    }
    if (p1 != null) p.next = p1;
    if (p2 != null) p.next = p2;
    return dummy.next;
}

2. 链表的分解

对应题目:LeetCode 86 / 力扣 86(分隔链表)

核心思路:将原链表分解为两个子链表(小于x / 大于等于x),最后拼接。类似合并有序链表的逆操作。

关键技巧

  • 使用两个虚拟头结点 dummy1dummy2 分别管理两个子链表
  • 必须断开原链表节点的next指针,否则结果链表可能包含环
ListNode partition(ListNode head, int x) {
    ListNode dummy1 = new ListNode(-1), dummy2 = new ListNode(-1);
    ListNode p1 = dummy1, p2 = dummy2;
    ListNode p = head;
    while (p != null) {
        if (p.val >= x) {
            p2.next = p;
            p2 = p2.next;
        } else {
            p1.next = p;
            p1 = p1.next;
        }
        ListNode temp = p.next;
        p.next = null;  // 断开原链接
        p = temp;
    }
    p1.next = dummy2.next;
    return dummy1.next;
}

3. 合并 k 个有序链表

对应题目:LeetCode 23 / 力扣 23

核心思路:使用**优先级队列(最小堆)**快速获取k个节点中的最小值,逐步构建结果链表。

时间复杂度:O(N log k),其中k是链表条数,N是所有链表的节点总数。

ListNode mergeKLists(ListNode[] lists) {
    if (lists.length == 0) return null;
    ListNode dummy = new ListNode(-1);
    ListNode p = dummy;
    PriorityQueue<ListNode> pq = new PriorityQueue<>(
        lists.length, (a, b) -> (a.val - b.val));
    for (ListNode head : lists) {
        if (head != null) pq.add(head);
    }
    while (!pq.isEmpty()) {
        ListNode node = pq.poll();
        p.next = node;
        if (node.next != null) pq.add(node.next);
        p = p.next;
    }
    return dummy.next;
}

4. 寻找单链表的倒数第 k 个节点

对应题目:LeetCode 19 / 力扣 19(删除链表的倒数第N个结点)

核心思路快慢指针——快指针先走k步,然后快慢指针同步前进,快指针到达末尾时,慢指针恰好指向倒数第k个节点。

关键技巧:删除倒数第n个节点时,需要先找到倒数第n+1个节点,同样使用虚拟头结点处理边界。

// 返回链表的倒数第 k 个节点
ListNode findFromEnd(ListNode head, int k) {
    ListNode p1 = head;
    for (int i = 0; i < k; i++) {
        p1 = p1.next;
    }
    ListNode p2 = head;
    while (p1 != null) {
        p2 = p2.next;
        p1 = p1.next;
    }
    return p2;
}

// 删除倒数第 n 个节点
ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode x = findFromEnd(dummy, n + 1);  // 找倒数第 n+1 个
    x.next = x.next.next;  // 删除
    return dummy.next;
}

5. 寻找单链表的中点

对应题目:LeetCode 876 / 力扣 876

核心思路快慢指针——慢指针每次走1步,快指针每次走2步,快指针到达末尾时,慢指针指向中点。

注意:当链表长度为偶数时,返回的是靠后的那个中点

ListNode middleNode(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}

6. 判断单链表是否包含环并找出环起点

对应题目:LeetCode 141(环形链表)、LeetCode 142(环形链表 II)

判断环:快慢指针,相遇则有环,走到空则无环。

找环起点(LeetCode 142):

  1. 快慢指针相遇后,将任一个指针重新指向head
  2. 两个指针以相同速度前进
  3. 再次相遇的节点就是环的起点

原理:设环起点到head距离为 k-m,相遇点到环起点距离为 m,快指针比慢指针多走 k 步(k 是环长度的整数倍)。从相遇点走 k-m 步或从头走 k-m 步都能到达环起点。

// 判断是否有环
boolean hasCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

// 找到环的起点
ListNode detectCycle(ListNode head) {
    ListNode fast = head, slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow) break;
    }
    if (fast == null || fast.next == null) return null;
    // 重新指向头结点,同步前进
    fast = head;
    while (slow != fast) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

7. 判断两个单链表是否相交并找出交点

对应题目:LeetCode 160 / 力扣 160

核心思路:让两个指针 p1p2 分别遍历A、B链表,走到末尾后转到另一条链表头部继续。这样两个指针走的路程相同(A+B),会在交点处相遇;如果不相交,则同时走到null。

另一种思路:计算两链表长度差,让长链表的指针先走长度差步,然后同步前进比较。

第三种思路(评论区):将一条链表首尾相连,转换为"寻找环起点"问题。

ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    ListNode p1 = headA, p2 = headB;
    while (p1 != p2) {
        if (p1 == null) p1 = headB;
        else p1 = p1.next;
        if (p2 == null) p2 = headA;
        else p2 = p2.next;
    }
    return p1;  // 相交返回交点,不相交返回null
}

时间复杂度总结

技巧 时间复杂度 空间复杂度 说明
合并两个有序链表 O(N+M) O(1) N、M为两链表长度
链表分解 O(N) O(1) 遍历一次
合并k个有序链表 O(N log k) O(k) 优先级队列开销
倒数第k个节点 O(N) O(1) 一次遍历(快慢指针)
寻找中点 O(N) O(1) 一次遍历(快慢指针)
判断环/找环起点 O(N) O(1) 快慢指针
判断交点 O(N) O(1) 双指针拼接逻辑

通用技巧总结

虚拟头结点(Dummy Head)使用时机

何时使用:当你需要创造一条新链表的时候(合并、分解等),使用虚拟头结点可以简化边界情况的处理。

双指针模式

模式 快指针速度 慢指针速度 应用场景
倒数第k个 k步领先 同步 从后向前定位
寻找中点 2步/次 1步/次 定位中间位置
环检测 2步/次 1步/次 判断是否有环

相关题目

LeetCode 力扣 难度 对应技巧
21. Merge Two Sorted Lists 21. 合并两个有序链表 简单 合并两个有序链表
86. Partition List 86. 分隔链表 中等 链表分解
23. Merge k Sorted Lists 23. 合并 K 个升序链表 困难 合并k个有序链表
19. Remove Nth Node From End of List 19. 删除链表的倒数第 N 个结点 中等 倒数第k个节点
876. Middle of the Linked List 876. 链表的中间结点 简单 寻找中点
142. Linked List Cycle II 142. 环形链表 II 中等 环检测与环起点
160. Intersection of Two Linked Lists 160. 相交链表 简单 判断交点
141. Linked List Cycle 141. 环形链表 简单 判断环
LCR 140. 训练计划 II LCR 140. 训练计划 II 简单 倒数第k个节点

相关概念

  • 链表:链式存储的线性数据结构
  • 单链表:每个节点只含后继指针的单向链表
  • 虚拟头尾节点:通过虚拟节点统一链表操作逻辑的技巧
  • 优先级队列:基于二叉堆实现,按优先级顺序取出元素
  • 二叉堆:完全二叉树,sink/swim操作,优先级队列的底层结构
  • 双指针:本文核心方法论
  • 快慢指针:双指针的特例,用于中点查找和环检测

相关实体

  • labuladong:算法学习平台 labuladong.online 作者
  • leetcode:全球知名的在线算法题库与编程面试准备平台

总结

双指针技巧是单链表算法的核心工具:快慢指针用于定位(中点、倒数第k个、环检测),双指针拼接用于解决交点问题,虚拟头结点则是创造新链表时的通用简化技巧。掌握这7种模式,可以覆盖大部分单链表算法题。


Interactive Graph