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不可能为空!
接下来是进行两两交换结点的逻辑:
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;
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个结点的目的。
Q:为什么要采用虚拟头节点?
A:方便我们采用统一的方式,不需要对操作的结点是不是头结点进行特殊判断。
🦄本题关键:如何找到倒数第n个结点?
可以定义快指针和慢指针,
快指针先走n步,再快、慢指针同时移动,直到快指针指向了空结点,那么慢指针就指向了要删除的这个结点。这样就找到了倒数第n个结点。
⚠️但是存在问题:我们要删除第n个结点,那么操作指针 slow 就应该指向要删除的这个结点的前一个,可实际上 slow 刚好指向的是要删除的第n个结点。
解决方法:让快指针fast走 n + 1步;然后再快、慢指针同时移动,这样(当fast指向null时),慢指针才能成功落在要删除结点的前一个。
然后 slow->next = slow->next->next; 这样就实现了把该结点从链表中移除的一个效果。
快指针 fast 指向了null,慢指针 slow 直接指向待删除结点的前一个结点:
删除结点:让慢指针 slow 指向的结点,指向下一个的下一个,这样就能达到把第n个结点从链表中移除的目的。
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.环形链表Ⅱ
算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口。
难度指数:😀😕🙁
判断环:
用双指针(快慢指针)来判断链表是否有环
快、慢指针一旦相遇,说明链表是有环的。 (若链表是一条直线,根本不可能相遇。)
下面具体说明细节:
快指针fast每次走2个结点,慢指针slow每次走1个结点,快、慢指针先后进入环内,快指针追赶慢指针,快、慢一定会相遇。
😁妙处:快指针如果每次走3个结点,可能就会跳过慢指针了,导致无法相遇。 (因为
fast相对于slow来说每次走2个结点)而快指针每次走2个结点,那么
fast相对于slow每次只走1个结点,fast是在一步步逼近slow,因此一定会在环里相遇。
动画:
找出环的入口:
把这3个变量定义出来之后是可以写出一个等式的,这个等式的连接的桥梁就是快指针每次走2个结点,慢指针每次走1个结点。
slow = x + y;fast = x + y + n(y + z);
n:在fast、slow 相遇的时候,fast已经在环里面转了n圈了。
等式:
2(x + y) = x + y + n(y + z);
化简得:x + y = n(y + z);
得:x = n(y + z) - y;
必然有
n >= 1,即快指针fast至少在环里转了一圈,才和慢指针slow相遇。
(想不懂可以自己模拟一下,或者举反例)
fast 与 slow 相遇,一定是 fast 去追 slow 的过程(Because fast 先入环)
针对上面的x = n(y + z) - y;,我再让出来一圈,看看x和什么正数有关系(因为x是看不出来和负数有什么关系)。
得:x = (n - 1)(y + z) + z;
当n = 1时(思路就很清晰),即 fast 转了一圈之后,和slow相遇了,得出:x = z; (🦄得到了这个很关键的式子!)
说明:
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;
}
};