为什么这个通用祖先解决方案具有更好的最坏情况性能?

时间:2019-06-17 19:54:23

标签: java algorithm tree time-complexity ancestor

我正在研究两种解决方案,以找到二叉树(不一定是二叉搜索树)中两个节点的第一个共同祖先。有人告诉我第二种解决方案提供了更好的最坏情况下的运行时间,但我不知道为什么。有人可以启发我吗?

解决方案1:

  • 找到两个节点中每个节点的深度:p,q
  • 计算深度的差异
  • 在较浅的节点处设置一个指针,在较深的节点处设置一个指针
  • 将较深的节点指针向上移动德尔塔,以便我们可以从相同的高度开始遍历
  • 递归地访问两个指针的部分节点,直到我们到达第一个共同祖先之外的同一节点
import com.foo.graphstrees.BinaryTreeNodeWithParent;

/*
   Find the first common ancestor to 2 nodes in a binary tree.
*/
public class FirstCommonAncestorFinder {

    public BinaryTreeNodeWithParent find(BinaryTreeNodeWithParent p, BinaryTreeNodeWithParent q) {

        int delta = depth(p) - depth(q);
        BinaryTreeNodeWithParent first = delta > 0 ? q: p; // use shallower node
        BinaryTreeNodeWithParent second = delta > 0 ? p: q; //use deeper

        second = goUp(second, delta); // move up so they are level, if 1 node is deeper in the tree than the other, their common ancestor obviously cannot be below the shallower node, so we start them off at the same height in the tree


        //keep going up the tree, once first == second, stop
        while(!first.equals(second) && first !=null && second !=null) {
            first = first.getParent();
            second = second.getParent();
        }

        return first == null || second == null ? null : first;

    }

    private int depth(BinaryTreeNodeWithParent n) {
        int depth = 0;
        while (n != null) {
            n = n.getParent();
            depth++;
        }
        return depth;
    }

    private BinaryTreeNodeWithParent goUp(BinaryTreeNodeWithParent node, int delta) {

        while (delta > 0 && node != null) {
            node = node.getParent();
            delta--;
        }
        return node;
    }
}

解决方案2:

  • 验证两个节点(p,q)是否存在于从根节点开始的树中
  • 遍历子树,确认q不是p的子代,p不是q的子代
  • 递归检查p的连续父节点的子树,直到找到q
import com.foo.graphstrees.BinaryTreeNodeWithParent;

public class FirstCommonAncestorImproved {

    public BinaryTreeNodeWithParent find(BinaryTreeNodeWithParent root,
                                         BinaryTreeNodeWithParent a,
                                         BinaryTreeNodeWithParent b) {

        if (!covers(root, a) || !covers(root, b)) {
            return null;
        } else if (covers(a, b)) {
            return a;
        } else if (covers(b, a)) {
            return b;
        }

        var sibling = getSibling(a);
        var parent = a.getParent();

        while (!covers(sibling, b)) {
            sibling = getSibling(parent);
            parent = parent.getParent();
        }
        return parent;
    }

    private BinaryTreeNodeWithParent getSibling(BinaryTreeNodeWithParent node) {
        if (node == null || node.getParent() == null) return null;
        var parent = node.getParent();
        return node.equals(parent.getLeft()) ? node.getRight() : node.getLeft();
    }

    private boolean covers(BinaryTreeNodeWithParent root,
                           BinaryTreeNodeWithParent node) {

        if (root == null) return false;
        if (root.equals(node)) return true;
        return covers(root.getLeft(), node) || covers(root.getRight(), node);

    }
}

1 个答案:

答案 0 :(得分:0)

这取决于问题的结构。

如果起始节点位于一棵大树的深处,并且祖先就在附近,则第一种算法仍将需要遍历到根的整个路径以找到深度。通过仅检查一个小的子树,第二个将成功。

另一方面,如果节点较深且公共祖先离根很近,则第一个只会遍历到根的两条路径,而第二个可能会遍历整个树。

请注意,通常情况下,您可以通过以空间换取速度来获得渐近更快的算法。维护一组节点。从两个起始节点以交替的顺序向上移动,添加到集合中,直到找到一个已经存在的节点。那是共同的祖先。给定集合操作为O(1),则此算法为O(k),其中k是从公共祖先到最远的起始节点的路径长度。你做得更好。

Set<Node> visited = new HashSet<>();
while (a != null && b != null) {
  if (visited.contains(a)) return a;
  if (visited.contains(b)) return b;
  visited.add(a);
  visited.add(b);
  a = a.parent();
  b = b.parent();
}
while (a != null) {
  if (visited.contains(a)) return a;
  a = a.parent();
}
while (b != null) {
  if (visited.contains(b)) return b;
  b = b.parent();
}
return null;