24. 两两交换链表中的节点
文档讲解:代码随想录
视频讲解:B站视频
状态:没有做出来,在了解了亮亮交换的三个步骤之后做出来了。我认为单这一道题而言,递归的方法更容易理解
首先介绍一下交换节点的一般步骤和迭代法,两两交换节点时,需要改变三个节点的指针,因此需要使用虚拟头节点,完成交换并返回正确的头节点。节点交换的方法可以分为以下三个步骤:
这种方法在每次迭代时需要三个节点,并且需要改变这三个节点的指针,每次迭代需要前进两个单位的索引。在编写代码的时候需要注意临界条件,奇数链表的最后一个节点不必交换
迭代的代码如下:
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode virhead = new ListNode(-1, head);
var index = virhead;
var first = index.next;
if (first == null || first.next == null) return virhead.next;
var second = index.next.next;
var temp = second.next;
while (second != null) {
// 三次指针交换
index.next = second;
first.next = second.next;
second.next = first;
//更新三个指针
if (first.next == null || first.next.next == null) break;
index = first;
first = first.next;
second = first.next;
}
return virhead.next;
}
}
递归的方法就不必关系节点的索引移动了,递归通常考虑以下几个问题
- 递归的退出条件
- 递归每次传入函数的参数变化
- 每一个子问题中应该完成的工作
在这个问题中,递归的要素如下:
- 当传入头节点为空或者头节点的下一个节点为空时,即遍历完成或者只剩最后一个节点时,函数退出
- 需要交换的节点的前一个节点作为头节点
- 与迭代法每次循环完成的工作一致,交换节点。
递归方法的代码如下,相比起迭代方法,空间复杂度为O(n)。
过程中遇到的问题大多是临界条件的问题。
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) return head;
var first = head;
var second = head.next;
var newnode = swapPairs(second.next);
second.next = first;
first.next = newnode;
return second;
}
}
19. 删除链表的倒数第N个节点
这道题较为简单,可以使用两次循环,先得到链表长度,再将倒排索引转换为正排索引解决。
也可以使用双指针,前一个指针延迟后一个指针N个索引,遍历一次链表解决;
注意边界条件为:前一个指针需要指向链表最后一个元素而不指向空,代码如下:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode virhead = new ListNode(-1, head);
var fast = virhead;
var slow = virhead;
for (int i = 0; i < n; i++) {
fast = fast.next;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return virhead.next;
}
}
160. 链表相交
文档讲解:代码随想录
状态:由于两个链表的长度不同,第一次刷无从下手,看文档后得知解决方法
使用双指针的方法如下:
- 使用两个指针指向两个链表头,然后遍历得到两个链表的长度
- 根据长度同步两个链表的指针,让它们有可能同时指向相交节点
- 遍历节点,找到相交节点,遍历结束后没找到则返回null
代码如下:
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
var indexA = headA;
int sizeA = 0;
var indexB = headB;
int sizeB = 0;
while (indexA != null) { // 得到A链表长度
sizeA++;
indexA = indexA.next;
}
while (indexB != null) { // 得到B链表长度
sizeB++;
indexB = indexB.next;
}
if (sizeA > sizeB) {
int offset = sizeA - sizeB;
for (int i = 0; i < offset; i++) {
headA = headA.next;
}
}
else {
int offset = sizeB - sizeA;
for (int i = 0; i < offset; i++) {
headB = headB.next;
}
} // 完成指针同步,两指针会一同指向汇聚节点或者一同指向null
while (headA != null && headA != headB) { //这个条件是表示若存在相交节点触发第二个条件,否则触发第一个条件
headA = headA.next;
headB = headB.next;
}
return headA;
}
}
另外,代码随想录网站给出更简洁的方法,有空再看
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// p1 指向 A 链表头结点,p2 指向 B 链表头结点
ListNode p1 = headA, p2 = headB;
while (p1 != p2) {
// p1 走一步,如果走到 A 链表末尾,转到 B 链表
if (p1 == null) p1 = headB;
else p1 = p1.next;
// p2 走一步,如果走到 B 链表末尾,转到 A 链表
if (p2 == null) p2 = headA;
else p2 = p2.next;
}
return p1;
}
}
142. 环形链表Ⅱ
文档讲解:代码随想录
视频讲解:B站视频
状态:可以使用快慢指针检测出是否存在环,但是无法判断入环节点,看文档学会了,但是需要复习。
这个问题可以分解为两个问题
- 如何检测是否存在环
- 如何检测出环的入口
首先第一个问题,快慢指针可以解决,这个视频讲的很形象,最后得到的结论是,若有环,必然存在两个指针指向同一个节点。
第二个问题:如何找到入口节点,这一部分需要一些数学推导,画一个图说明
假设环的长度cirlen = index + z
,其中index
表示快慢指针相遇的位置相比环入口的偏移
pos
表示环入口相对头节点的偏移,则可以得知快慢指针相遇时, fast走过2倍于slow的路程
(pos + index) * 2 = n(index + z) + pos + index
,,提出pos得到pos = (n-1)(index + z) + z
以上公式表示可以用环的长度和慢指针的位置得到环入口位置。也就是说,如果两个指针同步移动,当一个指针偏移了pos时,一个位于环中的指针正好从慢节点位置便宜到入口节点,此时两个指针指向同一个节点,代码如下:
class Solution {
public ListNode detectCycle(ListNode head) {
var fast = head;
var slow = head;
int counter = 0;
byte flag = 0;
// 检测是否存在环
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
counter++;
if (fast == slow) {
flag = 1;
break;
}
}
// 如果存在环,则寻找入口
while (flag == 1) {
if (head == slow) return head;
slow = slow.next;
head = head.next;
}
return null;
}
}