【发布时间】:2011-07-28 21:54:12
【问题描述】:
这是一个流行的面试问题,我能找到的唯一一篇关于该主题的文章来自TopCoder。对我来说不幸的是,从面试答案的角度来看,它看起来过于复杂。
除了绘制到两个节点的路径并推断出祖先之外,没有更简单的方法吗? (这是一个流行的答案,但面试问题的一个变体要求一个恒定的空格答案)。
【问题讨论】:
标签: java binary-tree
这是一个流行的面试问题,我能找到的唯一一篇关于该主题的文章来自TopCoder。对我来说不幸的是,从面试答案的角度来看,它看起来过于复杂。
除了绘制到两个节点的路径并推断出祖先之外,没有更简单的方法吗? (这是一个流行的答案,但面试问题的一个变体要求一个恒定的空格答案)。
【问题讨论】:
标签: java binary-tree
一个简单的(但涉及的版本少得多)可能只是(这里的.NET 家伙 Java 有点生疏,所以请原谅语法,但我认为你不必调整太多)。这是我一起扔的。
class Program
{
static void Main(string[] args)
{
Node node1 = new Node { Number = 1 };
Node node2 = new Node { Number = 2, Parent = node1 };
Node node3 = new Node { Number = 3, Parent = node1 };
Node node4 = new Node { Number = 4, Parent = node1 };
Node node5 = new Node { Number = 5, Parent = node3 };
Node node6 = new Node { Number = 6, Parent = node3 };
Node node7 = new Node { Number = 7, Parent = node3 };
Node node8 = new Node { Number = 8, Parent = node6 };
Node node9 = new Node { Number = 9, Parent = node6 };
Node node10 = new Node { Number = 10, Parent = node7 };
Node node11 = new Node { Number = 11, Parent = node7 };
Node node12 = new Node { Number = 12, Parent = node10 };
Node node13 = new Node { Number = 13, Parent = node10 };
Node commonAncestor = FindLowestCommonAncestor(node9, node12);
Console.WriteLine(commonAncestor.Number);
Console.ReadLine();
}
public class Node
{
public int Number { get; set; }
public Node Parent { get; set; }
public int CalculateNodeHeight()
{
return CalculateNodeHeight(this);
}
private int CalculateNodeHeight(Node node)
{
if (node.Parent == null)
{
return 1;
}
return CalculateNodeHeight(node.Parent) + 1;
}
}
public static Node FindLowestCommonAncestor(Node node1, Node node2)
{
int nodeLevel1 = node1.CalculateNodeHeight();
int nodeLevel2 = node2.CalculateNodeHeight();
while (nodeLevel1 > 0 && nodeLevel2 > 0)
{
if (nodeLevel1 > nodeLevel2)
{
node1 = node1.Parent;
nodeLevel1--;
}
else if (nodeLevel2 > nodeLevel1)
{
node2 = node2.Parent;
nodeLevel2--;
}
else
{
if (node1 == node2)
{
return node1;
}
node1 = node1.Parent;
node2 = node2.Parent;
nodeLevel1--;
nodeLevel2--;
}
}
return null;
}
}
【讨论】:
使用什么样的树很重要。您总是可以判断一个节点是否是恒定空间中另一个节点的祖先,并且顶部节点始终是共同祖先,因此在恒定空间中获取最低公共祖先只需要向下迭代。在二叉搜索树上,这很容易快速完成,但它适用于任何树。
许多不同的权衡与此问题相关,树的类型很重要。如果您有指向父节点的指针,而不仅仅是指向子节点的指针,问题往往会容易得多(Mirko 的代码使用了这个)
【讨论】:
常数空间答案:(虽然不一定有效)。
有一个函数 findItemInPath(int index, int searchId, Node root)
然后从树的 0 .. 深度迭代,在两个搜索路径中找到第 0 项、第 1 项等。
当你发现 i 使得函数对两者都返回相同的结果,但不是 i+1 时, 那么路径中的第 i 个项目是最低的共同祖先。
【讨论】:
使用 log(n) 空间的明显解决方案(n 是节点数)是您提到的算法。这是一个实现。在最坏的情况下,它需要 O(n) 时间(假设您正在搜索共同祖先的节点之一包括最后一个节点)。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication2
{
class Node
{
private static int counter = 0;
private Node left = null;
private Node right = null;
public int id = counter++;
static Node constructTreeAux(int depth)
{
if (depth == 0)
return null;
Node newNode = new Node();
newNode.left = constructTree(depth - 1);
newNode.right = constructTree(depth - 1);
return newNode;
}
public static Node constructTree(int depth)
{
if (depth == 0)
return null;
Node root = new Node();
root.left = constructTreeAux(depth - 1);
root.right = constructTreeAux(depth - 1);
return root;
}
private List<Node> findPathAux(List<Node> pathSoFar, int searchId)
{
if (this.id == searchId)
{
if (pathSoFar == null)
pathSoFar = new List<Node>();
pathSoFar.Add(this);
return pathSoFar;
}
if (left != null)
{
List<Node> result = left.findPathAux(null, searchId);
if (result != null)
{
result.Add(this);
return result;
}
}
if (right != null)
{
List<Node> result = right.findPathAux(null, searchId);
if (result != null)
{
result.Add(this);
return result;
}
}
return null;
}
public static void printPath(List<Node> path)
{
if (path == null)
{
Console.Out.WriteLine(" empty path ");
return;
}
Console.Out.Write("[");
for (int i = 0; i < path.Count; i++)
Console.Out.Write(path[i] + " ");
Console.Out.WriteLine("]");
}
public override string ToString()
{
return id.ToString();
}
/// <summary>
/// Returns null if no common ancestor, the lowest common ancestor otherwise.
/// </summary>
public Node findCommonAncestor(int id1, int id2)
{
List<Node> path1 = findPathAux(null, id1);
if (path1 == null)
return null;
path1 = path1.Reverse<Node>().ToList<Node>();
List<Node> path2 = findPathAux(null, id2);
if (path2 == null)
return null;
path2 = path2.Reverse<Node>().ToList<Node>();
Node commonAncestor = this;
int n = path1.Count < path2.Count? path1.Count : path2.Count;
printPath(path1);
printPath(path2);
for (int i = 0; i < n; i++)
{
if (path1[i].id == path2[i].id)
commonAncestor = path1[i];
else
return commonAncestor;
}
return commonAncestor;
}
private void printTreeAux(int depth)
{
for (int i = 0; i < depth; i++)
Console.Write(" ");
Console.WriteLine(id);
if (left != null)
left.printTreeAux(depth + 1);
if (right != null)
right.printTreeAux(depth + 1);
}
public void printTree()
{
printTreeAux(0);
}
public static void testAux(out Node root, out Node commonAncestor, out int id1, out int id2)
{
Random gen = new Random();
int startid = counter;
root = constructTree(5);
int endid = counter;
int offset = gen.Next(endid - startid);
id1 = startid + offset;
offset = gen.Next(endid - startid);
id2 = startid + offset;
commonAncestor = root.findCommonAncestor(id1, id2);
}
public static void test1()
{
Node root = null, commonAncestor = null;
int id1 = 0, id2 = 0;
testAux(out root, out commonAncestor, out id1, out id2);
root.printTree();
commonAncestor = root.findCommonAncestor(id1, id2);
if (commonAncestor == null)
Console.WriteLine("Couldn't find common ancestor for " + id1 + " and " + id2);
else
Console.WriteLine("Common ancestor for " + id1 + " and " + id2 + " is " + commonAncestor.id);
}
}
}
【讨论】:
文章的解决方案更复杂的主要原因是它正在处理一个两阶段的问题 - 预处理然后查询 - 而从你的问题听起来你只做一个查询所以预处理不会感觉。它还处理任意树而不是二叉树。
最佳答案当然取决于树的详细信息。对于多种树,时间复杂度将是 O(h),其中 h 是树的高度。如果您有指向父节点的指针,那么简单的“恒定空间”答案是,就像在 Mirko 的解决方案中一样,找到两个节点的高度并比较相同高度的祖先。请注意,这适用于任何具有父链接的树,无论是二元还是无。我们可以通过迭代高度函数并将“到达相同深度”循环与主循环分开来改进 Mirko 的解决方案:
int height(Node n){
int h=-1;
while(n!=null){h++;n=n.parent;}
return h;
}
Node LCA(Node n1, Node n2){
int discrepancy=height(n1)-height(n2);
while(discrepancy>0) {n1=n1.parent;discrepancy--;}
while(discrepancy<0) {n2=n2.parent;discrepancy++;}
while(n1!=n2){n1=n1.parent();n2=n2.parent();}
return n1;
}
“常量空间”周围的引号是因为通常我们需要 O(log(h)) 空间来存储高度和它们之间的差异(例如 3 个 BigIntegers)。但是,如果您要处理的树的高度太大而无法长时间填充,那么您可能需要担心其他比存储几个节点的高度更紧迫的问题。
如果您有一个 BST,那么您可以轻松地获取一个共同祖先(通常以 root 开头)并检查其子项以查看它们中的任何一个是否是共同祖先:
Node LCA(Node n1, Node n2, Node CA){
while(true){
if(n1.val<CA.val & n2.val<CA.val) CA=CA.left;
else if (n1.val>CA.val & n2.val>CA.val) CA=CA.right;
else return CA;
}
}
正如 Philip JF 所提到的,同样的想法可以在任何树中用于常数空间算法,但是对于一般树,这样做会非常慢,因为反复弄清楚 CA.left 还是 CA.right 是一个共同的祖先会重复很多工作,所以你通常更喜欢使用更多的空间来节省一些时间。进行这种权衡的主要方法基本上是您提到的算法(从根存储路径)。
【讨论】:
here 描述的自底向上方法是 O(n) 时间,O(1) 空间方法:
http://www.leetcode.com/2011/07/lowest-common-ancestor-of-a-binary-tree-part-i.html
Node *LCA(Node *root, Node *p, Node *q) {
if (!root) return NULL;
if (root == p || root == q) return root;
Node *L = LCA(root->left, p, q);
Node *R = LCA(root->right, p, q);
if (L && R) return root; // if p and q are on both sides
return L ? L : R; // either one of p,q is on one side OR p,q is not in L&R subtrees
}
【讨论】: