代码随想录算法训练营Day4 链表的经典问题
链表经典问题
今天主要写的几个都是比较基础的链表的题目。对我来说,主要是熟悉链表的操作了。关于环的部分,可以后面再看看,前面的感觉多写几次就OK了。
Leetcode 19. 删除链表的倒数第 N 个结点
题目链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/
题目描述:给你一个链表,删除链表的倒数第\(n\)个结点,并且返回链表的头结点。
题目解析:考虑模拟。我们需要先找到链表当中的倒数第\(n\)个节点,然后对他进行删除操作。
(法一:朴素思路)
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyhead = new ListNode(0, head);
ListNode* cur = dummyhead;
int size = 0;
while (cur != nullptr) {
cur = cur->next;
size ++;
}
if (n > size) return head;
cur = dummyhead;
for (int i = 1; i < size - n; i ++) {
cur = cur->next;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
return dummyhead->next;
}
};
但是这样还不够快,因为我们扫了两次链表。
考虑双指针。我们通过快慢指针来模拟这个过程。
“想象有一把长度固定的尺子,左端点在链表头部,右端点在正数第\(n\)个节点。向右移动尺子,当尺子右端点到达链表末尾时,左端点就在倒数第\(n\)个节点。”
但是因为删除操作涉及到第\(n\)个节点的前一个节点,那我们只需要让右端点再正数第\(n+1\)个节点就可以了。
(法二:双指针)
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyhead = new ListNode(0, head);
ListNode* slow = dummyhead;
ListNode* fast = dummyhead;
while (n -- && fast != nullptr) {
fast = fast->next;
}
fast = fast->next;
while (fast != nullptr) {
fast = fast->next;
slow = slow->next;
}
ListNode* tmp = slow->next;
slow->next = tmp->next;
delete tmp;
return dummyhead->next;
}
};
Leetcode 29. 两两交换链表中的节点
题目链接:https://leetcode.cn/problems/swap-nodes-in-pairs/description/
题目描述:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即:只能进行节点交换)。
题目解析:考虑模拟。我们需要去模拟这个两两交换的过程,不妨先画一个图出来。

不难发现,其实这个过程就是让我们去更新每个点的后继节点,关键点在于更新了某个点之后,没有太好的方式获取到这个点本来的后继节点。于是我们需要存一下每个点的后继节点。
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;
cur->next->next->next = temp1;
cur = cur->next->next;
}
ListNode* res = dummyhead->next;
delete dummyhead;
return res;
}
};
Leetcode 面试题 02.07. 链表相交
题目链接:https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/description/
题目描述:给你两个单链表的头节点\(headA\)和\(headB\),请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回\(null\)。
图示两个链表在节点\(c1\)开始相交:

题目数据保证整个链式结构中不存在环。
注意,函数返回结果后,链表必须保持其原始结构。
题目解析:考虑模拟。看到题目的图就可以得到一个思路:两个链表\(A、B\)的交集大小最大是\(min(len, lenB)\),那么我们只需要从这里开始枚举每一个节点作为可能是相交的起始节点,如果找到了,返回;反之,最后返回一个空指针。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != nullptr) {
curA = curA->next;
lenA ++;
}
while (curB != nullptr) {
curB = curB->next;
lenB ++;
}
curA = headA, curB = headB;
if (lenB > lenA) {
swap(curA, curB);
swap(lenA, lenB);
}
int gap = lenA - lenB;
while (gap --) {
curA = curA->next;
}
while (curA != nullptr) {
if (curA == curB) {
return curA;
}
curA = curA->next;
curB = curB->next;
}
return nullptr;
}
};
Leetcode 142. 环形链表 II
题目链接:https://leetcode.cn/problems/linked-list-cycle-ii/description/
题目描述:给定一个链表的头节点\(head\),返回链表开始入环的第一个节点。 如果链表无环,则返回\(null\)。
如果链表中有某个节点,可以通过连续跟踪\(next\)指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数\(pos\)来表示链表尾连接到链表中的位置(索引从\(0\)开始)。如果 \(pos\) 是 \(-1\),则在该链表中没有环。注意:\(pos\)不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改链表。
题目解析:考虑Floyd判圈算法(双指针)。简单来说就是,定义两个速度不一样快的指针,那么如果他们能够从同一点出发并且相遇,说明他们所行走的路径当中一定存在环。
《代码随想录》中的思路感觉没有灵神的好,这里记录一下更好的思路(本质上都是推式子,最后的代码也差不多,但是这种思路更加好理解一点吧)

那么代码就很好写了。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (fast == slow) {
while (slow != head) {
slow = slow->next;
head = head->next;
}
return slow;
}
}
return nullptr;
}
};
再贴一个《代码随想录》的图和思路吧。
假设从头结点到环形入口节点的节点数为\(x\)。环形入口节点到\(fast\)指针与\(slow\)指针相遇节点节点数为\(y\)。从相遇节点再到环形入口节点节点数为 \(z\)。那么相遇时: \(slow\)指针走过的节点数为: \(x + y\), \(fast\)指针走过的节点数:\(x + y + n (y + z)\),\(n\)为\(fast\)指针在环内走了\(n\)圈才遇到\(slow\)指针, \((y+z)\)为 一圈内节点的个数\(A\)。
因为\(fast\)指针是一步走两个节点,\(slow\)指针一步走一个节点, 所以\(fast指针走过的节点数\) = \(slow指针走过的节点数 * 2\):
\((x + y) * 2 = x + y + n (y + z)\)
两边消掉一个\((x+y)\): \(x + y = n (y + z)\)
因为要找环形的入口,那么要求的是\(x\),因为\(x\)表示 头结点到 环形入口节点的的距离。
所以要求\(x\) ,将\(x\)单独放在左面:\(x = n (y + z) - y\) ,
再从\(n(y+z)\)中提出一个 \((y+z)\)来,整理公式之后为如下公式:\(x = (n - 1) (y + z) + z\) 注意这里\(n\)一定是大于等于1的,因为 \(fast\)指针至少要多走一圈才能相遇\(slow\)指针。
这个公式说明什么呢?
先拿\(n\)为1的情况来举例,意味着\(fast\)指针在环形里转了一圈之后,就遇到了 \(slow\)指针了。
当 \(n\)为1的时候,公式就化解为 \(x = z\),
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
也就是在相遇节点处,定义一个指针\(index1\),在头结点处定一个指针\(index2\)。
让\(index1\)和\(index2\)同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index1;
}
}
return nullptr;
}
};

浙公网安备 33010602011771号