两个二叉树是否相等

7

可能是重复问题:
确定两个二叉树是否相等

昨天参加了面试,有一个问题让我很困惑,问题如下:

描述

2 个二叉树,请检查它们是否相等。

只有当 tree1->child == tree2->child,且其中一个树的左右 子节点可以相互交换 时,它们才相等。

例如:

    5     6
   / \   / \           they are equal.
   1 2   2  1

    5         6
   / \       / \           they are equal.
  1   2     2   1
 /     \   /    / 
3       4 4     3

非常感谢您的建议。


他们让你测试相等的定义吗?那么请展示它是可传递的,自反的和对称的。 - user684934
@PengOne,嗯,我不认为你给的链接是重复的,在我的问题中,一棵树的左/右子节点可以交换,然后与另一棵树相等进行比较。 - Alcott
@bdares,我不太明白,请您能否更具体一些? - Alcott
6个回答

9

等于运算符是可传递的:如果A=B,B=C,则A=B=C,因此A=C。

等于运算符是自反的:无论它们的值如何,A=A,B=B和C=C。

等于运算符是对称的。如果A=B,则B=A。(它们的顺序无关紧要。)

现在,看一下他们给你的定义:

如果子节点相等,则树等于另一个树。我们可以假设在底部比较节点,否则定义就没什么用了。但是他们没有告诉你如何解决比较,整个定义都取决于它。

简而言之,这是一个糟糕的问题。

但是,让我们看看如果我们决定尝试解开这个问题会发生什么。

但是,请等一下,他们还告诉你任何树的两个子节点都可以交换。这增加了一个约束条件,即任何与其他任何东西(包括它本身)相等的树必须等于其镜像。以及其子树的任何变化。

并且请记住,这应该是一个搜索树。因此,我们可以假设由同一算法处理的两个不同搜索树如果相等,则必须给出相同的结果。因此,如果我们交换树的元素,则搜索时间会受到影响。因此,没有每个节点都在其位置上的树彼此不相等。

将其与此等式的“可互换”性质结合起来,我们可以看到这不是一个有效的等式定义。(如果我们尝试应用它,那么结果只有具有特定级别每个节点的相同节点的树才相等,并且仅相等于它们自己,这违反了等于运算符的自反性部分。)


@Alcott:这个回答并不是在说你的问题有问题。它是在说这是一个糟糕的面试问题,因为相等的定义是无效的。除非这是一个恶作剧问题,否则我不想在这家公司工作。 - ninjagecko
@Alcott:是的,tree1->childtree2->child也没有定义清楚。这是他们措辞不当的错误,因为一个节点不只有一个子节点。 - ninjagecko
@ninjagecko,“tree->child”表示它可以是左子树或右子树,因为您可以交换子树。 - Alcott
@Alcott:你只能在等式测试期间交换子树;你声称孩子不重要意味着所有层级的所有节点都必须相等。 - ninjagecko
顺便提一下,编程不是数学,所以 A=A 并不总是成立的,例如 nan != nan - jfs
显示剩余2条评论

8

我认为这不是一个不合理的问题。一个简单的递归解决方案是

boolean equals(x, y)
{
  if (x == null)
  {
    return y == null;
  }
  if (y == null)
  {
    return false;
  }
  if (x.val != y.val)
  {
    return false;
  }
  if (equals(x.left, y.left) && equals(x.right, y.right))
  {
    return true;
  }
  if (equals(x.left, y.right) && equals(x.right, y.left))
  {
    return true;
  }
  return false;
}

这可能非常昂贵,例如,在两个形状相似的大树中,所有非叶节点都具有相同的关联值,而其中一个的叶节点是另一个的排列。
为了解决这个问题,您可以首先根据所需更改左和右,使得 left < right,对于某些递归定义的 <。这也可能很昂贵,但比检查每个排列要少得多,并且我认为选择 < 的定义会有所帮助。然后,这将允许您使用普通定义来检查相等性。
这种http://en.wikipedia.org/wiki/Canonicalization的概念,随后是普通的相等性,也解决了关于您是否真正拥有等价关系的问题。等价关系等效于分区。普通的相等性显然是一种分区。如果通过比较 f(x) 和 f(y) 后跟等价关系来比较 x 和 y,则可以将 x 和 y 分区,因此具有等价关系。
思考了一下,我认为使规范化或相等性测试合理高效的方法是从下往上工作,用一个标记注释每个节点,其值反映与其他节点的比较结果,这样你就可以通过比较标记来比较节点和它们下面的子树。
因此,相等性的第一步是例如使用哈希表来用标记注释每个叶子,这些标记仅在叶子处的值相等时才相等。然后,对于只有叶子节点的节点,使用例如哈希表来分配进一步的标记,以便这些节点中的标记仅在这些节点下方的叶子(如果有)匹配时才相等。然后,您可以再往上走一步,这次您可以比较子节点处的标记,而不是递归到那里的树。以这种方式分配标记的成本应该与涉及的树的大小呈线性关系。在顶部,您可以通过比较根处的标记来比较树。

我提供了一个递归解决方案,基本上和你的一样,但面试官说那个太耗费资源了。 - Alcott
1
这是一个重要的观点,你应该在你的问题中加上。 - Louis Ricci
(编辑后添加了自下而上的方法,我认为现在相当有效,因此回答了您面试官的反对意见。) - mcdowella
我已经在Python中实现了您建议的规范化方法的解决方案。 - jfs

3
如果您使用了他们的“等同性”定义实现了翻转不变性,那么您将违反等同性的定义。这个定义甚至没有意义,因为这不是二叉搜索树相等的方式(除非每个节点都有一个指针指向哪个子树是“较大”的,哪个子树是“较小”的)。
您有两个合理定义的选择:
1. 拓扑(翻转不可知)等价性(在这种情况下,您不能称其为“二叉搜索树”,因为它没有排序): tree1==tree2 意味着 set(tree1.children)==set(tree2.children)
2. 正常搜索树(翻转关注)等价性: tree1==tree2 意味着 list(tree1.children)==list(tree2.children)
对于二叉树,上述定义将按原样在任何支持list和set数据类型的语言中工作(但python集合可能会针对不可哈希数据类型而产生问题)。尽管如此,以下是一些更冗长且不美观的C / Java式定义:
1. 拓扑等价性: t1==t2 意味着 (t1.left==t2.left and t1.right==t2.right) or (t1.left==t2.right and t1.right==t2.left)
2. 排序树等价性: t1==t2 意味着 (t1.left==t2.left and t1.right==t2.right)
上述定义是递归的;也就是说,它们假定已经为子树和基本情况定义了等同性,实际上也是如此。
附带说明:
这不是一个有效的语句,因为树节点没有单个孩子。

关于你的旁注:如果你将child视为节点对象的集合,则可能是有效的...我曾经使用一堆Map和List对象递归地实现了一棵树,每个节点的子节点都存储在一个列表中,该列表由字符串“child”映射到节点。 - user684934
@bdares:是的,同意,但在这种情况下,我会称之为.children(就像我在答案中所做的那样)。 - ninjagecko
@ninjagecko:你是不是想说“因为这不是二叉搜索树相等的方式”? - Rob
@robjb:是的,谢谢你发现那个错字并修正了它。 - ninjagecko

1

使用@mcdowella建议的规范化方法来比较树。不同之处在于,我的方法不需要与树中节点数量成比例的O(N)额外内存:

# in Python
from collections import namedtuple
from itertools import chain

# Tree is either None or a tuple of its value and left, right trees
Tree = namedtuple('Tree', 'value left right')

def canonorder(a, b):
    """Sort nodes a, b by their values.

    `None` goes to the left
    """
    if (a and b and a.value > b.value) or b is None:
        a, b = b, a # swap
    return a, b

def canonwalk(tree, canonorder=canonorder):
    """Yield all tree nodes in a canonical order.

    Bottom-up, smaller children first, None is the smallest
    """
    if tree is not None:
        children = tree[1:]
        if all(t is None for t in children): return # cut None leaves
        children = canonorder(*children)            
        for child in chain(*map(canonwalk, children)):
            yield child
    yield tree 

canonwalk()需要执行O(N*M)步骤和使用O(log(N)*M)内存来返回树中的所有节点,其中N是节点总数,M是每个节点拥有的子节点数量(对于二叉树,它为2)。

canonorder()可以轻松地推广到任何节点表示和任意子节点数量。 canonwalk()仅需要一棵树可以访问其直接子节点作为序列。

调用canonwalk()的比较函数:

from itertools import imap, izip_longest

unset = object() 
def cmptree(*trees):
    unequal = False # allow root nodes to be unequal
    # traverse in parallel all trees under comparison
    for nodes in izip_longest(*imap(canonwalk, trees), fillvalue=unset):
        if unequal:
            return False # children nodes are not equal
        if any(t is unset for t in nodes):
            return False # different number of nodes
        if all(t is not None for t in nodes):
            unequal = any(nodes[-1].value != t.value for t in nodes)
        else: # some are None
            unequal = any(t is not None for t in nodes)
    return True # equal

例子

    5         6
   / \       / \           they are equal.
  1   2     2   1
 /     \   /    / 
3       4 4     3

tree1 = Tree(5, 
             Tree(1, 
                  Tree(3, None,None), None), 
             Tree(2, 
                  None, Tree(4, None, None)))
tree2 = Tree(6, 
             Tree(2, Tree(4, None, None), None),
             Tree(1, Tree(3, None, None), None))
print cmptree(tree1, tree2)

输出

True

0

在Ruby中不使用递归的解决方案

def same? top_t1, top_t2
  for_chek << [top_t1, top_t2]   # (1) put task for check into queue

  while t1,t2 = for_check.shift  # (2)
    return false unless t1.children.count == t2.children.count  # generally for non-binary tree, but also needed for controlling of nil children
    break if t1.children.empty?

    t1_children = t1.children.sort # this is sorted arrays
    t2_children = t2.children.sort # of childrens      
    return false unless t1_children == t2_children  # (3)

    0.upto(t1_children.count - 1) do |i|
      for_check << [t1_children[i], t2_children[i]]  # put equivalent child pairs into queue
    end
  end
  return true
end

Ruby语法提示:

  • (1)将元素放入数组中:arr << elem;在这种情况下,for_check是一个数组的数组
  • (2)并行赋值:t1,t2 = [item1, item2]。与arr = [item1, item2]; t1 = arr[0]; t2 = arr[1]相同
  • (3)t1_children == t2_children假定对于这种对象,==具有相应的行为。更详细的写法是t1_children.map { |el| el.val } == t2_children.map { |el| el.val } - 这里map生成val的数组。

0
我理解这个问题是:给定两棵二叉树,对于树中的每个深度,找出它们的子节点集合是否相互覆盖。
这个问题可以相对容易地编码实现。

这不是问题陈述的内容,也没有尝试回答问题的任何一种解释。 - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接