[路飞]_前端算法第二十一弹-138. 复制带随机指针的链表

161 阅读5分钟

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

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

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

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

返回复制链表的头节点。

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

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0n-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]]

示例 4:

输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。

回溯+哈希

本题需要我们对一个特殊的链表进行深拷贝。如果是普通的链表,我们可以直接按照遍历的顺序创建链表节点。而本题中,因为有随机指针的出现,导致当我们依旧按照原来的方式拷贝节点的时候,「当前节点的随机指针指向的节点」可能还没有创建,因此我们必须转变思路。我们可以利用回溯的方法,让每个节点的拷贝操作相互独立。对于当前节点,我们首先要进行拷贝,然后我们进行「当前节点的后继节点」和「当前节点的随机指针指向的节点」拷贝,拷贝完成后将创建的新的节点的指针返回,即可完成当前节点的两指针的赋值。

具体实现方式,我们用哈希表记录下每一个节点对应的新节点。遍历该链表的过程中,我们检查「当前节点的后继节点」和「当前节点的随机指针指向的节点」的创建情况。如果这两个节点中的任何一个节点的新节点没有被创建,我们都立刻递归地进行创建。当我们拷贝完成,回溯到当前层时,我们即可完成当前节点的指针赋值。注意一个节点可能被多个其他节点指向,因此我们可能递归地多次尝试拷贝某个节点,为了防止重复拷贝,我们需要首先检查当前节点是否被拷贝过,如果已经拷贝过,我们可以直接从哈希表中取出拷贝后的节点的指针并返回即可。

通俗的讲就是我们遍历拷贝第一个节点时,我们观察其next和random有没有被创建过,如果有,返回拷贝,如果没有,创建,继续遍历其子结点,一次创建,回溯拷贝,直至第一个节点。

var copyRandomList = function(head, cachedNode = new Map()) {
    if (head === null) {
        return null;
    }
    if (!cachedNode.has(head)) {
        cachedNode.set(head, {val: head.val}), Object.assign(cachedNode.get(head), {next: copyRandomList(head.next, cachedNode), random: copyRandomList(head.random, cachedNode)})
    }
    return cachedNode.get(head);
}

复杂度分析

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

迭代+节点拆分

上述方法中,使用了哈希表来记录每一个节点对应的新节点的创建,其实这一步我们完全可以通过一个小技巧简化掉,降低其空间复杂度。

简单来说就是进行三次遍历,第一次遍历将原链表的长度*2,即原链表为A→B→C,新链表为A→a→B→b→C→c。将原节点ABC拷贝至新节点abc,这样,我们可以通过第二次遍历直接找到每一个新节点的随机指针,将其原本指向的原节点ABC更改为指向abc,此时记得要判断null。

第三次遍历则是将原节点与新节点进行拆分。这样新节点便组成了我们想要的全新的链表。

var copyRandomList = function(head) {
    if (head === null) {
        return null;
    }
		// 因为注入了新节点,所以原节点的next此时变成了next.next
    for (let node = head; node !== null; node = node.next.next) {
				// 创建新节点,也是深拷贝的过程。
        const nodeNew = new Node(node.val, node.next, null);
				// 原节点的下一个节点指向新节点,新节点的下一个结点是原节点的原下一个节点
        node.next = nodeNew;
    }
		// 第二次遍历,更改random的指向,每次循环都是遍历原节点所以是next.next
    for (let node = head; node !== null; node = node.next.next) {
				// 找到新节点
        const nodeNew = node.next;
				// 新节点的random指向新节点,注意判断null
        nodeNew.random = (node.random !== null) ? node.random.next : null;
    }
		// 拆分新老节点。
    const headNew = head.next;
    for (let node = head; node !== null; node = node.next) {
        const nodeNew = node.next;
        node.next = node.next.next;
        nodeNew.next = (nodeNew.next !== null) ? nodeNew.next.next : null;
    }
    return headNew;
};

复杂度分析

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