24. 两两交换链表中的节点
题目介绍
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例1:
输入:
h
e
a
d
=
[
1
,
2
,
3
,
4
]
head = [1,2,3,4]
head=[1,2,3,4]
输出:
[
2
,
1
,
4
,
3
]
[2,1,4,3]
[2,1,4,3]
示例2:
输入:
h
e
a
d
=
[
]
head = []
head=[]
输出:
[
]
[]
[]
示例3:
输入:
h
e
a
d
=
[
1
]
head = [1]
head=[1]
输出:
[
1
]
[1]
[1]
思路
本题的思路为模拟两两交换节点时的过程。首先,构建一个虚拟节点以减少对是否头节点的讨论和方便后续的操作。想象眼前出现一个链表,则需要指针的是虚拟节点,交换时的两个节点和这两个节点之后的一个节点,否则在交换之后可能会无法找到接下来的节点。
在操作过程中,首先保存操作节点之后的节点,然后开始操作,让指向虚拟节点的指针指向第二个操作节点,第二个操作节点指向第一个操作节点,第一个操作节点指向保存的之后节点。同时,将指向初始虚拟节点的指针移动到第一个操作节点处,也就是已交换的链表的最后一个节点处。
解法
/**
* 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 swapPairs(ListNode head) {
ListNode fir = new ListNode(-1);
fir.next = head;
ListNode cur = fir;
ListNode tmp;
ListNode first;
ListNode second;
while(cur.next != null && cur.next.next!=null){
tmp = cur.next.next.next;
first = cur.next;
second = cur.next.next;
cur.next = second;
second.next = first;
first.next = tmp;
cur = first;
}
return fir.next;
}
}
总结
本题最重要的是要想清楚要规定多少指针及其作用。同时,模拟的过程中要注意指针改变的顺序以防后续无法找到节点的情况发生。
19. 删除链表的倒数第N个节点
题目描述
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例1:
输入:
h
e
a
d
=
[
1
,
2
,
3
,
4
,
5
]
,
n
=
2
head = [1,2,3,4,5], n = 2
head=[1,2,3,4,5],n=2
输出:
[
1
,
2
,
3
,
5
]
[1,2,3,5]
[1,2,3,5]
示例1:
输入:$head = [1], n = $
输出:
[
]
[]
[]
示例1:
输入:
h
e
a
d
=
[
1
,
2
]
,
n
=
1
head = [1,2], n = 1
head=[1,2],n=1
输出:
[
1
]
[1]
[1]
思路
本题可以想到是需要使用双指针的方法来进行位置的标记,但需要仔细思考的是需要快指针走到什么位置才能恰好使慢指针能够达到应删除元素的前一位。
在解题时,首先构建一个虚拟头节点以避免与头节点相关的讨论,使快慢指针同时指向虚拟头节点。
接下来,令快指针首先走n步,这样能够使快指针比慢指针领先n个位置,那么在接下来只需要快慢指针同时前进,则在快指针指向的节点为最后一个节点时,慢指针即可指向需要删除的节点。
解法
/**
* 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 removeNthFromEnd(ListNode head, int n) {
ListNode fir = new ListNode(-1);
fir.next = head;
ListNode fast = fir;
ListNode slow = fir;
for(int i=0;i<n;i++){
fast = fast.next;
}
while(fast!=null && fast.next!=null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return fir.next;
}
}
总结
本题中需要注意的仍然是想清楚使用快慢指针的具体过程。首先让快指针走n步后,快慢指针共同前进时的跳出循环边界条件也是一个需要仔细思考的地方。这里如果循环到快指针指向空指针时则慢指针就会指向需要被删除的节点处,这会导致其无法被直接删除。
面试题 02.07.链表相交
题目描述
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例1:
输入:
i
n
t
e
r
s
e
c
t
V
a
l
=
8
,
l
i
s
t
A
=
[
4
,
1
,
8
,
4
,
5
]
,
l
i
s
t
B
=
[
5
,
0
,
1
,
8
,
4
,
5
]
,
s
k
i
p
A
=
2
,
s
k
i
p
B
=
3
intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
intersectVal=8,listA=[4,1,8,4,5],listB=[5,0,1,8,4,5],skipA=2,skipB=3
输出:Intersected at ‘8’
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例2:
输入:
i
n
t
e
r
s
e
c
t
V
a
l
=
2
,
l
i
s
t
A
=
[
0
,
9
,
1
,
2
,
4
]
,
l
i
s
t
B
=
[
3
,
2
,
4
]
,
s
k
i
p
A
=
3
,
s
k
i
p
B
=
1
intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
intersectVal=2,listA=[0,9,1,2,4],listB=[3,2,4],skipA=3,skipB=1
输出:Intersected at ‘2’
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例3:
输入:
i
n
t
e
r
s
e
c
t
V
a
l
=
0
,
l
i
s
t
A
=
[
2
,
6
,
4
]
,
l
i
s
t
B
=
[
1
,
5
]
,
s
k
i
p
A
=
3
,
s
k
i
p
B
=
2
intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
intersectVal=0,listA=[2,6,4],listB=[1,5],skipA=3,skipB=2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null。
思路
本题使用了一些数学方法来解决,其证明方法在力扣的官方解法中写的十分仔细和细节,这里就不再赘述了。此处仅使用该方法。
首先,排除一下特殊情况,当两者中存在空链时,两条链不可能相交。
下面再来讨论非特殊情况,当两条链都不为空时,分别使用指针1,2来指向两条链的头节点。
构造一个循环,只要两个指针并不同时指向一个节点,那么就都会同时向后遍历。当指向的链遍历到了最后一个节点之后,就开始遍历另一条链,直到二者指向同一节点。
这里存在一个问题,即为什么不会陷入死循环。
首先,当两条链相交时,两指针一定会在相交的节点处相遇,那么此时即可跳出循环。
其次,当两条链不相交时,两指针因为都是以相同的步长移动了两个链表的长度,那么自然会在同一时间指向空节点,此时也会跳出循环。
解法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null){
return null;
}
ListNode p = headA;
ListNode q = headB;
while(p != q){
p = p==null? headB:p.next;
q = q==null? headA:q.next;
}
return p;
}
}
总结
初看此题时会有一种无处下手的感觉,但在看过数学原理之后就会发现豁然开朗,数学证明,我想不到但yyds!
142. 环形链表 Ⅱ
题目描述
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改链表。
示例1:
输入:
h
e
a
d
=
[
3
,
2
,
0
,
−
4
]
,
p
o
s
=
1
head = [3,2,0,-4], pos = 1
head=[3,2,0,−4],pos=1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例2:
输入:
h
e
a
d
=
[
1
,
2
]
,
p
o
s
=
0
head = [1,2], pos = 0
head=[1,2],pos=0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例3:
输入:
h
e
a
d
=
[
1
]
,
p
o
s
=
−
1
head = [1], pos = -1
head=[1],pos=−1
输出:返回 null
解释:链表中没有环。
思路
首先,本题能够看出有快慢指针的感觉。但关键在于如何使用和使用快慢指针可以得到什么。
由数学证明或一些自己的尝试可以发现,若链表中存在环,则快慢指针一定会在环中相遇。
那么此时,我们需要确定相遇的位置是否为进入环的第一个节点。但我们会发现,并不是,那么此时就又需要搜索到进入环的首个节点。
那么接下来需要考虑的就是如何搜索的问题。
在官方的解法中提供了很完整的数学推理和证明,这里就不再赘述了,一下是我的一些简单理解:
当快慢指针相遇时,其实可以理解为慢指针没走完环就被快指针拦下来了,而如果需要找到环的入口,只需要让慢指针走完环即可。那么慢指针还剩下多少没有走完呢,答案是还剩下快指针从头节点走到环入口的路程没有走完。这里需要查看官方的数学推理才能更好地理解。
那么当两个指针第二次相遇时,他们相遇的地点即为环入口。
实现
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(true){
if(fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if(fast == slow) break;
}
fast = head;
while(slow!=fast){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
总结
这里比较难的点其实在于数学上的推理,通过数学推理来理解两个指针的位置关系才能够想到两个指针的行动逻辑。