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

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

本题是一道模拟过程的题目。搞清楚两两交换的步骤之后,写出对应的代码也就不是难题了。不过在学习题解的过程中发现,两两交换的步骤也有很多种实现方式。自己在做题目的时候使用的思路如下:

进行两两交换之前,设置三个指针,分别指向dummyheadhead.next。因为需要指向headhead.next,所以需要预先判断这两者是否为null。其余交换步骤如图所示。
但可以发现,如果按照这种交换方法,当链表节点个数为偶数时,完成最后一对节点的交换时,cur会指向最后一个节点,此时tmp = cur.next.next会出现空指针异常。因此,在进行指针移位操作前,进行一次判断。

class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode dummy = new ListNode();
        dummy.next = head;
        ListNode pre = dummy;
        ListNode cur = head;
        ListNode tmp = head.next;
        while (pre.next != null && pre.next.next != null) {		// 确保之后还存在至少一对可交换节点
            cur.next = tmp.next;
            tmp.next = cur;
            pre.next = tmp;

            if (cur.next == null) {
                break;
            } else {
                tmp = cur.next.next;
                pre = cur;
                cur = cur.next;
            }
        }
        return dummy.next;
    }
}

卡哥博客内的Java题解的思路图解如下:
image
与自己的思路不同,题解选择存储head.next.next。在进行两两交换操作时,第二步改变head.nextnext
附记:本题还存在递归解法,因为时间问题今天没能学习,作为后续要处理掉的遗留问题之一。

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

最直观的思路就是通过一次遍历统计出链表的长度,然后再根据链表长度遍历到倒数第n个节点的前一个节点(想要对一个节点进行操作,必须移动到该节点的前一个节点),然后删除目标节点。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode cur = head;
        ListNode dummy = new ListNode(0, head);
        int size = 0;
        while (cur != null) {	// 遍历链表,获取链表长度
            size++;
            cur = cur.next;
        }
        System.out.println(size);
        ListNode pre = dummy;
        cur = head;
        for (int i = 0; i < size - n; i++) {	// 移动到目标节点的前一个节点
            pre = cur;
            cur = cur.next;
        }
        pre.next = cur.next;	// 删除节点
        return dummy.next;
    }
}

题目提示存在只需遍历一次链表的方法。在自己做题的过程中没能想出来,查看卡哥给出的思路之后直呼精妙.jpg。这个方法使用了双指针,预先将双指针拉开n个节点的长度,当靠后的节点移动至链表的最后一个节点时,靠前的节点便正好移动到了目标节点的前一个节点(再次强调,想要对一个节点进行操作,必须移动到该节点的前一个节点),进行删除操作即可。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        ListNode pre = dummy;
        ListNode cur = dummy;
        for (int i = 0; i < n; i++) {
            pre = pre.next;		// 确保cur最后停在目标节点的前一个节点 #1
        }
        while (pre.next != null) {	// 确保cur最后停在目标节点的前一个节点 #2
            pre = pre.next;
            cur = cur.next;
        }
        cur.next = cur.next.next;	// 删除操作
        return dummy.next;

    }
}

面试题 02.07. 链表相交

自己一开始的写法非常暴力,直接将两个链表的所有节点分别存储在ArrayList里,然后倒着比较。需要对一整个链表与另一个链表的一部分相交进行额外的判断。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }

        ArrayList<ListNode> listA = new ArrayList<ListNode>();
        ArrayList<ListNode> listB = new ArrayList<ListNode>();
        ListNode cur = headA;
        while(cur != null) {  // 暴力存储A
            listA.add(cur);
            cur = cur.next;
        }
        cur = headB;
        while (cur != null) {  // 暴力存储B
            listB.add(cur);
            cur = cur.next;
        }

        if (listA.get(listA.size() - 1) != listB.get(listB.size() - 1)) {  // 最后一个节点都不相等的两个链表不可能存在相交点
            return null;
        } else {
            int i = 1;
            for (; i <= Math.min(listA.size(), listB.size()); i++) {
                if (listA.get(listA.size() - i) != listB.get(listB.size() - i)) {
                    return listA.get(listA.size() - i).next;
                }
            }
            if (listA.get(listA.size() - i + 1) == listB.get(listB.size() - i + 1)) {  // 对一整个链表都和另外一个链表的一部分相交的情况进行额外判断
                return listA.get(listA.size() - i + 1);
            }
        }
        return null;
    }
}

当然这并不是优秀的算法,需要额外花费两个ArrayList的空间(也就是O(n)的空间复杂度?)。
题目提示存在空间复杂度为O(1)的算法。寻找链表相交的节点,本质可以看作寻找链表的倒数第n个相同节点。当链表遇到与倒数第n个节点节点相关的问题时,就可以考虑使用双指针,将两个指针拉开指定距离以达到进行与倒数第n个节点相关联的操作。
使用双指针时,先分别遍历两个链表,得到链表的长度sizeAsizeB(在这里,假设sizeA > sizeB便于之后的叙述)。然后,使指针pre指向链表A,cur指向链表B。之后移动pre,使pre指向倒数第sizeB个节点。pre所指向的链表A的节点及其之后的节点便是有可能与链表B相交的部分。之后,同时移动precur,当遇到pre == cur时返回这一节点。
上述文字描述画图如下:

理解逻辑之后,实现代码如下:

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }

        int sizeA = 0;
        int sizeB = 0;

        ListNode cur = headA;
        while (cur != null) {
            sizeA++;
            cur = cur.next;
        }
        cur = headB;
        while (cur != null) {
            sizeB++;
            cur = cur.next;
        }

        ListNode pre = sizeA > sizeB ? headA : headB;  // 指向较长链表
        cur = pre == headA ? headB : headA;  // 指向较短链表

        for (int i = 0; i < Math.abs(sizeA - sizeB); i++) {  // 移动至较长链表可能与较短链表发生相交的部分的起始位置
            pre = pre.next;
        }
        while (pre != null && cur != null) {
            if (pre == cur) {
                return pre;
            }
            pre = pre.next;
            cur = cur.next;
        }
        return null;
    }
}

142.环形链表II

读完题目整个人处于“我是谁,我在哪,我要做什么”的状态,纠结良久决定放弃,直接学习卡哥博客。
看完博客消化良久的我:这就是程序员才能做的追及问题吗.jpg
重点在于如何制造条件使得确定环入口成为可能。这里使用的是双指针变体,快慢指针。
设定slowfast指针,令slow指针每次只移动一步,而fast指针每次移动两步。那么,如果存在环,两个指针一定会在环内相遇。

  • Q1:为什么一定在环内相遇?
    fast指针一定先于slow指针进环,那么,两者的相遇就一定只可能在环内。

  • Q2:那么为什么两个节点一定会相遇?
    设想一下两个指针都进入环内的场景,这个时候,问题就变成了一个非常经典的追及问题。在fast走两步,slow走一步的情况下,每一次行动时fast都会离slow更近一个节点。因此,不存在fast没有与slow相遇就已经越过slow的可能。

确定了fastslow一定会在环内相遇后,就可以将相遇转变成数学问题,找到相遇与环入口之间的关系了。

  • Q3:光看这张图仍然无法确定数学关系,我们无法确定slow移动的步数。
    实际上是可以确定的。上文提到,一旦两个指针都进入环内,问题就演变成了追及问题。当slow入环时,追及问题处于最开始的阶段。此时两个指针之间的距离不可能大于等于环本身的长度。因此,以fast一次追一个节点的速度,当fastslow相遇时,slow一定没有转完一整圈。

确定这一点后就可以列出数学式子了。设链表头距离环入口的节点数为x,相遇时slow在环内移动的节点数为y,环剩余的节点数为z,可以列出以下数学表达式:
(x + y) * 2 = x + y + n * (y + z)
等式左侧是slow移动的总步数,右侧是fast移动的总步数,其中n为在环内转的圈数。根据之前的设置,可以确定fast移动的步数是slow的两倍。由于本题我们想求的是x,因此将等式变形得:
x = n * (y + z) - y
由于在与slow相遇之前,fast至少会在环内移动一整圈,因此取n = 1,可以发现x = z。当n > 1时,增加的只是y + z,即在环内完整地多转了多少圈。因此,我们可以通过设置precur指针,pre指向链表头,cur指向fastslow相遇的位置,两个指针每次都只移动一步。不难发现,当两者相遇时,相遇位置一定是环的入口。
逻辑如上,根据逻辑写出来的代码如下:

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;

        while (fast != null && fast.next != null) {
            slow = slow.next;  // 移动一步
            fast = fast.next.next;  // 移动两步

            if (slow == fast) {
                ListNode pre = head;
                ListNode cur = fast;
                while (pre != cur) {
                    pre = pre.next;
                    cur = cur.next;
                }
                return pre;
            }
        }

        return null;
    }
}

代码并不长,难点在于设计出这样一套逻辑,希望自己二刷的时候能够完全消化本题。

posted @   RenewableGit  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示