给定两个二叉树,计算它们的差异

时间:2019-03-19 15:43:34

标签: algorithm data-structures tree binary-tree

我的一个朋友在一次采访中被问到这个问题。

给出两棵二叉树,解释如何创建差异,以使如果您具有该差异以及其中的任何一棵树,您都应该能够生成另一棵二叉树。实现函数createDiff(Node tree1, Node tree 2)返回该差异。

Tree 1
       4
     /   \
    3     2
   / \   /  \
  5   8 10   22

Tree 2
       1
         \
          4
         /  \
       11   12

如果为您提供树2和差异,则应该能够生成树1。

我的解决方案: 将两个二叉树都转换为数组,其中左子节点在2n+1,右子节点在2n+2,并用-1表示空节点。然后只需对数组进行逐元素减法即可创建差异。如果树的节点值为-1,则该解决方案将失败,我认为必须有一个更好,更整洁的解决方案,但我无法弄清楚。

4 个答案:

答案 0 :(得分:0)

将其视为可怕的问题,并打印到每个叶子项的路径的排序列表

树1变为:

4/2/10
4/2/22
4/3/5
4/3/8

可以diff设置这些列表格式,并从这样的列表中重新创建树。

答案 1 :(得分:0)

有很多方法可以做到这一点。

我建议您将树变成(parent, child, direction)的三元组排序数组。因此,从tree1开始:

       4
     /   \
    3     2
   / \   /  \
  5   8 10   22

这很快变成:

(None, 4, None) # top
(4, 3, L)
(3, 5, L)
(3, 8, L)
(4, 2, R)
(2, 10, L)
(2, 22, R)

您想得到什么

(None, 4, None) # top
(2, 10, L)
(2, 22, R)
(3, 5, L)
(3, 8, L)
(4, 2, R)
(4, 3, L)

与另一个做相同的事情,然后比较它们。

给出一棵树和diff,您可以首先将树变成这种形式,查看diff,了解它的方向并使用patch获得所需的表示。然后,您可以递归地重建另一棵树。

之所以要使用这种表示,是因为如果两棵树共享任何共同的子树-即使它们在主树中的位置不同,它们也会共同出现。因此,如果树确实以某种有趣的方式匹配,则您可能会获得相对较小的差异。


编辑

对于@ruakh中的每个点,这确实假定值不会在树中重复。如果他们这样做,那么您可以这样做:

       4
     /   \
    3     2
   / \   /  \
  5   8 10   22

成为

(, 4)
(0, 3)
(00, 5)
(01, 8)
(1, 2)
(10, 10)
(11, 22)

现在,如果您移动子树,它们将显示为较大的差异。但是,如果您只更改一个节点,那仍然是一个很小的差异。

答案 2 :(得分:0)

(问题(/ interview)中的示例在不显示任何大小无关的共享子结构方面不是很有帮助。或者在发起客户与开发人员之间的对话时突出显示了访谈问题。)
子树的重用需要一种表示法来识别。能够重建较小的树而不必走大部分差异似乎很有用。用固定的':

表示大写字母和可重复使用的可识别子树的“定义”
     d            e             d--------e
  c     b "-"   c    b   =>   C   B'   C'  b
 b a   a       b a    a      B a            a
a             a             a

(问题陈述不是 diff是线性的。)
注意事项:

  • 在T1的两个地方有一个子树 B
  • 在T2中,还有另一个 b ,其中有一个叶子子级 a ,而 not 则是另一个 B
  • 没有尝试分享叶子

如果我现在想像(或面试官建议)两棵大树,它们相同,但中间的某个节点具有不同的值,该怎么办
好吧,至少它的子树将被共享,而“其他子树”将一直共享到根。如果树已退化并且几乎所有节点都是该路径的一部分,那就太糟糕了。
巨大的树木与孩子的根交换了吗?
(检测不止一次的树木有机会在这里闪耀。)
更大的问题似乎是“ diff”中表示的整个树,而要求可能是

  

给出一棵树, diff 将支持使用很少的空间和处理来重建另一棵树。

(它可能包括设置差异也应该很便宜-我会立即挑战: small diff 看起来与编辑距离。)
需要一种方法来识别每棵树中的“关键节点” -btilly's suggestion of "left-right-string"就像金子一样。
然后,需要一种方法来保持孩子与价值观的差异。


这是我期望在采访中达成的交流的终点。

要检测重用的树,我将 height 添加到每个内部节点。对于原理证明,我可能会在合适的序列化中使用查找重复字符串的现有实现。

答案 3 :(得分:0)

有很多方法可以考虑可行的差异结构。

天真的解决方案

一种幼稚的方法是将两棵树存储在一个元组中。然后,当您需要重新生成一棵树时,给定另一棵树和diff,则只需将给定树与diff的第一个元组条目中的树进行比较时寻找一个不同的节点。如果找到,则从第一个元组条目返回该树。如果找不到,则从diff元组中返回第二个。

差异较小的小差异

面试官可能会要求减少内存消耗的替代方案。人们可能会想一想一种结构,当只有几个值或节点不同时,它的大小会很小。在两棵树都相等的极端情况下,这种差异也将是(近)空的。

定义

在定义diff的结构之前,我先定义以下术语:

  • 想象一下,树得到了额外的NIL叶节点,即一棵空树将由1个NIL节点组成。一棵只有一个根节点的树将有两个NIL节点作为其直接子节点,等等。

  • 当通过根的相同路径(例如,左-左-右)到达节点时,这两个树都是公用的,而不管它们是否包含相同的值或具有相同的子代。当一个节点是一棵或两棵树(如上定义)中的NIL节点时,它甚至可以是公共节点。

  • 公共节点(包括NIL节点,当它们是公共节点时)获得预序号(0、1、2,...)。在此编号过程中, 不常见的节点将被丢弃。

差异结构

区别可能是元组列表,其中每个元组都具有以下信息:

  • 上述预购序列号,标识一个公共节点
  • 一个值:当两个节点都不是NIL节点时,这是值的差异(例如XOR)。当一个节点是NIL节点时,该值是另一个节点对象(因此有效地包括其下面的整个子树)。在无类型语言中,这两种信息都可以位于相同的元组位置。在强类型语言中,您将在元组中使用一个额外的条目(例如atomicValue,子树),其中只有两个之一具有显着的值。

仅当一个公共节点的值不同,并且两者中至少有一个是非NIL节点时,才将其添加到一个公共节点。

算法

可以通过在树的公共节点上进行预排序来创建差异。

这是JavaScript的实现:

class Node {
    constructor(value, left, right) {
        this.value = value;
        if (left) this.left = left;
        if (right) this.right = right;
    }
    clone() {
        return new Node(this.value, this.left ? this.left.clone() : undefined,
                                    this.right ? this.right.clone() : undefined); 
    }
}

// Main functions:
function createDiff(tree1, tree2) {
    let i = -1; // preorder sequence number
    function recur(node1, node2) {
        i++;
        if (!node1 !== !node2) return [[i, (node1 || node2).clone()]];
        if (!node1) return [];
        const result = [];
        if (node1.value !== node2.value) result.push([i, node1.value ^ node2.value]);
        return result.concat(recur(node1.left, node2.left), recur(node1.right, node2.right));
    }
    return recur(tree1, tree2);
}

function applyDiff(tree, diff) {
    let i = -1; // preorder sequence number
    let j = 0; // index in diff array
    function recur(node) {
        i++;
        let diffData = j >= diff.length || diff[j][0] !== i ? 0 : diff[j++][1];
        if (diffData instanceof Node) return node ? undefined : diffData.clone();
        return node && new Node(node.value ^ diffData, recur(node.left), recur(node.right));
    }
    return recur(tree);
}

// Create sample data:
let tree1 = 
    new Node(4,
        new Node(3,
            new Node(5), new Node(8)
        ),
        new Node(2,
            new Node(10), new Node(22)
        )
    );
let tree2 =
    new Node(2,
        undefined,
        new Node(4,
            new Node(11), new Node(12)
        )
    );

// Demo:
let diff = createDiff(tree1, tree2);
console.log("Diff:");
console.log(diff);

const restoreTree2 = applyDiff(tree1, diff);
console.log("Is restored second tree equal to original?");
console.log(JSON.stringify(tree2)===JSON.stringify(restoreTree2));

const restoreTree1 = applyDiff(tree2, diff);
console.log("Is restored first tree equal to original?");
console.log(JSON.stringify(tree1)===JSON.stringify(restoreTree1));

const noDiff = createDiff(tree1, tree1);
console.log("Diff for two equal trees:");
console.log(noDiff);