问题 采访编码 - 将指向Node结构的指针作为参数,并返回传入数据结构的完整副本


这是一个我觉得很有趣的面试问题。

编写一个方法,将指向Node结构的指针作为参数,并返回传入数据结构的完整副本。

Node结构包含两个指向其他Node结构的指针。 例如,方法签名可能如下所示:

Node* Copy(Node* root);

注意  - 不要对数据结构做任何假设 - 它可以是树,链表,图表等。

如何为任何数据结构做到这一点?


3634
2017-10-06 22:24


起源

为什么downvote :( - Legolas
不是我的,但看 stackoverflow.com/faq#dontask 。 - MSalters
@MSalters道歉!我明白这一点。我一直在打破这个问题,我想在StackOverflow上解决这个问题。我无意违反StackOverflow政策。 - Legolas
@Legolas我花了很多时间试图理解这个问题,但却无法理解。你有可能详细说明这个问题吗?谢谢 - brainydexter


答案:


在通用图形的情况下,您需要从原始图形中的节点到新图形中的节点的映射,以便在遇到循环时,创建正确的链接。如果您在每个节点中碰巧有额外的临时空间,大到足以容纳指针,那么您可以将映射直接存储在节点中;否则,您将需要使用外部映射,例如关联数组或散列表。

然后,只需遍历图形,复制节点,查找相应的边缘即可。像这样的东西:

struct Node
{
    Node(int _data) : data(_data) { memset(links, 0, sizeof(links)); }

    int data;
    Node *links[2];
}

Node *Copy(Node *root)
{
    typedef std::map<Node*, Node*> NodeMap;
    NodeMap nodeMap;
    std::deque<Node*> nodesToVisit;

    // Set up initial new root and mapping for the root
    Node *newRoot = new Node(root->data);
    nodeMap[root] = newRoot;

    // Breadth-first search the graph
    nodesToVisit.push_back(root);

    while(!nodesToVisit.empty())
    {
        Node *cur = nodesToVisit.front();
        nodesToVisit.pop_front();

        Node *newCur = nodeMap[cur];
        for(int i = 0; i < 2; i++)
        {
            Node *link = cur->links[i];
            if(link)
            {
                // If we've already created the corresponding node for this
                // link, use that.  Otherwise, create it and add it to the map.
                NodeMap::iterator mappedLink = nodeMap.find(link);
                if(mappedLink != nodeMap.end())
                {
                    newCur->links[i] = mappedLink->second;
                }
                else
                {
                    Node *newLink = new Node(link->data);
                    nodeMap[link] = newLink;
                    newCur->links[i] = newLink;
                    nodesToVisit.push_back(link);
                }
            }
        }
    }

    return newRoot;
}

5
2017-10-06 22:40



我想你的意思是 push_back(link) 最后,自那以后 nodesToVisit 包含指向旧结构中节点的指针,而不是新结构中的节点。 - Steve Jessop
@Steve:谢谢,是的,你是对的。固定。 - Adam Rosenfield
我试图理解你的想法过程....这看起来很棒。 - Legolas
@AdamRosenfield我知道这是一个非常古老的问题,但我花了很多时间试图理解这个问题和你的答案。特别是,我无法理解你的意思:`这样当遇到一个循环时,会创建正确的链接。如果你能详细说明,我会很感激。谢谢 - brainydexter


所述问题是不可能的。您必须假设整个数据结构完全存储在可从该初始节点访问的节点内容中。但这不是你被允许做出的假设。即使您的标准基本双链表也可能不符合该描述。


2
2017-10-06 22:32



当然,从技术上讲,数据结构是一个有节点的图,每个节点有两个外点;您只是声明您无法复制给定单根的林。如果您理解将根复制为仅复制树而不复制林,那么这是可能的。是的,它需要一个根。 - MSalters
我想+1,但肯定是在一个双向链表中,整个结构 是 可从任何节点访问?如果不是,那么它不是双向链表,它是别的东西。 - Steve Jessop
@Steve:可以访问整个结构,但可能缺少其他数据,例如指向两端的额外指针。然而,看着MSalters的评论,并重读这个问题,也许我误解了。我拿了 the data structure 意思是 the data structure the node is a part of 而不是 the data structure defined by the node。 - Dennis Zickefoose
啊,我明白你的意思了。同意的元数据(以及节点所属的任何周围数据结构对象的类型)无法确定。我很确定原始面试问题将会是什么,是从该节点可以到达的节点集的克隆,具有所有相应的边缘。但是我们在这里看到的问题的版本并不确定。 - Steve Jessop


class Copier {
  std::map <Node*, Node*> copies;

  Node* Copy(Node* n) {
    if (!n) return 0;
    Node*& copy = copies[n];
    if (!copy) {
      copy = new Node();
      copy.node1 = Copy(n.node1);
      copy.node2 = Copy(n.node2);
    }
    return copy;
  }
}

2
2017-10-06 22:37



不是C ++,但也不错。但这是不正确的,它可能会添加一个额外的节点。 - Mooing Duck
哦,我错过了C ++标签;我以为我是最后一个活着的C ++程序员:-) - kevin cline
这会添加一个“NULL”节点。然后是段错误。 - Mooing Duck
看起来不错,除非它应该是 copy->node1 = Copy(n->node1); 对于2.另外,在最坏的情况下,这会在O(n)深处进行,这令人担忧。哦,而且 Copy 可能应该公开;-) - Steve Jessop
@Steve:关于递归深度的优点。所以 那是 为什么其他海报使用队列。 - kevin cline


Node* Copy(Node* root) {
   if (root == NULL)
       return root;
   std::unordered_map<Node*, Node*> completed; 
   std::deque<Node*> todo;
   Node *ret = new Node(*scur);
   completed.push_back(std::make_pair(root, ret));
   todo.push_pack(root); 

   //while there's more nodes to duplicate
   do { 
       //duplicate the node
       Node* oldNode = todo.back();
       Node* newNode = completed[cur];
       todo.pop_back();

       if(oldNode->left) {
           auto iter = completed.find(oldNode->left);
           //if it has a left child that needs duplicating, add it to the todo list
           if (iter == completed.end()) {
               newNode->left = new Node(*(oldNode->left));
               completed.push_back(std::make_pair(oldNode->left, newNode->left));
               todo.push_back(oldNode->left);
           } else {
               newNode->left = completed[oldNode->left];
           }
       }
       if(oldNode->right) {
           auto iter = completed.find(oldNode->right);
           //if it has a right child that needs duplicating, add it to the todo list
           if (iter == completed.end()) {
               newNode->right = new Node(*(oldNode->right));
               completed.push_back(std::make_pair(oldNode->right, newNode->right));
               todo.push_back(oldNode->right);
           } else {
               newNode->right= completed[oldNode->right];
           }
       }

   } while(todo.empty() == false)
   //return the translation of the root
   return ret;
}

没有堆栈溢出,root可以为NULL,如果left或right为NULL则不会失败。

[编辑]亚当罗森菲尔德让我意识到,如果网络中存在循环,这是不正确的。不得不从头开始重写。由于需要大量代码,我更喜欢他的代码for循环。


2
2017-10-06 22:53





return new Node(*node);

特技提问?


1
2017-10-06 22:34



我不这么认为。我认为这个问题与Deep Copy和Shallow Copy有关。 - Legolas
@Legolas这取决于复制构造函数的定义方式。 - Ayjay
我认为这个问题应该区分“节点结构”和“数据结构”。你复制了前者,但我的心理直觉是真正的面试问题不是在那之后 Node 只是一个包含两个指针的POD结构。 - Steve Jessop


你应该递归地写它;

Node * Copy( Node * root )
{
    Node * node_copy;

    node_copy = new Node; // Assume Node1 and Node2 are initialized to 0
    node_copy->content = root->content;

    if( root->Node1 ) node_copy->Node1 = Copy( root->Node1 );
    if( root->Node2 ) node_copy->Node2 = Copy( root->Node2 );

    return node_copy;
}

因此,这不会对数据类型做出任何假设


0
2017-10-06 22:29



Node n = new Node(); n.node1 = n.node2 = n; Copy(n); 可能需要很长时间。 - kevin cline
是的,它会。如果需要处理该情况,则需要存储一对源节点/新节点并首先在该表中搜索该节点。如果找到它,设置指针,如果它不存在,只需克隆它并将其添加到列表中 - crazyjul
@xrazyjul:我认为这是必需的,因为问题说完整的数据结构可能是“图形”。如果它只是说它可能是一个DAG你可以假设没有周期。 - Steve Jessop
我的坏,你是对的,我错过了那条线 - crazyjul


鉴于存在复制构造函数,它只复制节点的内容而不复制其子节点:

Node* Copy(Node* root)
{
    Node* copy = new Node(*root);
    copy->left = Copy(root->left);
    copy->right = Copy(root->right);
    return copy;
}

在更一般的意义上,我将使用完全复制整个数据结构的复制构造函数:

Node* Copy(Node* root)
{
    return new Node(*root);
}

0
2017-10-06 22:31



如果它是一个双向链表导致堆栈溢出。另外,取消引用NULL指针。以先发生者为准。 - Mooing Duck