Skip to content

🍕 链表

链表分为单链表和双链表:

  • 单链表: 单向连接
  • 双链表: 双向连接

小练习 - 反转链表

【题目】: 分别实现反转单链表和反转双链表的函数。 【要求】: 如果链表长度为 N, 要求时间复杂度为 O(N), 额外空间复杂度为 O(1)。

【⚠️重点】: 当你断掉某个链条时, 你要清楚是否需要使用到断开之前所指向的节点! 如果需要, 那么一定要先将该节点存储起来!

【我的代码】:

py
def reversed_linked_list(head, is_double=False):
    # 判断边界条件
    if head is None or head.next is None:
        return head

    # 初始值
    pr = None
    p = head
    pn = p.next

    while True:
        p.next = pr # 反转
        if is_double:
            p.prev = pn # 反转双链表只多了这一步

        # 结束判断
        if pn is None:
            break

        # 更新
        pr = p
        p = pn
        pn = p.next

    return p

小练习 - 打印两个有序链表的公共部分

【题目】: 给定两个有序链表的头指针 head1 和 head2, 打印两个链表的公共部分。 【要求】: 如果两个链表的长度之和为 N, 时间复杂度要求为 O(N), 额外空间复杂度要求为 O(1)。

【算法思路】: 两个指针, 谁小谁移动。 相同时打印并一起移动。 直到某一指针越界。

【我的代码】:

py
def linked_list_common(head1, head2):
    common = []
    while head1 is not None and head2 is not None:
        if head1.data < head2.data:
            head1 = head1.next
        elif head1.data > head2.data:
            head2 = head2.next
        else:
            common.append(head1.data)
            head1 = head1.next
            head2 = head2.next
    return common

面试时链表解题方法论

笔试和面试要求不一样。

  • 笔试: 能过就行, 没人看你代码。 所以不用太在乎空间复杂度, 一切为了时间复杂度。
  • 面试: 时间复杂度仍然是第一位, 但是空间复杂度一定要省。 因为此时看你的不是机器, 是面试官。

【技巧】:

  • 额外数据结构记录(哈希表等)
  • 快慢指针。 比如一个指针走一步, 一个指针走两步。

练习 - 判断单链表回文

【做法1(笔试)】: 先遍历一遍链表, 并把值依次放入栈中。 再遍历一遍链表, 此时每次遍历都将栈的内容弹出, 然后比较是否相等。 直到不相等或者栈为空。

【做法2(笔试)】: 做法 1 中遍历了两遍链表, 而且栈占用的空间大小为 N。 但实际上, 当我们遍历到链表中间之后, 就可以将栈中的元素 pop 出来和链表进行比较了。 问题在于, 如何确定中间位置? 方法就是 快慢指针。 一个指针走一步, 一个指针走两步。 当快指针到头时, 就说明到中间附近了。

【做法3(面试)】:不借助栈实现:

  1. 找到中间位置
  2. 从中间, 将后面的链表节点反转。
  3. 从链表两端往中间一一比较, 右链表在比较的过程中再次将链表反转

【注意⚠️】: 思路都很简单, 需要注意的只有长度奇偶数情况下, 中点的选取。 根据题意的不同, 选取的方式也不同。

老师代码

java
// 方法1
public static boolean isPalindrome1(Node head) {
    Stack<Node> stack = new Stack<Node>();
    Node cur = head;
    // 将链表所有节点入栈
    while (cur != null) {
        stack.push(cur);
        cur = cur.next;
    }
    // 再次遍历链表, 并从栈中元素一一比较
    while (head != null) {
        if (head.value != stack.pop().value) {
            return false;
        }
        head = head.next;
    }
    return true;
}

// 方法2
public static boolean isPalindrome2(Node head) {
    if (head == null || head.next == null) {
        return true;
    }
    Node right = head.next;
    Node cur = head;
    // 找到中点位置
    while (cur.next != null && nul.next.next != null) {
        right = right.next;
        cur = cur.next.next;
    }
    // 从中点位置再开始将元素入栈
    Stack<Node> stack = new Stack<Node>();
    while (right != null) {
        stack.push(right);
        right = right.next;
    }
    // 再次遍历, 一一比较
    while (!stack.isEmpty()) {
        if (head.value != stack.pop().value) {
            return false;
        }
        head = head.next;
    }
}

// 方法3
public static boolean isPalindrome3(Node head) {
    if (head == null || head.next == null) {
        return true;
    }
    Node n1 = head;
    Node n2 = head;
    // 快慢指针找到中间
    while (n2.next != null && n2.next.next != null) {
        n1 = n1.next;
        n2 = n2.next.next;
    }
    // 反转后半链表
    n2 = n1.next;
    n1.next = null;
    Node n3 = null;
    while (n2 != null) {
        n3 = n3.next;
        n2.next = n1;
        n2 = n3;
    }
    // 从链表两端向中间一一比较
    n3 = n1;
    n2 = head;
    boolean res = true;
    while (n1 != null && n2 != null) {
        if (n1.value != n2.value) {
            res = false;
            break;
        }
        n1 = n1.next;
        n2 = n2.next;
    }
    // 完成后再次把后半链表反转
    n1 = n3.next;
    n3.next = null;
    while (n1 != null) {
        n2 = n1.next;
        n1.next = n3;
        n3 = n1;
        n1 = n2;
    }
}

我的代码

py
def is_palindrome_linked_list2(head):
    if head is None:
        return False
    if head.next is None:
        return True
    # 利用快慢指针, 走到中点就开始出栈比较
    slow = head
    quick = head.next
    mid_r = None  # 偶:偏右; 奇:中点右边
    half_stack = []  # 只会填充左侧。 偶:填一半; 奇:不包含中点
    while True:
        half_stack.append(slow.data)
        if quick.next is None:  # 偶
            mid_r = slow.next
            break
        if quick.next.next is None:  # 奇
            mid_r = slow.next.next
            break
        slow = slow.next
        quick = quick.next.next
    while mid_r is not None:
        if (mid_r.data != half_stack.pop()):
            return False
        mid_r = mid_r.next
    return True


def is_palindrome_linked_list3(head):
    if head is None:
        return False
    if head.next is None:
        return True
    # 不借助栈, 过了中点后将链表反转
    # 然后从链表两侧往回一一比较, 往回走时再次反转链表
    slow = head
    quick = head.next
    mid_r = None
    while True:
        if quick.next is None:
            mid_r = slow.next
            break
        if quick.next.next is None:
            mid_r = slow.next.next
            break
        slow = slow.next
        quick = quick.next.next
    # 从 mid_r 节点开始进行反转
    pr, p, pn = None, mid_r, mid_r.next
    while True:
        p.next = pr
        if pn is None:
            break
        pr = p
        p = pn
        pn = p.next
    # 此时的 p 是右链表头节点, mid_r 是尾节点
    # 现在再次反转右链表, 在反转的过程中与左链表一一比较
    left = head
    pr, p, pn = None, p, p.next
    is_palindrome = True
    while True:
        if left.data != p.data:
            is_palindrome = False
        p.next = pr
        if pn is None:
            break
        pr = p
        p = pn
        pn = p.next
        left = left.next
    return is_palindrome

练习 - 对单链表的值进行划分

【题目】: 给定一个单链表的头节点 head, 节点的值类型是整型, 再给定一个整数 pivot。 实现一个调整链表的函数, 将链表调整为左部分都是值小于 pivot 的节点, 中间部分都是值等于 pivot 的节点, 右部分都是值大于 pivot 的节点。 【进阶】: 要求实现稳定性, 同时时间复杂度为 O(N), 额外空间复杂度为 O(1)。

【做法 1(笔试)】: 把单链表转换为数组, 然后做快排的 partition。 做完后再把数组转换为单链表

【做法 2(面试)】: 定义三个区域的空链表。 遍历链表, 将每次遍历到的节点放到正确的区域内。 遍历完成后将三个区域的链表连接起来就可以了。

由于链表中节点的移动代价是 O(1), 所以仅需有限的几个变量就实现链表 partition。

【代码思路】:

  1. 定义 6 个变量, SH 表示 "< 区" 的头节点, ST 表示 "<" 区域的尾节点。 同理还有 "= 区" 的两个节点 EH 和 ET, "> 区" 的两个节点 BH 和 BT。 他们的初始值都是空。
  2. 遍历链表
  3. 当节点 < 划分值时, 将节点放到 "< 区" 的链表上。具体做法是: 第一个小于划分值的节点, 将它同时赋值给 SH 和 ST, 然后 SH 就不用变了。 后续再遇到符合条件的节点时, 将它作为 ST 的下一节点, 同时 ST 向下移动, 以此类推。
  4. 当节点 == 划分值或节点 > 划分值 时, 都是同样的做法。
  5. 结束时, 将三条链表连接起来。 具体做法:
    • 在 ST 不为空时: 若 EH 不为空, 则 ST 连接 EH, 否则连接 BH。 不需要再判断 BH 是否为空。
    • 在 ET 不为空时, ET 只会连接 BH。
    • 在 BT 不为空时, BT 必须连接"空"。
    • 连接好各自的尾部后, 只需要放回三条链表中的一个头节点即可, 三个头节点的优先级是 SH > EH > BH。

老师代码

java
// 方法1
public static Node listPartition1(Node head, int pivot) {
    if (head == null) {
        return head;
    }
    Node cur = head;
    // 获取链表长度
    int i = 0;
    while (cur != null) {
        i++;
        cur = cur.next;
    }
    // 将链表转换为数组
    Node[] nodeArr = new Node[i];
    i = 0;
    cur = head;
    for (i = 0; i != nodeArr.length; i++) {
        nodeArr[i] = cur;
        cur = cur.next;
    }
    // 在数组上执行 partition
    arrPartition(nodeArr, pivot);
    // 将数组重新连接成链表
    for (i = 1; i != nodeArr.length; i++) {
        nodeArr[i - 1].next = nodeArr[i];
    }
    nodeArr[i - 1].next = null;
    return nodeArr[0];
}

public static void arrPartition(Node[] nodeArr, int pivot) {
    int small = -1;
    int big = nodeArr.length;
    int index = 0;
    while (index != big) {
        if (nodeArr[index].value < pivot) {
            swap(nodeArr, ++small; index++);
        } else if (nodeArr[index].value == pivot) {
            index ++;
        } else {
            swap(nodeArr, --big, index);
        }
    }
}

// 方法2
public static Node listPartition2(Node head, int pivot) {
    // 创建三条链表, 分别存放三类值
    Node sH = null; // small head
    Node sT = null; // small tail
    Node eH = null;
    Node eT = null;
    Node bH = null;
    Node bT = null;
    Node next = null; // save next node

    // 将节点分别放入三类链表上
    while (head != null) {
        next = head.next;
        head.next = null;
        if (head.value < pivot) {
            if (sH == null) {
                sH = head;
                sT = head;
            } else {
                sT.next = head;
                sT = head;
            }
        } else if (head.value == pivot) {
            if (eH == null) {
                eH = head;
                eT = head;
            } else {
                eT.next = head;
                eT = head;
            }

        } else {
            if (bH == null) {
                bH = head;
                bT = head;
            } else {
                bT.next = head;
                bT = head;
            }
        }
        head = next;
    }


    if (sT != null) {
        // 若有 s, s 直接连 eH
        sT.next = eH;
        // 此时要是 eH 没有, 下一个 if 会捕获到, 并把 s 重新连到 b
        // 要是 e 有,                       那么 e 就需要连到 b
        // 也就是, 不管 e 有没有, 都需要有人连接到 b, 要么是 s 连, 要么是 e 连
        eT = eT == null ? sT : eT;
        // 此时的 eT 可能是 s, 也可能是 e
    }
    // 如果上一个 if 进去了, 那么这里的 if 也一定会进去
    // 如果上一个 if 没进去, 那么这里的 if 可能会进去
    if (eT != null) {
        eT.next = bH;
    }

    // 连接顺序是 s-e-b, 要是没有 sH, 那就返回 eH, 要是 eH 也没有, 那就返回 bH
    // 所以前面两个 if 只需要确保三个区域能连接起来就可以。
    return sH != null ? sH : (eH != null ? eH : bH);
}

我的代码

py
def linked_list_partition2(head, pivot):
    # 稳定
    if head is None or head.next is None:
        return head

    # 创建三条链表, 分别存放大于等于小于
    lH, lT = None, None
    eH, eT = None, None
    mH, mT = None, None
    p = head
    while p is not None:
        if p.data < pivot:
            if lH is None:
                lH = lT = p
            else:
                lT.next = p
                lT = p
        elif p.data == pivot:
            if eH is None:
                eH = eT = p
            else:
                eT.next = p
                eT = p
        else:
            if mH is None:
                mH = mT = p
            else:
                mT.next = p
                mT = p
        p = p.next

    # 将三条链表连在一起。 只需要对 tail.next 进行操作
    if lT is not None:  # lT 要么连 eH, 要么连 mH。 (自动包含了 None 的情况)
        lT.next = eH if eH is not None else mH
    if eT is not None:  # eT 只会连 mH (自动包含了 None 的情况)
        eT.next = mH
    if mT is not None:  # mT 只会连 None
        mT.next = None

    # 只需返回三个头中的一个, 优先级是 lH > eH > mH
    return lH if lH is not None else (eH if eH is not None else mH)

练习 - 复制含有随机指针节点的链表

【题目】: 一种特殊的单链表节点类描述如下

java
class Node {
    int value;
    Node next;
    Node rand;
    Node(int val){
        value = val;
    }
}

rand 指针是单链表节点结构中新增的指针, rand 可能指向链表中的任意一个节点, 也可能指向 null。 给定一个由 Node 节点类型组成的无环单链表的头节点 head, 请实现一个函数完成这个链表的复制, 并返回复制的新链表的头节点

   ╭ ---------- rand -------╮
   ↓                        ↑
╭  ─  ╮     ╭  ─  ╮      ╭  ─  ╮
│     │ --> │     │ -->  │     │ --> ....
╰  ─  ╯     ╰  ─  ╯      ╰  ─  ╯

【要求】: 时间复杂度 O(N), 额外空间复杂度 O(1)

【解释题目】: 这个链表和普通链表的区别在于, 他多了 rand 这个属性。 当我们复制链表时, 要求把每个节点上的 rand 的指向也成功复制。

【做法 1(笔记)】: 创建一个 Map, 它的 key 是旧链表节点, value 是新节点(即旧节点的克隆体)。 先遍历一遍链表, 完成所有节点值的拷贝, 此时的拷贝并不包含 next 和 rand。 再遍历旧链表, 通过旧链表获取 next 节点和 rand 节点, 将旧链表的节点作为 key, 可以获取就节点所对应的新节点的地址, 然后将新节点的地址赋值给 新节点的 next 和 rand。

【做法 2(面试)】: 如果不借助 Map 保存克隆出来的新节点, 那么就需要思考: 克隆出来的新节点要放在哪里能不占用额外空间, 并且还能根据旧节点找到对应的克隆节点? 答案就是将克隆节点插入到旧节点之后! 具体做法如下:

  1. 第一次遍历链表, 创建克隆节点并将其插在对应旧节点之后
  2. 第二次遍历链表, 为克隆节点找到他们的 rand 节点所对应的克隆节点。 (克隆 rand 节点 在 rand 节点的 next 中)
  3. 第三次遍历链表, 将克隆节点从中抽出出来
  • 注意: 第二次遍历链表和第三次遍历链表不能合并在一次遍历中。 因为 rand 节点是随机的, 如果找到一个克隆节点的 rand 节点后就将该克隆节点抽离出来, 那么后面的节点就可能找不到它的克隆 rand 节点了。
╭  ─  ╮                 ╭  ─  ╮                 ╭  ─  ╮                 ╭  ─  ╮                 ╭  ─  ╮
│old1 │      -->        │old2 │      -->        │old3 │      -->        │old4 │      -->        │old5 │      -->        ....
╰  ─  ╯                 ╰  ─  ╯                 ╰  ─  ╯                 ╰  ─  ╯                 ╰  ─  ╯
将新克隆的节点放到旧链表上
╭  ─  ╮     ╭  ─  ╮     ╭  ─  ╮     ╭  ─  ╮     ╭  ─  ╮     ╭  ─  ╮     ╭  ─  ╮     ╭  ─  ╮     ╭  ─  ╮     ╭  ─  ╮
│old1 │ --> │ new1│ --> │old2 │ --> │ new2│ --> │old3 │ --> │ new3│ --> │old4 │ --> │ new4│ --> │old5 │ --> │ new5│ ---> ....
╰  ─  ╯     ╰  ─  ╯     ╰  ─  ╯     ╰  ─  ╯     ╰  ─  ╯     ╰  ─  ╯     ╰  ─  ╯     ╰  ─  ╯     ╰  ─  ╯     ╰  ─  ╯

老师代码:

java
// 做法1
public static Node copyListWithRand1(Node head) {
    HashMap<Node, Node> map = new HashMap<Node, Node>();

    // 第一次遍历: 拷贝所有旧节点
    Node cur = head;
    while (cur != null) {
        map.put(cur, new Node(cur.value));
        cur = cur.next;
    }

    // 第二次遍历: 为新节点的 next 和 rand 赋值新节点的引用
    cur = head;
    while (cur != null) {
        // 将旧节点的 next 节点 所对应的新节点复制给新节点的 next
        map.get(cur).next = map.get(cur.next);
        mpa.get(cur).rand = map.get(cur.rand);
        cur = cur.next;
    }
    return map.get(head);
}

// 做法2
public static Node copyListWithRand2(Node head) {
    if (head == null) {
        return null;
    }

    // 1. 将新创建的克隆节点放到旧链表上。
    Node cur = head;
    Node next = null;
    while (cur != null) {
        next = cur.next;
        cur.next = new Node(cur.value);
        cur.next.next = next;
        cur = next;
    }

    // 2. 为新创建的克隆节点的 rand 赋值
    cur = head;
    Node curCopy = null;
    while (cur != null) {
        next = cur.next.next;
        curCopy = cur.next;
        curCopy.rand = cur.rand != null? cur.rand.next : null;
        cur = next;
    }

    // 3. 将克隆链表抽离出来
    Node res = head.next;
    cur = head;
    while (cur != null) {
        next = cur.next.next;
        curCopy = cur.next;
        cur.next = next;
        curCopy.next = next != null ? next.next : null;
        cur = next;
    }
    return res;

}

我的代码

py
def copy_random_node_linked_list2(head):
    if head is None:
        return None
    # 1. 创建克隆节点并将它们插入到源节点后
    p = head
    while p is not None:
        p_copy = Node(p.data)
        p_copy.next = p.next
        p.next = p_copy
        p = p_copy.next
    # 2. 找到克隆节点的 rand 所对应的克隆 rand 节点
    p = head
    while p is not None:
        p_copy = p.next
        pn = p.next.next
        if p.rand is not None:
            p_copy.rand = p.rand.next
        p = pn
    # 3. 将克隆节点抽离出来
    p = head
    copy_head = p.next
    while p is not None:
        p_copy = p.next
        pn = p.next.next
        p_copy.next = pn.next if pn is not None else None
        p.next = pn
        p = pn
    return copy_head

练习 - 两个单链表相交

【题目】: 给定两个可能有环也可能无环的单链表, 头节点 head1 和 head2 。 请实现一个函数, 如果两个链表相交, 请返回相交的第一个节点。 如果不相交, 返回空 【要求】: 如果两个链表长度之和为 N, 时间复杂度请达到 O(N), 额外空间复杂度请达到 O(1)。

先判断一个链表是否有环

判断链表是否有环

对于链表结构, 他只会有一个 next 节点。 所以:

  • 如果无环, 则一定会达到空
  • 如果有环, 则一定会遇到重复的节点

【方法1】: 利用 Set 哈希表。 遍历链表, 每次遍历过程中, 先判断是否已存在该节点, 然后将该节点加到 Set 中。如果已存在, 则有环, 如果遍历到空, 则无环。

【方法2(只给步骤)】: 无环时, 快指针直接到空。 有环时, 则:

  • 快指针慢指针都从头节点出发, 快指针一次走两步, 慢指针一次走一步。
  • 快指针和慢指针一定会相遇。 (并且一定在两环内相遇, 即有限时间内相遇)
  • 相遇后, 快指针回到头节点, 慢指针在原位置, 然后两者同时移动, 并且都是一次一步。
  • 两个指针再次相遇时, 所在的节点就是第一个相交节点(入环节点)

【老师代码】:

java
public static Node getLoopNode(Node head) {
    if (head == null || head.next == null || head.next.next == null) {
        return null;
    }
    Node n1 = head.next;  // 慢指针
    Node n2 = head.next.next; // 快指针
    while (n1 != n2) {
        if (n2.next == null || n2.next.next == null) {
            return null;
        }
        n2 = n2.next.next;
        n1 = n1.next;
    }
    n2 = head;
    while (n1 != n2) {
        n1 = n1.next;
        n2 = n2.next;
    }
    return n1;
}

判断链表相交

【注意】: 两条链表相交后, 一定会"一起走", 不可能分叉。 因为单链表节点只有一个 next 节点。

两条链表三种情况:

  • 两个链表都是无环链表。 无环链表相交, 就相当于两条线相交, 但由于链表只有一个 next 节点, 所以相交后一定是 Y 情况, 而不会出现 X 情况。
  • 一条有环, 一条无环。 这种情况不可能相交。 假设无环链表和有环链表相交, 由于两个链表相交后一定会在一条线上, 所以无环链表就一定会有环, 这与假设不符, 故假设不成立。
  • 两条链表都有环。 两条成环链表相交, 他们的环一定是同一个。 这个环, 要么是在相交后形成的, 要么是在相交成形成的。 如下所示
〇       〇                    
 \     /                     
  〇   〇       环的方向不重要,谁交谁都一样 
   \ /           此时环点就是交点    
    〇          〇           〇 
    |           \         /  
    〇            〇   〇   〇   
    |             \ / \ /    
    〇              〇   〇     
   / \             |   |     
  〇   〇            〇   〇     
   \ /              \ /      
    〇                〇

【两条不成环链表相交时的交点求取】:

  • 判断尾节点是否相同, 如果不相同则一定不相交。
  • 计算两条链表的长度差 d
  • 让长的链表从头节点先走 d 个节点
  • 然后让两个链表同时走下去, 他们迟早会相遇。

【两条成环链表相交时的交点求取】:

  • 判断两条链表的成环点是否相同, 如果相同说明他们是相交后才成环的。 即交点一点是在成环前, 故直接使用【两条不成环链表相交时的交点求取】方法求取即可
  • 成环点不同, 则两条链表可能相交也可能不相交。
    • 如果相交, 则说明有一条链表先成环, 然后另一条链表直接插入到环圈上的一个节点, 换句话说, 插入的这个节点就是这条链表的成环点, 同时也是他们的交点。
    • 如果不相交, 则一定无法再环圈上找到另一条链表的成环点。
    • 故, 只需要在一条链表的环圈上绕一圈, 看看是否有另一条链表的成环点, 如果有, 该环点就是交点。 如果饶了一圈还没找到, 说明不相交。

【老师代码】:

java

public static  Node getIntersectNode (Node head1, Node head2) {
    if (head1 == null || head2 == null) {
        return null;
    }
    Node loop1 = getLoopNode(head1);
    Node loop2 = getLoopNode(head2);
    if (loop1 == null && loop2 == null) {
        return noLoop(head1, head2);
    }
    if (loop1 != null && loop2 != null) {
        return bothLoop(head1, loop1, head2, loop2);
    }
    return null;
}

public static Node noLoop(Node head1, Node head2) {
    if (head1 == null || head2 == null) {
        return null;
    }
    Node cur1 = head1;
    Node cur2 = head2;
    int n = 0;
    while (cur1.next != null) {
        n++;
        cur1 = cur1.next;
    }
    while (cur2.next != null) {
        n--;
        cur2 = cur2.next;
    }
    if (cur1 != cur2) { // 尾节点不同, 一定不相交
        return null;
    }
    cur1 = n > 0 ? head1 : head2 ; // 谁长, 谁的头变成 cur1
    cur2 = cur1 == head1? : head2 : head1; // 谁短, 谁的头变成 cur2
    n = Math.abs(n); // 两条链表的差值
    // 先让长链表走完差值的长度
    while (n != 0) {
        n--;
        cur1 = cur1.next;
    }
    // 然后两个链表一起走, 相遇时就是第一个交叉的节点
    while (cur1 != cur2) {
        cur1 = cur1.next;
        cur2 = cur2.next;
    }
    return cur1;
}

public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
    Node cur1 = null;
    Node cur2 = null;
    if (loop1 == loop2) {
        // 跟判断 noLoop 类似, 但区别在于 noLoop 判断的是尾巴是 None, 而这里判断的是 loop1, loop2
        cur1 = head1;
        cur2 = head2;
        int n = 0;
        while (cur1 != loop1) {
            n++;
            cur1 = cur1.next;
        }
        while (cur2 != loop2) {
            n--;
            cur2 = cur2.next;
        }
        cur1 = n > 0 ? head1 : head2;
        cur2 = cur1 == head1 ? head2 : head1;
        n = Math.abs(n);
        while (n != 0) {
            n--;
            cur1 = cur1.next;
        }
        while (cur1 != cur2) {
            cur1 = cur1.next;
            cur2 = cur2.next;
        }
        return cur1;
    } else {
        cur1 = loop1.next;
        while (cur1 != loop1) {
            if (cur1 == loop2) {
                return loop1; // 返回 loop1 或 loop2 都行, 这两个都叫做第一个相交的节点
            }
            cur1 = cur1.next;
        }
        // cur1 转完一圈后都没遇到 loop2, 说明两个环没有相交
        return null;
    }
}