24. 两两交换链表中的节点
题目链接:24. 两两交换链表中的节点 - 力扣(Leetcode)
视频链接:帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点_哔哩哔哩_bilibili
解题思路:
本题的关键点在于两两交换,所以把链表的节点两两分组,分别交换完之后,指针由上一组的末尾节点指向下一组的第一个节点即可。
图解如下:
首先定义一个虚拟头指针,分别记录交换后第二、第三个节点的位置,也就是1和3;为什么2的位置不需要用temp存一下呢?因为2是交换后的第一个节点,操作的时候直接用cur指向cur->next->next就可以了。第一组交换完之后,将cur移动至第一组的最后一个节点,此时cur也就作为了下一组的虚拟头节点,循环操作即可,直到cur的next或者nextnext为空,这表示此时剩下的节点已经不能构成两个一组了,则不需要交换了。
/**
* 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){ //画图可知,包括虚拟头节点,三个三个一组,所以cur的next和nextnext都不能为空
ListNode *temp = cur->next; //记录原链表第一个节点,两两分组的第一组开头
ListNode *temp1 = cur->next->next->next; //记录原链表第三个节点。两两分组的第二组开头,用来让上一组最后一个节点指向这里;
cur->next=cur->next->next; //步骤一:cur指向原链表第二个节点
cur->next->next=temp; //步骤二:原链表第二个节点指向第一个节点,实现第一组的交换位置
cur->next->next->next=temp1; //第一组结束,最后一个节点指向下一组的第一个节点
cur= temp; //让cur到达第一组最后一个节点位置,成为下一组的新的头节点,然后继续循环
}
return dummyHead->next; //最后返回新的头节点
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
19.删除链表的倒数第N个节点
题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(Leetcode)
视频链接:链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点_哔哩哔哩_bilibili、
解题思路:
本体核心关键就在于如何在找到链表中倒数第n+1个数的指针位置,只要找到这个位置,让它指向nextnext就可以直接删除倒数第n个数了。之前我们通过while(n--)的方法可以找到正向第n个数,但是链表size未知,所以这种方法似乎不可行了。于是考虑双指针法,让一个指针先走n+1步,然后两个指针一起移动,直到先走的指针指到null了则表示以及遍历完链表了,此时后走的指针就是倒数第n+1个数的位置了。
/**
* 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!=nullptr){ //快指针先向前走n步
fast = fast->next;
}
fast = fast->next; //多走一步,为了让fast和slow之间差n步,这样同时走完时,slow会指向倒数第n个的前一个
while(fast!=nullptr){ //fast一直遍历,直到指向最后的null,fast和slow一起向后移动
fast = fast->next;
slow = slow->next;
} //此时结果,fast指向最后的null,slow指向倒数第n+1个节点
slow->next = slow->next->next; //利用slow删除倒数第n个节点
return dummyHead->next; //返回此时的头节点
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
面试题 02.07. 链表相交(160.链表相交)
题目链接:面试题 02.07. 链表相交 - 力扣(Leetcode)
解体思路:
根据题意,只要两个链表有相交,则从相交的位置开始后面的节点值都相同。那么如何找到第一个相同的节点呢,我们就可以想到先把链表尾对齐,然后让两个链表指针从较短的一条链表头节点位置开始同时移动,当两个相等的时候那就是交点。
方法一:暴力尾对齐
这就是如下的暴力对齐法,将链表尾对齐,通过计算两个链表长度,求出长度差gap,让长链表指针先移动gap,此时两个链表位置相同,同时步进求出相同节点,最后返回相交位置的指针。
/**
* 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 !=NULL){ //计算A链表长度
lenA++;
curA = curA->next;
}
while(curB !=NULL){ //计算B链表长度
lenB++;
curB = curB->next;
}
curA = headA; //把两个遍历的指针重置回头节点
curB = headB;
if(lenB>lenA){ //确保A链表长与B链表,逻辑上无所谓,主要是便于下面的代码编写,不需要考虑两种情况
swap(lenA,lenB); //swap交换位置
swap(curA,curB);
}
int gap = lenA - lenB; //计算两个链表长度差
while(gap--){
curA = curA->next;
} //让更长的链表上的cur节点先走gap步,把两个链表尾对齐,此时curA和curB已经对齐。
while(curA!=NULL){ //不为空的时候,两个cur一起往后遍历
if(curA == curB)return curA; //当两个cur的值相等则返回curA节点,或者curB也行,相当于是把两个融合了,舍去了另一边的后续
curA = curA->next;
curB = curB->next;
}
return curA; //最后返回curA,即为相交的节点,即使两个都遍历到null也不相交,此时cur指向了null,返回的也是null符合题意
}
};
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
方法二:对换头节点法
该方法核心思想是,当一个链表头节点遍历完到null位置后,就直接指向另一个链表的头节点,当第二个指针到达另一个链表上的时候,两个节点就必然处于方法一那张图的状态。
/**
* 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) {
if (headA == NULL || headB == NULL) { //当任意一个链表是空链表,则无法相交
return NULL;
}
ListNode *pA = headA, *pB = headB;
while (pA != pB) { //当两个节点不相同的时候
pA = pA == NULL ? headB : pA->next; //当pa不为空就遍历下一位,直到遍历完A链表,指向B的头节点
pB = pB == NULL ? headA : pB->next; //同上,B遍历完就指向A头节点
} //上述两行,pa和pb都指向了另一个链表之后,将尾巴对齐,pa和pb此时必然是并驾齐驱的同位置
return pA; //pa和pb同时移动,直到pa=pb*(此处其实是在判断数值相等),则返回此时的pa或pb节点,相当于把后续融合了
}
};
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
142.环形链表II
题目链接:142. Linked List Cycle II - 力扣(Leetcode)
视频链接:把环形链表讲清楚! 如何判断环形链表?如何找到环形链表的入口? LeetCode:142.环形链表II_哔哩哔哩_bilibili
解题思路:
本体代码逻辑并不复杂,难点在于代码前的数学推导,如何通过简单的指针移动来找到环的入口位置呢?本题的思想是通过定义两个快慢指针fast和slow,fast一次走两步,slow一次走一步,让fast先进入环内然后追击slow指针。
根据上图可以推导:
当fast和slow相遇时: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-1)y+nz ,
再从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同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
/**
* 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不能为空并且fast->next也不能为空
fast = fast->next->next; //fast一次移动两位
slow = slow->next; //slow一次移动一位
if(fast==slow){ //当fast和slow第一次相遇时,必然是在环内的
ListNode* index1 = fast; //将相遇位置定义一个指针index1
ListNode* index2 = head; //头节点定义一个临时指针index2,根据公式推导,可知当index1和index2同步移动,相遇位置就是环形入口
while(index1!=index2){ //同步移动直到两个index相同
index1= index1->next;
index2= index2->next;
}
return index1; //返回相遇位置index1或者index2,即为环形入口
}
}
return NULL; //如果链表没有环,则fast指针一定会指向null,则跳出了外层while循环,此时直接返回null即可
}
};
- 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
- 空间复杂度: O(1)
总结: