Day4 链表:24.两两交换链表中的节点 19.删除链表的倒数第N个节点 160.链表相交 142.环形链表Ⅱ

73 阅读7分钟

24.两两交换链表中的节点

难度指数:😀😕

操作的指针 cur 一定要指向要反转的那2个节点的前一个节点。

链表题有些细节需要注意,如:遍历这个链表的时候,什么时候终止?具体交换结点的时候应该怎么交换?

(⚠️同学经常会犯空指针异常或者直接死循环的问题)

一个虚拟头结点指向链表真正的头结点:dummyHead->next = head;

cur最开始指向dummyHead:cur = dummyHead; (只有这样我们才能一上来操作头结点和第二个结点)

接下来就是遍历的过程: while( )

  • 当链表的结点数是奇数,cur->next->next如果为null,那么这个遍历就结束了。
  • 当链表的结点数是偶数,cur->next = null;,这个遍历就结束了。 (空链表,即链表中结点数是0也同样适用)

while (cur->next != NULL && cur->next->next != NULL) 关键是要理解什么时候终止遍历条件

⚠️两个条件的顺序不能反!! 容易发生空指针异常。

Q:为什么?

A:如果你先写了 cur->next->next != NULL,万一 cur->next为空,那么 cur->next->next 就是对空指针取值了。

说一嘴:有同学会好奇不需要对cur设为空进行判断吗?

A:不需要,cur一开始指向dummyhead,dummyHead是我们自己定义的。因此cur不可能为空!


接下来是进行两两交换结点的逻辑:

04.01.png

04.02.png

cur->next = cur->next->next;

问题来了:如何获取到结点1? (此时的cur->next已经不是原先的cur->next)

A:需要用在上面代码前面定义一个临时结点 temp,把 结点1保存下来temp = cur->next;

同理,结点3也需要保存下来,因为"步骤二"中,结点2的指针会从指向结点3改为指向结点1

故:

temp = cur->next;
temp1 = cur->next->next->next;
1️⃣cur->next = cur->next->next;
2️⃣cur->next->next = temp;
3️⃣temp->next = temp1;
到此,完成了翻转的操作。

接下来需要移动cur指针:

cur = cur->next->next;

最后,返回链表的头结点:return dummyHead->next;

04.03.png AC代码: (核心代码模式)

 /**
  * 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);  //定义一个虚拟头结点
         ListNode* cur = dummyHead;
         dummyHead->next = head;
 ​
         while (cur->next != nullptr && cur->next->next != nullptr) {
             ListNode* temp = cur->next;  //记录第一个临时结点
             ListNode* temp1 = cur->next->next->next;  //记录第二个临时结点
 ​
             cur->next = cur->next->next;  //step 1
             cur->next->next = temp;  //step 2
             temp->next = temp1;  //step 3
 ​
             //移动cur指针
             cur = cur->next->next;
         }
 ​
         //返回头结点
         return dummyHead->next;
         
     }
 };

19.删除链表的倒数第N个节点

难度指数:😀😕

要删除的是倒数第n个结点,假设n = 2

操作指针指向的结点直接指向它的下一个的下一个,从而达到删除第n个结点的目的。

04.08.png

Q:为什么要采用虚拟头节点?

A:方便我们采用统一的方式,不需要对操作的结点是不是头结点进行特殊判断。

🦄本题关键:如何找到倒数第n个结点?

可以定义快指针慢指针快指针先走n步,再快、慢指针同时移动 ,直到快指针指向了空结点,那么慢指针就指向了要删除的这个结点。

这样就找到了倒数第n个结点。

⚠️但是存在问题:我们要删除第n个结点,那么操作指针 slow 就应该指向要删除的这个结点的前一个,可实际上 slow 刚好指向的是要删除的第n个结点。

04.04.png

解决方法:让快指针fastn + 1;然后再快、慢指针同时移动,这样(当fast指向null时),慢指针才能成功落在要删除结点的前一个。

然后 slow->next = slow->next->next; 这样就实现了把该结点从链表中移除的一个效果。

04.05.png

快指针 fast 指向了null,慢指针 slow 直接指向待删除结点的前一个结点:

04.06.png

删除结点:让慢指针 slow 指向的结点,指向下一个的下一个,这样就能达到把第n个结点从链表中移除的目的。

04.07.png

AC代码: (核心代码模式)

 /**
  * 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);  //虚拟头结点
         ListNode* fast = dummyHead;
         ListNode* slow = dummyHead;
         dummyHead->next = head;  //虚拟头结点指向原链表的头结点
 ​
         //移动快指针
         while (n-- && fast != nullptr) {
             fast = fast->next;
         }
 ​
         fast = fast->next;
         while (fast != nullptr) {
             fast = fast->next;
             slow = slow->next;
         }
 ​
         //删除结点
         slow->next = slow->next->next;
 ​
         return dummyHead->next;
     }
 };

160.链表相交

难度指数:😀😐

AC代码: (核心代码模式)

 /**
  * 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, lenB = 0;
         while (curA != NULL) { // 求链表A的长度
             lenA++;
             curA = curA->next;
         }
         while (curB != NULL) { // 求链表B的长度
             lenB++;
             curB = curB->next;
         }
         curA = headA;
         curB = headB;
         // 让curA为最长链表的头,lenA为其长度
         if (lenB > lenA) {
             swap (lenA, lenB);
             swap (curA, curB);
         }
         // 求长度差
         int gap = lenA - lenB;
         // 让curA和curB在同一起点上(末尾位置对齐)
         while (gap--) {
             curA = curA->next;
         }
         // 遍历curA 和 curB,遇到相同则直接返回
         while (curA != NULL) {
             if (curA == curB) {
                 return curA;
             }
             curA = curA->next;
             curB = curB->next;
         }
         return NULL;
     }
 };

142.环形链表Ⅱ

算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口。

难度指数:😀😕🙁

判断环:

04.09.png

用双指针(快慢指针)来判断链表是否有环

快、慢指针一旦相遇,说明链表是有环的。 (若链表是一条直线,根本不可能相遇。)

下面具体说明细节:

快指针fast每次走2个结点,慢指针slow每次走1个结点,快、慢指针先后进入环内,快指针追赶慢指针,快、慢一定会相遇

😁妙处:快指针如果每次走3个结点,可能就会跳过慢指针了,导致无法相遇。 (因为fast相对于slow来说每次走2个结点)

而快指针每次走2个结点,那么fast相对于slow每次只走1个结点,fast是在一步步逼近slow,因此一定会在环里相遇

动画:

04.10.gif

找出环的入口:

把这3个变量定义出来之后是可以写出一个等式的,这个等式的连接的桥梁就是快指针每次走2个结点,慢指针每次走1个结点。

04.11.png

  • slow = x + y;
  • fast = x + y + n(y + z);

n:在fastslow 相遇的时候,fast已经在环里面转了n圈了。

等式:

2(x + y) = x + y + n(y + z);
化简得:x + y = n(y + z);
得:x = n(y + z) - y;

必然有 n >= 1 ,即快指针 fast 至少在环里转了一圈,才和慢指针 slow 相遇

(想不懂可以自己模拟一下,或者举反例)

fastslow 相遇,一定是 fast 去追 slow 的过程(Because fast 先入环)

针对上面的x = n(y + z) - y;,我再让出来一圈,看看x和什么正数有关系(因为x是看不出来和负数有什么关系)。

得:x = (n - 1)(y + z) + z;

n = 1时(思路就很清晰),即 fast 转了一圈之后,和slow相遇了,得出:x = z; (🦄得到了这个很关键的式子!)

说明:

04.12.png

n可以 = 100,无非就是其中一个指针多转了99圈,然后最后在环形入口点相遇。

因此,我们就知道了如何去找相交的点: 在相遇的地方定义一个 index1 ,在头结点(起始位置)定义一个 index2 ,两者以相同的速度移动,

index1 可能在这里面转了几圈,但最后相遇的点就是这个环的入口处。

慢指针 slow 进入环里面的第一圈就被快指针 fast 追上了。

所以slow = x + y;就可以了,不需要slow = x + y + k(y + z)

AC代码: (核心代码模式)

 /**
  * 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 index2;
             }
         }
 ​
         return NULL;
     }
 };