算法训练营 Day4-链表 2 | 24.两两交换链表中的节点 | 19.删除链表的倒数第 N 个节点 | 面试题 02.07. 链表相交 | 142.环形链表 II
查阅文档地址:programmercarl.com/
本期题目地址:
- 24.两两交换链表中的节点 - 中等 - 力扣链接
- 19.删除链表的倒数第 N 个节点 - 中等 - 力扣链接
- 面试题 02.07. 链表相交 - 简单 - 力扣
- 142.环形链表 II - 中等 - 力扣
本期题目答案地址:
目录:
- 基本概念(做题前要理解的概念)
- 我的解法
- 疑问点(过程中产生了问题并且查找资料解决)
语言
采用C++,一些分析也是用于 C++,请注意。
基本概念
虚拟头节点(dummy head)是一种非常有用的技巧,它可以帮助我们简化链表操作的逻辑,避免特殊情况的处理。
24. 两两交换链表中的节点
我的代码
// 方法一:递归 + 栈(力扣模式)
// 时间复杂度:O(n)
// 空间复杂度:O(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* swapPairs(ListNode* head) {
if(!head || head->next == nullptr) return head;
ListNode* two = head->next;
ListNode* three = swapPairs(two->next);
two->next = head;
head->next = three;
return two;
}
};
// 方法二:迭代(力扣模式)
// 时间复杂度:O(n)
// 空间复杂度:O(1)
/**
* 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* swapPairs(ListNode* head) {
ListNode* dummyNode = new ListNode();
dummyNode->next = head; // 问题一:别忘记连上原来的链表
ListNode* temp = dummyNode;
// 问题二:循环条件很重要,关键点 temp = one(经过移动后)
while(temp->next != nullptr && temp->next->next != nullptr) {
ListNode* one = temp->next;
ListNode* two = temp->next->next;
temp->next = two;
one->next = two->next;
two->next = one;
temp = one;
}
ListNode* res = dummyNode->next;
delete dummyNode;
return res;
}
};
19.删除链表的倒数第 N 个节点
我的代码
// 进阶:双指针法(虚拟头节点)力扣模式
// 时间复杂度:O(m),这里的 m 是链表的长度。
// 空间复杂度:O(1),只使用了常数级的额外空间。
/**
* 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 必然有效
ListNode* dummyNode = new ListNode(0);
dummyNode->next = head;
ListNode* lNode = dummyNode;
ListNode* rNode = dummyNode ;
for(int i = 0; rNode->next != nullptr; i ++) {
rNode = rNode->next;
if(i >= n) {
lNode = lNode->next;
}
}
lNode->next = lNode->next->next;
return dummyNode->next;
}
};
我的问题
- 对于单链表只能从头节点进入遍历,先得到链表的 size;再遍历一遍进行删除。如果对于双链表,就可以从尾结点进行遍历倒着进行删除。
- 进阶:你能尝试使用一趟扫描实现吗?!双指针 + 虚拟头节点 !倒数为 num,前后指针一直保持这个距离 l - r + 1 = num,此时 l 维持在要删除节点的前一个节点。
面试题 02.07. 链表相交
面试题 02.07. 链表相交 - 简单 - 力扣 链表相交 - 官方答案链接
我的代码
// 方法一:哈希集合
// 时间复杂度 O(m+n)
// 空间复杂度 O(m)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
unordered_set<ListNode *> unset;
ListNode *cur = headA; // 问题一:将其赋值为 nullptr,之后却尝试访问其成员。
while(cur != nullptr) {
unset.insert(cur);
cur = cur->next;
}
cur = headB;
while(cur != nullptr) {
if(unset.count(cur)) {
return cur;
}
cur = cur->next;
}
return nullptr;
}
};
// 方法二、双指针(链表)
// 时间复杂度 O(m+n)
// 空间复杂度 O(1)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *nodeA = headA;
ListNode *nodeB = headB;
if(nodeA == nullptr || nodeB == nullptr) {
return nullptr;
}
while(nodeA != nodeB) {
nodeA = nodeA == nullptr ? headB : nodeA->next;
nodeB = nodeB == nullptr ? headA : nodeB->next;
}
return nodeA;
}
};
我的疑惑
- val 值相等不一定是要找的目标节点?问题的关键在于明确链表相交的定义以及链表数据结构的特点,基于一个隐含的前提:如果两个链表相交,从相交节点开始,后续节点必然是完全重合的。
- 可以要遍历整条,可以使用 cnt 次数标记,出现相同就++,出现不同就清零。(显式检查后续是否都相等,但是因为检查的是数值,可能出现错误)
- 还有别的技巧嘛?双指针!看官方给的答案,很详细。
- 哈希集合如何解题?unordered_set<ListNode *>visited;保存节点,遇上第一个节点相等的就结束。
142.环形链表 II
我的代码
// 力扣模式
// 哈希表
// 时间复杂度:O(n)
// 空间复杂度:O(n)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 进阶:使用 O(1)空间解决
unordered_map<ListNode *,int> unmp;
ListNode* cur = head;
while(cur != nullptr) {
if(unmp[cur]) {
return cur;
}
unmp[cur] ++;
cur = cur->next;
}
return nullptr;
}
};
// 力扣 模式
// 快慢指针法(易错 数学)
// 时间复杂度:O(n)
// 空间复杂度:O(1)
// slow 和 fast 同时前进,fast 的速度是 slow 的两倍。当 slow 抵达环的入口处时,fast 一定在环上
// 此时因为 fast 比 slow 快 1 个单位的速度,且 y 为整数,所以再经过 y 个单位的时间即可追上 slow。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while(fast != nullptr) {
slow = slow->next;
if(fast->next == nullptr) {
return nullptr;
}
fast = fast->next->next;
if(fast == slow) {
ListNode* ptr = head;
while(ptr != slow) {
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return nullptr;
}
};
// acm 模式
//假设输入是链表节点的值,用空格分隔,以 -1 表示链表结束,若链表存在环,则环的入口节点的前一个节点后会紧跟环的入口节点的值。
#include <iostream>
#include <vector>
// 定义链表节点
struct ListNode {
int val;
ListNode * next;
ListNode(int x):val(x),next(nullptr){}
};
// 查找链表中环的入口节点
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while(fast != nullptr) {
slow = slow->next;
if(fast->next == nullptr) {
return nullptr;
}
fast = fast->next->next;
if(fast == slow) {
ListNode *ptr = head;
if(slow != ptr) {
slow = slow->next;
ptr = ptr->next;
}
return ptr;
}
}
nullptr;
}
// 根据输入构建链表
ListNode *buildList(const std::vector<int>& values) {
if(values.empty())return nullptr;
ListNode *dummy = new ListNode(0);
ListNode *current = dummy;
std::vector<ListNode*> nodes;
for(int val : values) {
if(val == -1)break;
ListNode *newNode = new ListNode(val);
current->next = newNode;
current = newNode;
nodes.push_back(newNode);
nodes.push_back(newNode);
}
// 处理环
if(nodes.size() > 1) {
for(size_t i = 0;i < nodes.size() - 1; ++ i) {
if(nodes[i+1]->val == nodes[i]->val) {
nodes[i]->next = nodes[i + 1];
break;
}
}
return dummy->next;
}
}
// 主函数
int main() {
std::vector<int> values;
int val;
while(std::cin >> val) {
values.push_back(val);
}// 输入 -1 算是到末尾
ListNode *head = buildList(values); // 构建链表后返回头节点
ListNode *cycleEntry = detectCycle(head); // 查找链表中的入口节点
if(cycleEntry) {
std::cout << cycleEntry->val << std::endl;
} else {
std::cout << "No cycle" << std::endl;
}
return 0;
}
总结
这篇文章围绕算法训练营 Day4 的链表相关题目展开,介绍了 4 道力扣题目,包括两两交换链表中的节点、删除链表的倒数第 N 个节点、链表相交、环形链表 II,以 C++ 语言为例给出多种解题代码,涵盖递归、迭代、双指针、哈希表等方法,并分析了各方法的时间和空间复杂度,还提出解题过程中的疑问及思考,如虚拟头节点的运用、不同链表操作技巧、如何优化解法等内容。