代码随想录训练营D4-链表篇 day2| 24. 两两交换链表中的节点、19.删除链表的倒数第N个节点、 面试题 02.07. 链表相交、 142.环形链表II
(〇)理论
1.头结点:
使用虚拟头结点dummyHead的好处是:可以对第一个结点“一视同仁”,不用判断并单独处理第一个结点
(一) 24. 两两交换链表中的节点
用虚拟头结点,这样会方便很多。
1. 思路
我将一次while循环中的操作叫做一组。关于一组操作的划分:设当前结点为cur,起始时cur指向虚拟的头结点,此时需要进行调换位置的是,第一个、第二个有内容的结点。调换完之后变成了新的第一个、新的第二个节点,此时cur应指向新第二个结点,以此来进行第三个、第四个结点的调换。
while的终止条件呢?如果cur后没有结点,或只有一个结点都是不需要调换的,即while能运行下去的条件是while(cur.next != null && cur.next.next != null) 注意是*&&,A或B的否命题是非A且非B*
力扣上的链表都是没有虚拟头结点的,所以如果想用的话,要自己设置一个。
2. 代码
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;//虚拟头结点创建完毕
ListNode cur = dummyHead;//工作结点
ListNode temp2;//cur后的第2个结点
ListNode temp3;//cur后的第三个结点,防走丢
while(cur.next != null && cur.next.next != null){
temp3 = cur.next.next.next;
temp2 = cur.next.next;
//把2插入 cur和1之间
temp2.next = cur.next;
cur.next = temp2;
//新2 与 旧3 连接
temp2.next.next = temp3;
//更新 cur
cur = temp2.next;
}
return dummyHead.next;
}
}
(二) 19.删除链表的倒数第N个节点
双指针的操作,要注意,删除第N个节点,那么我们当前遍历的指针一定要指向 第N个节点的前一个节点
1. 思路
采用快慢指针的思想。
首先,明确 要删除倒数第n个结点,那么查找的指针要落在倒数第n+1个结点上,这样才好删除倒数第n个。
其次,如何让一个指针准确的落在n+1呢?我们目前可以准确落的位置包括:倒数第0个(从头遍历到null时)。所以可以与a指针保持n+1个距离发出一个b指针,当a指针到达最末null位置时,b指针刚好到达倒数n+1的位置上。
最终,可以开始删除了。
快指针a先出发,慢指针b延后n+1个身位再出发。
直到快指针a为null时停止。开始进行删除操作。
2. 代码
public ListNode removeNthFromEnd(ListNode head, int n) {
//先自己加一个虚拟头结点
ListNode dummyHead = new ListNode();
dummyHead.next = head;
//设置快慢指针
ListNode fast = dummyHead, slow = dummyHead;
int count = 0;//计数 看fast已走了几步
while(fast != null){
fast = fast.next;
count++;
if(count > n + 1){//1
slow = slow.next;
}
}
slow.next = slow.next.next;
return dummyHead.next;//2
}
3. 实现过程中的问题
1.注意何时slow开始走 是count > n + 1
2.最后返回的是dummy.next,因为有可能原本的head头结点也要被删除。!
(三) 面试题 02.07. 链表相交
链表相交,指有结点地址相同。而非数值相同
并且由于每个结点只有一个next指针,所以一旦相交,后面就一直重合。
1. 思路
链表1在相交之前的长度为m,链表2在相交之前的长度为n,剩余的两者重合的链表长度为a。m + a + n == n + a + m。指针p遍历链表1,结束后,去遍历链表2;指针q遍历链表2,结束后去遍历1;这样当指针p走过m + a + n时,指针q也走过了n + a + m;两指针刚好相遇。
若两个链表没有交点呢?!指针p走过m+n ,指针q走过n+m,两个指针都会指向null,还是会相等的。
2. 代码
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA;
ListNode q = headB;
//链表有相交,最终在相交的结点处,while会停止;若无交点,最终两个指针都会指向null,也会相等
while(p != q){
//p不为null 就next,p为null 就切到headB;q同理
p = p == null ? headB : p.next;
q = q == null ? headA : q.next;
}
return p;
}
}
(四) 142.环形链表II
算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口。
1. 思路
1.1 直观思路
遍历;检查是否在set中已有该结点,若有返回;否则,将刚遍历过的结点存入哈希set中
1.2 其他思路
使用双指针法。其他可以使用双指针法题目还包括:寻找距离尾部第 K 个节点、寻找环入口、寻找公共尾部入口等。
1.指针第一次相遇
设置fast,slow 两个指针均指向head。fast每次走两步,slow每次走一步。=>f = 2s (1)
1)若无环,则fast会走到null,此时停止即可
2)若有环。
fast先进入环。当slow进入环后,相对slow而言,fast每次都会接近slow一步,最终 fast会追上slow。
设在环的入口前有结点a个,环的结点数b个。在fast与slow在环中相遇时。fast已经走过f = s + nb(2)(f除了走过s走过的路,还一直在环里转圈等b,所以多走了nb)
根据式(1) f=2s,式(2) f = s + nb 可以解出(3) s = nb; (4)f = 2nb
2.分析
再想一下,若从头结点开始走,走到环入口位置的指针都是走多少步呢?k = a + nb。s指针已经走了nb了,再走a步就会到达入口处。但我们却不知道a是多少,如何找到帮我们衡量a长度的方法呢?想到a的定义 本身就是从头结点到入口处的距离。所以此时让fast结点指向head,与slow一起走a步就会 一起到达入口处!
2. 代码
1.set
public class Solution {
//返回链表开始入环的第一个节点
public ListNode detectCycle(ListNode head) {
HashSet<ListNode> set = new HashSet<>();
while(head != null){
if(set.contains(head)){
return head;
}
set.add(head);
head = head.next;
}
return null;
}
}
时间复杂度 O(n)
空间复杂度O(n)
2.双指针
public ListNode detectCycle1(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
//两指针相遇时,就要将fast指向head了
if(fast == slow){
fast = head;
while (fast != slow){
fast = fast.next;
slow = slow.next;
}
//两指针相遇了 到达环的入口了
return fast;
}
}
//只有fast == null || fast.next == null 才会到这 说明没环
return null;
}
时间复杂度O(n)//通过慢指针考虑
空间复杂度O(1)
(五) 今日收获
关于链表的问题。经常使用到的是双指针法。
包括但不限于:寻找距离尾部第 K 个节点、寻找环入口、寻找公共尾部入口等。