24. 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode temp = dummyHead;
while (temp.next != null && temp.next.next != null) {
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1;
}
return dummyHead.next;
}
}
这段代码定义了一个名为Solution
的类,其中包含一个方法swapPairs
,用于实现链表中每相邻两个节点的值进行交换。链表节点的定义(ListNode
)虽未直接给出,但可根据上下文推断其结构大致如下:
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
方法逻辑解析:
-
创建哑节点(dummyHead):
ListNode dummyHead = new ListNode(0); dummyHead.next = head; ListNode temp = dummyHead;
- 创建一个值为0的哑节点
dummyHead
,其作用是为了简化边界条件处理,避免在交换头节点时的特殊判断。 - 将
dummyHead
的下一个节点指向原链表的头节点head
。 - 引入一个辅助指针
temp
指向dummyHead
,用于遍历链表。
- 创建一个值为0的哑节点
-
循环交换节点:
while (temp.next != null && temp.next.next != null) { // 交换逻辑 }
- 循环条件确保至少有两个节点可以参与交换,直到遍历到倒数第二个节点为止。
-
具体交换操作:
ListNode node1 = temp.next; ListNode node2 = temp.next.next; temp.next = node2; node1.next = node2.next; node2.next = node1; temp = node1;
node1
和node2
分别暂存待交换的两个节点。- 更新
temp.next
为node2
,完成第一次交换,此时temp
后的两个节点已经交换。 - 更新
node1.next
为node2.next
,准备将node1
置于node2
之后。 - 更新
node2.next
为node1
,完成第二次交换,恢复链表的连续性。 - 移动
temp
到node1
,准备下一轮交换。
-
返回结果:
return dummyHead.next;
- 由于最初的头节点可能已被交换,直接返回
dummyHead.next
作为新链表的头节点。
- 由于最初的头节点可能已被交换,直接返回
总结
这段代码通过引入哑节点简化了链表头节点处理的问题,使用循环和简单的指针操作实现了链表中相邻节点值的成对交换,其时间复杂度为O(n),空间复杂度为O(1),是一种高效且简洁的解决方案。
为了将 swapPairs
方法改成符合ACM竞赛模式,我们需要调整代码以使用标准输入和输出,并定义 ListNode
类。以下是修改后的代码:
import java.util.Scanner;
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode temp = dummyHead;
while (temp.next != null && temp.next.next != null) {
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1;
}
return dummyHead.next;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取链表的元素个数
int n = scanner.nextInt();
// 创建链表并读取链表元素
ListNode dummy = new ListNode(0);
ListNode current = dummy;
for (int i = 0; i < n; i++) {
current.next = new ListNode(scanner.nextInt());
current = current.next;
}
// 创建Solution对象并调用swapPairs方法
Solution solution = new Solution();
ListNode result = solution.swapPairs(dummy.next);
// 输出结果链表
current = result;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.println();
}
}
说明
-
输入格式:
- 第一行输入一个整数
n
,表示链表的元素个数。 - 接下来的一行输入
n
个整数,表示链表的元素。
- 第一行输入一个整数
-
输出格式:
- 输出经过两两交换后的链表,每个元素之间用空格隔开。
使用示例
假设输入如下:
4
1 2 3 4
程序会输出:
2 1 4 3
因为链表 [1, 2, 3, 4]
经过两两交换后,结果链表为 [2, 1, 4, 3]
。
可以将这段代码保存到一个文件中,比如 Main.java
,然后在命令行中使用 javac Main.java
进行编译,使用 java Main
运行,并在运行时通过标准输入提供数据。
19. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
int length = getLength(head);
ListNode cur = dummy;
for (int i = 1; i < length - n + 1; ++i) {
cur = cur.next;
}
cur.next = cur.next.next;
ListNode ans = dummy.next;
return ans;
}
public int getLength(ListNode head) {
int length = 0;
while (head != null) {
++length;
head = head.next;
}
return length;
}
}
这段代码定义了一个名为Solution
的类,其中包含两个方法:removeNthFromEnd
和getLength
。这两个方法共同作用于单链表(ListNode类型),用于实现从链表的末尾移除第n
个节点的功能。
方法一:removeNthFromEnd
功能:移除链表中倒数第n
个节点,并返回修改后的链表头节点。
参数:
ListNode head
:链表的头节点。int n
:要移除的节点位置,从链表末尾计数。
逻辑:
- 创建哑节点(dummy node):首先创建一个值为0的新节点
dummy
,并将它的下一个节点指向head
。这样做是为了处理边界情况,即当移除头节点时,可以直接通过dummy
返回新的头节点。 - 计算链表长度:调用
getLength
方法获取链表的长度。 - 定位并移除目标节点:从哑节点开始遍历,遍历到倒数第
n+1
个节点(即需要移除节点的前一个节点)停止,然后更新该节点的next
指针,绕过待移除节点。 - 返回结果:返回
dummy.next
,即修改后的链表头节点。
方法二:getLength
功能:计算链表的长度。
参数:
ListNode head
:链表的头节点。
逻辑:
- 通过一个循环遍历链表,每遇到一个节点就将长度加1,直到链表末尾(
head == null
)。
总结
这段代码通过引入哑节点简化了对头节点的特殊处理,先计算链表长度,再通过一次遍历定位并移除目标节点,实现了从链表末尾移除指定节点的功能。这种方法的时间复杂度为O(N),空间复杂度为O(1),其中N为链表长度。
为了将 removeNthFromEnd
方法改成符合ACM竞赛模式,我们需要调整代码以使用标准输入和输出,并定义 ListNode
类。以下是修改后的代码:
import java.util.Scanner;
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
ListNode(int x, ListNode next) {
val = x;
this.next = next;
}
}
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
int length = getLength(head);
ListNode cur = dummy;
for (int i = 1; i < length - n + 1; ++i) {
cur = cur.next;
}
cur.next = cur.next.next;
return dummy.next;
}
public int getLength(ListNode head) {
int length = 0;
while (head != null) {
++length;
head = head.next;
}
return length;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取链表的元素个数
int n = scanner.nextInt();
// 创建链表并读取链表元素
ListNode dummy = new ListNode(0);
ListNode current = dummy;
for (int i = 0; i < n; i++) {
current.next = new ListNode(scanner.nextInt());
current = current.next;
}
// 读取要删除的第 n 个节点
int k = scanner.nextInt();
// 创建Solution对象并调用removeNthFromEnd方法
Solution solution = new Solution();
ListNode result = solution.removeNthFromEnd(dummy.next, k);
// 输出结果链表
current = result;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
System.out.println();
}
}
说明
-
输入格式:
- 第一行输入一个整数
n
,表示链表的元素个数。 - 接下来的一行输入
n
个整数,表示链表的元素。 - 最后一行输入一个整数
k
,表示要从链表末尾删除的第k
个节点。
- 第一行输入一个整数
-
输出格式:
- 输出删除第
k
个节点后的链表,每个元素之间用空格隔开。
- 输出删除第
使用示例
假设输入如下:
5
1 2 3 4 5
2
程序会输出:
1 2 3 5
因为链表 [1, 2, 3, 4, 5]
中删除倒数第 2 个节点后,结果链表为 [1, 2, 3, 5]
。
可以将这段代码保存到一个文件中,比如 Main.java
,然后在命令行中使用 javac Main.java
进行编译,使用 java Main
运行,并在运行时通过标准输入提供数据。
链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> visited = new HashSet<ListNode>();
ListNode temp = headA;
while (temp != null) {
visited.add(temp);
temp = temp.next;
}
temp = headB;
while (temp != null) {
if (visited.contains(temp)) {
return temp;
}
temp = temp.next;
}
return null;
}
}
这段代码是一个Java程序,实现了一个名为Solution
的类,其中包含一个方法getIntersectionNode
。这个方法用于找到两个链表(ListNode
类型)的交点,即第一个同时存在于两个链表中的节点。如果两个链表没有交点,该方法返回null
。这里的ListNode
是一个链表节点类,虽然具体定义没有给出,但通常包含一个值字段(val
)和一个指向下一个节点的指针(next
)。
方法的具体实现思路是:
-
使用HashSet记录第一个链表的所有节点:
- 首先,程序创建一个名为
visited
的哈希集(HashSet),用于存储链表A中访问过的节点。 - 通过一个
while
循环遍历链表A(headA
),将遍历到的每个节点都添加到visited
集合中。这样,集合visited
最终将包含链表A的所有唯一节点。
- 首先,程序创建一个名为
-
检查第二个链表的节点是否存在于哈希集中:
- 然后,程序开始遍历链表B(
headB
),使用另一个while
循环,并在每次迭代中检查当前节点temp
是否已经在visited
集合中。 - 如果发现某个节点存在于
visited
集合中,这意味着该节点同时存在于链表A和B中,即为两个链表的交点,于是直接返回这个节点。 - 如果遍历完链表B都没有找到交点,则返回
null
,表示两个链表没有交集。
- 然后,程序开始遍历链表B(
这种方法的时间复杂度为O(m+n),其中m和n分别是两个链表的长度。空间复杂度也是O(min(m,n)),因为在最坏的情况下,需要存储一个链表的所有节点。这是一种简单直观的解法,但不是最优的,最优解法(如双指针法)可以在O(m+n)时间内完成,且只需O(1)的额外空间。
为了将 getIntersectionNode
方法改成符合ACM竞赛模式,我们需要调整代码以使用标准输入和输出,并定义 ListNode
类。以下是修改后的代码:
import java.util.Scanner;
import java.util.HashSet;
import java.util.Set;
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> visited = new HashSet<>();
ListNode temp = headA;
while (temp != null) {
visited.add(temp);
temp = temp.next;
}
temp = headB;
while (temp != null) {
if (visited.contains(temp)) {
return temp;
}
temp = temp.next;
}
return null;
}
}
public class Main {
private static ListNode buildList(Scanner scanner) {
int n = scanner.nextInt();
if (n == 0) return null;
ListNode dummy = new ListNode(0);
ListNode current = dummy;
for (int i = 0; i < n; i++) {
current.next = new ListNode(scanner.nextInt());
current = current.next;
}
return dummy.next;
}
private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}
private static ListNode findNode(ListNode head, int pos) {
while (pos-- > 0 && head != null) {
head = head.next;
}
return head;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Build first linked list
ListNode headA = buildList(scanner);
// Build second linked list
ListNode headB = buildList(scanner);
// Read intersection position for the first list (0-indexed, -1 if no intersection)
int intersectA = scanner.nextInt();
// Read intersection position for the second list (0-indexed, -1 if no intersection)
int intersectB = scanner.nextInt();
// If both positions are valid, create intersection
if (intersectA != -1 && intersectB != -1) {
ListNode nodeA = findNode(headA, intersectA);
ListNode nodeB = findNode(headB, intersectB);
if (nodeA != null && nodeB != null) {
nodeB.next = nodeA;
}
}
// Create Solution object and find the intersection node
Solution solution = new Solution();
ListNode intersection = solution.getIntersectionNode(headA, headB);
// Output the result
if (intersection != null) {
System.out.println(intersection.val);
} else {
System.out.println("null");
}
}
}
说明
-
输入格式:
- 第一个整数表示第一个链表的长度
n
。 - 接下来
n
个整数表示第一个链表的元素。 - 第二个整数表示第二个链表的长度
m
。 - 接下来
m
个整数表示第二个链表的元素。 - 一个整数
intersectA
表示第一个链表中的交叉点(0索引,-1表示没有交叉)。 - 一个整数
intersectB
表示第二个链表中的交叉点(0索引,-1表示没有交叉)。
- 第一个整数表示第一个链表的长度
-
输出格式:
- 输出交点的值,如果没有交点则输出 “null”。
使用示例
假设输入如下:
5
4 1 8 4 5
6
5 6 1 8 4 5
2
3
程序会输出:
8
因为链表在节点值为 8
处相交。
可以将这段代码保存到一个文件中,比如 Main.java
,然后在命令行中使用 javac Main.java
进行编译,使用 java Main
运行,并在运行时通过标准输入提供数据。
方法二:双指针
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
这段代码同样定义了一个名为Solution
的类,其中包含一个方法getIntersectionNode
,用于寻找两个链表(由ListNode
节点组成)的交点。与之前提供的解决方案不同,这个方法采用了“双指针”技巧,其核心思想是让两个指针分别从两个链表的头开始遍历,当一个指针到达链表末尾时,将其重新定位到另一个链表的头部继续遍历。这种方法假设链表是循环的,或者两个链表在某个节点相遇(如果有交点的话)。下面是详细解释:
-
参数说明:
headA
:链表A的头节点。headB
:链表B的头节点。
-
逻辑步骤:
-
初始化指针:定义两个指针
pA
和pB
,分别指向两个链表的头节点headA
和headB
。 -
循环遍历:进入一个循环,条件是
pA
和pB
不相等。在循环体内:- 如果
pA
走到链表末尾(即pA == null
),则将其重置为headB
的头节点,继续遍历。 - 如果
pB
走到链表末尾(即pB == null
),则将其重置为headA
的头节点,继续遍历。 - 分别将
pA
和pB
向后移动一位(pA = pA.next
和pB = pB.next
)。
- 如果
-
返回交点:当
pA == pB
时,说明它们相遇在交点,直接返回该节点。如果循环结束仍未相遇,则说明没有交点,但由于题目的设定保证了如果有交点一定会在有限步内相遇,所以这种情况理论上不会发生,但正确的做法是在循环外部加一个返回null
的逻辑以应对非预期情况,尽管在这个特定的循环逻辑中这不是必须的,因为循环必定会结束于一个交点或两指针同时为null(这在给定的代码逻辑中不会发生)。
-
-
时间复杂度:O(N+M),其中N和M分别是两个链表的长度。在最坏的情况下,两个指针会遍历完两个链表的全部节点。
-
空间复杂度:O(1),只使用了两个指针,没有使用额外的存储空间。
这种方法巧妙地利用了链表可能形成环的特性(即便原始链表不构成环,通过相互遍历也形成了一个逻辑上的环),从而避免了额外的存储空间(如哈希表)需求,提高了空间效率。
为了将 getIntersectionNode
方法改成符合ACM竞赛模式,我们需要调整代码以使用标准输入和输出,并定义 ListNode
类。以下是修改后的代码:
import java.util.Scanner;
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
public class Main {
private static ListNode buildList(Scanner scanner) {
int n = scanner.nextInt();
if (n == 0) return null;
ListNode dummy = new ListNode(0);
ListNode current = dummy;
for (int i = 0; i < n; i++) {
current.next = new ListNode(scanner.nextInt());
current = current.next;
}
return dummy.next;
}
private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}
private static ListNode findNode(ListNode head, int pos) {
while (pos-- > 0 && head != null) {
head = head.next;
}
return head;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Build first linked list
ListNode headA = buildList(scanner);
// Build second linked list
ListNode headB = buildList(scanner);
// Read intersection position for the first list (0-indexed, -1 if no intersection)
int intersectA = scanner.nextInt();
// Read intersection position for the second list (0-indexed, -1 if no intersection)
int intersectB = scanner.nextInt();
// If both positions are valid, create intersection
if (intersectA != -1 && intersectB != -1) {
ListNode nodeA = findNode(headA, intersectA);
ListNode nodeB = findNode(headB, intersectB);
if (nodeA != null && nodeB != null) {
nodeB.next = nodeA;
}
}
// Create Solution object and find the intersection node
Solution solution = new Solution();
ListNode intersection = solution.getIntersectionNode(headA, headB);
// Output the result
if (intersection != null) {
System.out.println(intersection.val);
} else {
System.out.println("null");
}
}
}
说明
-
输入格式:
- 第一个整数表示第一个链表的长度
n
。 - 接下来
n
个整数表示第一个链表的元素。 - 第二个整数表示第二个链表的长度
m
。 - 接下来
m
个整数表示第二个链表的元素。 - 一个整数
intersectA
表示第一个链表中的交叉点(0索引,-1表示没有交叉)。 - 一个整数
intersectB
表示第二个链表中的交叉点(0索引,-1表示没有交叉)。
- 第一个整数表示第一个链表的长度
-
输出格式:
- 输出交点的值,如果没有交点则输出 “null”。
使用示例
假设输入如下:
5
4 1 8 4 5
6
5 6 1 8 4 5
2
3
程序会输出:
8
因为链表在节点值为 8
处相交。
你可以将这段代码保存到一个文件中,比如 Main.java
,然后在命令行中使用 javac Main.java
进行编译,使用 java Main
运行,并在运行时通过标准输入提供数据。
142. 环形链表 II
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
方法一:哈希表
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode pos = head;
Set<ListNode> visited = new HashSet<ListNode>();
while (pos != null) {
if (visited.contains(pos)) {
return pos;
} else {
visited.add(pos);
}
pos = pos.next;
}
return null;
}
}
这段代码定义了一个名为Solution
的类,其中包含一个方法detectCycle
,用于检测一个链表(ListNode类型)中是否存在环,并返回环中的任意一个入口节点。如果链表中没有环,则返回null
。这里的ListNode
通常包含两个成员:一个值(val
)和一个指向下一个节点的指针(next
)。
方法的逻辑非常直接:
-
初始化:首先,创建一个名为
pos
的指针指向链表的头节点head
,并初始化一个哈希集合(HashSet
)visited
,用于记录已访问过的节点。 -
遍历链表:接下来,进入一个
while
循环,条件是pos
不为null
。在循环中:- 检查当前节点
pos
是否已经被访问过(即visited
集合中是否包含pos
)。- 如果已访问过,说明当前节点处于环中,直接返回该节点作为环的入口。
- 如果未访问过,将当前节点添加到
visited
集合中,并将pos
指针移动到下一个节点(pos = pos.next
)。
- 检查当前节点
-
未发现环:如果遍历完整个链表都没有发现重复的节点,说明链表中没有环,最后返回
null
。
时间复杂度:O(N),其中N是链表中的节点数量。在最坏的情况下,需要遍历整个链表。
空间复杂度:O(N),因为在最坏情况下,需要将链表中所有节点都存储在哈希集合中。
这种方法简单且易于理解,但需要注意,当链表很长时,哈希集合的使用会增加额外的空间开销。对于空间敏感的应用场景,可以考虑使用“快慢指针”(Floyd判圈算法)方法来检测环,该方法仅使用常数级别的额外空间。
为了将 detectCycle
方法改成符合ACM竞赛模式,我们需要调整代码以使用标准输入和输出,并定义 ListNode
类。以下是修改后的代码:
import java.util.Scanner;
import java.util.HashSet;
import java.util.Set;
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
class Solution {
public ListNode detectCycle(ListNode head) {
ListNode pos = head;
Set<ListNode> visited = new HashSet<>();
while (pos != null) {
if (visited.contains(pos)) {
return pos;
} else {
visited.add(pos);
}
pos = pos.next;
}
return null;
}
}
public class Main {
private static ListNode buildList(Scanner scanner) {
int n = scanner.nextInt();
if (n == 0) return null;
ListNode dummy = new ListNode(0);
ListNode current = dummy;
for (int i = 0; i < n; i++) {
current.next = new ListNode(scanner.nextInt());
current = current.next;
}
return dummy.next;
}
private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}
private static ListNode findNode(ListNode head, int pos) {
while (pos-- > 0 && head != null) {
head = head.next;
}
return head;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Build the linked list
ListNode head = buildList(scanner);
// Read the position of the cycle node (0-indexed, -1 if no cycle)
int cyclePos = scanner.nextInt();
// If cyclePos is valid, create cycle
ListNode cycleNode = findNode(head, cyclePos);
if (cycleNode != null) {
ListNode lastNode = cycleNode;
while (lastNode.next != null) {
lastNode = lastNode.next;
}
lastNode.next = cycleNode;
}
// Create Solution object and find the node where the cycle begins
Solution solution = new Solution();
ListNode cycleStart = solution.detectCycle(head);
// Output the result
if (cycleStart != null) {
System.out.println(cycleStart.val);
} else {
System.out.println("null");
}
}
}
说明
-
输入格式:
- 第一个整数表示链表的长度
n
。 - 接下来
n
个整数表示链表的元素。 - 一个整数
cyclePos
表示循环开始的位置(0索引,-1表示没有循环)。
- 第一个整数表示链表的长度
-
输出格式:
- 输出循环开始的节点的值,如果没有循环则输出 “null”。
使用示例
假设输入如下:
5
3 2 0 -4 1
1
程序会输出:
2
因为链表在节点值为 2
处开始循环。
你可以将这段代码保存到一个文件中,比如 Main.java
,然后在命令行中使用 javac Main.java
进行编译,使用 java Main
运行,并在运行时通过标准输入提供数据。
方法二:快慢指针
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head, fast = head;
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
if (fast == slow) {
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
}
这段代码是使用Java编写的,它定义了一个名为Solution
的类,其中包含一个方法detectCycle
,用于检测一个链表(由ListNode
节点组成)中是否存在环,并返回环中的入口节点。如果链表无环,则返回null
。这里利用了著名的“快慢指针”(Floyd判圈算法)技术来解决此问题。
逻辑解释:
-
初始化:首先,定义两个指针
slow
和fast
,均初始化为链表的头节点head
。这两个指针将在链表中前进,但fast
指针每次移动两个节点,而slow
指针每次移动一个节点。 -
寻找交点:进入一个循环,条件是
fast
不为null
且fast.next
也不为null
(防止空指针异常)。在循环中,slow
和fast
按照各自的速率前进。如果链表中存在环,fast
指针最终会追上slow
指针(在环内的某个节点相遇)。 -
判断是否有环:当
fast == slow
时,说明链表中存在环。 -
寻找环的入口:
- 一旦发现环,定义一个新的指针
ptr
从链表头开始移动,同时保持slow
指针继续在环内移动。两者以相同的速度(每次移动一个节点)前进,当它们相遇时的节点即为环的入口节点,因为从头节点和环内某点出发,两者到环入口的距离相等。
- 一旦发现环,定义一个新的指针
-
无环处理:如果循环结束,说明链表无环,直接返回
null
。
时间复杂度和空间复杂度:
- 时间复杂度:O(N),其中N是链表中的节点数量。在最坏情况下,快指针会遍历链表两次(一次追上慢指针,一次与慢指针从头节点同时出发再次相遇)。
- 空间复杂度:O(1),只使用了常数个额外指针,没有使用额外的数据结构。
这种方法相比使用哈希集合减少了空间复杂度,是非常高效的链表环检测算法。
为了将 detectCycle
方法改成符合ACM竞赛模式,我们需要调整代码以使用标准输入和输出,并定义 ListNode
类。以下是修改后的代码:
import java.util.Scanner;
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head, fast = head;
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
if (fast == slow) {
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
}
public class Main {
private static ListNode buildList(Scanner scanner) {
int n = scanner.nextInt();
if (n == 0) return null;
ListNode dummy = new ListNode(0);
ListNode current = dummy;
for (int i = 0; i < n; i++) {
current.next = new ListNode(scanner.nextInt());
current = current.next;
}
return dummy.next;
}
private static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}
private static ListNode findNode(ListNode head, int pos) {
while (pos-- > 0 && head != null) {
head = head.next;
}
return head;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Build the linked list
ListNode head = buildList(scanner);
// Read the position of the cycle node (0-indexed, -1 if no cycle)
int cyclePos = scanner.nextInt();
// If cyclePos is valid, create cycle
ListNode cycleNode = findNode(head, cyclePos);
if (cycleNode != null) {
ListNode lastNode = cycleNode;
while (lastNode.next != null) {
lastNode = lastNode.next;
}
lastNode.next = cycleNode;
}
// Create Solution object and find the node where the cycle begins
Solution solution = new Solution();
ListNode cycleStart = solution.detectCycle(head);
// Output the result
if (cycleStart != null) {
System.out.println(cycleStart.val);
} else {
System.out.println("null");
}
}
}
说明
-
输入格式:
- 第一个整数表示链表的长度
n
。 - 接下来
n
个整数表示链表的元素。 - 一个整数
cyclePos
表示循环开始的位置(0索引,-1表示没有循环)。
- 第一个整数表示链表的长度
-
输出格式:
- 输出循环开始的节点的值,如果没有循环则输出 “null”。
使用示例
假设输入如下:
5
3 2 0 -4 1
1
程序会输出:
2
因为链表在节点值为 2
处开始循环。
你可以将这段代码保存到一个文件中,比如 Main.java
,然后在命令行中使用 javac Main.java
进行编译,使用 java Main
运行,并在运行时通过标准输入提供数据。