代码随想录算法训练营第四天 | 链表 Part02


每日任务

一,leetcode 024 两两交换链表中的节点
二,leetcode 019 删除链表中的倒数第N个节点
三,面试题-02.07 链表相交
四,leetcode 142 环形链表II

一、leetcode 024 (两两交换链表中的节点)

1、原题链接

leetcode 024 两两交换链表中的节点
在这里插入图片描述

2、解题思路及代码展示

本题推荐使用虚拟头节点,避免每次针对头结点(没有前一个指针指向头结点),还要单独处理。
所以初始化操作有两点:
(1)定义一个虚拟头节点dummyHead指向真实头节点;
(2)定义一个cur节点,指向虚拟头节点,用来指示当前遍历位置。
交换逻辑如下图
在这里插入图片描述

(1)在正式交换前,利用temp1、temp2和temp3存一下两个后续会使用到的节点1,节点2和节点3,分别为cur.nextcur.next.nextcur.next.next.next
(2)第一步,先将cur指向节点2;
(3)第二步,将节点2指向临时节点temp1,也即是节点1;
(4)第四步,将节点1指向临时节点temp3,也即是节点3;
此时,就完成了一对的两两交换,交换完成如下图所示:
在这里插入图片描述接下来,就是执行下一步的两两交换,将cur移动到节点1的位置(此时已经是节点1和节点2交换以后的结果)
在这里插入图片描述
具体代码实现:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode() {}
 * ListNode(int val) { this.val = val; }
 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        //创建一个虚拟头节点,并初始化
        ListNode dummyHead = new ListNode(-1);
        // 将虚拟指针指向真实头节点
        dummyHead.next = head;
        // 再定义一个cur节点用来指示当前遍历位置,从虚拟头节点的位置开始
        ListNode cur = dummyHead;
        // 定义三个临时节点分别暂存会涉及到的三个节点
        ListNode temp1;
        ListNode temp2;
        ListNode temp3;

        while(cur.next != null && cur.next.next != null){
            temp1 = cur.next; // 存本次交换的两个目标节点靠前的那个(节点1)
            temp2 = cur.next.next; // 存本次交换的两个目标节点靠后的那个(节点2)
            temp3 = cur.next.next.next;  // 存下次交换的目标节点靠前的那个(节点3)
            // 步骤一
            cur.next = temp2;
            // 步骤二
            temp2.next = temp1;
            // 步骤三
            temp1.next = temp3;
            // 将当前指针cur移动到temp1的位置,准备下一轮交换
            cur = temp1;
        }
        return dummyHead.next;
    }
}

二、leetcode 019 (删除链表第倒数第N个节点)

1、原题链接

leetcode 019 删除链表第倒数第N个节点
在这里插入图片描述

2、解题思路及代码展示

本题也是双指针(快慢)的一个典型应用,现在题解的要求是找到倒数第N个节点,但是受到链表自身结构的限制,需要从头节点开始一个一个遍历。
同理,删除倒数第N个节点,有可能是头节点,所以为了避免头节点删除的单独处理,使用虚拟头节点的方法,然后定义两个指针,分别为快指针和慢指针,初始值为虚拟头节点。问题来了:快指针快在哪里?如何能找到倒数第N个节点?
我们就让快指针先走n+1步,然后快指针和慢指针同时向后移动,直到快指针指向null,此时我们的慢指针指向的就是想要删除节点的前一个节点,可以执行后续的删除操作
举个例子:
在这里插入图片描述
上图,想要删除的节点是节点3,此时n=2,所以将快指针走n+1 = 3步:
在这里插入图片描述
之后,slow和fast同步向后移动,当fast指向空时,slow指向的就是节点2,即指向删除节点的上一个节点(方便做删除操作),如下图:
在这里插入图片描述在这里插入图片描述

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 使用双指针思路
        
        //  先初始化一个虚拟头节点,然后让它指向头节点,方便统一删除头节点和非头节点的执行逻辑
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;

        // 再定义两个节点,分别指示快指针和慢指针, 初始都指示虚拟头节点
        ListNode fast = dummyHead;
        ListNode slow = dummyHead;
        
        // 先让fast快走n+1步,和慢指针差n+1步,到最后快指针指向null时,此时的满指针指向索要删除节点的前一个节点,方便删除操作
        n ++ ;
        while(n-- > 0 && fast!= null){
            fast = fast.next;
        }
        
        // 接下来,让快慢指针同步移动
        while(fast != null){
            fast = fast.next;
            slow = slow.next;
        }
        // 上边的循环之后,此时slow指向的就是删除目标节点的前一个节点,加一个避免空指针异常判断
        if(slow.next != null){
            slow.next = slow.next.next;
        }
        return dummyHead.next;
    }
}

三、面试题-02.07 链表相交

1、原题链接

leetcode 链表相交
在这里插入图片描述
在这里插入图片描述

2、解题思路及代码展示

简单来说,就是求两个链表交点节点的指针。 这里同学们要注意,交点不是数值相等,而是指针相等。
为了方便举例,假设节点元素数值相等,则节点指针相等。
目前如下的两个链表,curA指向链表A的头结点,curB指向链表B的头结点:
在这里插入图片描述
接下来,求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,这里的含义,因为两个链表,如果有交点的话,交点后面的部分两个链表是共享的,长度肯定是相同。所以,当我们让较长的链表先行移动长度差的步数后,两个链表剩下的长度就相同了。然后,我们可以同时遍历这两个链表,当它们相遇时,就是交点。
在这里插入图片描述

直接上代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {

        // 定义两个cur节点,指示在链表A和链表B中遍历的当前位置,初始化为两个链表的头节点
        ListNode curA = headA;
        ListNode curB = headB;

        // 分别求一下两个链表的长度
        int lenA=0  , lenB = 0;
        while(curA != null){
            lenA++;
            curA = curA.next;
        }
        curA = headA; // 恢复curA的初始状态即头节点A
        while(curB != null){
            lenB++;
            curB = curB.next;
        }
        curB = headB; // 恢复curB的初始状态即头节点B

        // 根据题解,我们统一保证curA为长链表的头节点,lenA为其对应长度
        if(lenB > lenA){
            // 第一步,交换lenA和lenB
            int tempLen = lenA;
            lenA = lenB;
            lenB = tempLen;
            // 第二步,交换两个头节点
            ListNode tempNode = curA;
            curA = curB;
            curB = tempNode;
        }
        // 求长度差
        int gap = lenA - lenB;

        // 让长链表中的cur节点向后移动
        while(gap-- > 0){
            curA = curA.next;
        }

        // 同步遍历curA和curB,遇到相同节点直接返回
        while(curA != null){
            if(curA == curB){
                return curA;
            }
            curA = curA.next;
            curB = curB.next;
        }
        return null;
    }
}



四、leetcode 142 (环形链表)

1、原题链接

leetcode 142 环形链表

在这里插入图片描述
在这里插入图片描述

2、解题思路及代码展示

本题的解题思路主要两点:
(1)判断提供的链表是不是一个环形链表
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢?

假设现在有一个环,然后让 fast指针在任意一个节点开始追赶slow指针,发现只要在环内,都会是下面的情况:fast和slow各自再走一步,fast和slow就相遇了
在这里插入图片描述
可以这么理解,fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

(2)如何找到环形链表的入口

确定有环以后,就该找到对应的环形链表的入口,如下图,我们假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。
在这里插入图片描述
所以有如下关系,当快慢指针相遇时:
slow指针走过的节点数为: x + y
fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针,
y+z为 一圈内节点的个数

而且根据fast每次两个节点,slow每次1个节点,可知:fast指针走过的节点数 = slow指针走过的节点数的 2 倍:
x + y + n (y + z)= 2(x + y)
化简之后:x + y = n (y + z)
我们的目标是找到环入口,也即x,对上述公式变形:
x = n (y + z) - y = (n - 1) (y + z) + z 注意,n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针
对于上述公式:
(1)当n = 1 时,x = z,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了,也就是说从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点
在这里插入图片描述
在这里插入图片描述
(2)当n > 1 时,就是fast指针在环形转n圈之后才遇到 slow指针。这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

补充说明:
在推理过程中,有一个问题:为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢(慢指针再走自己第一圈的时候,就被快指针追上了)?

首先slow进环的时候,fast一定是先进环来了。

假如,slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
在这里插入图片描述
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。但是slow进环的时候,fast一定是在环的任意一个位置,如图:
在这里插入图片描述那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。 也即,slow一定没有走到环入口3,而fast已经到环入口3了。
也就是说:在slow开始走的那一环已经和fast相遇了:
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 定义快慢指针,初始化为头节点
        ListNode fast = head;
        ListNode slow = head;

        // 判断是否有环
        while(fast != null && fast.next != null){
            slow = slow.next; // 慢指针一次走1个节点
            fast = fast.next.next; // 快指针一次走2个节点

            // 判断有环,即二者相遇了,是同一个节点,既是fast也是slow
            if(fast == slow){
                // 节点index1 记录从链表头节点开始
                ListNode index1 = head; 
                // 节点index2 记录从相遇节点开始
                ListNode index2 = fast; 
                // 让index1和index2同时向后移动,每次都走1步,直到相遇,相遇之处为环的入口
                while(index1 != index2){
                    index1 = index1.next;
                    index2 = index2.next;
                }
                return index1; 
            }
        }
        //  没有环直接返回null
        return null;
    }
}

提示:这里对文章进行总结:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值