今日任务(2024/05/19~2024/05/21)
本是2024/05/11的任务,又花了好些天才补完。
这段时间在面试中常常遇到类似的题目,甚至是原题(特别是链表篇章的内容),而写解析的过程让我把很多细节都想得更清楚了,所以在面试遇到的时候觉得轻松很多,也觉得很值得。
- 24.两两交换链表中的节点
- 19.删除链表的倒数第N个节点
- 面试题02.07.链表相交
- 142.环形链表Ⅱ
- 总结
24. 两两交换链表中的节点
题目建议:
- 这道题比较基础,用虚拟头结点可以方便地以同一种方式处理所有节点
- 需要注意的是:当只有1个节点或者没有节点时,是不需要做交换的。这也要求我们在交换操作之前先判断是否有2个节点。
题目:24. 两两交换链表中的节点 - 力扣(LeetCode)
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4] 输出:[2,1,4,3]
示例 2:
输入:head = [] 输出:[]
示例 3:
输入:head = [1] 输出:[1]
提示:
- 链表中节点的数目在范围
[0, 100]
内0 <= Node.val <= 100
虚拟头节点[ 用时: 11 m 8 s ]
思路
设置虚拟头节点,其后每2个节点做一次交换,交换的步骤如下:
初始化
pre = dummyHead; cur1 = head;
,要交换的是pre
后的2个节点:cur1
、cur2
- 保存
cur2->next
为temp
- 让
pre
指向cur2
- 让
cur2
指向cur1
- 让
cur1
指向temp
经过以上操作,完成两个节点的交换,后面还需要更新节点:
pre
更新为cur1
cur1
更新为temp
注意,cur2
暂不更新,因为需要判断下面2个条件是否满足,如果不满足则说明后面没有节点 或者 只有1个节点了,这时就不需要交换了:
cur1
不为空节点- 条件1满足时,
cur1->next
也不能为节点
代码
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode *dummyHead = new ListNode(-1, head);
ListNode *pre = dummyHead, *cur1 = head;
while (cur1 != nullptr && cur1->next != nullptr) {
ListNode *cur2 = cur1->next;
ListNode *temp = cur2->next;
// 交换节点
pre->next = cur2;
cur2->next = cur1;
cur1->next = temp;
// 更新节点
pre = cur1;
cur1 = temp;
}
return dummyHead->next;
}
};
-
时间复杂度: O ( n ) O(n) O(n)
-
空间复杂度: O ( 1 ) O(1) O(1)
Carl思路
19.删除链表的倒数第N个节点
题目建议:
- 双指针的操作,要注意,删除第N个节点,那么我们当前遍历的指针一定要指向 第N个节点的前一个节点
题目:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。示例 1:
输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1 输出:[]
示例 3:
输入:head = [1,2], n = 1 输出:[1]
提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
**进阶:**你能尝试使用一趟扫描实现吗?
两次扫描[ 用时: 8 m 15 s ]
思路
暴力解法:
- 扫描1:获取链表总长度
len
,确定要删除的是第 ( l e n − n + 1 ) (len - n + 1) (len−n+1)个节点 - 扫描2:删除节点
本题涉及到删除节点,还是设置一个虚拟头节点来便于操作。
代码
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *dummyHead = new ListNode(-1, head);
// 扫描1:确定链表长度
int len = 0;
ListNode *pre = dummyHead;
while (pre->next != nullptr) {
++len;
pre = pre->next;
}
int toDel = len - n + 1;
// 扫描2:找到第(len - n)个节点
pre = dummyHead; // 我们要删除的是pre->next
while (--toDel) {
pre = pre->next;
}
// 删除第(len - n + 1)个节点
ListNode *temp = pre->next;
pre->next = temp->next;
delete temp; // 释放内存空间
temp = nullptr; // 避免野指针
return dummyHead->next;
}
};
-
时间复杂度: O ( n ) O(n) O(n)
- 2次扫描,时间复杂度为 O ( 2 n ) O(2n) O(2n)
-
空间复杂度: O ( 1 ) O(1) O(1)
进阶:一次扫描
我的思路[ 用时: 10 m 45 s ]
如果做一次扫描,我有一个想法,但是空间复杂度为 O ( n ) O(n) O(n):
- 通过一次扫描完成以下事情:
- 将链表上每个节点及其对应位置
index
存放在一个容器vector<ListNode*> node
中 - 确定链表长度
len
- 将链表上每个节点及其对应位置
- 删除第
(
l
e
n
−
n
+
1
)
(len - n + 1)
(len−n+1)个节点,因此要获取第
(
l
e
n
−
n
)
(len - n)
(len−n)个节点,即
node[len - n]
- 删除
node[len - n]
的下一个节点
代码
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *dummyHead = new ListNode(-1, head);
ListNode* node[31] = {dummyHead}; // 避免频繁emplace_back()开销
// 扫描1:1.存储节点;2.获取节点总数
// 节点总数
int len = 0;
ListNode *pre = dummyHead;
while (pre->next != nullptr) {
node[++len] = pre->next; // 下标从1开始
pre = pre->next;
}
// 获取第(len - n)个节点
pre = node[len - n];
// 删除第(len - n + 1)个节点
ListNode *temp = pre->next;
pre->next = temp->next;
delete temp; // 释放内存
temp = nullptr; // 避免野指针
return dummyHead->next;
}
};
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
Carl思路:双指针法
- 设置快慢指针,均初始化为
dummyHead
fast
先走n + 1
个节点fast
和slow
一起向后扫描,直到fast
为nullptr
,此时slow
指向倒数第n + 1
个节点(即要删除的节点的前一个)- 删除
slow->next
节点
代码
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *dummyHead = new ListNode(-1, head);
ListNode *fast = dummyHead, *slow = dummyHead;
// fast先前进n + 1个节点
for (int i = 0; i < n + 1; ++i) {
fast = fast->next;
}
// 扫描1:找到倒数第n + 1个节点
while (fast != nullptr) {
fast = fast->next;
slow = slow->next;
}
// 删除倒数第n个节点
ListNode *temp = slow->next;
slow->next = temp->next;
delete temp;
temp = nullptr;
return dummyHead->next;
}
};
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
面试题 02.07. 链表相交
题目建议:
- 注意 数值相同,不代表指针相同
题目:面试题 02.07. 链表相交 - 力扣(LeetCode)
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回null
。图示两个链表在节点
c1
开始相交**:**
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8' 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Intersected at '2' 解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。 在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 输出:null 解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。 由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 这两个链表不相交,因此返回 null 。
提示:
listA
中节点数目为m
listB
中节点数目为n
0 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
- 如果
listA
和listB
没有交点,intersectVal
为0
- 如果
listA
和listB
有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]
**进阶:**你能否设计一个时间复杂度
O(n)
、仅用O(1)
内存的解决方案?
暴力解法
这2个链表在后半段可能重合,困难的地方在于重合点之前的节点数不一定一样,如果暴力解决有2个思路:
- 思路1:利用后半段相同,倒序查找?这很离谱,无法实现,因为链表是单向的
- 思路2:扫描链表1,使用
unordered_set<ListNode*> st1
记录链表1的所有节点地址(值相等没有用),然后扫描链表2,逐个判断st1
中是否存在该地址,第一个找到的节点就是重合节点
思路2
- 扫描链表1,使用
unordered_set<ListNode*> st1
记录链表1的所有节点地址(值相等没有用) - 扫描链表2,逐个判断
st1
中是否存在该地址,第一个找到的节点就是重合节点
代码
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
// 扫描链表1
unordered_set<ListNode*> st1;
ListNode *preA = headA;
while (preA != nullptr) {
st1.insert(preA);
preA = preA->next;
}
// 扫描链表2
ListNode *preB = headB;
while (preB != nullptr) {
if (st1.find(preB) != st1.end()) return preB;
preB = preB->next;
}
return nullptr;
}
};
-
时间复杂度: O ( n ) O(n) O(n)
-
空间复杂度: O ( n ) O(n) O(n)
进阶:时间 O ( n ) O(n) O(n)内存 O ( 1 ) O(1) O(1)
在前面暴力解法分析时提到过*“困难的地方在于重合点之前的节点数不一定一样”*,这里Carl的思路其实就是解决这个问题——把两个链表的节点数调一致
Carl思路
通过两次扫描,确定两个链表长度;第三次扫描确定重合点:
-
扫描链表1:确定链表1长度
sz1
-
扫描链表2:确定链表2长度
sz2
-
求出长度差
-
尾端对齐:
- 将长链表前置多余节点走完(现在的位置即为出发点
sta1
) - 短链表的出发点即为头节点
sta2 = headA或headB
也可以通过
swap()
使得**sta1
始终为长链表起始点** - 将长链表前置多余节点走完(现在的位置即为出发点
-
同时扫描链表1、2:找到重合点
代码
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int sz1 = 0, sz2 = 0;
ListNode *cur1 = headA, *cur2 = headB;
// 扫描1:获取链表A长度
while (cur1) {
cur1 = cur1->next;
++sz1;
}
// 扫描2:获取链表B长度
while (cur2) {
cur2 = cur2->next;
++sz2;
}
// 令cur1为长链表
cur1 = headA;
cur2 = headB;
if (sz1 < sz2) {
swap(cur1, cur2);
swap(sz1, sz2);
}
// 尾端对齐
while (sz1 != sz2) {
cur1 = cur1->next;
--sz1;
}
// 扫描3:查找重合点
while (cur1 != nullptr && cur2 != nullptr) {
if (cur1 == cur2) return cur1; // 找到重合点(注意:重合点不是节点值相同,而是节点地址相同)
cur1 = cur1->next;
cur2 = cur2->next;
}
return nullptr;
}
};
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
Carl思路优化
Carl做了3次扫描解决了问题,实际上我觉得上面的步骤1~4可以合并在一次扫描中完成,总共2次扫描。
这个具体的实现过程还没想好。
142.环形链表II
题目建议:
- 算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口
- 拼多多一面遇到了,我突然想不清楚怎么找到环长度,面试官还详细解释了一下,并且跟我说没关系,这个其实是个智力题hh
题目:142. 环形链表 II - 力扣(LeetCode)
给定一个链表的头节点
head
,返回链表开始入环的第一个节点。 如果链表无环,则返回null
。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果pos
是-1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0 输出:返回索引为 0 的链表节点 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1 输出:返回 null 解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]
内-105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引**进阶:**你是否可以使用
O(1)
空间解决此题?
双指针法[ 用时: 44 m 29 s ]
思路
采用快慢指针方法,快慢指针步长分别为2、1:
- 判断是否有环:以不同速度从头节点出发,当指向同一个节点时,说明有环;否则无环。
- 寻找到环入口:双指针,指针1从头节点出发,指针2从重合节点出发,步长均为1,相遇节点即为环入口(这个证明过程稍稍有些复杂)
- 假设在步骤1中,快指针在与慢指针相遇前已在环内转了n圈,则有 y − x = n ⋅ l o o p y-x=n\cdot loop y−x=n⋅loop(其中y和x分别为快慢指针所走的路程)。因为快指针步长是慢指针2倍,所以路程也是2倍的关系 y = 2 x y=2x y=2x,代入上式可得: x = n ⋅ l o o p x=n\cdot loop x=n⋅loop。
- 假设在步骤1中,慢指针进环后所走长度为 d x + m ⋅ l o o p dx+m\cdot loop dx+m⋅loop,进环前所走长度为 z z z,则有 x = z + d x + m ⋅ l o o p x=z+dx+m\cdot loop x=z+dx+m⋅loop,又因为 x = n ⋅ l o o p x=n\cdot loop x=n⋅loop,所以有 d x = ( n − m ) ⋅ l o o p − z dx=(n-m)\cdot loop-z dx=(n−m)⋅loop−z。
- 在步骤2中,因为指针步长一样,假设指针1、2所走长度均为 w w w,当且仅当 w = k ⋅ l o o p − d x w=k\cdot loop-dx w=k⋅loop−dx且 w = z w=z w=z时,指针1、2才能在环入口处相遇。
- 将 d x = ( n − m ) ⋅ l o o p − z dx=(n-m)\cdot loop-z dx=(n−m)⋅loop−z代入 w w w的表达式,可以得到 w = k ⋅ l o o p − ( n − m ) ⋅ l o o p + z w=k\cdot loop-(n-m)\cdot loop+z w=k⋅loop−(n−m)⋅loop+z,整理得 w = ( k − n + m ) ⋅ l o o p + z w=(k-n+m)\cdot loop+z w=(k−n+m)⋅loop+z。
- 这代表着指针2可能会在环内跑几圈,但是两个指针确实会在环入口处相遇(指针1进了环之后就永远不能和指针2相遇了呀,因为步长是相同的)。
补充:
- 计算环长度:令快指针从重合节点出发(步长为1),慢指针一直在重合节点这等着,当快指针又到达重合节点时,所走的路程就是环长度。
代码
- 注意,在判断是否有环时,
while
遍历的条件容易写错。
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
// 判断是否有环
ListNode *fast = head, *slow = head;
// 【易错】
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
// 找到重合点,有环
if (slow == fast) {
// 寻找环入口
ListNode *cur1 = head, *cur2 = slow;
while (cur1 != cur2) {
cur1 = cur1->next;
cur2 = cur2->next;
}
return cur1;
}
}
return nullptr; // 无环
}
};
-
时间复杂度: O ( n ) O(n) O(n)
-
空间复杂度: O ( 1 ) O(1) O(1)
Carl思路
总结
-
24.两两交换链表中的节点
- 虚拟头节点的设置
- 注意:当后面不足2个节点时,就不需要再交换了
while (cur1 != nullptr && cur1->next != nullptr)
-
19.删除链表的倒数第N个节点
- 也用了虚拟头节点
- 暴力解法(2次扫描):
- 一次用于确定总长度,一次用于删除倒数第
n
个节点 - 注意:要删除某个点,需要找到其前一个节点
- 一次用于确定总长度,一次用于删除倒数第
- 双指针法(1次扫描):
- 快指针先行
n + 1
步,后面和慢指针一起同步前进 - 这样当快指针指向末尾的时候,慢指针正好在倒数第
n
个节点的前一个节点
- 快指针先行
-
面试题02.07.链表相交
- 3次扫描:
- 扫描1、2用于确定2个链表的长度
- 用一次小小的扫描移动长链表的起始节点,使两个链表尾端对齐
- 扫描3用于找到相交点
- 3次扫描:
-
142.环形链表Ⅱ
- 快慢指针法的灵活应用
- 关键在于数学推导的过程,俺觉得确实是需要智力才能理解的hh
- 注意:快慢指针遍历时的判断条件很容易错(跟前面的24.两两交换链表中的节点是一样的哦)
while (fast != nullptr && fast->next != nullptr)
- 本文若存在侵权,烦请指出,本人会立马删除相关内容;
- 注意:当后面不足2个节点时,就不需要再交换了
while (cur1 != nullptr && cur1->next != nullptr)
-
19.删除链表的倒数第N个节点
- 也用了虚拟头节点
- 暴力解法(2次扫描):
- 一次用于确定总长度,一次用于删除倒数第
n
个节点 - 注意:要删除某个点,需要找到其前一个节点
- 一次用于确定总长度,一次用于删除倒数第
- 双指针法(1次扫描):
- 快指针先行
n + 1
步,后面和慢指针一起同步前进 - 这样当快指针指向末尾的时候,慢指针正好在倒数第
n
个节点的前一个节点
- 快指针先行
-
面试题02.07.链表相交
- 3次扫描:
- 扫描1、2用于确定2个链表的长度
- 用一次小小的扫描移动长链表的起始节点,使两个链表尾端对齐
- 扫描3用于找到相交点
- 3次扫描:
-
142.环形链表Ⅱ
- 快慢指针法的灵活应用
- 关键在于数学推导的过程,俺觉得确实是需要智力才能理解的hh
- 注意:快慢指针遍历时的判断条件很容易错(跟前面的24.两两交换链表中的节点是一样的哦)
while (fast != nullptr && fast->next != nullptr)
- 本文若存在侵权,烦请指出,本人会立马删除相关内容;
- 本文内容若有不正确或不规范指出,请大家不吝赐教~