07------------链表
1.链表的核心特性与操作详解
一、链表的基本定义与类型
链表是一种线性数据结构,由一系列节点组成,每个节点包含数据和指向下一节点的指针(引用)。根据指针类型,链表可分为:
-
单链表:每个节点仅含一个指向下一节点的指针。
-
双向链表:每个节点含前驱和后继两个指针。
-
循环链表:尾节点的指针指向头节点,形成环。
二、链表与数组的核心特性对比
特性 | 链表 | 数组 |
---|---|---|
内存分配 | 动态分配,节点地址不连续 | 连续内存空间,固定大小 |
插入 / 删除 | 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)。
六、链表操作的关键点与技巧
-
指针操作的安全性:
-
每次访问
curr.next
前检查curr != null
,避免空指针异常。
-
-
虚拟头节点:
-
在插入、删除操作中使用虚拟头节点(dummy node),避免处理头节点时的特殊逻辑。
-
-
快慢指针:
-
用于找中点、判圈等问题,快指针每次走 2 步,慢指针走 1 步。
-
-
递归思维:
-
链表的递归操作(如反转、归并排序)需明确终止条件和子问题关系。
-
七、总结:链表的适用场景
-
优势场景:
-
频繁插入 / 删除操作(如链表实现的队列、栈)。
-
动态数据结构(无需预先知道数据量)。
-
-
劣势场景:
-
频繁随机访问(如数组更适合)。
-
内存紧张场景(链表的指针占用额外空间)。
-
通过掌握链表的基本特性和操作技巧,结合双指针、递归等算法思想,可高效解决链表相关的算法问题(如相交、反转、排序等)。
2.160. 相交链表
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
-
intersectVal
- 相交的起始节点的值。如果不存在相交节点,这一值为0
-
listA
- 第一个链表 -
listB
- 第二个链表 -
skipA
- 在listA
中(从头节点开始)跳到交叉节点的节点数 -
skipB
- 在listB
中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA
和 headB
传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
首先,我得明确什么是相交的链表。相交的话,应该是指两个链表从某个节点开始,之后的所有节点都是共享的。也就是说,两个链表在某个节点之后合并成一个链表。这时候,相交的起始节点就是这个共同的节点。那问题是怎么找到这个节点呢?
首先,我应该考虑链表的结构。单链表的节点只能指向下一个节点,所以如果两个链表相交,那么它们的相交部分一定是从某个节点开始,后面的部分完全重合。比如,链表 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个头结点相距相交节点的差值。
方法二:长度算法
第二种思路,是先计算两个链表的长度,然后让长的链表先移动差值的步数,然后两个链表同时移动,直到找到相同的节点。这应该是一个可行的方法。那具体怎么做呢?
这个思路的步骤应该是这样的:
-
先遍历两个链表,分别得到它们的长度 lenA 和 lenB。
-
计算长度差 d = |lenA - lenB|。
-
让较长的链表先移动 d 步,这样两个链表剩下的部分长度相同。
-
然后同时遍历两个链表,每次移动一步,直到找到相同的节点,或者遍历完都没找到。
那这样的时间复杂度是 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 + c
(c
为相交部分长度),则两链表总长度之和为 a + b + 2c
。
二、双指针法核心逻辑
-
指针遍历策略:
-
设指针
p1
、p2
分别从headA
、headB
出发,同时移动。 -
当
p1
到达链表 A 末尾时,跳转到链表 B 的头节点继续;当p2
到达链表 B 末尾时,跳转到链表 A 的头节点继续。 -
由于两指针总移动距离相等(
a + b + c
),若存在相交节点,最终会在相交起始节点相遇;若不相交,则同时指向null
。
-
-
数学推导:
-
若两链表相交:
-
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),其中
m
和n
为两链表长度。两指针最多移动m + n
步。 -
空间复杂度:O (1),仅使用常数级额外空间。
五、边界情况处理
-
空链表:若
headA
或headB
为空,直接返回null
(无相交可能)。 -
自相交:若链表自身成环,需额外处理,但本题中链表为单链表,不存在此情况。
-
相交于头节点:两指针首次移动即相遇,直接返回头节点。
3.206. 反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
方法一:迭代法(双指针)
思路:遍历链表,将当前节点的 next 指向前驱节点,同时维护前驱和后继指针。
通过三个指针(前驱 prev
、当前 curr
、后继 nextTemp
)逐步调整每个节点的指针方向,实现链表反转。
-
关键点:在修改当前节点的
next
指针前,必须先保存其后继节点,避免链表断裂。
步骤演示
以链表 1 → 2 → 3 → null
为例:
-
初始状态:
prev = null
,curr = 1
,nextTemp
未初始化。 -
第一轮循环:
-
保存
curr.next
(即2
)到nextTemp
。 -
修改
curr.next
指向prev
(即null
),链表变为1 → null
。 -
prev
移动到curr
(即1
),curr
移动到nextTemp
(即2
)。 -
此时链表状态:
null ← 1
,curr = 2
。
-
-
第二轮循环:
-
保存
curr.next
(即3
)到nextTemp
。 -
修改
curr.next
指向prev
(即1
),链表变为2 → 1 → null
。 -
prev
移动到curr
(即2
),curr
移动到nextTemp
(即3
)。 -
此时链表状态:
null ← 1 ← 2
,curr = 3
。
-
-
第三轮循环:
-
保存
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
为例:
-
递归到最深层:
-
调用
reverseList(1)
,递归调用reverseList(2)
,继续递归reverseList(3)
。 -
当
head = 3
时,满足终止条件(head.next == null
),返回3
。
-
-
回溯过程:
-
返回到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
)。 -
尾递归法:在递归调用前就完成指针反转,通过参数传递状态(
prev
和cur
),递归返回时直接得到结果。
/** * 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
为例:
-
初始调用:
reverseList(head) → reverse(null, 1)
-
第一层递归:
-
cur = 1
,prev = null
。 -
保存
temp = 1.next = 2
。 -
反转
1.next = null
,链表变为1 → null
。 -
递归调用
reverse(1, 2)
。
-
-
第二层递归:
-
cur = 2
,prev = 1
。 -
保存
temp = 2.next = 3
。 -
反转
2.next = 1
,链表变为2 → 1 → null
。 -
递归调用
reverse(2, 3)
。
-
关键点总结
-
指针反转时机:在递归调用前完成当前节点的指针反转。
-
状态传递:通过参数
prev
和cur
传递前驱节点和当前节点,避免回溯操作。 -
终止条件:处理完最后一个节点(
cur == null
)时返回前驱节点prev
。
方法三:头插法(迭代)
思路:创建虚拟头节点,遍历原链表将节点依次插入虚拟头节点之后。
创建虚拟头节点 dummy
,遍历原链表,将每个节点依次插入到 dummy
之后,形成反转链表。
-
关键点:插入操作需先保存当前节点的后继,避免链表断裂。
步骤演示
以链表 1 → 2 → 3 → null
为例:
-
初始状态:
dummy → null
,curr = 1
。 -
处理节点
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
。 -
全局指针
front
初始指向头节点1
。
-
-
回溯比较:
-
比较
front.val = 1
与当前节点1
,相等,front
后移到2
。 -
回溯到上一层,当前节点为
2
,比较front.val = 2
与2
,相等,front
后移到2
。 -
继续回溯,比较
front.val = 2
与2
,相等,front
后移到1
。 -
最后比较
front.val = 1
与1
,相等,返回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
-
局部变量无法跨方法共享:
isPalindrome
方法内的front
对check
方法不可见。 -
参数传递会导致复制:通过参数传递
front
会创建副本,无法同步移动。 -
类成员变量是唯一选择:只有类成员变量能被所有递归层级共享,实现正序遍历与逆序回溯的同步。
递归调用栈的结构
每次递归调用都会在调用栈上创建一个新的栈帧,每个栈帧包含:
-
参数值:当前调用的输入参数(如
curr
指针)。 -
局部变量:方法内定义的变量。
-
返回地址:递归返回后继续执行的位置。
当递归深入到链表尾部时,每个栈帧都保存了当前节点的引用。
示例链表 1 → 2 → 2 → 1
的递归过程
-
第一层递归:
-
curr = 1
,调用check(2)
,当前栈帧保存curr = 1
。
-
-
第二层递归:
-
curr = 2
,调用check(2)
,当前栈帧保存curr = 2
。
-
方法三:快慢指针 + 反转后半部分
思路:快慢指针找中点,反转后半部分链表,与前半部分比较。
核心思路
-
找中点:使用快慢指针,快指针走两步,慢指针走一步,快指针到达末尾时,慢指针指向中点。
-
反转后半部分:从中点开始反转链表。
-
比较两部分:前半部分从表头开始,后半部分从反转后的新头节点开始,逐元素比较。
关键点总结
-
快慢指针终止条件:
-
while (fast.next != null && fast.next.next != null)
-
确保奇数长度时
slow
指向中点,偶数长度时指向左半部分末尾。
-
-
总结:反转后的链表结构特点
-
对比方式:两部分的节点数相等(奇数长度时前半部分多一个中间节点,但后半部分长度为
floor(n/2)
,对比时前半部分的中间节点不参与比较)。 -
核心目的:反转后半部分后,通过逐节点对比前半部分和反转后的后半部分,判断是否对称,无需关心两部分的物理连接关系。
-
-
比较终止条件:
-
只需检查后半部分是否遍历完(
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)。