《算法日记》- 138. 随机链表的复制

138 阅读4分钟

题目

leetcode 138. 随机链表的复制

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码  接受原链表的头节点 head 作为传入参数。

示例 1:

输入: head = [[7,null],[13,0],[11,4],[10,2],[1,0]]

输出: [[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入: head = [[1,1],[2,1]]

输出: [[1,1],[2,1]]

示例 3:

输入: head = [[3,null],[3,0],[3,null]]

输出: [[3,null],[3,0],[3,null]]

解题思路

  1. 优先处理特殊场景,当 headnull 时,直接返回 null
  2. 为每一个节点创建一个新节点,然后按照原有节点的关系处理新节点之间的关系

真正的重点就在于,新节点创建在哪里?又如何来处理新节点之间的关系?这里就产生了两种不同的方案。

方案一 「迭代 + 节点拆分」

在原有链表的基础上,进行新节点的创建,然后进行断链处理,示意图如下:

process.png

实现代码:

/**
 * Definition for Node.
 * class Node {
 *     val: number
 *     next: Node | null
 *     random: Node | null
 *     constructor(val?: number, next?: Node, random?: Node) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *         this.random = (random===undefined ? null : random)
 *     }
 * }
 */


function copyRandomList(head: Node | null): Node | null {
  if (head === null) return null;
  let current = head;

  // 在原有链表的基础上,原节点 next 位置插入深拷贝新节点
  while (current) {
    let newCurr = new Node(current.val);
    newCurr.next = current.next || null;
    current.next = newCurr;

    current = newCurr.next
  }

  // 重置指针位置
  current = head;
  
  // 处理新节点的 random 指向 random 的拷贝节点
  while (current && current.next) {
    if (current.random) {
      current.next.random = current.random.next || null;
    }

    current = current.next.next
  }

  // 创建新的链表,重置链表指针位置为新链表的 head
  let newHead = head.next;
  let preNode = head;
  current = head.next;
  
  // 将拓展后的链表拆分为新旧两个链表
  while(current && current.next) {
    preNode.next = preNode.next.next || null;
    current.next = current.next.next
    preNode = preNode.next;
    current = current.next;
  }

  // 将旧链表的最后一个节点与新链表最后一个节点的链切断
  preNode.next = null;

  // 返回深拷贝后新链表的 head
  return newHead

复杂度分析

时间复杂度:O(n),其中 n 是链表的长度。我们只需要遍历该链表三次。

读者们也可以自行尝试在计算拷贝节点的随机指针的同时计算其后继指针,这样只需要遍历两次。

空间复杂度:O(1)。注意返回值不计入空间复杂度。

方案二 「哈希表」

使用 hashMap (dict),也就是我们 js 中常说的 Map,用来进行新节点的存储,用旧节点作为key,以此来完成新旧节点的关系映射与处理。

实现代码

/**
 * Definition for Node.
 * class Node {
 *     val: number
 *     next: Node | null
 *     random: Node | null
 *     constructor(val?: number, next?: Node, random?: Node) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.next = (next===undefined ? null : next)
 *         this.random = (random===undefined ? null : random)
 *     }
 * }
 */


function copyRandomList(head: Node | null): Node | null {
  if (head === null) return null;

  let current = head;
  const cacheMap = new Map();

  // 创建新节点,使用 Map 进行新旧节点之间的映射
  while (current !== null) {
    cacheMap.set(current, new Node(current.val));
    current = current.next
  }

  current = head;

  // 利用 Map 参照旧节点,完成新节点之间关系的建立
  while (current !== null) {
    cacheMap.get(current).next = cacheMap.get(current.next) || null;
    cacheMap.get(current).random = cacheMap.get(current.random) || null;
    current = current.next
  }

  return cacheMap.get(head);
}

复杂度分析

时间复杂度:O(n),其中 n 是链表的长度。对于每个节点,我们至多访问其「后继节点」和「随机指针指向的节点」各一次,均摊每个点至多被访问两次。

空间复杂度:O(n),其中 n 是链表的长度。为哈希表的空间开销。