代码随想录算法训练营第四天 | 24. 两两交换链表中的节点 19.删除链表的倒数第N个节点 面试题 02.07. 链表相交 142.环形链表II
今天的题目我大部分都三刷过了,所以基本都是一遍过的,不过第一遍做的时候还是很有难度的。链表章节今天就结束了
24. 两两交换链表中的节点
题目链接/文章讲解/视频讲解: https://programmercarl.com/0024.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.html
思路
这题抓住两两交换节点的步骤(尤其是先后操作顺序)就没有问题了,直接上图
-
交换节点:
这里我们使用的顺序是1->3 , 2->1 , cur->2,但是需要注意的是当1的next指向3时,此时2无法再访问到,所以需要先用temp指针保存节点2(当然如果你就像先cur->2,再2->1 1>3,也没问题,只不过此时你需要用temp指针保存节点1,因为cur的next指向2后,1无法被访问
- 总结一下就是指向x节点的next指针如果先被改变,那么就让temp指针指向x
-
边界条件:
-
通过上图可以发现我们操作的节点是cur的后两个节点,所以后两个指针不能为空指针。
-
而且cur每次走两步,一开始在dummyhead处,那么最终结束时有两种情况,第一就是节点总数是奇数,那么最后cur会位于倒数第二个节点处(此时
cur->next->next==nullptr
),第二种情况是节点总数是偶数,那么最后cur会位于最后一个节点处(此时cur->next==nullptr
)
-
代码
/**
* 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* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* cur = dummyhead;
while(cur->next!=nullptr && cur->next->next!=nullptr)
{
//存下2
ListNode* temp = cur->next->next;
//交换,操作3下
cur->next->next = temp->next;
temp->next = cur->next;
cur->next = temp;
//cur往前走两步
cur = cur->next->next;
}
return dummyhead->next;
}
};
删除链表的导数第N个结点
思路
-
暴力解法:
暴力方法很简单,就是先遍历链表得出一共有多少个节点,再让cur从dummyhead前进 size - N步(注意:前面(上一篇博客)已经讲过删除节点需要cur位于需要删除节点处的上一个位置)
两次for循环,应该很好实现,我就不写代码了
-
双指针法
这题的双指针法的思想就是一开始快慢指针都位于dummyhead处,接着让快指针比慢指针先走n步,这样当快指针走到终点时,慢指针就位于要删除节点的前面了。
-
我们说过双指针法最重要的一点就是理解两个指针分别的作用:
快指针:探寻最后一个节点
慢指针:探寻需要的删除节点
-
如上图所示,加入我们要删除的是倒数第二个节点,那么fast先走两步到2处,两者再同步向右走,直至fast指向最后一个节点,此时slow也处于要删除节点的前一位了
代码
/**
* 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) {
//双指针法 cur是快指针,pre是慢指针
ListNode* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* pre = dummyhead;
ListNode* cur = pre;
//倒数第n个,那两个指针就相隔几(下标相减为n)
for(int i =0; i<n ; i++)
{
cur = cur->next;
}
//当cur的下一个是nullptr时,说明此时pre已经位于要删除节点的前面了
while(cur->next!=nullptr)
{
pre = pre->next;
cur = cur->next;
}
//删除操作
ListNode* temp = pre->next;
pre->next = pre->next->next;
delete temp;
return dummyhead->next;
}
};
链表相交
思路
这题算需要注意的是:数值相等不一定是相同节点,一定要指针相同(即地址相同)
-
暴力解法:
这个没啥好说的,就是两层for循环,对两个链表一个一个检查是否是同一个节点
-
双指针法:
既然暴力解法是双层for循环,那么双指针法一定是值得我们考虑的(前面说过双指针法就是化两层for循环为一层for循环),这题处理双指针的关键就是抓住两个链表的长度差
-
两个指针遍历链表计算出两个两个链表的长度,并计算长度差
dif = lenA - lenB
-
让指向长度更长的链表的指针curA先走lenA步,此时curA和curB就位于与交点处同样距离处了,让他们同时前进直至相等
-
代码
/**
* 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* curA = headA;
ListNode* curB = headB;
int lenA = 0;
int lenB = 0;
while(curA!=nullptr)
{
++lenA;
curA = curA->next;
}
while(curB!=nullptr)
{
++lenB;
curB = curB->next;
}
//别忘了返回起点
curA = headA;
curB = headB;
//为了方便操作,让curA指向长的那一个链表
if(lenA<lenB)
{
swap(lenA,lenB);
swap(curA,curB);
}
int dif = lenA - lenB;
//计算长度差,差几就让curA提前走几步,最后curA就会和curB相遇
while(dif--)//走n步,就while(n--),这个可以看出模版了
{
curA = curA->next;
}
while(curA!=nullptr)
{
//一般都是先进行该循环内该做的事,在最后结束循环时才让指针前进
if(curA==curB)
{
return curA;
}
curA = curA->next;
curB = curB->next;
}
return nullptr;
}
};
细心的读者肯定发现这道题并没有使用dummyhead,别急,稍后我会总结
环形链表
题目链接/文章讲解/视频讲解:https://programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html
思路
-
哈希表标记:
看到这题我的第一反应就是使用一个数组来标记走过的节点,这样就可以检测到重复走过的节点。也就是说没走一个节点都需要查一次数组,那么使用哈希表的时间复杂度是最低的,因为哈希表查表的时间复杂度是O(1)
-
双指针法:
这题用双指针法其实就是纯数学题了
如上图所示
- 我们让slow每次走一步,fast走两步—在这种情况下,fast相对于slow的速度就是一步,也就不会出现fast跳过slow的情况了
- 都从头结点开始走,那么一定是fast先进入链表环,并在环形中相遇。那么假设头结点与环形入口节点距离为x,按顺时针方向环形入口节点与相遇节点距离y,相遇节点与环形入口节点距离为z(如图)。
- 第三点非常重要:两个指针一定在slow的第一个循环中相遇
- fast想要追尾必须要比slow多走一圈
- 假设当slow第一次进入环形入口时,fast与slow相距d(按顺时针方向),那么以slow为参考系,fast速度为1,想要追尾slow就需要相对slow走
z+y-d
步,时间也就为z+y-d,所以当fast追上slow时,slow连自己的第一个循环都没走完(slow走一个循环需要z+y步,速度为1,也即时间也是z+y) - 此时你也可以发现刚好slow走过的距离就是y嘛(因为slow走过z+y-d相遇,而走过y也是相遇),所以
z+y-d=y
,即z=d
- 所以$2x = n(y+z)+d+x=n(y+z)+z+x \rightarrow^{令n=0} x =z $,等式左边是slow走的路程的两倍,右边是fast走过的路程(n为slow进入环形入口前走过的圈数),因为fast的速度为slow的两倍,所以该等式成立
x=z
意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
(看完上面的推导我觉得不少人会有点晕吧,还是第一种方法好呢)
代码
/**
* 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* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
//----------------------------------
//哈希表法
unordered_set<ListNode*> mark;
while(head!=nullptr)
{
//检查是否遍历到过
if(mark.count(head)!=0)
{
return head;
}
//第一次遍历到就放入哈希表中
mark.insert(head);
head = head->next;
}
//没有环
return nullptr;
}
}
};
总结:
对于有增删改,用虚拟头结点
而对于只有查操作的时候,无需使用