代码随想录算法训练营第四天 | 24. 两两交换链表中的节点 19.删除链表的倒数第N个节点 面试题 02.07. 链表相交 142.环形链表II
24. 两两交换链表中的节点
本题是一道模拟过程的题目。搞清楚两两交换的步骤之后,写出对应的代码也就不是难题了。不过在学习题解的过程中发现,两两交换的步骤也有很多种实现方式。自己在做题目的时候使用的思路如下:
进行两两交换之前,设置三个指针,分别指向dummy
,head
和head.next
。因为需要指向head
和head.next
,所以需要预先判断这两者是否为null
。其余交换步骤如图所示。
但可以发现,如果按照这种交换方法,当链表节点个数为偶数时,完成最后一对节点的交换时,cur
会指向最后一个节点,此时tmp = cur.next.next
会出现空指针异常。因此,在进行指针移位操作前,进行一次判断。
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode dummy = new ListNode();
dummy.next = head;
ListNode pre = dummy;
ListNode cur = head;
ListNode tmp = head.next;
while (pre.next != null && pre.next.next != null) { // 确保之后还存在至少一对可交换节点
cur.next = tmp.next;
tmp.next = cur;
pre.next = tmp;
if (cur.next == null) {
break;
} else {
tmp = cur.next.next;
pre = cur;
cur = cur.next;
}
}
return dummy.next;
}
}
卡哥博客内的Java题解的思路图解如下:
与自己的思路不同,题解选择存储head.next.next
。在进行两两交换操作时,第二步改变head.next
的next
。
附记:本题还存在递归解法,因为时间问题今天没能学习,作为后续要处理掉的遗留问题之一。
19.删除链表的倒数第N个节点
最直观的思路就是通过一次遍历统计出链表的长度,然后再根据链表长度遍历到倒数第n个节点的前一个节点(想要对一个节点进行操作,必须移动到该节点的前一个节点),然后删除目标节点。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode cur = head;
ListNode dummy = new ListNode(0, head);
int size = 0;
while (cur != null) { // 遍历链表,获取链表长度
size++;
cur = cur.next;
}
System.out.println(size);
ListNode pre = dummy;
cur = head;
for (int i = 0; i < size - n; i++) { // 移动到目标节点的前一个节点
pre = cur;
cur = cur.next;
}
pre.next = cur.next; // 删除节点
return dummy.next;
}
}
题目提示存在只需遍历一次链表的方法。在自己做题的过程中没能想出来,查看卡哥给出的思路之后直呼精妙.jpg。这个方法使用了双指针,预先将双指针拉开n个节点的长度,当靠后的节点移动至链表的最后一个节点时,靠前的节点便正好移动到了目标节点的前一个节点(再次强调,想要对一个节点进行操作,必须移动到该节点的前一个节点),进行删除操作即可。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
ListNode pre = dummy;
ListNode cur = dummy;
for (int i = 0; i < n; i++) {
pre = pre.next; // 确保cur最后停在目标节点的前一个节点 #1
}
while (pre.next != null) { // 确保cur最后停在目标节点的前一个节点 #2
pre = pre.next;
cur = cur.next;
}
cur.next = cur.next.next; // 删除操作
return dummy.next;
}
}
面试题 02.07. 链表相交
自己一开始的写法非常暴力,直接将两个链表的所有节点分别存储在ArrayList里,然后倒着比较。需要对一整个链表与另一个链表的一部分相交进行额外的判断。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ArrayList<ListNode> listA = new ArrayList<ListNode>();
ArrayList<ListNode> listB = new ArrayList<ListNode>();
ListNode cur = headA;
while(cur != null) { // 暴力存储A
listA.add(cur);
cur = cur.next;
}
cur = headB;
while (cur != null) { // 暴力存储B
listB.add(cur);
cur = cur.next;
}
if (listA.get(listA.size() - 1) != listB.get(listB.size() - 1)) { // 最后一个节点都不相等的两个链表不可能存在相交点
return null;
} else {
int i = 1;
for (; i <= Math.min(listA.size(), listB.size()); i++) {
if (listA.get(listA.size() - i) != listB.get(listB.size() - i)) {
return listA.get(listA.size() - i).next;
}
}
if (listA.get(listA.size() - i + 1) == listB.get(listB.size() - i + 1)) { // 对一整个链表都和另外一个链表的一部分相交的情况进行额外判断
return listA.get(listA.size() - i + 1);
}
}
return null;
}
}
当然这并不是优秀的算法,需要额外花费两个ArrayList的空间(也就是O(n)的空间复杂度?)。
题目提示存在空间复杂度为O(1)的算法。寻找链表相交的节点,本质可以看作寻找链表的倒数第n个相同节点。当链表遇到与倒数第n个节点节点相关的问题时,就可以考虑使用双指针,将两个指针拉开指定距离以达到进行与倒数第n个节点相关联的操作。
使用双指针时,先分别遍历两个链表,得到链表的长度sizeA
和sizeB
(在这里,假设sizeA > sizeB
便于之后的叙述)。然后,使指针pre
指向链表A,cur
指向链表B。之后移动pre
,使pre
指向倒数第sizeB
个节点。pre
所指向的链表A的节点及其之后的节点便是有可能与链表B相交的部分。之后,同时移动pre
与cur
,当遇到pre == cur
时返回这一节点。
上述文字描述画图如下:
理解逻辑之后,实现代码如下:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
int sizeA = 0;
int sizeB = 0;
ListNode cur = headA;
while (cur != null) {
sizeA++;
cur = cur.next;
}
cur = headB;
while (cur != null) {
sizeB++;
cur = cur.next;
}
ListNode pre = sizeA > sizeB ? headA : headB; // 指向较长链表
cur = pre == headA ? headB : headA; // 指向较短链表
for (int i = 0; i < Math.abs(sizeA - sizeB); i++) { // 移动至较长链表可能与较短链表发生相交的部分的起始位置
pre = pre.next;
}
while (pre != null && cur != null) {
if (pre == cur) {
return pre;
}
pre = pre.next;
cur = cur.next;
}
return null;
}
}
142.环形链表II
读完题目整个人处于“我是谁,我在哪,我要做什么”的状态,纠结良久决定放弃,直接学习卡哥博客。
看完博客消化良久的我:这就是程序员才能做的追及问题吗.jpg
重点在于如何制造条件使得确定环入口成为可能。这里使用的是双指针变体,快慢指针。
设定slow
与fast
指针,令slow
指针每次只移动一步,而fast
指针每次移动两步。那么,如果存在环,两个指针一定会在环内相遇。
-
Q1:为什么一定在环内相遇?
fast
指针一定先于slow
指针进环,那么,两者的相遇就一定只可能在环内。 -
Q2:那么为什么两个节点一定会相遇?
设想一下两个指针都进入环内的场景,这个时候,问题就变成了一个非常经典的追及问题。在fast
走两步,slow
走一步的情况下,每一次行动时fast
都会离slow
更近一个节点。因此,不存在fast
没有与slow
相遇就已经越过slow
的可能。
确定了fast
和slow
一定会在环内相遇后,就可以将相遇转变成数学问题,找到相遇与环入口之间的关系了。
- Q3:光看这张图仍然无法确定数学关系,我们无法确定
slow
移动的步数。
实际上是可以确定的。上文提到,一旦两个指针都进入环内,问题就演变成了追及问题。当slow
入环时,追及问题处于最开始的阶段。此时两个指针之间的距离不可能大于等于环本身的长度。因此,以fast
一次追一个节点的速度,当fast
与slow
相遇时,slow
一定没有转完一整圈。
确定这一点后就可以列出数学式子了。设链表头距离环入口的节点数为x
,相遇时slow
在环内移动的节点数为y
,环剩余的节点数为z
,可以列出以下数学表达式:
(x + y) * 2 = x + y + n * (y + z)
等式左侧是slow
移动的总步数,右侧是fast
移动的总步数,其中n为在环内转的圈数。根据之前的设置,可以确定fast
移动的步数是slow
的两倍。由于本题我们想求的是x
,因此将等式变形得:
x = n * (y + z) - y
由于在与slow
相遇之前,fast
至少会在环内移动一整圈,因此取n = 1
,可以发现x = z
。当n > 1
时,增加的只是y + z
,即在环内完整地多转了多少圈。因此,我们可以通过设置pre
和cur
指针,pre
指向链表头,cur
指向fast
与slow
相遇的位置,两个指针每次都只移动一步。不难发现,当两者相遇时,相遇位置一定是环的入口。
逻辑如上,根据逻辑写出来的代码如下:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 移动一步
fast = fast.next.next; // 移动两步
if (slow == fast) {
ListNode pre = head;
ListNode cur = fast;
while (pre != cur) {
pre = pre.next;
cur = cur.next;
}
return pre;
}
}
return null;
}
}
代码并不长,难点在于设计出这样一套逻辑,希望自己二刷的时候能够完全消化本题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?