代码随想录算法训练营第四天| 24. 两两交换链表中的节点 19.删除链表的倒数第N个节点 面试题 02.07. 链表相交 142.环形链表II 链表总结


一、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 == NULLfast->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 n1 圈的环长,恰好等于从链表头部到入环点的距离。其中, n n nfast在环中走过的圈数;

 因此,算法可设计为:从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点.
在这里插入图片描述
动图演示如下:
在这里插入图片描述
该方法在算法实现与运行速度方面明显是优于下标比较的。

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/)

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值