代码随想录算法训练营第四天 | LeetCode24.两两交换链表中的节点、LeetCode19.删除链表中的倒数第N个节点、面试题02.07.链表相交、LeetCode142.环形链表II
01 链表专题总结
题型 | 关键理念 |
---|---|
移除链表元素 | 虚拟头节点 |
设计链表(增删改查) | 虚拟头节点 |
翻转链表 | 双指针 |
两两交换链表中的节点 | 虚拟头节点操作多个元素 |
删除链表中倒数第N个节点 | 快慢指针 |
链表相交 | 指针对齐 |
判断链表是否有环以及入口节点 | 快慢指针 |
链表最大的特点是在内存中是离散分布的,通过指针将其串联起来,导致其增删改操作比较简单,复杂度为O(1),而查询操作需要从头开始遍历一遍,复杂度为O(n)。相较于数组,后者增删操作比较复杂,需要整体的移动,复杂度为O(n),而改查操作比较简单,复杂度为O(1)。
链表还有一个显著的特点是头指针的存在使得增删操作对于头节点和内部节点不同,为了统一,一般创建一个虚拟头节点dummyHead以及指针cur。链表的算法题同样可以从基础的增删改查入手,即设计链表(增删改查)和移除链表元素
高级删操作:要想删除链表中倒数第N个节点,提供一种快慢指针的方式,即快指针先移动n+1个节点,然后快慢指针同时移动直至快指针移动到nullptr处
高级改操作:两两交换链表中的节点,同样以虚拟头节点的方式立足当前,放眼两步,进行处理
翻转链表、链表相交和找环都是通过两个指针实现
ref:链表专题总结
02-1 LeetCode24.两两交换链表中的节点
相关资源
文章讲解:两两交换链表中的节点
视频讲解:两两交换链表中的节点
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
第一想法:这道题没有思路,一开始就想要创建两个指针,然后进行交换,但是头节点和内部节点交换处理逻辑和迭代逻辑想不明白,遂放弃。
看完代码随想录之后的想法: 之前学过虚拟头节点去便捷地删除元素、插入元素和更改元素,但仅仅局限于一个节点,迭代过程也只是cur = cur->next
的操作,但这道题很有启发,对好几个节点进行操作的迭代过程也可以利用虚拟头节点实现;另外!要勤画图,空想写代码很慢。
实现:看完代码随想录复现的:
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0, head);
ListNode* cur = dummyHead;
while (cur->next != nullptr && cur->next->next != nullptr) {
ListNode* temp = cur->next;
ListNode* temp1 = cur->next->next->next;
cur->next = cur->next->next;
cur->next->next = temp;
temp->next = temp1;
cur = cur->next->next;
}
return dummyHead->next;
}
};
收获:虚拟头节点实现实现两两交换链表中的节点!
ToDo:复习,这道题没做出来!
02-2 LeetCode19.删除链表中的倒数第N个节点
相关资源
题目链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/
文章讲解:删除链表倒数第N个节点
视频讲解:删除链表倒数第N个节点
题目:
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
第一想法:没想出来咋一趟扫描实现,只能暴力先扫描一遍获得链表的元素个数,然后确定要删除的节点是顺数第几个,然后再删除。
实现:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 先遍历一遍,看看有几个元素
ListNode* cur = head;
int sum = 0;
while (cur != nullptr) {
cur = cur->next;
sum++;
}
ListNode* dummyHead = new ListNode(0, head);
int index = sum - n;
cur = dummyHead;
while (index) {
cur = cur->next;
index--;
}
cur->next = cur->next->next;
return dummyHead->next;
}
};
存在的问题:当n不符合LeetCode输入时会操作空指针、没有想出来一遍扫描实现目标
看完代码随想录之后的想法:可以采用快慢指针的方法,先让快指针从虚拟头节点开始移动n+1步,然后快慢指针一起移动,知道快指针来到nullptr的位置,此时慢指针指向要删除的倒数第n个元素的前一个位置,才能够实现删除
收获:快慢指针实现倒数第n个节点的删除
ToDo:实现快慢指针
02-3 面试题02.07.链表相交
相关资源
题目:给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
进阶:你能否设计一个时间复杂度 O(n)
、仅用 O(1)
内存的解决方案?
第一想法:没想出来咋时间复杂度能到O(n)
,很暴力地通过两次循环遍历所有情况找出指针相等的节点(看完代码随想录发现相交要求尾巴重合!我以为是数学那样有一个点相交就行)
实现:
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* getIntersectionNode(ListNode* headA, ListNode* headB) {
if (headA == nullptr || headB == nullptr) {
return nullptr;
}
ListNode* curA = headA;
ListNode* curB = headB;
while (curA != nullptr) {
while (curB != nullptr) {
if (curB == curA) {
return curB;
}
curB = curB->next;
}
curB = headB;
curA = curA->next;
}
return nullptr;
}
};
遇到的问题:审题出问题
看完代码随想录之后的想法: 就算是尾巴重合作为相交我也想不出来这种先移动长链表的指针的解法
收获:交点不是数值相等,而是指针相等
ToDo:复刻代码随想录算法
02-4 LeetCode142.环形链表II
相关资源
题目:
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
第一想法:想法很简单,首先创建一个虚拟头节点,然后cur一直向前,向前的过程维护一个record迭代器(存放ListNode类型指针),每次比较record中是否有当前指针,一旦出现,则找到了入环的第一个节点
实现:
#include <vector>
using namespace std;
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* detectCycle(ListNode* head) {
ListNode* dummyNode = new ListNode(0, head);
vector<ListNode*> record;
record.resize(10000,nullptr);
int visitedNum = 0;
ListNode* cur = dummyNode;
while (cur->next != nullptr) {
for (int j = 0;j < visitedNum;j++) {
if (cur->next == record[j]) {
return record[j];
}
}
record[visitedNum] = cur->next;
visitedNum++;
cur = cur->next;
}
return nullptr;
}
};#include <vector>
using namespace std;
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* detectCycle(ListNode* head) {
ListNode* dummyNode = new ListNode(0, head);
vector<ListNode*> record;
record.resize(10000,nullptr);
int visitedNum = 0;
ListNode* cur = dummyNode;
while (cur->next != nullptr) {
for (int j = 0;j < visitedNum;j++) {
if (cur->next == record[j]) {
return record[j];
}
}
record[visitedNum] = cur->next;
visitedNum++;
cur = cur->next;
}
return nullptr;
}
};
看完代码随想录之后的想法: 妙啊,通过一个速度为2的快指针和一个速度为1的慢指针即可判断是否成环,然后相遇点满足x=(n-1)(y+z)+z
。头结点到环形入口节点 的节点数为x
。 环形入口节点到 fast指针与slow指针相遇节点数y
。 从相遇节点再到环形入口节点节点数为z
,因此从相遇处和起点位置同时出发一个同速指针,必然在入口节点处相遇,进而找到了入口节点。
收获:快慢指针方式确定是否成环以及环入口位置
ToDo:复刻代码随想录代码