Skip to content

Day 4


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

建议:先看视频,视频里讲解了注意事项,为什么需要 temp 保存临时节点。

题目链接:https://leetcode.cn/problems/swap-nodes-in-pairs/description/

文章链接: https://programmercarl.com/0024.两两交换链表中的节点.html

视频链接:https://www.bilibili.com/video/BV1YT411g7br

(1)思路分析

  • 本题的核心还是虚拟头节点的使用,为什么?因为涉及了链表的操作,使用虚拟头节点可以统一处理方法
  • 做题经验:虚拟头节点可以实现被操作的对象一定是cur->next
  • 解题关键
      1. 遍历什么时候结束? 需要分奇数节点和偶数节点两种情况
      • 对于偶数节点:cur -> next != nill 结束(从第一个元素开始,刚好是两两配对)
      • 对于奇数节点:cur -> next -> next != null(最后还剩余一个节点)
      • 注意:先写偶数条件再写奇数条件,否则会有空指针异常的风险
      1. 如何实现节点交换?交换时需要临时保存哪些节点?
      1. 易错点:在交换的过程中,指针的指向发生了变化,变化后的后继是谁

交换过程

alt text

交换后的结果

alt text

(2)题解

版本一:交换过程中 cur 的指向变化

java
class Soulution {
    public ListNode swapPairs(ListNode head) {
        // 添加虚拟头节点
        ListNode dummy = new ListNode();
        dummy.next = head;

        // 使用临时指针
        ListNode cur = dummy;

        /*
            遍历的结束条件
            偶数节点:cur.next != null
            奇数节点:cur.next.next != null
         */

        // 遍历节点
        while(cur.next != null && cur.next.next != null){
            // 首先用临时指针保存两个节点
            ListNode temp1 = cur.next; // 节点一
            ListNode temp2 = cur.next.next.next; // 节点三

            // 实现节点交换
            cur.next = cur.next.next; // dummy 指向节点二

            // 注意:此时cur的指向发生了变化,则后继节点的表示也需要变化
            cur.next.next = temp1; // 节点二指向节点一

            temp1.next = temp2; // 节点一指向节点三

            // 交换完成,cur向后移动一位,准备下一次交换
            cur = cur.next;
        }

        // 返回头节点
        return dummy.next;
    }
}

版本二:使用临时节点直接保存,更容易理解

java
class Solution {
  public ListNode swapPairs(ListNode head) {
        ListNode dumyhead = new ListNode(-1); // 设置一个虚拟头结点
        dumyhead.next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
        ListNode cur = dumyhead;
        ListNode temp; // 临时节点,保存两个节点后面的节点
        ListNode firstnode; // 临时节点,保存两个节点之中的第一个节点
        ListNode secondnode; // 临时节点,保存两个节点之中的第二个节点
        while (cur.next != null && cur.next.next != null) {
            temp = cur.next.next.next;
            firstnode = cur.next;
            secondnode = cur.next.next;
            cur.next = secondnode;       // 步骤一
            secondnode.next = firstnode; // 步骤二
            firstnode.next = temp;      // 步骤三
            cur = firstnode; // cur移动,准备下一轮交换
        }
        return dumyhead.next;
    }
}

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

建议:先看视频,双指针的操作,要注意,删除第 N 个节点,那么我们当前遍历的指针一定要指向 第 N 个节点的前一个节点

题目链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/

文章链接:https://programmercarl.com/0019.删除链表的倒数第N个节点.html

视频链接:https://www.bilibili.com/video/BV1vW4y1U7Gf

(1)思路分析

本题的精髓就在于如何找到倒数第 n 个节点,使用快慢双指针操作,同时使用虚拟头节点统一操作

如何实现找到倒数第 n 个节点 ?

初始时,让 slow 和 fast 相差 n 个节点,这样当 fast 到达末尾时,从后往前看,slow 指向的就是倒数第 n 个节点,但是删除操作应该是指向被删节点的前驱,即实际上 slow 和 fast 应该相差 n+1 个节点,这样 fast 到达末尾时,slow 就能指向被删节点的前驱

(2)题解

java
public class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 添加虚拟头节点
        ListNode dummy = new ListNode();
        dummy.next = head;

        // 定义快慢指针
        ListNode fast = dummy;
        ListNode slow = dummy;

        /*
            1. 找到倒数第n个节点,其次慢指针需要指向操作节点的前一个节点
            2. 即二者初始时相差 n + 1 个位置
            3. 到达末尾时,slow 指向的位置刚好是倒数第 n 个节点的前驱
            4. 然后就可以进行删除操作了
         */
        for (int i = 0; i <= n; i++) {
            fast = fast.next;
        }

        // 快慢指针同时移动
        while(fast != null){
            fast = fast.next;
            slow = slow.next;
        }

        // 此时慢指针指向需要被操作节点的前一个节点
        if(slow.next!=null){
            slow.next = slow.next.next;
        }

        // 返回头节点
        return dummy.next;
    }
}

class ListNode {
    int val;
    ListNode next;

    ListNode() {
    }

    ListNode(int val) {
        this.val = val;
    }

    ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

面试题 02.07. 链表相交

说明:本题没有视频讲解,大家注意 数值相同,不代表指针相同。

题目链接:https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/

文章链接:https://programmercarl.com/面试题02.07.链表相交.html

(1)思路分析

  • 本题的关键在于如何处理不等长的情况,首先通过计算长度,让二者处于同起点的位置
  • 这里有三个易错点
    • 计算完表长后,此时指针处于表尾为 null,头指针指向需要重置,否则会有空指针异常
    • 由于差值可能是负数,为了避免混淆,指定 pa 指向的就是最长的链表
      • 交换链表长度
      • 交换链表头指针的指向
    • 经过处理后,二者处于同起点,应该是先判断,再移动否则会遗漏同起点这个点的判断

(2)题解

java
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode pa;
        ListNode pb;

        pa = headA;
        pb = headB;

        int len_a = 0;
        int len_b = 0;

        // 1. 首先计算链表长度,目的是让二者同起点

        // 计算 a 的表长
        while (pa != null) {
            pa = pa.next;
            len_a++;
        }

        // 计算 b 的表长
        while (pb != null) {
            pb = pb.next;
            len_b++;
        }

        // 易错点:此时 pa 和 pb 都已经到达了标为,需要重置一下
        pa = headA;
        pb = headB;

        // 2. 由于表长差值可能是负数,就不知道谁是最长的,假定 pa 指向的是最长的链表
        if (len_b > len_a) {

            // 交换表长
            int temp_len = len_a;
            len_a = len_b;
            len_b = temp_len;

            // 交换头指针指向
            ListNode temp = pa;
            pa = pb;
            pb = temp;

        }

        // 3. 得到差值,实现同起点
        int gap = len_a - len_b;
        while(gap>0){
            pa = pa.next;
            gap--;
        }

        // 4. 同起点后,同步移动
        while(pa != null){
            // 易错:先判断再移动,否则会遗漏同起点这个点的判断
            if(pa == pb){
                return pa;
            }
            pa = pa.next;
            pb = pb.next;

        }

        // 5. 没有交点,返回空
        return null;
    }
}

class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
        next = null;
    }
}

⚠️142.环形链表 II

建议:先看视频,算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口

题目链接:https://leetcode.cn/problems/linked-list-cycle-ii/

文章链接:https://programmercarl.com/0142.环形链表II.html

视频讲解:https://www.bilibili.com/video/BV1if4y1d7ob

(1)思路分析

本题主要考察一下两个点

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

思路:采用快慢指针

1. 判断是否有环

  • 让快指针走两步,慢指针走一步,二者最终会相遇

  • 相遇分析

    • (1)fast 指针一定先进入环中,如果 fast 指针和 slow 指针相遇的话,一定是在环中相遇,这是毋庸置疑的
    • (2)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 指针)
  • 根据关系,快指针走两步,满指针走一步,有如下关系式
    • (x + y) * 2 = x + y + n (y + z)
    • 经过化简可以得到
      • x = n (y + z) - y
      • x = (n - 1) (y + z) + z
        • n 一定是大于等于 1 的,因为 fast 指针至少要多走一圈才能相遇 slow 指针
    • n = 1时,可以得到:x = z

结论如下

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

(2)题解

java
public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 定义快慢指针
        ListNode fast;
        ListNode slow;

        fast = head;
        slow = head;

        // 快指针走两步,满指针走一步
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            // 如果相遇,说明有环,需要找到环的入口
            if(slow == fast){
                ListNode index1 = head;
                ListNode index2 = fast;
                // 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
                while(index1 != index2){
                    index1 = index1.next;
                    index2 = index2.next;
                }
                return index1; // 环的入口
            }
        }
        return null;
    }
}

class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
        next = null;
    }
}

阶段二结束:链表总结

文章链接:https://programmercarl.com/链表总结篇.html

(1)虚拟头节点

方便统一操作,尤其是增删改查,对于头节点无序特殊处理

(2)链表的基本操作

  • 获取链表第 index 个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第 index 个节点前面插入一个节点
  • 删除链表的第 index 个节点的数值

(3)翻转链表

还是双指针思想,重点就在遍历什么时候结束,在原基础上改进可以变成递归写法,优先弄懂迭代法,递归写法虽然代码简洁,但是很容易混乱

(4)删除倒数第 N 个节点

还是双指针(快慢指针)思想,关键在于如何理解倒数第 N 个节点,是以两个指针的间隔实现的(注意是 N+1 个间隔),需要观察到最后快指针为空慢指针指向的最终状态

(5)链表相交

对于不同长度的链表,理解如何实现同起点遍历是核心

(6)环形链表

理解什么时候会有环,理解如何寻找环入口的推导过程是关键