原计划2023年3月4日,2023年3月6日 补打卡
● day 1 任务以及具体安排:训练营一期day 1
● day 2 任务以及具体安排:day 2 第一章数组
● day 3 任务以及具体安排:3 第二章 链表
今日任务
● 24. 两两交换链表中的节点
● 19.删除链表的倒数第N个节点
● 面试题 02.07. 链表相交
● 142.环形链表II
● 总结
24. 两两交换链表中的节点
【链接】(文章,视频,题目)
用虚拟头结点,这样会方便很多。
本题链表操作就比较复杂了,建议大家先看视频,视频里我讲解了注意事项,为什么需要temp保存临时节点。
题目链接/文章讲解/视频讲解: 代码随想录
【第一想法与实现(困难)】
手动实现虚拟头结点
-
关键是想明白pre, cur, next之间的下一个关系,从而实现两两交换
-
最后要手动设置新链表的头并返回
【看后想法】
-
交换1,2,需要1的前继,2的后继指针,因此本身最好用几个指针来提升代码可读性。这里我使用的是cur, next1, next2, next3
-
最后返回的新链表头
【实现困难】看明白了,没啥困难,整体链表为了提升泛化性,会增加虚拟头结点,dummy_head很好用
【自写代码】
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
// 虚拟头结点
ListNode* dummy_head = new ListNode(0, head);
ListNode* cur = dummy_head;
while (cur && cur->next && cur->next->next) {
ListNode* next1 = cur->next;
ListNode* next2 = cur->next->next;
ListNode* next3 = cur->next->next->next;
// 交换next1与next2。从0123变成0213
cur->next = next2;
next2->next = next1;
next1->next = next3;
cur = cur->next->next; // 后跳两个结点
}
head = dummy_head->next;
delete dummy_head;
return head;
}
};
【收获与时长】半小时
【链接】(文章,视频,题目)
19.删除链表的倒数第N个节点
双指针的操作,要注意,删除第N个节点,那么我们当前遍历的指针一定要指向 第N个节点的前一个节点,建议先看视频。
题目链接/文章讲解/视频讲解:代码随想录
【第一想法与实现(困难)】
遍历了两次,一次找到最大size,并确认需要删除的index,第二次才做删除
【看后想法】根据“倒数”的想法设计双指针。
双指针,快慢指针,快指针比慢指针多走n+1步。这样,快指针指向链表尾巴fast == nullptr时,慢指针指向待删除的倒数第n个结点的前一个
【实现困难】理解思路之后不难,关键是想到双指针各自的定义是什么
【自写代码】
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 双指针,快慢指针,快指针比慢指针多走n+1步。这样,快指针指向链表尾巴fast == nullptr时,慢指针指向待删除的倒数第n个结点的前一个
ListNode* dummy_head = new ListNode(0, head);
ListNode* fast = dummy_head;
ListNode* slow = dummy_head;
while (n-- && fast) { // fast移动n步
fast = fast->next;
}
// fast多移动一步,使得慢指针指向待删除的倒数第n个结点的前一个
fast = fast->next;
while (fast) {
fast = fast->next;
slow = slow->next;
}
// 删除slow的下一个
ListNode* next1 = slow->next;
ListNode* next2 = slow->next->next;
slow->next = next2;
delete next1;
head = dummy_head->next;
delete dummy_head;
return head;
}
};
【收获与时长】半小时,比较巧妙的双指针设计方法
【链接】(文章,视频,题目)
面试题 02.07. 链表相交
本题没有视频讲解,大家注意 数值相同,不代表指针相同。
题目链接/文章讲解:代码随想录
【第一想法与实现(困难)】
-
暴力,两个循环,遇到问题,curB定义完成之后,需要在第二层while循环复位成curB = headB,才是完成了两个循环的操作。因此循环要注意循环变量初始化。
-
时间复杂度O(mn),性能比较差
【看后想法】
-
避免循环嵌套,希望线性时间复杂度,因此各自遍历一次,先求取size / length
-
如果有交叉,其尾部必然是对齐的。那么只要遍历两次就可以了
-
长短的判断,不需要多写一次条件,只需要std::swap(lengthA, lengthB); std::swap(curA, curB)即可
【实现困难】
-
线性时间复杂度,各自求长度,对齐末尾再遍历
-
判断条件不是值相等,而是指针相等(地址相同)
【自写代码】
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
// 线性时间复杂度
int lengthA = 0;
int lengthB = 0;
ListNode* curA = headA;
ListNode* curB = headB;
// 各自求长度
while (curA) {
lengthA++;
curA = curA->next;
}
while (curB) {
lengthB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 设置A为较长的,B为较短的
if (lengthB > lengthA) {
std::swap(lengthA, lengthB);
std::swap(curA, curB);
}
// 对齐末尾
int gap = lengthA - lengthB;
while (gap--) {
curA = curA->next;
}
while (curA && curB) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return nullptr;
}
};
【收获与时长】不到一小时,一开始没有转过弯来
142.环形链表II
【链接】(文章,视频,题目)
算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口,建议先看视频。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html
【第一想法与实现(困难)】
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 暴力,记录已经访问过的结点地址集合
std::set<ListNode*> node_set;
ListNode* cur = head;
int max_size = 1e4;
while (cur && max_size--) {
if (node_set.find(cur) != node_set.end()) {
return cur;
}
node_set.insert(cur);
cur = cur->next;
}
return nullptr;
}
};
【看后想法】
-
看来想要常数空间复杂度,一般都需要用快慢指针,用两个指针代替掉需要额外存储的O(n)数据结构
-
看了整个数学推导证明,自己负数一遍。借用卡哥的图片和notation
-
已知快慢指针相遇,也即有环,环入口前长度x,环部分相遇前长度y,环部分相遇后长度z
-
快慢指针关系有2(x+y) = x + n(y+z) + y,其中n为快指针走的完整环圈数,n>=1。如果n=0,则快指针都还没走完完整环,快慢指针无法相遇
-
所求为环入口位置x = (n-1)(y+z) + z, 其中n>=1,因此在头结点与相遇处各自开始,每次走一步,会在环入口相遇。
-
感性理解为,相遇处走z之后到达环入口,n=1时候x=z,直接相遇。n>=2,头结点index到达环入口时,相遇结点index2多走了n-1圈,但是也肯定在环入口,因此他们会在环入口相遇
【实现困难】
- 先开始走(fast两步,slow一步),再判断相遇,否则头结点就直接相遇了
【自写代码】
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 快慢指针,快指针走两步,慢指针走一步,相遇说明有环。相遇结点与头结点再重新走各一步,在环入口碰上
ListNode* fast = head;
ListNode* slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
// 快慢结点相遇说明有环
if (fast == slow) {
// 从头结点,相遇结点开始单步前进,在环入口相遇,返回环入口
ListNode* index1 = head;
ListNode* index2 = fast;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index1;
}
}
return nullptr;
}
};
【收获与时长】
2小时