第二章-链表 part02
今日任务
-
- 两两交换链表中的节点
- 19.删除链表的倒数第 N 个节点
- 面试题 02.07. 链表相交
- 142.环形链表 II
- 总结
第一题:24. 两两交换链表中的节点
用虚拟头结点,这样会方便很多。
本题链表操作就比较复杂了,建议大家先看视频,视频里我讲解了注意事项,为什么需要 temp 保存临时节点。
题目链接:24. 两两交换链表中的节点 - 力扣(LeetCode)
文章讲解/视频讲解:代码随想录 (programmercarl.com)
方法一 - 虚拟头结点
- 题目强制要求交换结点,而不是交换数值
- 使用虚拟头结点 一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序!!!
每一次循环:
将 currentNode 移动到 tempNode1
下一次循环
直到 currentNode.next != null && currentNode.next.next != null 退出循环
- 前者判断偶数情况 currentNode.next != null
- 后者判断奇数情况 currentNode.next.next != null
Code:
/**
* 方法一 - 虚拟头结点
*/
class Solution {
public ListNode swapPairs(ListNode head) {
// 虚拟头结点
ListNode dummyHead = new ListNode(Integer.MAX_VALUE, head);
// 当前节点
ListNode currentNode = dummyHead;
// 由题意可知有两种情况
// 1. 偶数时 两两交换 循环结束条件为 currentNode.next != null
// 2. 奇数时 两两交换 剩一个 结束条件为 currentNode.next.next != null
// 注意 一定是 currentNode.next != null 写在前面 防止偶数时出现空指针异常
while (currentNode.next != null && currentNode.next.next != null) {
// 两两交换的策略
// 1. 将当前节点 currentNode -> 当前节点的 next 的 next 那个节点
// 2. 将当前节点的 next 的 next 那个节点 -> 当前节点的 next 那个点
// 3. 将当前节点的 next 那个点 -> 当前节点的 next 的 next 的 next 的节点
// 注意:在操作第一步前 要把当前节点的 next 节点保存下来 - tempNode1
// 这样就可以避免出现 当前节点已经指向其他点导致获取不到 当前点的 next 点
// 同理 第二步前 也要存一个临时值 - tempNode2
ListNode tempNode1 = currentNode.next;
currentNode.next = currentNode.next.next;
ListNode tempNode2 = currentNode.next.next;
currentNode.next.next = tempNode1;
tempNode1.next = tempNode2;
currentNode = tempNode1;
}
return dummyHead.next; // 返回虚拟节点的下一个节点就是我们合法的链表
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(1)
方法二 - 递归法
- 挖坑 以后熟练在研究~
第二题:19.删除链表的倒数第 N 个节点
双指针的操作,要注意,删除第 N 个节点,那么我们当前遍历的指针一定要指向 第 N 个节点的前一个节点,建议先看视频。
题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
文章讲解/视频讲解:代码随想录 (programmercarl.com)
思路
双指针的经典应用,如果要删除倒数第 n 个节点,让 fast 移动 n 步,然后让 fast 和 slow 同时移动,直到 fast 指向链表末尾。删掉 slow 所指向的节点就可以了。
-
首先使用虚拟头结点,这样方便处理删除实际头结点的逻辑
-
定义 fast 指针和 slow 指针,初始值为虚拟头结点,如图:
- fast 首先走 n + 1 步 ,为什么是 n+1 呢,因为只有这样同时移动的时候 slow 才能指向删除节点的上一个节点(方便做删除操作),如图:
- fast 和 slow 同时移动,直到 fast 指向末尾,如题:
- 删除 slow 指向的下一个节点,如图:
方法一 - 快慢指针
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyHead = new ListNode(Integer.MAX_VALUE,head);
ListNode fast = dummyHead; // 快指针
ListNode slow = dummyHead; // 慢指针
// 快指针先向后移动 n + 1步
while (n-- != 0){
fast = fast.next;
}
fast = fast.next;
// 当快指针走到 null 时,慢指针正好在需要删除节点的前一个节点
while (fast != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next; // 删除 链表的倒数第 N 个结点
return dummyHead.next;
}
}
第三题:面试题 02.07. 链表相交
本题没有视频讲解,大家注意 数值相同,不代表指针相同。
思路
- 简单来说,就是求两个链表交点节点的指针。
- 注意,交点不是数值相等,而是指针相等。
看如下两个链表,目前 curA 指向链表 A 的头结点,curB 指向链表 B 的头结点:
我们求出两个链表的长度,并求出两个链表长度的差值,然后让 curA 移动到,和 curB 末尾对齐的位置,如图:
此时我们就可以比较 curA 和 curB 是否相同,如果不相同,同时向后移动 curA 和 curB,如果遇到 curA == curB,则找到交点。
否则循环退出返回空指针。
方法一 - 对齐法
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 初始化
int lengthA = 0; // A 链表长
int lengthB = 0; // B 链表长
ListNode curA = headA;
ListNode curB = headB;
while (curA != null) {// 求链表A的长度
lengthA++;
curA = curA.next;
}
while (curB != null) { // 求链表B的长度
lengthB++;
curB = curB.next;
}
// 重置
curA = headA;
curB = headB;
// 求长度差
int lengthAB = lengthA - lengthB;
if (lengthAB >= 0) { // 如果 A 链表长 则让A先走 使得 A B 对齐
while (lengthA-- != lengthB) {
curA = curA.next;
}
} else { // 如果 B 链表长 则让B先走 使得 A B 对齐
while (lengthA++ != lengthB) {
curB = curB.next;
}
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != null) {
if (curA == curB) {
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}
}
第四题:142.环形链表 II
算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口,建议先看视频。
题目链接:142. 环形链表 II - 力扣(LeetCode)
文章讲解/视频讲解:代码随想录 (programmercarl.com)
思路
- 判断链表是否环
- 如果有环,如何找到这个环的入口
判断链表是否有环
-
使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发
- fast 指针每次移动两个节点
- slow 指针每次移动一个节点
-
如果 fast 和 slow 指针在途中相遇 ,说明这个链表有环
- 因为两者的相对速度是 1 因此如果有环必定会相遇~
找到这个环的入口
-
slow 指针走过的节点数为: x+y
-
fast 指针走过的节点数:x+y+n(y+z)
- n 为 fast 指针在环内走了 n 圈才遇到 slow 指针,(y+z)为 一圈内节点的个数 A。
-
因为 fast 指针是一步走两个节点,slow 指针一步走一个节点,
-
所以 fast 指针走过的节点数 = slow 指针走过的节点数 * 2:
-
整理得到:
- 注意这里 n 一定是大于等于 1 的,因为 fast 指针至少要多走一圈才能相遇 slow 指针。
-
-
这个公式说明
-
先拿 n 为 1 的情况来举例,意味着 fast 指针在环形里转了一圈之后,就遇到了 slow 指针了。
- 当 n 为 1 的时候,公式就化解为 x = z,
-
从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点
- 这里的 n 就是表示 相遇点出发的指针 index1 在环形链表中的圈数
-
-
定义一个指针 index1,在头结点处定一个指针 index2。
- 让 index1 和 index2 同时移动,每次移动一个节点, 那么他们相遇的地方就是环形入口的节点。
方法 - 快慢指针 + 双指针
/**
* 快慢指针 + 双指针
*/
class Solution888 {
public ListNode detectCycle(ListNode head) {
ListNode fast = head; // 快指针
ListNode slow = head; // 慢指针
ListNode index1 = null; // 相遇点
ListNode index2 = head; // 起点
// 判断链表是否有环
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) { // 相遇点
index1 = fast;
break;// 别忘了退出哦~
}
}
// 找到这个环的入口
while (index1 != null) {
if (index1 == index2) {
return index1;
}
index1 = index1.next;
index2 = index2.next;
}
return null;
}
}
总结
- 感谢大佬!!!海螺人