代码随想录算法训练营第四天 | LeetCode24 链表中节点两两交换、LeetCode19 删除倒数第n个节点、LeetCode02.07 求两链表相交值、LeetCode142 环形链表

本文详细解析了LeetCode中涉及链表操作的四个题目,包括24题链表节点两两交换、19题删除倒数第n个节点、2.07题求两链表相交值以及142题环形链表。主要介绍了如何利用快慢指针、虚拟头节点等技巧解决问题,并提供了相应的代码实现和思路分析。
摘要由CSDN通过智能技术生成

1. LeetCode24 链表中节点两两交换

题目

在这里插入图片描述

代码

/**
 * 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) {
    if (head == null || head.next == null){
            return head;
        }
        
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode cur = dummy;
        
        while (cur.next != null && cur.next.next != null){
            ListNode temp3 = cur.next.next.next;
            ListNode temp1 = cur.next;
            cur.next = cur.next.next;
            cur.next.next = temp1;
            temp1.next = temp3;
            cur = temp1;
        }
        return dummy.next;
    }
}

思路

cur指针的定义及遍历:

  1. 知道定义一个指向dummy head的cur指针来遍历链表后:首先要确定的事情,每次遍历,cur指针指向哪里?经过分析和画图,节点两两为一组,cur指向每组的上一个节点。
  2. 还要确定的一件事情就是遍历什么时候结束?根据前面分析,节点是两两交换位置的,所以可分奇偶讨论。如果节点个数为奇,则cur.next.next为空时停止遍历;如果节点个数为偶,则cur.next为空时停止遍历。所以可以一直遍历下去的条件即为cur.next != null && cur.next.next != null。为什么是&& 而不是 || ?奇数节点就不需要交换了,所以只有满足后面有偶数个节点的时候才会进入循环。
  3. 这个while里的条件还要注意一下,cur.next != nullcur.next.next != null顺序不能反,因为一旦反过来,如果cur.next为空的话,先执行cur.next.next,就会报空指针异常了。

遍历时的操作:

  1. 什么样的节点需要提前定义一个temp指针指向它?当进行改变某个节点的next节点操作后,这个节点会断线,没有指针可以指向它了,则需要在进行这个操作前先定义一个temp指针指向它。
  2. 画图画图画图!把原来的指针走向,和操作后的指针走向都画出来,拆分一个或几个节点为一个单位进行操作,再定义针对这个单位的操作,循环遍历即可。
  3. 最后return,return头节点,但是函数接收到的实参head有可能已经不再是头节点了,所以不能直接return head,而是return dummy.next

画图

在这里插入图片描述

2. LeetCode19 删除倒数第n个节点

题目

在这里插入图片描述

思路

整体思路为,找到倒数第n个节点的前一个节点,改变这个节点的next指向,即可。

如何找到倒数第n个节点的前一个节点?

  1. 第一种方法:先for循环遍历一下,获取整个链表的size。然后再for int i = 0…i < …重新遍历到倒数第n个节点的上一个节点处(用size-n来定位),改变其next指向。即完成要求。
  2. 第二种方法:不需要获取整个链表的size。用快慢指针法。需要保证,当快指针指向最后一个节点的next的null节点时,慢指针指向倒数第n个节点的前一个节点,也即:快慢指针要相差n+1个节点。因此,首先,快指针先走n+1步,随后快慢指针一起向后遍历,直至快指针指向null为止。
  3. 第二种方法不太好理解,总感觉是已知题解倒退思路能理解,但正推就想不到这样的思路。今天想到了一种解释,觉得还挺贴合:快指针的作用就是一个指示,当它指向null的时候,就指示着慢指针到了倒数第n个节点。是为了让慢指针在不知道size的情况下,知道现在到了倒数第n个节点。

代码

第一种思路的代码:

/**
 * 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) {
        if (head == null){
            return head;
        }
        //cur第一遍遍历,用来查找size大小
        ListNode cur = head;
        int size = 0;        
        while (cur != null){
            size++;
            cur = cur.next;
        }
        
        //cur第二遍遍历,用来找倒数第n个节点的前一个节点
        ListNode dummy = new ListNode(-1, head);
        cur = dummy;
        int index = 0;
		
		//用while的写法
        while (cur.next != null){
            if (index == size-n){
                cur.next = cur.next.next;
                break;
            }else {
                cur = cur.next;
                index++;
            }
        }
        return dummy.next;

		//用for的写法
        
        for (int i = 0; i < size; i++){
            if (i == size-n){
                cur.next = cur.next.next;
                i++;
            }
            cur = cur.next;
        }
        return dummy.next;

}

第二种思路的代码:

/**
 * 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) {
        if (head == null){
            return head;
        }
        ListNode dummy = new ListNode(-1, head);
        ListNode fast = dummy;
        ListNode slow = dummy;

        for (int i = 0; i <= n; i++){
            fast = fast.next;
        }
        while (fast != null){
            slow = slow.next;
            fast = fast.next;
        }
        slow.next = slow.next.next;
        return dummy.next;
    }
}

3. LeetCode02.07 求两链表相交值

题目

在这里插入图片描述

思路

提醒:
交点不是数值相等,而是指针相等

刚开始的思路:
curA遍历A链表,curB遍历B链表;while (curA != null && curB != null),如果curA == curB,则为交点,如果 !=,则继续遍历curA = curA.next;curB = curB.next;
结果当然报错了。。因为这相当于比较A(1)和B(1),A(2)和B(2)…没办法找出来交点在比如A(3)和B(5)的地方

于是借鉴代码随想录里的思路:
审题后可得,两链表相交之后不会再分开,所以从相交开始的节点到最后一个节点都是相同的,值也相同,数量也相同。如果链表A长度为5,链表B长度为3,那么最多最多这两个链表从链表A的第三个节点开始相交,即链表A的第三个节点与链表B的第一个节点相交。所以可得如下思路:求出sizeA和sizeB,作差,假设A链表比B链表多n个节点,即差值为n,则可跳过A链表的前n个节点,从A链表的第n+1个节点开始,与B链表的第一个节点相比较,之后二者可以同时往后遍历。

思路图:
在这里插入图片描述

代码

/**
 * 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) {
        if (headA == null || headB == null){return null;}

        ListNode curA = headA;
        ListNode curB = headB;

        int sizeA = 0;
        int sizeB = 0;
        int diff = 0;

        while (curA != null){
            sizeA++;
            curA = curA.next;
        }
        while (curB != null){
            sizeB++;
            curB = curB.next;
        }
    
        curA = headA;
        curB = headB;
        
        if (sizeA >= sizeB){
            diff = sizeA - sizeB;
            for (int i = 0; i < diff; i++){
                curA = curA.next;
            }
            while (curA != null){
                if (curA == curB){
                    return curA;
                }else {
                    curA = curA.next;
                    curB = curB.next;
                }
            }
        }else {
            diff = sizeB - sizeA;
            for (int i = 0; i < diff; i++){
                curB = curB.next;
            }
            while (curB != null){
                if (curA == curB){
                    return curA;
                }else {
                    curA = curA.next;
                    curB = curB.next;
                }
            }
        }
        return null;
    }
}

上述代码如果想更简短的话,可以优化一下。现在是两种情况分开讨论:sizeA >= sizeB和 sizeB > sizeA。可优化为,统一让 sizeA >= sizeB,如果 sizeB > sizeA 的话,将sizeA与sizeB交换一下。交换二者的size以及cur指针。

力扣上另一种高赞题解,很妙

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
力扣上有详细的算法执行过程示意图。二刷的时候温习。。

4. LeetCode142 环形链表

题目

在这里插入图片描述

思路

此题两个关键点:
其一是如何确定该链表有环?
其二是如何确定环的起始位置?

总体思路,快慢指针法。快指针一次走两步,慢指针一次走一步。

针对第一个问题,如果快慢指针相遇了,即某个节点处,快指针等于慢指针,则一定有环。如果没有相遇,则一定没环。

那么问题来了,为什么快指针一次走两步,慢指针一次走一步?能不能快指针一次走三步,慢指针一次走一步。答案是不行的。因为快指针一次走两步,慢指针一次走一步这个规则,保证了每一步中,快指针是多走一步的,所以如果有环,在某个节点,当快指针多走一步之后,快慢指针一定能保证相遇。但如果多走了两步,有可能就正好多走的这两步跳过了慢指针,错过了相遇。

针对第二个问题,如何确定环的起始位置,上推导——

在这里插入图片描述
本人第一遍推的时候犯了两个错误:

  1. fast指针走过的节点数写成了x+y+z+n(y+z),实际上应为x+n(y+z)+y。即fast指针在环里绕了n圈后又走了y,遇上了slow节点。
  2. 因为求的变量是x,所以把x单独放一边,得 x = n (y + z) - y。再进一步优化时,优化成了 x = (n-1)y + nz。实际上y+z表示一圈,应该以整体形式出现,所以应该优化成上图所示的式子,x = (n - 1) (y + z) + z

针对上图还有一个问题:为什么第一次在环中相遇,slow的步数是 x+y 而不是和fast一样, x + 若干环的长度 + y 呢?
答案是想像环为追及问题,快的顺时针跑去追慢的。如果环里有m个节点的话,快追慢最坏的情况是,快指针走到环里第二个节点的时候,慢指针进入环的第一个节点,则此时快要追慢m-1个节点。每一步快比慢多1个节点,所以只需m-1步,快就能追上慢。那么慢经过m-1步,也就刚走完一圈回到环的第一个节点。所以当fast和slow在环里相遇的时候,这一定是slow在环里呆的第一圈。
那为什么fast有可能在环里呆了很多圈了呢?因为如果x很大很大,环很小很小的话,fast比slow在走x的时候多走了很多步,那么在slow入环前,fast就要不停地在环里转啊转啊转了。

解决完上述疑惑,再来看最后得到的式子:x = (n - 1) (y + z) + z。这里n一定大于等于1的,也就是fast至少在环里转了一圈了。那么不管fast在环里转了多少圈,定义两个指针,temp1和temp2,一个指针从fast和slow相遇的位置出发,一个指针从head出发,每次都往后移一步,则这两个指针一定会在环的入口处相遇。这是上面一大段推导后得到的终极结论,也是这一段中唯一能指导下面代码如何写的结论。代码如下:

ListNode temp1 = fast;
ListNode temp2 = head;
while (temp1 != temp2){
   temp1 = temp1.next;
   temp2 = temp2.next;
 }
return temp1;

所以最终的题解代码如下。第一个while里的判断条件,还是遵循了不能报空指针异常的原则来设计的。

最终代码都写完之后,再想一下下面两种边界情况,都可通过。
在这里插入图片描述

代码

/**
 * 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) {
        if (head == null){return null;}
        
        ListNode fast = head;
        ListNode slow = head;
        
        while (fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow){
                ListNode temp1 = fast;
                ListNode temp2 = head;
                while (temp1 != temp2){
                    temp1 = temp1.next;
                    temp2 = temp2.next;
                }
                return temp1;
        }
        
        }
        return null;
    }
}

最后,有一个小tips:

关于链表的虚拟头节点的使用

在遇到无法定位头节点,头节点可能被移动删除导致定位失效的时候,可以考虑用虚拟头节点,并不是每道题都要用的!!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值