今日任务:
24. 两两交换链表中的节点 ;19.删除链表的倒数第N个节点 ;
面试题 02.07. 链表相交 ;142.环形链表II
Part_1 力扣24. 两两交换链表中的节点
题目描述:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/swap-nodes-in-pairs
个人解答:
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (!(head && head->next)) return head;
else {
ListNode* p = head;
ListNode* q = p->next;
//第一组组内
head = q;
p->next = q->next;
q->next = p;
while (p->next && p->next->next) {
ListNode* tmp = p;//暂存
q = p->next->next;//移位
p = p->next;
tmp->next = q;//组间
p->next = q->next;//组内
q->next = p;
}
return head;
}
}
};
本题的两两交换是默认两个一组,因此交换节点涉及组内操作以及组间操作两部分,同时还有迭代移位到下一组,建议可以用三个节点简单画一下流程即可。下面看一下卡哥解答:
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
ListNode* cur = dummyHead;
while(cur->next != nullptr && cur->next->next != nullptr) {
ListNode* tmp = cur->next; // 记录临时节点
ListNode* tmp1 = cur->next->next->next; // 记录临时节点
cur->next = cur->next->next; // 步骤一
cur->next->next = tmp; // 步骤二
cur->next->next->next = tmp1; // 步骤三
cur = cur->next->next; // cur移动两位,准备下一轮交换
}
return dummyHead->next;
}
};
卡哥使用了虚拟头节点避开了个人解答中对于第一组无需组间操作的单独讨论,可以说是真正灵活掌握了真假头结点的适用场景。
Part_2 力扣19.删除链表的倒数第N个节点
题目描述:
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list
个人解答:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* p = head;
ListNode* q = head;
int m = n + 1;//使p到达目标节点的上一个节点
while (m && q) {
q = q->next;
m -= 1;
}
if (!q) {
if (m) {//删除head:q先指向NULL跳出while,m=1
ListNode* tmp = head;
head = head->next;
delete tmp;
}
else {//删除head->next:m=0时恰好q也指向NULL
ListNode* tmp = head->next;
head->next = head->next->next;
delete tmp;
}
return head;
}
while (q) {
q = q->next;
p = p->next;
}
ListNode* tmp = p->next;
p->next = p->next->next;
delete tmp;
return head;
}
};
双指针法,快指针q先走n+1步,两个指针再一起走,直至q到达NULL,此时p到达倒数第N个节点的上一个节点。然后改变p->next,跳过倒数第N个节点即可。但为了追求这一步的差距,在单独讨论部分需要考虑两种跳出while的情况,即删除head和head->next节点。改进思路是可以改变while循环的跳出条件,如将q改成q->next,应该可以避免对head->next的讨论。下面引用一下卡哥代码:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
while(n-- && fast != NULL) {
fast = fast->next;
}
fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummyHead->next;
}
};
可以看出主要区别确实是在快指针的while循环,以及卡哥再次展现了虚拟头节点减少一次单独讨论情况的作用。
Part_3 力扣面试题 02.07. 链表相交
题目描述:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci
个人解答:
class Solution {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
ListNode* p = headA;
int a = 0;
while (p) {
a++;
p = p->next;
}
ListNode* q = headB;
int b = 0;
while (q) {
b++;
q = q->next;
}
int c=a-b;
ListNode* head1 = headA;
ListNode* head2 = headB;
if (a < b) {
head1 = headB;
head2 = headA;
c = b - a;
}
ListNode* pp = head1;
while (c--)pp = pp->next;
ListNode* qq = head2;
while (pp &&qq && qq != pp) {
qq = qq->next;
pp = pp->next;
}
return qq;
}
};
与part_2类似,双指针法,不过需要先计算所差步数,通过分别遍历两个链表计算长度差,然后使长链表的指针先走,两个指针再一起走,直至相遇。不过这个做法在写的过程中也感到很无奈,几乎是复制粘贴的while循环与变量定义。实际上,这是笔者第一次接触面试题,所以,在本题之前,我先完成了part_4,这样一来,就诞生了一种很有趣的解法,不过由于会改变链表结构,因此,仅供娱乐,具体思路将在part_4后面提供。笔者在代码细节方面也确实仍有很多提升空间,下面同样附上卡哥的代码,供感兴趣的各位学习参考:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != NULL) { // 求链表A的长度
lenA++;
curA = curA->next;
}
while (curB != NULL) { // 求链表B的长度
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
if (lenB > lenA) {
swap (lenA, lenB);
swap (curA, curB);
}
// 求长度差
int gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap--) {
curA = curA->next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != NULL) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
Part_4 力扣142.环形链表II
题目描述:
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/linked-list-cycle-ii
个人解答:
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
ListNode* s = head;
ListNode* f = head;
if (!(head)||!(head->next)||!(head->next->next)) return nullptr;//短单链
else {
s = s->next;
f = f->next->next;
while (s != f) {
s = s->next;
if (!(f->next) || !(f->next->next)) return nullptr;//长单链
f = f->next->next;
}
ListNode* p = head;
while (p != s) {
p = p->next;
s = s->next;
}
return p;
}
}
};
本题比较有趣的是采用步长为2的快指针f和步长为1的慢指针s会相遇在环中一个节点,这个节点到环的入口的节点数等于head到环的入口的节点数,这里笔者不做纯数学推导尝试表达一下自己的理解,假定快慢指针在环中某处(出发点)同时开始运动,因为它们第一次完成追及问题必然是恰好“扣圈”(想要追及则必须多跑一圈),并且速度分别为1和2,因此它们必然会再次相遇于这个出发点。相信大家已经意识到这个出发点就是head,而head可以被抽象迁移到环中入口后方pos距离处。换句话说,既然head是还没到入口,而环中负一圈也有距离入口对应距离的位置,二者等效。然后再从相遇点派出一个速度为1 的指针(s即可),同时,从head也派出一个,它们就会在环的入口相遇。下面看一下卡哥代码:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};
最后说一下part_3的有趣有结果但不会通过的解法。
如此,便成环,即part_4