- 删除链表的倒数第N个结点
- 链表相交
- 环形链表Ⅱ
删除链表的倒数第N个结点
解法:双指针(快慢指针)
首先一定要有删除结点的思想。所以这个题是用虚拟头结点比较方便。
先上模拟图,然后看流程:
这里后移根据不同的想法有不同的后移步数。
这里我比较喜欢移动对应的上的步数。这里图里面的解法是移动n+1步。其实移动n步也可以做。
我写一个移动n步的。
我比较喜欢这个图的做法。
流程:
1.初始化fast=dummyhead,slow=dummyhead。
2.先让fast走n步。
3.然后fast和slow一起走。
4.一起走的循环停止条件:fast.next!=null。
5.此时完成删除操作即可,slow.next = slow.next.next;
/**
* 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 dummyhead = new ListNode(0); //创建一个虚拟头结点
dummyhead.next = head; //把虚拟头结点加在链表前面
ListNode fast = dummyhead; //快慢指针都指向头结点
ListNode slow = dummyhead;
for(int i = 0;i<n;i++){ //让快的指针先走n步
fast = fast.next;
}
while(fast.next!=null){ //两个一起走。注意这个终止条件,按图来。
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next; //删除操作
return dummyhead.next; //返回链表头结点。
}
}
链表相交
这里进行一个小总结:虚拟头结点一般是需要做修改操作时才用到。查找一般不用
这个解法很特殊,建议记下来。
也是双指针解法,但是有数学规律。
解法总结一句话:
只要两个有相交,A,B同时往后走,A到底(null)后指回B,B到底后指回A。依然同时往后走,最终一定会相遇。
根据这句话代码很容易就写出来了。
/**
* 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) {
//之前说的,建议定义迭代指针.各指向每个链表的头结点
ListNode a = headA;
ListNode b = headB;
//终止的条件就是二者相遇。
while(a!=b){
if(a==null){
a = headB; //a走到底了就到B
}else{
a = a.next;
}
if(b==null){
b = headA;//b走到底了就到A
}else{
b = b.next;
}
}
return a; //最后a=b了。随便返回一个。
}
}
如果不懂为什么这么干就可以得到相等。可以看看这个数学推导。
按刚刚的流程会发现,a走到相交结点走的步数就是a-c+c+b-c = a+b-c
b走到相交结点的步数是b-c+c+a-c = a+b-c
会发现其实a,b沿着这样的路线走到首个公共结点。走的步数是一样的。
所以就可以推出两个一直往后走,然后走到底了a到B开始,b到A开始。肯定会到首个公共结点相遇。因为走的步数是一样的。
补充:这个题我第二次写的时候,发现有个问题我没思考清楚。
那就是链表不相交的时候我没去模拟过这个过程。
之前写的逻辑就已经考虑到了不相交的情况。我之前在想,不相交的情况会出现二者都同时指向null的情况吗?
答案是会的。因为二者就算不相交,最后走的步数都是lengthA+lengthB。所以说最后都会停在null这个位置。但是这里必须要注意一个要点,最后这个null必须也要看做一个可踩的点,如果最后的null看做不可踩的点,那么就会出现担心的无限循环的情况,想想也是,如果null不可踩那就永远遇不到二者同时为null。
环形链表Ⅱ
直接看这个题解:
https://leetcode.cn/problems/linked-list-cycle-ii/solutions/12616/linked-list-cycle-ii-kuai-man-zhi-zhen-shuang-zhi-
里面有一张图,看完我只能说就做出来了。
做法总结:
设置双指针,fast和slow。fast两次从head头结点出发。
第一次:
fast一次走两步,slow一次走一步。如果有环那slow和fast一定会相遇。
第二次:也就是slow和fast相遇了,fast回到head。之后,fast和slow同时走,两个都是每次只走一步。最终fast和slow一定会再相遇。而且相遇的这个点就是环的第一个点。
注意循环条件。
第一次两个相遇,这个循环必须要永真循环。因为这个循环还必须做一个特判。如果fast走下去走到了null。就必须直接return null了。
那么这里就要写个if判断,由于fast要走两步,所以就要判断fastnull || fast.nextnull。并且这样有一个短路的效果。
第二个相遇就是两个无脑后退,while的条件就是slow != fast。
/**
* 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; //起点都在head
ListNode slow = head;
while(true){ //第一个循环
if(fast == null || fast.next == null){//而且还弄了个短路效果
return null; //特判,万一没有环。
//由于每次都要走两步,这里还有个细节,还防止了head=null的情况,那么就要判断,fast==null,然后再判断下一步,如果fast.next=null。那么就也没必要走了。因为下一次一次走两步,会导致fast = null。所以逻辑是对的。
}
fast = fast.next.next; //fast一次走两步
slow = slow.next; //slow一次一步
if(fast == slow){
break; //第一次相遇就停下来
}
}
fast = head; //fast回到头结点。
while(slow!=fast){ //下一次二者相遇的时候就是环的第一个结点
slow = slow.next; //二者一次走一步
fast = fast.next;
}
return fast; //返回fast和slow都行
}
}
数学推导:
设:链表头部到环入口(不包括环的入口元素) 为a,环有b个结点,则链表总共有a+b个结点。
在第一轮。两个指针分别走了。f和s步。
fast走的步数是slow的两倍。即f=2s。
fast比slow多走了n个环的长度,即f=s+nb(这个自己多想想)。
两式相减得到s=nb。f =2nb。所以可得到。fast和slow分别走了2n和n个环的周长。
如果让指针从链表头部一直往前走并统计步数k,那么所有走到链表入口节点时的步数是k=a+nb,即先走a步到入口节点。之后每绕一圈环(b步)都会再次到入口节点。而目前slow指针走了nb步。因此,我们只要想办法slow再走a步停下来,就可以到环的入口了。
但是我们不知道这个到底是多少,那么此时仍然是双指针法,考虑构建一个指针,此指针需要有这样的性质:和slow同时往前走a步后,两者在入口节点重合。此时显而易见,从哪里走到入口节点需要a步?那就是head头节点。
所以第二次相遇的逻辑就来了。
1.令fast重新指向head。此时f = 0,s = nb。
2.slow和fast同时每轮往前走一步
3.当fast指针走到f = a步时,slow指针就走到了s = a+nb,此时fast和slow在环入口重合。返回slow指向的结点即可。
补充:
我第二次写的时候在if判断那里有点问题,一定要想清楚。