通过使用顶点数和边数之间的关系在可能格式错误的二叉树中查找循环

时间:2012-07-06 03:20:05

标签: algorithm data-structures binary-tree

我最近在一次采访中遇到了这个问题。 最初的问题是

  

给定一个指向结构的指针(其结构使得它可以指向二进制树或双向链表),编写一个函数,返回它是指向二叉树还是DLL。结构是像这样定义

struct node
    {
     /*data member*/
     node *l1;
     node *l2;
    };

我直接陷入了问题但后来我意识到这个问题存在一些含糊之处。如果指针没有指向它们中的任何一个(即它是格式错误的DLL或格式错误的树),该怎么办?因此,面试官告诉我,然后我必须编写功能,以便它可以返回所有三种情况。因此函数的返回值变为形式的枚举

enum StatesOfRoot 
   {
   TREE,
   DLL,
   INVALID_DATA_STRUCTURE,  /* case of malformed dll or malformed tree */
   EITHER_TREE_DLL,         /* case when there is only 1 node */
   };

因此问题减少到验证二叉树和DLL的属性。对于DLL来说这很容易。 对于二叉树,我能想到的唯一验证是从根目录到节点的路径不应该多于一个。(或者不应该有任何循环) 因此,我提议我们使用HashMap(访问者直接拒绝)或使用BST维护一组访问节点来进行深度优先搜索并继续跟踪被访问节点(我想使用std :: set但是访问者突然弹出另一个限制,我不能使用STL)。他拒绝了这个想法,说我不允许使用任何其他数据结构。然后我提出了乌龟和野兔问题的修改版本(考虑到二叉树的每个分支作为一个单独的链接列表),他说这不起作用。 之后我继续提出了一些丑陋的解决方案(涉及删除节点,维护树的副本等)

问题的核心

然后面试官提出了他的解决方案。他说我们可以计算顶点数和边数,并断言关系顶点数=边数+1 (必须为二叉树保留的属性)。困扰我的是我们如何计算顶点的数量(不使用任何其他数据结构)?他说可以通过简单地执行任何遍历(预订,后序,顺序)来完成。我质疑如果树中有循环,我们将如何阻止无限循环,因为我们没有跟踪被访问的节点。他说这是可能的,但没有告诉如何。我很怀疑他的态度。谁能提供一些关于他提出的解决方案是否正确的见解?如果是,您将如何明确地保持不同顶点的数量?请注意,您传递的内容只是一个指针,您没有其他信息。

PS:后来我收到通知,说明我还没有回答面试官的最终解决方案。它应该被骗了吗?

编辑

只是为了说清楚,如果我们假设第三种情况不存在(即我们保证它是一个dll或二叉树)那么这个问题非常简单。它是第三种情况的树部分是快把我逼疯了。在回答时请注意这一点。

3 个答案:

答案 0 :(得分:1)

你对他的解决方案持怀疑态度是正确的。

双重链接列表很容易。 DLL强制执行不变量:

  1. 除了空节点,节点的左侧节点的右侧节点本身就是。
  2. 除了空节点,节点的右节点的左节点本身就是。
  3. 非循环DLL最终会在您继续向左移动时达到空。
  4. 非循环DLL最终会在您遵循正确的情况下达到空。
  5. 循环DLL将最终到达起始节点,因为你一直跟在左边。
  6. 只需要一个额外的临时变量,然后遍历DLL即可轻松检查前提。

    (注意:检查3和4,或5可能需要很长时间。)

    二叉树很难。 BT强制执行不变量:

    1. “No Loops”可以通过以下任何一种方式显示:
      • 证明没有两个节点指向同一节点,没有节点指向根。
      • 证明来自根的所有路径最终都以叶子结束。
      • 证明所有引用的节点都是不同的。
    2. “No Merges”可以通过以下任何一种方式显示:
      • 证明没有两个节点指向同一个节点。
      • 证明所有引用的节点都是不同的。
    3. 正如您所建议的那样,这些可以通过遍历树并标记访问的每个节点来确定,以确保没有任何节点被访问两次,或者存储访问的每个节点的列表(例如在散列集或其他结构中)如果节点是不同的,快速查找。

      你可以通过简单地遍历树并在树中保留当前深度的值来验证树中没有其他数据结构的循环,如果你在树中的深度比在计算机(或访问更多节点),你肯定会有一个无限循环。

      但是,这无法帮助我们区分Binary“Directed Acyclic Graphs”(DAG)和Binary Trees。

      但是,如果我们知道树中元素的数量,那么二叉树的库实现通常就是这种情况。您可以通过计算边缘数量来检测无限循环,这与先前已知的节点数量相比,就像采访者建议的那样。

      如果不提前知道这个数字,很难知道无限大树和大有限树之间的区别。 (除非你知道计算机的内存大小,或者其他信息,比如制作树的时间等等)。

      这仍然无法帮助我们检测出“No Merges”不变量。

      我想不出有任何有用的方法来确定No Merges是否存在,没有通过在外部数据结构中存储访问节点或在访问时将每个节点标记为已访问而没有显示任何节点被引用两次。 / p>

      作为最后的手段,您可以执行以下操作:

      1. 根据与计算机内存相比的树深度(或访问节点数)显示“无循环”。 (或如下,在编辑中)
      2. 通过此方法演示“No Merges”。
        • 从root的左边孩子开始,即树的深度为1。
        • 访问深度1和深度0的每个节点,并验证只有直接父节点引用所选节点。
        • 为root的右孩子做同样的事。
        • 对树中的每个节点继续此过程:
          1. 选择一个节点,保留对其直接父级的引用,
          2. 访问树中较高的每个节点,与所选节点的深度相同,
          3. 验证访问过的节点之外,只有直接父级引用所选子级。
        • 完成此操作后,再次遍历树以验证来自每个节点的左右指针都不指向同一节点。
      3. 此过程只需要一些额外的变量,但需要花费大量时间,因为您将每个节点单独比较到树中较高或相同深度的每个节点。

        我的直觉告诉我,上面的程序是一个v-squared算法,而不仅仅是订单v。

        如果有人想到另一种解决方法,请添加评论。


        编辑:您可以通过简单地将搜索范围扩展到相同深度和更高深度的每个节点来验证“无循环”,但可以与树中的每个节点进行比较。您需要在渐进算法中执行此操作,将每个节点与树中其上方的每个节点及其自身深度进行比较,然后检查树中距离比其深1到5个节点的所有节点,然后检查6-10代更低,等等。如果你以非渐进的方式检查,你可能会无限期地陷入困境。

答案 1 :(得分:0)

首先,原始问题清楚地表明正确的输入是DLL或树,因此IMO没有歧义:如果输入错误,代码的工作方式无关紧要。

无论如何,你和你的面试官被驱逐到'假设'的土地上。

但是,他说'不使用其他数据结构'是什么意思,因为你不能在不使用堆栈记住转折点的情况下遍历保证正确的二叉树(使用递归机制或手动创建堆栈数据结构) )。

所以我假设我们可以使用堆栈和递归。

一点注意:是的,我知道如果node结构包含指向树的指针,我们可以在常量内存中执行它(我们可以修改指针并将它们带回到最后),但是这里我们不知道没有那些,所以我放弃了这个的证明并假设这“显而易见”:我们必须能够至少使用递归。

好吧,我不打电话给以下'简单的顺序遍历'但是你在这里:

#include <stdio.h>
#include <stdbool.h>

struct node
    {
     /*data member*/
     struct node *l1;
     struct node *l2;
    };

// This one counts the nodes in a subtree of V with a depth no more than l that are equal to V0
int CountEqual(struct node* V0, struct node* V, int l)
{
    int thisOneIsEqual = 0;
    if( V == NULL ) {
        return 0;
    }

    if( l == 0 ) {
        return 0;
    }

    if( V0 == V ) {
        thisOneIsEqual = 1;
    }

    return thisOneIsEqual +
        CountEqual(V0, V->l1, l - 1) +
        CountEqual(V0, V->l2, l - 1);
}

// This one checks whether there're equal nodes in a subtree of root with a depth of L
bool Eqs(struct node* root, int L, struct node* V, int l)
{
    if( V == 0 ) {
        return false;
    }

    if( l == 0 ) {
        return false;
    }

    if( CountEqual(V, root, L) > 1 ) {
        return true;
    }

    return
        Eqs(root, L, V->l1, l - 1) ||
        Eqs(root, L, V->l2, l - 1);
}

// This checks whether the depth of the tree rooted at V is no more than l
bool HeightLessThanL(struct node* V, int l)
{
    if( V == 0 ) {
        return true;
    }

    if( l == 0 ) {
        return false;
    }

    return
        HeightLessThanL(V->l1, l - 1) &&
        HeightLessThanL(V->l2, l - 1);
}

bool isTree(struct node* root)
{
    int l = 1;
    while( 1 ) {
        if( HeightLessThanL(root, l - 1) ) {
            return true;
        }

        if( Eqs(root, l, root, l) ) {
            return false;
        }

        l++;
    }
}

// A simple test: build a correct tree, then add cycles, equal nodes etc.
#define SIZE 5
int main()
{
    struct node graph[SIZE];
    int i;

    for( i = 0; i < SIZE; ++i ) {
        graph[i].l1 = 0;
        graph[i].l2 = 0;
        if( 2 * i + 1 < SIZE ) {
            graph[i].l1 = graph + 2 * i + 1;
        }
        if( 2 * i + 2 < SIZE ) {
            graph[i].l2 = graph + 2 * i + 2;
        }
    }

    graph[1].l2 = graph + 3;

    printf( "%d\n", isTree( graph ) );
    return 0;
}

我们的想法是,对于某些L,我们知道我们有一个高度为L的树,或者在深度为L的子树中有两个相等的节点。

答案 2 :(得分:-1)

您必须假设DLL和树的一些通用接口。抽象父级可以定义一个虚拟的toHead(),其中DLL将转到头节点,而一个树将转到root并返回节点obeject等。这里的哈希表被过度杀死。我的C / C ++是生锈的,所以指针可能有点不对,但是,你要找的是内存中的位置与“copyHead”的值相同,因为存储在“copyHead”中的值是位置头部......希望这对你有所帮助。

type *myType;
myType = &structure;

node *copyHead = myType.toHead(); // Where toHead() returns a pointer to the head.

while( copyHead != &(*myType.next()) ) {
    if(*myType.curr() == null) { return "is tree"}
}

return "is DLL";