每日算法 — 使用java实现链表:双指针环检测与反转操作

链表是计算机科学中最基础也最重要的线性数据结构之一。与数组不同,链表通过指针将分散在内存中的节点串联起来,支持O(1)时间复杂度的插入与删除操作。本文用Java完整实现单链表,并深入讲解三大核心算法:链表反转(迭代与递归两种范式)、Floyd判圈算法(快慢指针检测环路与定位环入口),以及快慢指针找中点合并有序链表的组合技巧。每个算法均附带可直接运行的完整代码与复杂度分析。

链表节点定义与基础操作

首先定义链表节点ListNode,包含整型数据域val和指向下一个节点的指针next

/**
 * 单链表节点定义
 * val 存储数据,next 指向下一个节点(尾节点为null)
 */
public class ListNode {
    int val;
    ListNode next;

    ListNode(int val) {
        this.val = val;
    }

    ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

为了方便测试,再提供一个工具类用于快速构建链表和打印链表内容。

/**
 * 链表工具类:构建链表与格式化输出
 */
public class LinkedListUtil {

    /**
     * 根据整数数组构建单链表,返回头节点
     */
    public static ListNode buildList(int[] arr) {
        if (arr == null || arr.length == 0) return null;
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        for (int v : arr) {
            cur.next = new ListNode(v);
            cur = cur.next;
        }
        return dummy.next;
    }

    /**
     * 将链表转换为字符串表示,如 "1 -> 2 -> 3"
     */
    public static String toString(ListNode head) {
        StringBuilder sb = new StringBuilder();
        while (head != null) {
            sb.append(head.val);
            if (head.next != null) sb.append(" -> ");
            head = head.next;
        }
        return sb.toString();
    }

    /**
     * 手动构造一个带环的链表,用于测试环检测算法
     * @param arr 链表节点值数组
     * @param pos 环入口在数组中的索引,-1表示无环
     */
    public static ListNode buildCyclicList(int[] arr, int pos) {
        if (arr == null || arr.length == 0) return null;
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        ListNode cycleEntry = null;
        for (int i = 0; i < arr.length; i++) {
            cur.next = new ListNode(arr[i]);
            cur = cur.next;
            if (i == pos) cycleEntry = cur;
        }
        if (pos >= 0) cur.next = cycleEntry;
        return dummy.next;
    }
}

核心算法一:链表反转

链表反转是面试中出现频率最高的链表题目。要求将链表中所有节点的next指针方向翻转,使原尾节点成为新头节点。

迭代法反转

迭代法使用三个指针协同工作:prev记录已反转部分的头节点,cur为当前待反转节点,nextTemp暂存cur的下一个节点以防断链。

/**
 * 迭代法反转单链表
 * 时间复杂度 O(n),空间复杂度 O(1)
 */
public class ListReversal {

    public static ListNode reverseIterative(ListNode head) {
        ListNode prev = null;   // 已反转部分的头节点(初始为空)
        ListNode cur = head;    // 当前正在处理的节点

        while (cur != null) {
            ListNode nextTemp = cur.next; // 暂存后继,防止断链后丢失
            cur.next = prev;              // 将当前节点指向已反转部分
            prev = cur;                   // prev 前进一位
            cur = nextTemp;               // cur 前进一位
        }

        return prev; // prev 最终指向原尾节点,即新头节点
    }
}

执行流程图解(以 1 -> 2 -> 3 为例):
1. 初始:prev=null, cur=1;暂存next=21.next=nullprev=1, cur=2
2. 第二步:prev=1, cur=2;暂存next=32.next=1prev=2, cur=3
3. 第三步:prev=2, cur=3;暂存next=null3.next=2prev=3, cur=null
4. 结束,返回3,链表变为 3 -> 2 -> 1

递归法反转

递归法从链表尾部开始反转,利用函数调用栈保存节点信息,回溯时调整指针方向。

    /**
     * 递归法反转单链表
     * 时间复杂度 O(n),空间复杂度 O(n)(递归栈深度)
     */
    public static ListNode reverseRecursive(ListNode head) {
        // 基准情况:空链表或只有一个节点,无需反转
        if (head == null || head.next == null) {
            return head;
        }

        // 递归反转后继链表,newHead 是反转后的新头节点(原尾节点)
        ListNode newHead = reverseRecursive(head.next);

        // 回溯阶段:让后继节点指向当前节点,形成反向连接
        head.next.next = head;

        // 断开当前节点原来的正向连接,防止成环
        head.next = null;

        return newHead;
    }

反转前N个节点

扩展场景:只反转链表的前N个节点,后续节点保持原序。

    /**
     * 反转链表的前 n 个节点,返回新的头节点
     * successor 记录第 n+1 个节点,用于拼接
     */
    private ListNode successor = null;

    public ListNode reverseFirstN(ListNode head, int n) {
        if (n == 1) {
            successor = head.next; // 记录第 n+1 个节点
            return head;
        }

        ListNode newHead = reverseFirstN(head.next, n - 1);
        head.next.next = head;
        head.next = successor; // 将反转后的尾节点指向后继
        return newHead;
    }

核心算法二:Floyd判圈算法

链表中的环是指某个节点的next指针指向了链表中前面的某个节点,形成闭环。检测环并定位环入口是Floyd判圈算法的经典应用。

算法原理

Floyd判圈算法使用快慢双指针
slow指针每次走1步,fast指针每次走2步
– 若链表无环,fast会先到达尾部(null
– 若链表有环,fast终将追上slow(两者在环内某点相遇)

为什么一定能相遇? 设环外长度为a,环长度为bfast相对slow的速度为1步/单位时间,若两者都在环内,fast最多追b-1步就能与slow相遇。

环入口定位

相遇后,将其中一个指针拨回头节点,两指针改为同速(均每次1步)前进,再次相遇点即为环入口。

数学证明:设头节点到环入口距离为a,环入口到相遇点距离为b,相遇点回到环入口距离为cb + c = 环长)。slow走了a + bfast走了a + b + n(b + c)nfast在环内绕的圈数)。因为fast速度是slow的2倍:

2(a + b) = a + b + n(b + c)
=> a = (n - 1)(b + c) + c

这意味着:从头节点走a步,与从相遇点走c步再加整数圈,会到达同一位置——即环入口。

/**
 * Floyd判圈算法:检测链表是否有环,并定位环入口
 */
public class CycleDetection {

    /**
     * 检测链表是否有环
     * @return 若存在环返回相遇节点,否则返回 null
     */
    public static ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) return null;

        ListNode slow = head;
        ListNode fast = head;

        // 第一阶段:快慢指针寻找相遇点
        while (fast != null && fast.next != null) {
            slow = slow.next;       // 慢指针走1步
            fast = fast.next.next;  // 快指针走2步
            if (slow == fast) {     // 相遇,存在环
                return slow;
            }
        }

        return null; // fast 到达链表尾部,无环
    }

    /**
     * 定位环的入口节点
     * @return 环入口节点,无环返回 null
     */
    public static ListNode findCycleEntry(ListNode head) {
        ListNode meetingPoint = detectCycle(head);
        if (meetingPoint == null) return null; // 无环

        // 第二阶段:一个指针从头出发,一个从相遇点出发,同速前进
        ListNode ptr1 = head;
        ListNode ptr2 = meetingPoint;

        while (ptr1 != ptr2) {
            ptr1 = ptr1.next;
            ptr2 = ptr2.next;
        }

        return ptr1; // 再次相遇点即为环入口
    }

    /**
     * 计算环的长度
     */
    public static int cycleLength(ListNode head) {
        ListNode meetingPoint = detectCycle(head);
        if (meetingPoint == null) return 0;

        int length = 1;
        ListNode cur = meetingPoint.next;
        while (cur != meetingPoint) {
            cur = cur.next;
            length++;
        }
        return length;
    }
}

扩展:判断两个链表是否相交

若两个链表相交,则它们的尾节点必然相同。利用环检测思想,可将问题转化为判断一个链表是否有环:将链表A的尾节点指向链表B的头节点,若形成环则相交。

    /**
     * 判断两个无环链表是否相交,返回相交起始节点
     */
    public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;

        ListNode pA = headA, pB = headB;

        // 双指针遍历,走完A走B,走完B走A
        // 若有交点,两指针会在交点相遇;若无交点,最终同时为null
        while (pA != pB) {
            pA = (pA == null) ? headB : pA.next;
            pB = (pB == null) ? headA : pB.next;
        }

        return pA;
    }

核心算法三:快慢指针找中点与合并有序链表

快慢指针找中点

快慢指针不仅能判圈,还能高效定位链表的中点:slow每次1步,fast每次2步,当fast到达尾部时,slow恰好位于中点。

/**
 * 快慢指针系列算法
 */
public class FastSlowPointer {

    /**
     * 查找链表的中点节点
     * 若链表长度为偶数,返回中间偏左的节点
     * 时间复杂度 O(n),空间复杂度 O(1)
     */
    public static ListNode findMiddle(ListNode head) {
        if (head == null || head.next == null) return head;

        ListNode slow = head;
        ListNode fast = head;

        // fast 每次走2步,slow 每次走1步
        // 当 fast 到达尾部时,slow 位于中点
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        return slow;
    }

    /**
     * 查找链表的倒数第 k 个节点
     */
    public static ListNode findKthFromEnd(ListNode head, int k) {
        ListNode fast = head, slow = head;

        // fast 先走 k 步
        for (int i = 0; i < k; i++) {
            if (fast == null) return null; // k 超出链表长度
            fast = fast.next;
        }

        // fast 和 slow 同步前进,fast 到尾时 slow 即为倒数第 k 个
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }

        return slow;
    }
}

合并两个有序链表

合并两个升序链表是归并排序的底层操作,使用双指针逐一比较节点值。

/**
 * 合并两个升序链表,返回合并后的头节点
 */
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0); // 哑节点简化边界处理
    ListNode cur = dummy;

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

    // 拼接剩余部分
    cur.next = (l1 != null) ? l1 : l2;

    return dummy.next;
}

综合应用:判断回文链表

回文链表要求正读反读相同。最优解法结合快慢指针找中点链表反转双指针比较,时间复杂度O(n),空间复杂度O(1)。

/**
 * 判断单链表是否为回文结构
 * 策略:找中点 -> 反转后半部分 -> 双指针比较 -> 恢复链表(可选)
 */
public class PalindromeLinkedList {

    public static boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) return true;

        // 步骤1:快慢指针找中点
        ListNode slow = head, fast = head;
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }

        // 步骤2:反转后半部分链表
        ListNode secondHalfStart = reverseList(slow.next);

        // 步骤3:双指针比较前半部分和反转后的后半部分
        ListNode p1 = head;
        ListNode p2 = secondHalfStart;
        boolean result = true;
        while (p2 != null) {
            if (p1.val != p2.val) {
                result = false;
                break;
            }
            p1 = p1.next;
            p2 = p2.next;
        }

        // 步骤4:恢复链表(将后半部分再次反转回来)
        slow.next = reverseList(secondHalfStart);

        return result;
    }

    private static ListNode reverseList(ListNode head) {
        ListNode prev = null, cur = head;
        while (cur != null) {
            ListNode nextTemp = cur.next;
            cur.next = prev;
            prev = cur;
            cur = nextTemp;
        }
        return prev;
    }
}

完整测试与演示

/**
 * 链表算法综合测试主程序
 */
public class LinkedListDemo {

    public static void main(String[] args) {
        System.out.println("=== 链表反转测试 ===");
        ListNode list1 = LinkedListUtil.buildList(new int[]{1, 2, 3, 4, 5});
        System.out.println("原链表: " + LinkedListUtil.toString(list1));
        ListNode reversed = ListReversal.reverseIterative(list1);
        System.out.println("反转后: " + LinkedListUtil.toString(reversed));

        System.out.println("\n=== Floyd判圈算法测试 ===");
        ListNode cyclicList = LinkedListUtil.buildCyclicList(new int[]{1, 2, 3, 4, 5}, 2);
        ListNode cycleEntry = CycleDetection.findCycleEntry(cyclicList);
        if (cycleEntry != null) {
            System.out.println("环入口节点值: " + cycleEntry.val);
            System.out.println("环长度: " + CycleDetection.cycleLength(cyclicList));
        } else {
            System.out.println("无环");
        }

        ListNode noCycle = LinkedListUtil.buildList(new int[]{1, 2, 3});
        System.out.println("无环链表检测结果: " + (CycleDetection.detectCycle(noCycle) == null ? "无环" : "有环"));

        System.out.println("\n=== 快慢指针找中点测试 ===");
        ListNode list2 = LinkedListUtil.buildList(new int[]{1, 2, 3, 4, 5});
        ListNode mid = FastSlowPointer.findMiddle(list2);
        System.out.println("链表 1->2->3->4->5 的中点: " + mid.val);

        ListNode list3 = LinkedListUtil.buildList(new int[]{1, 2, 3, 4, 5, 6});
        ListNode mid2 = FastSlowPointer.findMiddle(list3);
        System.out.println("链表 1->2->3->4->5->6 的中点(偏左): " + mid2.val);

        System.out.println("\n=== 倒数第 k 个节点测试 ===");
        ListNode list4 = LinkedListUtil.buildList(new int[]{10, 20, 30, 40, 50});
        ListNode kth = FastSlowPointer.findKthFromEnd(list4, 2);
        System.out.println("倒数第2个节点: " + (kth != null ? kth.val : "null"));

        System.out.println("\n=== 合并有序链表测试 ===");
        ListNode l1 = LinkedListUtil.buildList(new int[]{1, 3, 5});
        ListNode l2 = LinkedListUtil.buildList(new int[]{2, 4, 6});
        ListNode merged = FastSlowPointer.mergeTwoLists(l1, l2);
        System.out.println("合并 1->3->5 和 2->4->6: " + LinkedListUtil.toString(merged));

        System.out.println("\n=== 回文链表测试 ===");
        ListNode palindrome = LinkedListUtil.buildList(new int[]{1, 2, 3, 2, 1});
        System.out.println("1->2->3->2->1 是回文: " + PalindromeLinkedList.isPalindrome(palindrome));

        ListNode notPalindrome = LinkedListUtil.buildList(new int[]{1, 2, 3, 4, 5});
        System.out.println("1->2->3->4->5 是回文: " + PalindromeLinkedList.isPalindrome(notPalindrome));
    }
}

预期输出:

=== 链表反转测试 ===
原链表: 1 -> 2 -> 3 -> 4 -> 5
反转后: 5 -> 4 -> 3 -> 2 -> 1

=== Floyd判圈算法测试 ===
环入口节点值: 3
环长度: 3
无环链表检测结果: 无环

=== 快慢指针找中点测试 ===
链表 1->2->3->4->5 的中点: 3
链表 1->2->3->4->5->6 的中点(偏左): 3

=== 倒数第 k 个节点测试 ===
倒数第2个节点: 40

=== 合并有序链表测试 ===
合并 1->3->5 和 2->4->6: 1 -> 2 -> 3 -> 4 -> 5 -> 6

=== 回文链表测试 ===
1->2->3->2->1 是回文: true
1->2->3->4->5 是回文: false

复杂度总结

算法 时间复杂度 空间复杂度 核心思想
链表反转(迭代) O(n) O(1) 三指针顺序翻转指针方向
链表反转(递归) O(n) O(n) 递归到尾部后回溯调整指针
Floyd判圈 O(n) O(1) 快慢指针,速度差为1必然相遇
环入口定位 O(n) O(1) 相遇后同速从头与从相遇点出发再相遇
找中点 O(n) O(1) 快指针速度是慢指针2倍
倒数第k个 O(n) O(1) 先让快指针领先k步
合并有序链表 O(n+m) O(1) 双指针逐一比较拼接
回文判断 O(n) O(1) 找中点+反转+比较+恢复

总结

本文围绕单链表展开,系统实现了三大核心算法族:

  • 链表反转:迭代法空间最优,递归法代码最简洁。前N个节点反转是更通用的扩展模式。
  • Floyd判圈算法:快慢指针的经典应用,不仅能检测环,还能通过数学推导精确定位环入口并计算环长。其思想可延伸至判断两链表相交等问题。
  • 快慢指针与合并:找中点、找倒数第k个、合并有序链表均是双指针技巧的变体。将这些子算法组合,即可在O(n)时间、O(1)空间内解决回文链表等复杂问题。

链表的精髓在于指针的重新布线。掌握上述算法后,读者可继续挑战更复杂的问题:重排链表(L0→Ln→L1→Ln-1…)、复制带随机指针的链表、K个一组反转链表等。