文档讲解:代码随想录
视频讲解:帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点_哔哩哔哩_bilibili
链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点_哔哩哔哩_bilibili
把环形链表讲清楚! 如何判断环形链表?如何找到环形链表的入口? LeetCode:142.环形链表II_哔哩哔哩_bilibili
第四天,周六也要继续坚持,练习链表的第二部分。
24. 两两交换链表中的结点
感受:第一眼:嗯,两两交换要怎么交换,1和2交换了2是不是要跟3交换这样一直进行下去,后来又明白过来题意时觉得自己想多了,也想简单了。因为按照题意本身理解来看,我们只需要1、2交换,2、3交换...如此往复。那么不可避免的得考虑到两种情况,就是链表本身结点个数为奇数个亦或者是偶数个。这是我们循环的出口,因此得想明白。
思路:我们仍然建立虚拟头结点方便对链表实际头结点进行操作。具体操作按卡哥的说法分为三步,首先让虚拟头结点指向实际头结点的next域:因为虚拟头结点是我们定义出的便于操作实际头结点,本身无法参与操作,我们先要将前两个结点交换,所以只能由它指向实际头结点的下一位;这个域再指向实际头结点:因为要完成这个结点和头结点的交换嘛;最后实际头结点指向下一位的下一位:因为前两步已经完成了1和2的交换,这一步的目的是要将临时数组temp的值指向3,要进行3和4的交换了。至此三步完成,需要注意的细节就是我们需要提前用两个临时数组temp和temp1去保存结点1和3的值。因为当第一步操作开始后,cur->next(虚拟头结点的next域)指向的是实际头结点的下一结点,那么意味着之后的几步操作涉及到的cur->next就不是之前的了。不用保存结点2的原因也是因为第一步操作的关系,已经获取到了cur->next的值。最后,我们再将cur向后移动两个结点到第2个结点,因为要进行后面3和4的交换了。结合代码就会很好理解:
/**
* 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 != NULL && cur->next->next != NULL){
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;
}
return dummyHead->next;
}
};
19.删除链表的倒数第N个结点
感受:抓住关键词,删除,链表,结点...好熟悉,这正是我每天在练的操作,可为什么还是会看到题目无从下手呢。言归正传,这同样是一道比较简单的考验对链表掌握程度的题。记住凡是它让我们找链表中的某个结点进行删除或插入的,我们可以首选双指针的方法,那这题其实也不例外。
思路:定义两个指针和一个虚拟头结点,fast指针先对链表遍历N次,因为我们的目的是要找到这倒数第N个结点。怎么找?fast先移动n+1位,因为我们要删除的是倒数第n那个位置,只有先移动了n+1位后,两个指针再一起移动(这次移动的位置由fast决定,fast何时位于null了,两指针就何时停止)。这一点需要我们好好想清楚。举个例子,我们得到一个有4个结点的链表1、2、3、4,要删除倒数第2个结点,开始两个指针置于虚拟头结点,我们可以理解为第0个原本不存在的结点,现在要fast先从0开始移n+1也就是3位,那么现在fast位于3,之后两个指针一起移动,null这个空指针我们可以理解为第5个本不存在的结点,fast从3移到null=5,移动了2位,另一指针slow跟着移动2位是不是移动到1了,此时slow移到n之前了,这就是我们想要的。以上的解释都是为了描述我们想要slow指针指向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* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* fast = dummyHead;
ListNode* slow = dummyHead;
while(n-- && fast != NULL){
fast = fast->next;
}
fast = fast->next;
while(fast != NULL){
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
return dummyHead->next;
}
};
142.环形链表II
感受:第一眼:这也太难了。第二眼:爆炸(bushi)。这道题跟之前的题性质很像,典型的是那种不理解不懂就会很难,但一旦明白了思路其实很简单代码也很好写。在我看来,需要注意的是理解这道题更好的方式是摒弃掉它本身是个链表的定式思维,因为它的逻辑比之前的题目更偏向数学甚至就是纯数学理论。至于为什么我们往下看。
思路: 本题依旧用双指针法定义两个指针,初始化都指向头结点。要解决的问题就两个:如何判断它是一个环?怎么找到环的入口?
判断方式简单讲就是让快指针fast一次走两步,慢指针slow一次走一步,这样快指针一定比慢指针先进入环(环本身就存在,我们只需要找到它)。待慢指针还在环外往环内移动时,快指针在环内已经移动了n圈了,当慢指针移动至环内后,快指针尚在环内处于某个位置,由于快指针相较慢指针多移动一步,则说明两指针终究会在环内某个位置相遇。此时我们就判断了是否有环(无环的话fast会一直向前移动且slow永远追不上,因此相遇即代表有环)。
接下来如何找到环的入口呢?我们需要用更强的数学逻辑来帮助理解:假设从头结点至环的入口这段距离为x,slow进入环内后移动的距离为y,那么剩下部分的环的距离也就是fast追上slow的距离我们设为z。等量关系的确立:fast走过的结点数等于slow走过的结点数的两倍。很好理解,fast一次走两步,slow一次一步嘛。那么用字符表现出来即:n(y+z) + x + y = 2*(x+y). 化简并用x表现出来即:x = n(y + z) - y. 此时我们看不出任何关系,再尝试变形得:x = (n - 1)(y + z) + z. 这个式子表明当一个新结点从头结点开始出发一次一步,另一新结点从两者相遇点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){
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 NULL;
}
};
总结:总花费时长3h左右,周末停课不停学,继续保持,希望现在写的这些以后我还能看得懂哈哈。