24. 两两交换链表中的节点
题目链接:click here
文章讲解:click here
题目描述:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路:
这道题要求两两交换,所以如果如果节点数少于两个就直接退出
交换的时候,要遵循昨天总结的“先记录,再修改”的原则,特别是为了能继续遍历链表,需要先记录好next,才能修改next。循环内部的具体的操作比较简单:拿到三个连续节点,last,cur 和 kid,
交换:cur 和 kid 要交换,于此行为相配合的是这两个节点的前、后两个节点,即
重指:last 的 next 要指向 kid,cur 的 next 则要指向 kid 的 下一个节点 kkid。
位移:随后 last 指针向右移动两位,到 原 kid 现 cur 处,cur 指针顺延到 last 指针后一位处。
循环上述交换、重指、位移操作即可。循环从哪里开始?
用虚拟头节点 vhead ,减少不必要的特殊处理。
所以可以从 last = vhead 开始,这样就算一共只有两个节点,按上面的流程也是能满足要求的交换他们的位置。什么时候退出?
单次交换完成后,last 依旧是有效的,所以需要看的是 cur 及其后一位,也就是下一次进行交换的两个节点是否都有效,只要有一个无效,就可以退出循环了。代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class Solution
{
public:
ListNode *swapPairs(ListNode *head)
{
if (!head || !(head->next))
return head;
ListNode *vhead = new ListNode(0, head);
ListNode *last = vhead;
ListNode *cur = last->next;
while (cur && (cur->next))
{
auto kid = cur->next;
auto kkid = kid->next;
kid->next = cur;
cur->next = kkid;
last->next = kid;
last = cur;
cur = last->next;
}
return vhead->next;
}
};时空复杂度分析:
完整遍历链表一次:O(n) 时间复杂度
没有额外存储:O(1) 空间复杂度
19. 删除链表的倒数第N个节点
题目链接:click here
文章讲解:click here
题目描述:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?思路:
基础要求:
先遍历整个链表,一共有多少个,然后看 倒数第 n 个 是 正数 第几个,再遍历到该位置进行删除即可。
进阶要求:
很奇怪啊很奇怪,我怎么能正好遍历到 倒数第 n 个 节点上?我似乎只能确保自己遍历到最后一个节点上了。
这俩节点有什么关系呢?那就是俩节点之间的距离是确定的,定义为从一个节点到另一个节点要走多少步。
显然,倒数第 n 个 节点与倒数第 1 个 节点之间的距离为 n-1。
那么,当我遍历的时候,采用双指针法:
让快指针先行 n-1 步,使得双指针的节点距离是 n-1,
那么当快指针到最后一个节点的时候,慢指针正好走到要删除的那个节点上。
实现中要注意,为了删一个节点,最好是拿到它的前一个节点,进行跳链接到它后一个节点来实现删除。
所以快指针要先偷跑一步,好让慢指针届时指向要删除的节点的前一个节点。
为了减少对头节点的特殊处理,采用虚拟头节点。代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class Solution
{
public:
ListNode *removeNthFromEnd(ListNode *head, int n)
{
ListNode *vhead = new ListNode(0, head);
auto slow = vhead;
auto fast = vhead->next; // 偷跑一步
for (int i = 0; i < n - 1; i++)
{
fast = fast->next; // 再跑n-1步
}
while (fast->next) // 当快指针到达最后一个节点时,慢指针到了该删的节点的前一个。
{
fast = fast->next;
slow = slow->next;
}
auto temp = slow->next;
slow->next = slow->next->next; // 跳链接
delete temp; // 删除
temp = nullptr;
return vhead->next;
}
};
面试题 02.07. 链表相交
题目链接:click here
文章讲解:click here
题目描述:简述为,两个单链表,找他们俩之间相交的部分的起点。
思路:
我的想法:
既然节点内有数据,且数据具有正数的特点,那么我先遍历A链表,把里面的数据全部改为负数。
再遍历B链表,挨个检查里面的数据是不是负数,是则意味着这个节点在A中也存在,即为二者的交点。
按照题目描述:不允许修改链表的结构,我并没有修改结构,只是修改了数据罢了。
不过提交测试后发现链表的数据也不能修改,那么就在返回交点结果前,再遍历一次A,把内部的值都改为原值即可。代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38class Solution
{
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
ListNode *vheadA = new ListNode(0, headA);
while (headA)
{
headA->val *= -1;
headA = headA->next;
}
while (headB)
{
if (headB->val < 0)
{
headA = vheadA->next;
while (headA)
{
headA->val *= -1;
headA = headA->next;
}
return headB;
}
else
{
headB = headB->next;
}
}
headA = vheadA->next;
while (headA)
{
headA->val *= -1;
headA = headA->next;
}
return nullptr;
}
};时间复杂度:O(2n+m)=O(n+m)
空间复杂度:O(1)
讲解中的思路:
利用了一个链表相交的特点,那就是两个非循环链表如果相交,那么必然拥有相同的终点。
利用这一特点,就比较好检查两个链表有没有相交了,以及相交在何处了。
如果两个链表一样长,那尾部本就是对齐的,直接以相等速度向后遍历,挨个比较当前节点的地址是否一样,是则相交。
如果两个链表不一样长,根据上述特性,若他们相交,必然拥有相同的终点,
于是乎先把尾部对齐,即让较长链表的迭代指针先走几步。
然后再两个迭代指针一起向前走,并挨个比较当前节点的地址是否一样,是则相交。时间复杂度:O(2n+2m)=O(n+m)
空间复杂度:O(1)
我实现的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66class Solution
{
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
ListNode *vheadA = new ListNode(0, headA);
ListNode *vheadB = new ListNode(0, headB);
auto temp_a = vheadA->next;
int size_a = 0;
while (temp_a)
{
size_a++;
temp_a = temp_a->next;
}
auto temp_b = vheadB->next;
int size_b = 0;
while (temp_b)
{
size_b++;
temp_b = temp_b->next;
}
temp_a = vheadA->next;
temp_b = vheadB->next;
int iter_size = 0;
if (size_a > size_b)
{
int offset = size_a - size_b;
iter_size = size_b;
while (offset--)
{
temp_a = temp_a->next;
}
}
else if (size_a < size_b)
{
int offset = size_b - size_a;
iter_size = size_a;
while (offset--)
{
temp_b = temp_b->next;
}
}
else
{
iter_size = size_a;
}
while (iter_size--)
{
if (temp_a == temp_b)
{
return temp_a;
}
else
{
temp_a = temp_a->next;
temp_b = temp_b->next;
}
}
return nullptr;
}
};
142. 环形链表II
题目链接:click here
文章讲解:click here
题目描述:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
思路:
这个技巧性太强了,属于是必背题,之前刷过,所以依稀记得要快慢指针,快指针每次跳两格,慢指针每次跳一格。
但是后续记得不太清楚了,于是又自己推理了一通:假设链表入环前的长度为 s ,环的长度为 c
当慢指针走到入口处时,共 s 步,快指针已经走了 2s 步,在环前、环上分别走了 s 步
很明显,快指针此时距离入口处的慢指针有 c - s 步距离(先假设环长 c 大于环前距离 s)。
现在把二者在环内的运动视为追赶运动,也就是快指针每次比慢指针快一步,他俩之间的距离就缩小一步。
c - s 步的距离需要 c - s 次运动。
追赶上时,慢指针从入口处前向运动了 c - s 步,二者都停留在这个位置。下面的内容就是之前记不太清楚的后续:
精髓的地方到了:此时二者再走 s 步就能回到环入口,s 正好也是从链表头到环入口的距离。
于是,把慢指针移动到链表头,快指针位置不变,二者都以单步1格的速度向前走
当二者再次相遇时,就正好都处在环的入口处了。如果是环长 c 等于 环前距离 s 的时候,更简单了,二者第一次相遇就在入口处,所以上述方法也能处理。
如果是环长 c 小于 环前距离 s 的时候,
当慢指针走到入口处时,共 s 步,快指针已经走了 2s 步,在环前、环上分别走了 s 步
那么快指针此时距离入口处有多远呢?
首先看快指针超出入口处多远:s - c?应该是 s - n * c,这个 n 最小为 1。记为 y = s - n * c < c所以可知快指针此时距离入口处的慢指针为 c - y 步,不论这个值具体是多少,但它都小于 c。
那么根据追赶运动每次会缩进快慢指针之间的距离一格,共需要 c - y 步,二者相遇。慢指针从入口处累计走了 c - y 步,所以二者相遇的位置也就是环上的 c - y 处
此时把慢指针移动到链表头,让他再以单格的速度跳到环入口,需要 s 步
快指针也以单格的速度,从刚刚双指针相遇的地方向前跳 s 步,将到达 c - y + s 处。
c - y + s = c - (s - n * c) + s = (n * c + c)
显然,由于 s > c,所以需要用上面这个结果对 c 取模运算,才能得到最终快指针的位置:
(n * c + c) % c = 0,所以此时快指针也正好回到环入口了。综上,各种情况都能用上述算法处理。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class Solution
{
public:
ListNode *detectCycle(ListNode *head)
{
if (!head)
{
return nullptr;
}
auto fast = head;
auto slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if (slow == fast)
{
slow = head;
while (fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return fast;
}
}
return nullptr;
}
};
今日收获
个人博客更新。若Blog有误或是有其他想交流的,左侧有联系方式。
复习:
两两交换链表中的节点:模拟题,需要把握循环块内的操作:交换、重指、移动,以及循环退出条件:下一次操作的两个节点任一无效。
删除链表的倒数第N个节点:根据确定性的能遍历到链表尾 + 待删除节点与链表尾的距离信息,用双指针控制相同的距离,来实现快指针到尾部时,慢指针指向待删除节点的前一个节点(快指针需要偷跑1格)。
链表相交:利用相交链表尾部对齐的特性,先将长短链表变成尾部对齐,忽略长链表的前部分,再逐一比对地址即可确认是否相交。
环形链表II:技巧题,直接记解法:快慢双指针,快的跳2格,慢的跳1格,相遇后慢指针移动到链表头,二者一起以1格速度移动,再次相遇处即为环形入口。耗时:3h