在O(n)时间内创建链接列表的副本

Create a duplicate copy of Linked list in O(n) time

本文关键字:链接 创建 列表 副本 时间      更新时间:2023-10-16

链接列表中有两个指针,第一个指向下一个节点,另一个指向随机指针。随机指针指向LinkedList的任何节点。编写一个完整的程序来创建链表(c,c++,c#)的副本,而不更改原始列表和O(n)。

在一次采访中,有人问我这个问题,我想不出答案。我们将不胜感激。

在线性时间内复制正常链表显然是微不足道的。唯一有趣的部分是"随机"指针。假设"随机"实际上是指它指向同一链表中另一个随机选择的节点。据推测,其目的是链表的副本重新创建完全相同的结构——即"下一个"指针创建一个线性列表,并且其他指针指代相同的相对节点(例如,如果原始列表的第一个节点中的随机指针指向原始列表中的第五个节点,则重复列表中的随机指示器也将指向重复列表的第五节点

在N2的时间内这样做是相当容易的。首先正常复制列表,忽略随机指针。然后一次遍历原始列表中的一个节点,并且对于每个节点再次遍历列表,找到随机指针引用的列表中的哪个节点(即,通过next指针遍历多少个节点,以找到与当前节点的random指针具有相同地址的next指针。然后遍历重复列表并反转它——找到第N个节点的地址,并将其放入当前节点的随机指针中。

这是O(N2)的原因主要是对右节点的线性搜索。为了得到O(N),这些搜索需要以恒定的复杂度而不是线性复杂度进行。

显而易见的方法是构建一个哈希表,将原始列表中每个节点的地址映射到该节点在列表中的位置。然后,我们可以构建一个数组,其中包含新列表中节点的地址。

有了这些,修复随机指针就相当容易了。首先,我们通过next指针遍历原始列表,复制节点,并构建通过next指针连接的新列表,但不使用随机指针。在执行此操作时,我们将每个节点的地址和位置插入哈希表,并将新列表中每个节点的位置插入数组。

当我们完成这项工作时,我们会锁定地浏览旧列表和新列表。对于旧列表中的每个节点,我们查看该节点的随机指针中的地址。我们在哈希表中查找与该地址相关的位置,然后在该位置获得新列表中节点的地址,并将其放入新列表的当前节点的随机指针中。然后我们前进到新旧列表中的下一个节点。

当我们完成后,我们丢弃/销毁哈希表和数组,因为我们的新列表现在复制了旧列表的结构,并且我们不再需要额外的数据。

方法概述
我们不会一开始就尝试创建新的链表。首先复制原始链接列表中的项目,然后将列表拆分为两个相同的列表。

详细信息
步骤1:使用下一个指针运行链表,并创建每个节点的副本。

original :: A -> B -> C -> D -> E ->null
updated :: A -> A' -> B -> B' -> C -> C' -> D -> D' ->E -> E' -> null

这可以在一个循环中轻松完成

步骤2:再次遍历列表,并修复像这样的随机指针

Node *current= start;
Node *newListNode, *random;
while (current!= null) {
    // If current node has a random pointer say current = A, A.random = D;
    if ( (*current)->random != null ) {
        // newListNode will point to A'
        newListNode = (*current)->next;
        random = (*current)->random;
        random = (*random)->next;
        // Make A' point to D's next, which is D'
        (*newListNode)->random = random;
    }
    // Here A'.random = D'
    // Current goes to the next node in the original list => B , and not A'
    current= (*newListNode)->next;
}

步骤3:将列表拆分为2个列表。你只需要在这里修正下一个指针。

Original :: A -> A' -> B -> B' -> C -> C' -> D -> D' ->E -> E' -> null
New :: A -> B -> C -> D -> E ->null
       A' -> B' -> C' -> D' -> E' ->null
Node *start1 = head;
Node *start2 = (*head) ->next      // Please check the boundary conditions yourself,
                                   // I'm assuming that input was a valid list
Node *next, *traverse = start2;
while (traverse != null) {
    next = (*traverse)->next;
    if (next == null) {
        (*traverse)->next = null;
        break;
    }
    (*traverse)->next = (*next)->next;
    traverse = next;
}

通过这种方式,您在O(n)时间内创建了原始列表的副本。您遍历列表3次,仍然是n的顺序。

澄清编辑:

正如BowieOwens所指出的,只有当"随机"指针是唯一的时,以下内容才有效
我只留下大致的答案,但请不要投赞成票,因为这肯定是错误的。


如果我没有完全错的话,你可以在不使用任何额外存储的情况下完成这项工作。

在复制旧列表时,将旧节点的random指针存储在新节点的random指针中
然后,将旧节点的random指针设置为指向新节点。

这将在旧列表和新列表之间提供一个"之字形"结构。

伪码:

Node* old_node = <whatever>;
Node * new_node = new Node;
new_node->random = old_node->random;
old_node->random = new_node;

一旦你像那样复制了旧列表,你就可以重新开始,但要像这样替换random指针,恢复旧列表中的指针,同时将新列表中的random指针设置为相应的新节点:

Node* old_random = old_node->random->random; // old list -> new list -> old list
Node* new_random = new_node->random->random; // new list -> old list -> new list
old_node->random = old_random;
new_node->random = new_random;

它在纸上看起来好多了,但恐怕我的ASCII艺术技能达不到

确实更改了原始列表,但它已恢复到原始状态
我想这是否允许取决于面试。

如您所知,O(n)表示线性。由于您需要线性数据结构的副本,因此不会有问题,因为您只需要迭代原始列表的节点,并且对于每个访问的节点,您将为新列表创建一个新节点。我认为这个问题没有很好的表述,因为你需要了解一些方面才能做好工作:这是一个循环执行吗?"下一个"节点是什么?节点的下一个节点还是列表的第一个节点?列表如何结束?

也许这个问题是一个技巧,用来测试你如何回答奇怪的问题,因为它非常简单:

1) 迭代到每个节点

2) 为其他链表创建新节点,并复制值。

成本总是O(n):因为您只需对整个链表进行1次迭代!!!

或者,你已经理解了他的问题上的一些错误。