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

72 阅读7分钟

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

题目链接

解题方法:虚拟头节点

解题步骤(如图示)

B{E$_V_3W{`TW4YEY_KH@D.png

说明:创建一个虚拟头节点(dummyHead),定义一个 cur 指针指向虚拟头节点用来遍历链表

需要思考和注意的问题:

  1. 遍历链表过程中,循环结束的条件是什么?
  2. 链表节点交换过程中是否需要保存部分节点位置?
  3. 交换完后 cur 指针该如何移动?
1. 循环遍历链表,结束的条件是什么?
  • 本题中,要想操作两个节点,指针必须指向两个节点的前一个节点(换句话说就是,指向第一个节点的指针决定后面两个节点的交换)。

  • 链表节点为奇数个时,需要判断 cur.next.next 是否为 null

  • 链表节点为偶数个时,需要判断 cur.next 是否为 null

  • 在写结束条件的时候,应该把 cur.next 的判断写在 cur.next.next 之前,确保不会出现空指针异常(细节点,需要注意!!!)

    代码表示为:while(cur.next != null && cur.next.next != null)

2.交换链表节点过程中是否需要保存部分节点?

需要保存的节点位置:

  • 两个交换节点中第一个节点的位置
  • 两个交换节点后面一个节点的位置
3.交换完后cur指针该如何移动?
  • cur 指针应该向后移动两个位置

解题代码:

/**
 * 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;
        ListNode cur = dummyHead;
        //定义两个临时节点保存第一个节点和第三个节点的位置
        ListNode temp;
        ListNode temp2;
        while(cur.next != null && cur.next.next != null){
            temp = cur.next;
            temp2 = cur.next.next.next;
            cur.next = cur.next.next;
            cur.next.next = temp;
            temp.next = temp2;
            //交换完后cur指针向后移动2位
            cur = cur.next.next;
        }
        return dummyHead.next;
    }
}

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

题目链接

解题方法:虚拟头节点 + 双指针

初始状态:定义一个虚拟头节点,定义快慢两个指针指向虚拟头节点

image-20231014215413691.png

指针移动过程:

image-20231014215510170.png

image-20231014215653096.png

需要思考的问题:

1.为什么 fast 指针要先走 n+1 步?

1.为什么 fast 指针要先走 n+1 步?

如果 fast指针 先走 n 步 ,然后 fast指针 和 slow指针 再同时移动 ,这样当 fast指针 指向 null 时,slow指针 刚好指向要删除的那个节点;

但是要删除节点需要得到它的前一个节点,所以应该让 fast指针 先走 n+1 步,再同时移动,这样 fast指针 指向 null 时,slow指针 就刚好指向要删除节点的前一个节点;

解题代码:

/**
 * 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 fastIndex = dummyHead;
        ListNode slowIndex = dummyHead;
        //先让快指针走n+1步
        for(int i = 0;i <= n;i++){
            fastIndex = fastIndex.next;
        }
        //快慢指针一起移动,结束时慢指针刚好指向删除节点的前一个节点
        while(fastIndex != null){
            fastIndex = fastIndex.next;
            slowIndex = slowIndex.next;
        }
        //移除节点
        slowIndex.next = slowIndex.next.next;
        return dummyHead.next;
    }
}

面试题 02.07.链表相交

题目链接

思路:求两个链表交点节点的指针

注意: 交点不是数值相等,而是指针相等

如下两个链表,curA 指向链表A的头结点,curB 指向链表B的头结点

image-20231014230841314.png

  1. 分别计算A、B两个链表的长度(计算完后别忘了让指针归位,很容易遗忘这个细节!!!

  2. curA 移动到和 curB 对齐的位置

image-20231014231121393.png

  1. 比较 curAcurB 是否相同
    • 相同,则为交点,返回
    • 不同,同时向后移动 curAcurB ,继续比较 curAcurB

注意:是比较指针,并不是比较指针对应的值(刚开始写的时候就比较成值了。。。)

解题代码

/**
 * 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) {
        //定义两个指针分别指向两个链表的头节点
        ListNode curA = headA;
        ListNode curB = headB;
        //分别计算两个链表的长度
        int lengthA = 0;
        int lengthB = 0;
        while(curA != null){
            lengthA++;
            curA = curA.next;
        }
        while(curB != null){
            lengthB++;
            curB = curB.next;
        }
        //让指针归位
        curA = headA;
        curB = headB;
       
        //如果B链表长度大于A,就交换两个链表的长度和指针,保证A链表的长度大于B链表
        //定义临时指针和长度用于交换
        if(lengthB > lengthA){
            int tempLength = 0;
            ListNode tempCur;
            tempLength = lengthB;
            lengthB = lengthA;
            lengthA = tempLength;
            tempCur = curB;
            curB = curA;
            curA = tempCur;
        }
        //计算两个链表的长度差
        //int n = Math.abs(lengthA - lengthB);
        int n = lengthA - lengthB;
        //移动curA让其与curB起始位置相同
        for(int i = 0;i < n;i++){
            curA = curA.next;
        }
        //此时两个链表的长度相同,遍历curA即可
        while(curA != null) {
            //相同,找到交点
            if(curA == curB){
                return curA;
            }else{
                curA = curA.next;
                curB = curB.next;
            }
        }
        return null;
    }
}

142.环形链表Ⅱ

题目链接

解题思路:数学思维+双指针

需要思考的问题:

  1. 如何确定这个链表是否有环
  2. 如果有环,应该如何找到环的入口处
1.如何确定这个链表是否有环

​ 定义快慢两个指针 快指针(fast)每次移动 2 个节点 ,慢指针(slow)每次移动 1 个节点,

如果 fast == slow ,则证明链表有环

说明:fast 指针 一定先进入环中,如果 fast指针 和 slow指针 相遇的话,一定是在环中相遇。

为什么fast指针和slow指针一定会相遇呢?

因为 fast 是走两步,slow 是走一步,相对于 slow 来说,fast 是一个节点一个节点的靠近 slow,所以fast一定可以和slow重合

2.如果有环,应该如何找到环的入口处

思路:

  • 假设从头结点到环形入口节点 的节点数为 x 。

  • 环形入口节点到 fast指针 与 slow指针 相遇节点的节点数为 y。

  • 从相遇节点 再到环形入口节点节点数为 z。

如图所示(来源:代码随想录):

20220925103433.png

快慢指针相遇时:

​ slow 指针 走过的节点数 : x + y

​ fast 指针 走过的节点数 : x + y + n*( z + y )

n 为 fast 指针 在环内走过的圈数,z + y 为一圈内所走的节点数

因为 fast 指针 速度是 slow 指针 的两倍 :

​ 所以 fast 指针 走过的节点数 = slow 指针 走过的节点数 * 2,

​ 即 2 * ( x + y ) = x + y + n * ( z + y )

整理得:x = n * (y + z) - y ,提出一圈 y + z 后 ====> x = ( n - 1 )( y + z ) + z

假设 n = 1 的情况,意味着 fast指针 在环形里转了一圈之后,就遇到了 slow指针 此时公式为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

如果 n > 1 的情况发生,就是 fast指针 在环形转 n 圈之后才遇到 slow指针

解题代码:

/**
 * 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){
            //快指针每次移动两个节点,慢指针每次移动一个节点
            fast = fast.next.next;
            slow = slow.next;
            //快慢指针相遇,证明链表有环
            if(fast == slow){
                //在相遇处定义一个指针,在头节点定义一个指针
                ListNode index1 = fast;
                ListNode index2 = head;
                //两个指针同时移动,直到相同,则为环的入口处
                while(index1 != index2){
                    index1 = index1.next;
                    index2 = index2.next;  
                }
                //找到入口,返回
                return index1;
            }
        }
        return null;
    }
}

补充说明:

为什么第一次在环中相遇,slow的 步数 是 x + y 而不是 x + y + 若干环的长度 呢?

这个就借用一下代码随想录网站里的解释吧。。。。。。

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

如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:

2021031816503266.png

如果 slow 和 fast 同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。

重点来了,slow 进环的时候,fast 一定是在环的任意一个位置,如图:

2021031816515727.png

那么 fast 指针走到环入口3的时候,已经走了k + n个节点,slow指针相应的应该走了(k + n)/2个节点。

因为 k 是小于 n 的,所以 (k + n) / 2 一定小于 n。

也就是说 slow 一定没有走到环入口3,而 fast 已经到环入口3了

说明在 slow 开始走的那一环就已经和 fast 相遇了