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

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

题目链接:24. 两两交换链表中的节点
文章讲解:代码随想录#24. 两两交换链表中的节点
视频讲解:帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点

题目描述

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例1

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例2

输入:head = []
输出:[]

示例3

输入:head = [1]
输出:[1]

思路

这道题画图后理解就比较清晰,继续盗用代码随想录的图O(∩_∩)O哈哈~
在这里插入图片描述
本道题画出图后,思路就比较清楚了,为了实现两两交换节点,需要分三步:
第一步,cur节点指向第二个节点,因为要修改cur的next,所以在执行之前需要备份第一个节点的指针
cur->next = cur->next->next
第二步,第二个节点指向第一个节点,因为要修改第二个节点的next,所以在执行之前需要备份第三个节点的指针
第三步,第一个节点指向第三个节点
然后就需要将cur指针向移动两个节点,指向第二个节点,下一个循环中,就会交换第三个节点和第四个节点。
注意在循环中的条件中,需要判断链表有奇数个节点还是偶数个节点。

参考代码

struct ListNode* swapPairs(struct ListNode* head) {
    struct ListNode *dummyNode = (struct ListNode *)malloc(sizeof(struct ListNode));
    dummyNode->next = head;
    struct ListNode *cur = dummyNode;

    // 终止条件需要判断奇数个节点和偶数个节点
    // 反过来想下,如果cur->next==NULL说明为偶数个节点,本次反转完就直接退出
    // 如果cur->next!=NULL但是cur->next->next==NULL,说明为奇数个节点,最后一个节点不用交换
    while (cur->next != NULL && cur->next->next != NULL) {
        struct ListNode *tmp1 = cur->next; // 备份节点指针,接下来要修改
        struct ListNode *tmp2 = cur->next->next->next; // 同样备份
        cur->next = cur->next->next;  // 步骤一:执行前cur->next存放到tmp1中
        cur->next->next = tmp1;       // 步骤二
        cur->next->next->next = tmp2; // 步骤三
        cur = cur->next->next; // 向后移动两个节点
    }
    head = dummyNode->next;
    free(dummyNode);
    return head;
}

总结

  1. 注意结束条件,需要考虑奇数个节点和偶数个节点的情况
  2. cur节点每次移动两个节点,每次指向两个交换节点的前一个节点
  3. 本题还可以使用递归法和迭代法实现,后面补充!!!

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

题目链接:19. 删除链表的倒数第 N 个结点
文章讲解:代码随想录#19. 删除链表的倒数第 N 个结点
视频讲解:链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点

题目描述

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例1

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例2

输入:head = [1], n = 1
输出:[]

示例3

输入:head = [1,2], n = 1
输出:[1]

思路

大体思路是,采用快慢双指针,刚开始双指针都指向head,先让快指针向后走n个节点,然后快慢指针同时向后走,直到快指针为NULL,此时慢指针刚好指向倒数第n个节点上。
这种确认倒数第n个节点的方法挺有趣的,可以画图自己模拟一下指针遍历的动态过程。

在链接中删除节点N时,通常采用的方法是遍历到N-1节点,然后设置N-1节点指向N+1节点,这样就可以删除节点N了,所以本题想要删除倒数第n个节点,关键是找到倒数第n+1个节点。那么快指针需要先后走n+1个节点,然后快慢指针同时向后走。

参考代码

struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
    if (n < 0)
        return NULL; // 异常情况,直接返回NULL

	// 构建虚拟头指针,记得函数结束前要释放内存
    struct ListNode* dummyNode =
        (struct ListNode*)malloc(sizeof(struct ListNode));
    dummyNode->next = head;
    struct ListNode* fast = dummyNode; // 快慢指针刚开始都指向虚拟头指针
    struct ListNode* slow = dummyNode;

    n++; // n+1的目的是为了遍历找到倒数第n+1个节点
    while (n-- && fast != NULL) { // 判断当前fast节点是否为空
        fast = fast->next; // fast节点向后遍历n+1个节点
    }

    while (fast != NULL) {
        fast = fast->next;
        slow = slow->next; // 找到倒数第n+1个节点
    }

    if (slow->next != NULL) {
        struct ListNode* tmp = slow->next;
        slow->next = slow->next->next; // 删除倒数第n个节点
        free(tmp);
    }

    head = dummyNode->next;
    free(dummyNode);
    return head;
}

总结

  1. 需要考虑异常情况,比如倒数第n个节点是否存在。
  2. 遍历的终止条件要考虑清楚,特别是while(fast!=NULL)与while(fast->next!=NULL)。
  3. 删除节点前需要存入到临时变量,然后再释放,因为删除节点时会改变slow->next的值。

面试题 02.07. 链表相交

题目链接:面试题 02.07. 链表相交
文章讲解:代码随想录#面试题 02.07. 链表相交

题目描述

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

示例1

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at ‘8’
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例2

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at ‘2’
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

思路

这道题是如果两个链表相交求相交节点的指针,有两点需要注意下:

  • 节点相交指的是地址相同,而不是节点的值相同。
  • 如果两条链表相交的话,那么这两条链表的最后n个节点的地址肯定是相同的。(这点是整个问题的关键,自己可以好好想想,推导一下)

因此对于两个链表ListA与ListB,求出各自的长度LenA与LenB。接着求出两个链表长度的差值diff=|LenA - LenB|,并判断出哪个链表是长链表。
假如链表ListA是长链表,那么从ListA的头节点开始遍历diff个节点时的节点为ListA’,此时ListA’与ListB的节点个数是相同的。
最后,同时开始遍历ListA’与ListB,如果有节点的指针(地址)相同,那么此时的节点就是两条链表的相交节点,返回该节点,否则两条链表不存在相关节点,返回NULL。

参考代码

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    struct ListNode *tmpA = headA;
    struct ListNode *tmpB = headB;
    int lenA = 0;
    int lenB = 0;
    int diff = 0;

    while (tmpA != NULL) { // 计算链表A的长度
        lenA++;
        tmpA = tmpA->next;
    }
    while (tmpB != NULL) { // 计算链表B的长度
        lenB++;
        tmpB = tmpB->next;
    }

    if (lenA > lenB) { // 计算两个链表的长度差
        diff = lenA - lenB;
        tmpA = headA;
        tmpB = headB;
    } else {
        diff = lenB - lenA;
        tmpA = headB;
        tmpB = headA;
    }

    while (diff--) {
        tmpA = tmpA->next; // 对长链表进行偏移
    }
    
    while (tmpA != NULL || tmpB != NULL) {
        if (tmpA == tmpB) {
            return tmpA;
        }
        tmpA = tmpA->next;
        tmpB = tmpB->next;
    }
    return NULL;
}

总结

  1. 写代码前一定要把思路理顺,这样写代码才不会磕磕碰碰。
  2. 再次强调,循环的条件需要考虑清楚。

LeetCode 142.环形链表II

题目链接:142.环形链表II
文章讲解:代码随想录#142.环形链表II
视频讲解:把环形链表讲清楚! 如何判断环形链表?如何找到环形链表的入口? LeetCode:142.环形链表II

题目描述

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例1

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例2

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例3

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

思路

这道题竟然考察到了数学推理,强烈推荐看一下视频讲解,简直是太精彩了。
比较形象的比喻就是两个人在操场跑步,一个快一个慢,快得人在跑过n圈后追上慢的人(n>=1)。
所以此题需要使用快慢双指针法,从头结点出发,fast指针每次向后走两个节点,slow指针每次向后走一个节点,那么fast相对slow的速度是每次向后走一个节点,这样如果fast在移动的过程中追上了slow(这是一道数学推理题,是可以证明在环形中fast肯定会追上slow的),则说明这个链表存在环形。

最好看一下卡哥的解题思路,以下是我基于卡哥的思路再记录一下,主要是讲得太好了,我再整理一下思路。
首先我们假设从头结点到环形入口节点处有x个节点,从环形入口节点到相遇节点处有y个节点数,从相遇节点处再到环形入口节点处有z个节点。
继续盗用[图片]展示一下。(https://programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html#%E6%80%9D%E8%B7%AF)来解释一下如何找到环形的入口
在这里插入图片描述
然后我们设定fast指针每次向后走两个节点,slow指针每次向后走一个节点。
那在相遇点处,slow指针总共走的节点数为 x+y,而fast指针总共走的节点数为 x+y+n(y+z),n为在环形内走的圈数,n>=1。
接下来重头戏来了,因为slow每次移动一个节点,fast每次移动两个节点,在fast与slow相遇时,它们存在如下关系:

fast走过的节点总数是slow的2倍,即 fast = 2*slow

因此可以得出这样的等式:x+y+n(y+z) = 2(x+y)*

等式可做如下的转化:
① x+y = n(y+z)
② x = n(y+z) - y >>>>切记我们为了求环形入口处的节点,与x有关
③ x = nz + (n-1)y
④ x = (n-1)z + z + (n-1)y
⑤ x = (n-1)(y+z) + z >>> y+z就是环形圈内的节点个数
当n=1时,x=z,说明从头结点到环形入口节点x与从相遇节点处再到环形入口节点处有z个节点有关。
也就是说,从头结点向后移动一个指针,同时从相遇节点向后也移动一个指针,这两个指针每次只移动一个节点,当他们相遇时就是环形入口的节点
当n大于1时,无非就是在环形节点中多走了几圈,最后还是会相遇的。

因此这道题首先得找到是否存在环形,如果不存在,则直接返回NULL。

参考代码

struct ListNode* detectCycle(struct ListNode* head) {
    struct ListNode* fast = head; // 快指针每次走两个节点
    struct ListNode* slow = head; // 慢指针每次走一个节点

    // 因为每次移动两个节点,所以需要判断fast与fast->next
    while (fast != NULL && fast->next != NULL) {
        fast = fast->next->next;
        slow = slow->next;

        if (fast == slow) {              // 相遇了说明存在环形
            struct ListNode* cur = head; // 从头开始遍历节点
            while (cur != fast) {
                cur = cur->next;
                fast = fast->next;
            }
            return fast;
        }
    }
    return NULL;
}

总结

  1. 先找到相遇的节点,然后再找环形入口节点。
  2. 快慢双指针灵活使用,fast每走两个节点,slow每次走一个节点,如果存在环形,肯定会相遇,即fast==slow。
  3. 比较新奇的一道题,只能说明自己遇到的题太少,还是得多刷题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值