LeetCode Hot100----07-链表篇上,包含多种方法,详细思路与代码,让你一篇文章看懂所有!

07------------链表

1.链表的核心特性与操作详解

一、链表的基本定义与类型

链表是一种线性数据结构,由一系列节点组成,每个节点包含数据和指向下一节点的指针(引用)。根据指针类型,链表可分为:

  1. 单链表:每个节点仅含一个指向下一节点的指针。

  2. 双向链表:每个节点含前驱和后继两个指针。

  3. 循环链表:尾节点的指针指向头节点,形成环。

二、链表与数组的核心特性对比
特性链表数组
内存分配动态分配,节点地址不连续连续内存空间,固定大小
插入 / 删除O (1)(已知前驱时)O (n)(需移动后续元素)
随机访问O (n)(需从头遍历)O (1)(直接通过索引访问)
空间效率需额外存储指针,空间利用率低无额外开销,空间利用率高
三、链表的基本操作与实现

以下以单链表为例,展示核心操作的 Java 实现:

1. 节点定义
class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
2. 创建链表
// 创建单链表:1→2→3→null
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
3. 遍历链表
void traverse(ListNode head) {
    ListNode curr = head;
    while (curr != null) {
        System.out.print(curr.val + "→");
        curr = curr.next;
    }
    System.out.println("null");
}
4. 插入节点
  • 头部插入:O(1)

    ListNode insertAtHead(ListNode head, int val) {
        return new ListNode(val, head); // 新节点指向原头节点,返回新头节点
    }
  • 指定位置插入:O(n)

    ListNode insertAtPosition(ListNode head, int val, int pos) {
        if (pos == 0) return new ListNode(val, head);
        ListNode curr = head;
        for (int i = 0; i < pos - 1 && curr != null; i++) {
            curr = curr.next;
        }
        if (curr == null) return head; // 位置超出链表长度,不插入
        curr.next = new ListNode(val, curr.next);
        return head;
    }
5. 删除节点
  • 删除头部节点:O(1)

    ListNode deleteHead(ListNode head) {
        return head == null ? null : head.next;
    }
  • 删除指定值的节点:O(n)

    ListNode deleteNode(ListNode head, int val) {
        if (head == null) return null;
        if (head.val == val) return head.next; // 头节点是目标节点
        ListNode curr = head;
        while (curr.next != null && curr.next.val != val) {
            curr = curr.next;
        }
        if (curr.next != null) {
            curr.next = curr.next.next; // 跳过目标节点
        }
        return head;
    }
四、链表的高级操作与算法
1. 链表反转
  • 迭代法:O(n)

    ListNode reverseList(ListNode head) {
        ListNode prev = null, curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }
  • 递归法:O(n)

    ListNode reverseListRecursive(ListNode head) {
        if (head == null || head.next == null) return head;
        ListNode newHead = reverseListRecursive(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }
2. 找链表中点(快慢指针法)
ListNode middleNode(ListNode head) {
    if (head == null || head.next == null) return head;
    ListNode slow = head, fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow; // 当链表长度为偶数时,返回中间偏左的节点
}
3. 检测链表是否有环(Floyd 判圈算法)
boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) return false;
    ListNode slow = head, fast = head.next;
    while (slow != fast) {
        if (fast == null || fast.next == null) return false;
        slow = slow.next;
        fast = fast.next.next;
    }
    return true;
}
五、链表操作在算法题中的应用

以 LeetCode 160. 相交链表为例,双指针法利用链表遍历特性:

ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) return null;
    ListNode p1 = headA, p2 = headB;
    // 两指针遍历路径:A→B 和 B→A,长度相等,若相交则相遇于交点
    while (p1 != p2) {
        p1 = p1 == null ? headB : p1.next;
        p2 = p2 == null ? headA : p2.next;
    }
    return p1;
}

核心逻辑:利用链表遍历的灵活性,通过指针跳转实现路径互补,时间复杂度 O (m+n),空间 O (1)。

六、链表操作的关键点与技巧
  1. 指针操作的安全性:

    • 每次访问 curr.next 前检查 curr != null,避免空指针异常。

  2. 虚拟头节点:

    • 在插入、删除操作中使用虚拟头节点(dummy node),避免处理头节点时的特殊逻辑。

  3. 快慢指针:

    • 用于找中点、判圈等问题,快指针每次走 2 步,慢指针走 1 步。

  4. 递归思维:

    • 链表的递归操作(如反转、归并排序)需明确终止条件和子问题关系。

七、总结:链表的适用场景
  • 优势场景:

    • 频繁插入 / 删除操作(如链表实现的队列、栈)。

    • 动态数据结构(无需预先知道数据量)。

  • 劣势场景:

    • 频繁随机访问(如数组更适合)。

    • 内存紧张场景(链表的指针占用额外空间)。

通过掌握链表的基本特性和操作技巧,结合双指针、递归等算法思想,可高效解决链表相关的算法问题(如相交、反转、排序等)。

2.160. 相交链表

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交

img

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0

  • listA - 第一个链表

  • listB - 第二个链表

  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数

  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headAheadB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案

首先,我得明确什么是相交的链表。相交的话,应该是指两个链表从某个节点开始,之后的所有节点都是共享的。也就是说,两个链表在某个节点之后合并成一个链表。这时候,相交的起始节点就是这个共同的节点。那问题是怎么找到这个节点呢?

首先,我应该考虑链表的结构。单链表的节点只能指向下一个节点,所以如果两个链表相交,那么它们的相交部分一定是从某个节点开始,后面的部分完全重合。比如,链表 A 是 a1→a2→c1→c2→c3,链表 B 是 b1→b2→c1→c2→c3,那么相交的起始节点就是 c1。

接下来,我需要想如何高效地找到这个节点。首先,可能的思路有哪些呢?

方法1:哈希表法

第一种思路,可能是用哈希表。遍历其中一个链表,把每个节点的引用存入哈希表,然后遍历另一个链表,检查当前节点是否在哈希表中。如果存在,那就是相交的起始节点。这种方法的时间复杂度是 O (m+n),空间复杂度是 O (m) 或者 O (n),取决于哪个链表更长。不过,有没有更优的方法呢?比如不用额外空间的方法?

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Set<ListNode> set = new HashSet<>();
        ListNode curr = headA;
        while (curr != null) {
            set.add(curr);
            curr = curr.next;
        }
        curr = headB;
        while (curr != null) {
            if (set.contains(curr)) {
                return curr;
            }
            curr = curr.next;
        }
        return null;
    }
}

主要要做的其实就两步,1.判断是否存在相交,2.判断相交起点是哪个节点。如果存在相交,,那链表最后一个节点一定是相同的,且2链表长度差值就是2个头结点相距相交节点的差值。

方法二:长度算法

第二种思路,是先计算两个链表的长度,然后让长的链表先移动差值的步数,然后两个链表同时移动,直到找到相同的节点。这应该是一个可行的方法。那具体怎么做呢?

这个思路的步骤应该是这样的:

  1. 先遍历两个链表,分别得到它们的长度 lenA 和 lenB。

  2. 计算长度差 d = |lenA - lenB|。

  3. 让较长的链表先移动 d 步,这样两个链表剩下的部分长度相同。

  4. 然后同时遍历两个链表,每次移动一步,直到找到相同的节点,或者遍历完都没找到。

那这样的时间复杂度是 O (m+n),空间复杂度是 O (1),这应该是比较优的方法。

那现在需要考虑具体的实现步骤。首先,如何计算链表的长度?可以写一个辅助函数,遍历链表,统计节点数。然后,确定哪个链表更长,让长的先移动差值的步数。然后同时遍历,比较节点是否相等。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA = getLength(headA), lenB = getLength(headB);
        ListNode currA = headA, currB = headB;
        
        // 调整指针位置,使剩余长度相同
        if (lenA > lenB) {
            for (int i = 0; i < lenA - lenB; i++) {
                currA = currA.next;
            }
        } else {
            for (int i = 0; i < lenB - lenA; i++) {
                currB = currB.next;
            }
        }
        
        // 同步遍历找交点
        while (currA != null && currB != null) {
            if (currA == currB) {
                return currA;
            }
            currA = currA.next;
            currB = currB.next;
        }
        return null;
    }
    
    private int getLength(ListNode head) {
        int len = 0;
        ListNode curr = head;
        while (curr != null) {
            len++;
            curr = curr.next;
        }
        return len;
    }
}

另外,还有一种更巧妙的方法,不需要计算长度,而是利用双指针。比如,让两个指针分别从两个链表的头出发,当一个指针走到链表末尾时,跳到另一个链表的头继续走。这样,当两个指针相遇时,就是相交的节点。如果不相交,两个指针会同时走到 null。

这个方法的原理是什么呢?假设链表 A 的长度是 a + c,链表 B 的长度是 b + c,其中 c 是相交部分的长度。那么,指针 pA 从 A 出发,走到末尾后跳到 B,走的总长度是 a + c + b。指针 pB 从 B 出发,走到末尾后跳到 A,走的总长度是 b + c + a。这时候,两者走的长度相同,所以当 pA 和 pB 相遇时,正好是在相交的起始节点。如果不相交的话,a + b = b + a,所以两个指针会同时走到 null。

这个思路可能更高效,而且不需要计算长度,代码也更简洁。

方法三:双指针法寻找相交链表的起始节点

一、问题分析

两个单链表相交的条件是:从某个节点开始,两个链表的后续节点完全重合。相交的起始节点是该共同节点。 关键特性:若链表 A 长度为 a + c,链表 B 长度为 b + cc为相交部分长度),则两链表总长度之和为 a + b + 2c

二、双指针法核心逻辑
  1. 指针遍历策略

    • 设指针 p1p2 分别从 headAheadB 出发,同时移动。

    • p1 到达链表 A 末尾时,跳转到链表 B 的头节点继续;当 p2 到达链表 B 末尾时,跳转到链表 A 的头节点继续。

    • 由于两指针总移动距离相等(a + b + c),若存在相交节点,最终会在相交起始节点相遇;若不相交,则同时指向 null

  2. 数学推导

    • 若两链表相交:

      • p1 路径:A链表全程 + B链表非相交部分(a + c) + b = a + b + c

      • p2 路径:B链表全程 + A链表非相交部分(b + c) + a = a + b + c

      • 两指针在相交起始节点(距 A 头 a 步,距 B 头 b 步)相遇。

    • 若两链表不相交:

      • 两指针移动距离均为 a + b,最终同时指向 null

三、代码实现(双指针法)
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 处理空链表情况
        if (headA == null || headB == null) {
            return null;
        }
        ListNode p1 = headA, p2 = headB;
        // 当两指针未相遇时继续移动
        while (p1 != p2) {
            // p1走到A末尾则跳转到B,否则正常移动
            p1 = p1 == null ? headB : p1.next;
            // p2走到B末尾则跳转到A,否则正常移动
            p2 = p2 == null ? headA : p2.next;
        }
        return p1; // 相遇时p1=p2,可能为null(不相交)或相交节点
    }
}
四、复杂度分析
  • 时间复杂度:O (m + n),其中 mn 为两链表长度。两指针最多移动 m + n 步。

  • 空间复杂度:O (1),仅使用常数级额外空间。

五、边界情况处理
  1. 空链表:若 headAheadB 为空,直接返回 null(无相交可能)。

  2. 自相交:若链表自身成环,需额外处理,但本题中链表为单链表,不存在此情况。

  3. 相交于头节点:两指针首次移动即相遇,直接返回头节点。

3.206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

方法一:迭代法(双指针)

思路:遍历链表,将当前节点的 next 指向前驱节点,同时维护前驱和后继指针。

通过三个指针(前驱 prev、当前 curr、后继 nextTemp)逐步调整每个节点的指针方向,实现链表反转。

  • 关键点:在修改当前节点的 next 指针前,必须先保存其后继节点,避免链表断裂。

步骤演示

以链表 1 → 2 → 3 → null 为例:

  1. 初始状态prev = null, curr = 1, nextTemp 未初始化。

  2. 第一轮循环:

    • 保存 curr.next(即 2)到 nextTemp

    • 修改 curr.next 指向 prev(即 null),链表变为 1 → null

    • prev 移动到 curr(即 1),curr 移动到 nextTemp(即 2)。

    • 此时链表状态:null ← 1curr = 2

  3. 第二轮循环:

    • 保存 curr.next(即 3)到 nextTemp

    • 修改 curr.next 指向 prev(即 1),链表变为 2 → 1 → null

    • prev 移动到 curr(即 2),curr 移动到 nextTemp(即 3)。

    • 此时链表状态:null ← 1 ← 2curr = 3

  4. 第三轮循环:

    • 保存 curr.next(即 null)到 nextTemp

    • 修改 curr.next 指向 prev(即 2),链表变为 3 → 2 → 1 → null

    • prev 移动到 curr(即 3),curr 移动到 nextTemp(即 null)。

    • 循环结束,返回 prev(即新头节点 3)。

public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode nextTemp = curr.next; // 暂存后继节点
        curr.next = prev; // 当前节点指向前驱
        prev = curr; // 前驱 prev后移
        curr = nextTemp; // 当前节点 curr后移
    }
    return prev; // 返回新头节点
}

复杂度:时间 O (n),空间 O (1)。

方法二:递归法

递归反转链表的后续部分,再调整当前节点与后续节点的指针关系。

  • 关键点:明确递归函数的定义(返回反转后的头节点),并利用递归结果处理当前节点。

步骤演示

以链表 1 → 2 → 3 → null 为例:

  1. 递归到最深层:

    • 调用 reverseList(1),递归调用 reverseList(2),继续递归 reverseList(3)

    • head = 3 时,满足终止条件(head.next == null),返回 3

  2. 回溯过程:

    • 返回到reverseList(2):

      • newHead = 3(递归结果)。

      • 调整指针:2.next.next = 2(即 3.next = 2),2.next = null

      • 此时链表状态:null ← 2 ← 3,返回 newHead = 3

    • 返回到reverseList(1):

      • newHead = 3(递归结果)。

      • 调整指针:1.next.next = 1(即 2.next = 1),1.next = null

      • 此时链表状态:null ← 1 ← 2 ← 3,返回 newHead = 3

public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) {
        return head; // 终止条件:链表为空或只剩一个节点
    }
    ListNode newHead = reverseList(head.next); // 递归反转后续节点
    head.next.next = head; // 调整后续节点的next指向当前节点
    head.next = null;      // 断开当前节点的原next连接
    return newHead;        // 返回新头节点
}

复杂度:时间 O (n),空间 O (n)(递归栈)。

另一种递归:

递归函数 reverse 的定义

  • 参数:

    • prev:当前处理节点的前驱节点(初始为 null)。

    • cur:当前处理的节点。

  • 返回值:反转后的链表头节点。

递归终止条件

cur == null 时,说明已经处理完所有节点,此时 prev 即为反转后的头节点,直接返回。

与常规递归法的对比

这段代码的递归实现与我之前解释的递归法(通过回溯调整指针)的核心区别在于:

  • 常规递归法:先递归处理后续节点,再回溯调整当前节点的指针(需操作 head.next.next)。

  • 尾递归法:在递归调用前就完成指针反转,通过参数传递状态(prevcur),递归返回时直接得到结果。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }
​
    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null) {
            return prev;
        }
        ListNode temp = null;
        temp = cur.next;// 先保存下一个节点
        cur.next = prev;// 反转
        // 更新prev、cur位置
        // prev = cur;
        // cur = temp;
        return reverse(cur, temp);
    }
}

执行流程示例

以链表 1 → 2 → 3 → null 为例:

  1. 初始调用

    reverseList(head) → reverse(null, 1)
  2. 第一层递归

    • cur = 1, prev = null

    • 保存 temp = 1.next = 2

    • 反转 1.next = null,链表变为 1 → null

    • 递归调用 reverse(1, 2)

  3. 第二层递归

    • cur = 2, prev = 1

    • 保存 temp = 2.next = 3

    • 反转 2.next = 1,链表变为 2 → 1 → null

    • 递归调用 reverse(2, 3)

关键点总结

  1. 指针反转时机:在递归调用前完成当前节点的指针反转。

  2. 状态传递:通过参数 prevcur 传递前驱节点和当前节点,避免回溯操作。

  3. 终止条件:处理完最后一个节点(cur == null)时返回前驱节点 prev

方法三:头插法(迭代)

思路:创建虚拟头节点,遍历原链表将节点依次插入虚拟头节点之后。

创建虚拟头节点 dummy,遍历原链表,将每个节点依次插入到 dummy 之后,形成反转链表。

  • 关键点:插入操作需先保存当前节点的后继,避免链表断裂。

步骤演示

以链表 1 → 2 → 3 → null 为例:

  1. 初始状态dummy → nullcurr = 1

  2. 处理节点 1

    • 保存 curr.next(即 2)到 nextTemp

    • 1 插入到 dummy 之后:dummy → 1 → null

    • curr 移动到 nextTemp(即 2)。

public ListNode reverseList(ListNode head) {
    ListNode dummy = new ListNode(-1);
    ListNode curr = head;
    while (curr != null) {
        ListNode nextTemp = curr.next; // 暂存后继
        curr.next = dummy.next; // 插入到dummy后
        dummy.next = curr;
        curr = nextTemp; // 处理下一个
    }
    return dummy.next;
}

复杂度:时间 O (n),空间 O (1)。

4.234. 回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

方法一:转数组法

思路:将链表元素存入数组,双指针遍历数组判断回文。关键点:数组支持随机访问,可快速定位首尾元素。

确定数组列表是否回文很简单,我们可以使用双指针法来比较两端的元素,并向中间移动。一个指针从起点向中间移动,另一个指针从终点向中间移动。这需要 O(n) 的时间,因为访问每个元素的时间是 O(1),而有 n 个元素要访问。

然而同样的方法在链表上操作并不简单,因为不论是正向访问还是反向访问都不是 O(1)。而将链表的值复制到数组列表中是 O(n),因此最简单的方法就是将链表的值复制到数组列表中,再使用双指针法判断。

代码

public boolean isPalindrome(ListNode head) {
    List<Integer> list = new ArrayList<>();
    ListNode curr = head;
    while (curr != null) {
        list.add(curr.val);
        curr = curr.next;
    }
    int left = 0, right = list.size() - 1;
    while (left < right) {
        if (!list.get(left).equals(list.get(right))) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}

复杂度:时间 O (n),空间 O (n)。

方法二:递归回溯法

思路:利用递归栈的回溯特性,反向遍历链表,与正向遍历比较。

递归为我们提供了一种优雅的方式来反向打印节点。

function print_values_in_reverse(ListNode head)
    if head is NOT null
        print_values_in_reverse(head.next)
        print head.val

如果使用递归反向迭代节点,同时使用递归函数外的变量向前迭代,就可以判断链表是否为回文。

算法: currentNode 指针是先到尾节点,由于递归的特性再从后往前进行比较。frontPointer 是递归函数外的指针。若 currentNode.val != frontPointer.val 则返回 false。反之,frontPointer 向前移动并返回 true。

算法的正确性在于递归处理节点的顺序是相反的(回顾上面打印的算法),而我们在函数外又记录了一个变量,因此从本质上,我们同时在正向和逆向迭代匹配。

  • 关键点:递归的回溯过程等价于从链表尾部向前遍历。

步骤演示

以链表 1 → 2 → 2 → 1 为例:

  1. 递归深入:

    • 递归到最后一个节点 1,当前递归层级保存节点 1

    • 全局指针 front 初始指向头节点 1

  2. 回溯比较:

    • 比较 front.val = 1 与当前节点 1,相等,front 后移到 2

    • 回溯到上一层,当前节点为 2,比较 front.val = 22,相等,front 后移到 2

    • 继续回溯,比较 front.val = 22,相等,front 后移到 1

    • 最后比较 front.val = 11,相等,返回 true

代码

class Solution {
    private ListNode front;
​
    public boolean isPalindrome(ListNode head) {
        front = head;
        return check(head);
    }
​
    private boolean check(ListNode curr) {
        if (curr == null) return true;
        if (!check(curr.next)) return false;
        if (front.val != curr.val) return false;
        front = front.next;
        return true;
    }
}

复杂度:时间 O (n),空间 O (n)(递归栈)。

注意:必须使用全局共享的 front

  1. 局部变量无法跨方法共享isPalindrome 方法内的 frontcheck 方法不可见。

  2. 参数传递会导致复制:通过参数传递 front 会创建副本,无法同步移动。

  3. 类成员变量是唯一选择:只有类成员变量能被所有递归层级共享,实现正序遍历与逆序回溯的同步。

递归调用栈的结构

每次递归调用都会在调用栈上创建一个新的栈帧,每个栈帧包含:

  1. 参数值:当前调用的输入参数(如 curr 指针)。

  2. 局部变量:方法内定义的变量。

  3. 返回地址:递归返回后继续执行的位置。

当递归深入到链表尾部时,每个栈帧都保存了当前节点的引用。

示例链表 1 → 2 → 2 → 1 的递归过程

  1. 第一层递归:

    • curr = 1,调用 check(2),当前栈帧保存 curr = 1

  2. 第二层递归:

    • curr = 2,调用 check(2),当前栈帧保存 curr = 2

方法三:快慢指针 + 反转后半部分

思路:快慢指针找中点,反转后半部分链表,与前半部分比较。

核心思路

  1. 找中点:使用快慢指针,快指针走两步,慢指针走一步,快指针到达末尾时,慢指针指向中点。

  2. 反转后半部分:从中点开始反转链表。

  3. 比较两部分:前半部分从表头开始,后半部分从反转后的新头节点开始,逐元素比较。

关键点总结

  1. 快慢指针终止条件:

    • while (fast.next != null && fast.next.next != null)

    • 确保奇数长度时 slow 指向中点,偶数长度时指向左半部分末尾。

  2. 总结:反转后的链表结构特点

    1. 对比方式:两部分的节点数相等(奇数长度时前半部分多一个中间节点,但后半部分长度为 floor(n/2),对比时前半部分的中间节点不参与比较)。

    2. 核心目的:反转后半部分后,通过逐节点对比前半部分和反转后的后半部分,判断是否对称,无需关心两部分的物理连接关系。

  3. 比较终止条件:

    • 只需检查后半部分是否遍历完(p2 == null),因为前半部分可能包含多余的中点节点。

奇偶长度的统一处理逻辑:

链表长度中点位置后半部分起点反转后长度比较逻辑
奇数(n)n/2 个节点(从 0 开始计数)中点的下一个节点n/2前半部分前 n/2 个节点 vs 反转后的后半部分
偶数(n)n/2 - 1 个节点(左半部分末尾)中点的下一个节点n/2前半部分 n/2 个节点 vs 反转后的后半部分
  • 奇数长度链表(如1 → 2 → 3 → 2 → 1):

    • 中点为 3,反转后半部分 2 → 1 得到 1 → 2

    • 比较前半部分 1 → 2 → 3 和后半部分 1 → 2,完全相等。只需检查后半部分是否遍历完(p2 == null

  • 偶数长度链表

    (如1 → 2 → 2 → 1):

    • 中点为左半部分的末尾 2,反转后半部分 2 → 1 得到 1 → 2

    • 比较前半部分 1 → 2 和后半部分 1 → 2,完全相等。

代码

public boolean isPalindrome(ListNode head) {
    if (head == null || head.next == null) return true;
    
    // 快慢指针找中点
    ListNode slow = head, fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    
    // 反转后半部分
    ListNode secondHalf = reverseList(slow.next);
    ListNode p1 = head, p2 = secondHalf;
    
    // 比较前后两部分
    boolean result = true;
    while (result && p2 != null) {
        if (p1.val != p2.val) result = false;
        p1 = p1.next;
        p2 = p2.next;
    }
    
    // 恢复链表(可选)
    slow.next = reverseList(secondHalf);
    
    return result;
}
​
private ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode nextTemp = curr.next;
        curr.next = prev;
        prev = curr;
        curr = nextTemp;
    }
    return prev;
}

复杂度:时间 O (n),空间 O (1)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值