目录
一、LeetCode 24. 两两交换链表中的节点
题目链接:24. 两两交换链表中的节点
文章讲解:代码随想录
视频讲解:帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点
思路:
给出一个链表,要求两两为一组,每组之间两个节点调换顺序,且需使用节点操作实现。
本题对于节点的交换操作与我们昨日的反转链表有相似之处,可以借鉴反转链表的递归函数方式实现本题中的两两交换要求。
从给出的测试样例得出只含一个节点的组或不含节点的组无需交换,可写出递归函数的边界条件为pr->next == NULL || pr->next->next == NULL
,满足条件时rerurn
结束递归;
每两个节点的交换作为递归函数主体,交换操作算法描述如下:
- 函数传参为本组的前一个节点
pr
,从而得到本组的两个节点cur1 = pr->next
,cur2 = pr->next->next
- 将前一个节点
pr
指向cur2
- 将
cur1
指向下一组第一个节点,即cur2->next
- 将
cur2
指向cur1
- 主体完成,进入下一层递归,将
cur1
作为下一层递归的pr
参数
注意题目给出链表的head
是首元节点,因此需要增加一个头结点(虚拟头结点)Head_prior
实现递归函数的一致化操作。
C++代码
/**
* 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:
void swap(ListNode* pr){
if(pr->next == NULL || pr->next->next == NULL){
return;
}
ListNode* cur1 = pr->next;
ListNode* cur2 = cur1->next;
pr->next = cur2;
cur1->next = cur2->next;
cur2->next = cur1;
swap(cur1);
}
ListNode* swapPairs(ListNode* head) {
ListNode* Head_prior = new ListNode();
Head_prior->next = head;
swap(Head_prior);
return Head_prior->next;
}
};
Python代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swap(self, pr: Optional[ListNode]):
if(pr.next == None or pr.next.next == None):
return
cur1 = pr.next
cur2 = cur1.next
pr.next = cur2
cur1.next = cur2.next
cur2.next = cur1
self.swap(cur1)
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
Head_prior = ListNode(next = head)
self.swap(Head_prior)
return Head_prior.next
二、LeetCode 19. 删除链表的倒数第N个节点
题目链接:19.删除链表的倒数第N个节点
文章讲解:代码随想录
视频讲解:链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点
思路
题目要求实现删除链表倒数第n个元素,笔者对本题的思路除两次遍历以外,从倒数想到后入先出( L I F O LIFO LIFO)的栈结构。算法设计为:链表所有节点入栈,依次让 n + 1 n+1 n+1个元素出栈,定位到删除节点的上一个节点,从而进行删除操作。
使用栈方法是对两次遍历方法的一定程度的优化,但在最差情况下(删除节点为链表第一个节点)仍遍历了两遍,达不到遍历一遍的最优算法。
学习代码随想录方法得知可使用快慢指针的双指针法,实现一次遍历,即:
- 快指针先行,向后遍历
n+1
个节点。n+1
个是由于快指针fast
最终停在NULL
,保证慢指针slow
能正确停在倒数第n
个节点的前一个节点
- 此时快慢指针同时向后移动,直到
fast == NULL
时停止移动,慢指针所在的节点即为需要删除节点的前一个节点
- 利用慢指针完成删除操作
C++代码
栈
/**
* 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* Head_prior = new ListNode();
Head_prior->next = head;
stack<ListNode*> nodeStack;
ListNode* p = Head_prior;
while(p){
nodeStack.push(p);
p = p->next;
}
for(int i = 0; i < n; i++){
nodeStack.pop();
}
p = nodeStack.top();
ListNode* tmp = p->next;
p->next = tmp->next;
delete tmp;
return Head_prior->next;
}
};
双指针
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
while(n-- && fast != NULL) {
fast = fast->next;
}
fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
// ListNode *tmp = slow->next; C++释放内存的逻辑
// slow->next = tmp->next;
// delete tmp;
return dummyHead->next;
}
};
Python代码
栈
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
Head_prior = ListNode(next = head)
stack = []
p = Head_prior
while(p != None):
stack.append(p)
p = p.next
p = stack[-(n+1)]
p.next = p.next.next
return Head_prior.next
双指针
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
# 创建一个虚拟节点,并将其下一个指针设置为链表的头部
dummy_head = ListNode(0, head)
# 创建两个指针,慢指针和快指针,并将它们初始化为虚拟节点
slow = fast = dummy_head
# 快指针比慢指针快 n+1 步
for i in range(n+1):
fast = fast.next
# 移动两个指针,直到快速指针到达链表的末尾
while fast:
slow = slow.next
fast = fast.next
# 通过更新第 (n-1) 个节点的 next 指针删除第 n 个节点
slow.next = slow.next.next
return dummy_head.next
三、LeetCode 面试题 02.07. 链表相交
题目链接:面试题 02.07. 链表相交
文章讲解:代码随想录
思路
遍历两个链表,使指针停在最后一个节点上,如果两链表最后一个节点指针相同,则两链表相交,否则不相交,return 0
若相交,遍历得到两链表长度m, n
,使用快慢指针,用两链表长度之差作为快慢链表下标值的差,设定好以后同时移动两指针,最终两指针第一次相等的位置即为两链表相交的位置。
C++代码
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;
}
};
时间复杂度:O(n+m)
;
空间复杂度:O(1)
;
四、LeetCode 142.环形链表II
题目链接:142.环形链表II
文章讲解:代码随想录
视频讲解:把环形链表讲清楚! 如何判断环形链表?如何找到环形链表的入口? LeetCode:142.环形链表II
思路
环状检验
需要找出链表循环的入口,首先判断链表是否有环形结构。可采用快慢双指针的方式检验:
设立快慢指针
fast, slow
,使快指针每次前进两个节点,慢指针每次前进一个节点,即fast = fast->next->next; slow = slow->next
;
若链表有环状结构,那么在环状结构中fast
必定会与slow
相遇,在fast == slow
时停止循环;否则在某次循环中fast == NULL
或fast->next == NULL
,返回NULL
即可.
确定下标
若已确定有环状结构,则需要确定环入口的下标,这里提供笔者自己的思路与一个最优思路:
下标比较
笔者对于环入口下标的求解思路为:环入口下标一定是环内所有节点中下标最小的 ,因此可以遍历环中的节点,直到找到一个下标最小的节点。
具体算法描述如下:
- 在环形检验中已经使得
fast == slow
,设置循环终止条件为fast->next == slow
或满足当前下标值小于上一个节点下标值(环状结构回到入口,即下标最小)- 循环体为
fast = fast->next
依次向后遍历环状链表节点,使用head_distance
函数计算当前节点下标(即从头节点向后遍历到当前节点,返回下标值),index
存储上一个节点的下标值- 若
index > head_distance(head, fast)
,即上一个节点下标值大于当前结点下标值,那么当前节点为环状链表入口;若fast->next = slow
,那么slow
所指节点为环状链表入口.
C++代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
int head_distance(ListNode* head, ListNode* p){ //计算节点下标值函数
ListNode* meas = head;
int index = 0;
while(meas != p){
index ++;
meas = meas->next;
}
return index;
}
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast && fast->next){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
break;
}
}
if(fast && fast->next){
int index = head_distance(head, fast);
while(fast->next != slow){
fast = fast->next;
if(head_distance(head, fast) < index){ //循环入口节点的下标值最小
return fast;
}
else{
index = head_distance(head, fast);
}
}
return slow; //slow下标最小则slow为循环入口
}
return NULL;
}
};
代数计算
代码随想录提供的方法为代数运算得出的一个结论:
从相遇点到入环点的距离加上
n
−
1
n−1
n−1 圈的环长,恰好等于从链表头部到入环点的距离。其中,
n
n
n为fast
在环中走过的圈数;
因此,算法可设计为:从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点.
动图演示如下:
该方法在算法实现与运行速度方面明显是优于下标比较的。
C++代码
/**
* 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;
}
};
五、链表总结
总结
本日的题目开始上难度了(流汗黄豆),第一题为较为简单的指针操作问题,后三道都涉及到快慢双指针的灵活应用,较笔者的思路而言都要更优。说明笔者在链表的双指针操作方面仍有所欠缺,仍应继续学习强化。
在快慢指针的应用方面,应强调快慢指针设置的差值的重要性,通过快慢指针的下标差巧妙实现题目要求。对于题目的代数运算方面有些欠缺,对于某些题目与数学理论的联系探究还应更深入一些。
文章图片来源:代码随想录 (https://programmercarl.com/)