方法一:路径对比法 对于一棵二叉树,如果知道了从根结点到这两个结点的路径,就可以很容易地找出它们最近的公共父结点。因此,可以首先分别找出从根结点到这两个结点的路径(例如上图中从根结点到结点1的路径为6->3->2->1,从根结点到结点5的路径为6->3->5);然后遍历这两条路径,只要是相等的结点都是它们的父结点,找到最后一个相等的结点即为离它们最近的共同父结点,在这个例子中,3就是它们共同的父结点。
实现代码如下:
public class Test
{
//
//方法功能: 获取二叉树从根结点root到node结点的路径
//输入参数: root:根结点; node:二叉树中的某个结点; s: 用来存储路径的栈
//返回值: node在root的子树上, 或node=root时返回true, 否则返回false
//
private static bool getPathFromRoot(BiTNode root, BiTNode node, Stack<BiTNode>s)
{
if(root==null)
return false;
if(root==node)
{
s.Push(root);
return true;
}
//
//如果node结点在root结点的左子树或右子树上,
//那么root就是node的祖先结点,把它加到栈里
//
if(getPathFromRoot(root.Lchild, node,s)||
getPathFromRoot(root.Rchild, node, s)
{
s.Push(root);
return true;
}
return false;
}
//
//方法功能: 查找二叉树中两个节点最近的共同父结点
//输入参数: root:根结点; node1与node2为二叉树中两个结点
//返回值: node1与node2最近的共同父结点
//
public static BiTNode FindParentNode(BiTNode root, BiTNode node1, BiTNode node2)
{
Stack<BiTNode>stack1=new Stack<BiTNode>();//保存从root到node1的路径
Stack<BiTNode>stack2=new Stack<BiTNode>();//保存从root到node2的路径
//获取从root到node1的路径
getPathFromRoot(root, node1, stack1);
//获取从root到node2的路径
getPathFromRoot(root, node2, stack2);
BiTNode commomParent=null;
//获取最靠近node1和node2的父结点
while(stack1.Peek()==stack2.Peek())
{
commomParent=stack1.Peek();
stack1.Pop();
stack2.Pop();
}
return commomParent;
}
public static void Main(String[] args)
{
int[] arr={1,2,3,4,5,6,7,8,9,10};
BiTNode root;
root=Arraytotree(arr,0,arr.Length-1);
BiTNode node1=root.Lchild.Lchild.Lchild;
BiTNode node2=root.Lchild.Rchild;
BiTNode res=null;
res=FindParentNode(root, node1, node2);
if(res!=null)
Console.Write(node1.Data+"与"+node2.Data+"的最近公共父结点为: "+res.Data);
else
Console.WriteLine("没有公共父结点");
}
}
程序的运行结果为
1与5的最近公共父结点为:3
算法性能分析: 当获取二叉树从根结点root到node结点的路径时,最坏的情况就是把树中所有结点都遍历一遍,这个操作的时间复杂度为O(n),再分别找出从根结点到两个结点的路径后,找它们最近的公共父结点的时间复杂度也为O(n),因此,这个算法的时间复杂度为O(n)。此外,这个算法用栈保存了从根结点到特定结点的路径,在最坏的情况下,这个路径包含了树中所有的结点,因此,空间复杂度也为O(n)。
很显然,这个方法还不够理想。下面介绍另外一种能降低空间复杂度的方法。
方法二:结点编号法 把二叉树看成是一棵完全二叉树(不管实际的二叉树是否为完全二叉树,二叉树中的结点都可以按照完全二叉树中对结点编号的方式进行编号),下图为对二叉树中的结点按照完全二叉树中结点的编号方式进行编号后的结果,结点右边的数字为其对应的编号。
一个编号为n的结点,它的父亲结点的编号为n/2。假如要求node1与node2的最近的共同父结点,首先把这棵树看成是一棵完全二叉树(不管结点是否存在),分别求得这两个结点的编号n1,n2。然后每次找出n1与n2中较大的值除以2,直到n1==n2为止,此时n1或n2的值对应结点的编号就是它们最近的共同父结点的编号,接着可以根据这个编号信息找到对应的结点,具体方法为:通过观察二叉树中结点的编号可以发现:首先把根结点root看成1,求root的左孩子编号的方法为把root对应的编号看成二进制,然后向左移一位,末尾补0,如果是root的右孩子,则末尾补1,因此,通过结点位置的二进制码就可以确定这个结点。例如结点3的编号为2(二进制10),它的左孩子的求解方法为:10,向左移一位末尾补0,可以得到二进制100(十进制4),位置为4的结点的值为2。从这个特性可以得出通过结点位置信息获取结点的方法,例如要求位置4的结点,4的二进制码为100,由于1代表根结点,接下一个0代表是左子树root.lchild,最后一个0也表示左子树root.lchild.lchild,通过这种方法非常容易根据结点的编号找到对应的结点。实现代码如下:
class IntRef{
public int Num {get;set;}
}
public class Test1
{
//
//方法功能: 找出结点在二叉树中的编号
//输入参数: root:根结点; node:待查找结点; number: node结点在二叉树中的编号
//返回值: true:找到该结点的位置, 否则返回false
//
private static bool GetNo(BiTNode root, BiTNode node, IntRef number)
{
if (root==null)
return false;
if(root==node)
return true;
int tmp=number.Num;
number.Num=2*tmp;
//node结点在root的左子树中, 左子树编号为当前结点编号的2倍
if(GetNo(root.Lchild, node, number))
{
return true;
//node结点在root的右子树中, 右子树编号为当前结点编号的2倍加1
}
else
{
number.Num=tmp*2+1;
return GetNo(root.Rchild, node,number);
}
}
//
//方法功能: 根据结点的编号找出对应的结点
//输入参数: root根结点; number为结点的编号
//返回值: 编号为number对应的结点
//
private static BiTNode getNodeFromNum(BiFNode root,int number)
{
if (root==null||number<0)
return null;
if(number==1)
return root;
//结点编号对应二进制的位数(最高位一定为1, 由于根节点代表1)
int len=(int)(Math.Log(number)/Math.Log(2));
//去掉根结点表示的1
number-=1<<len;
for(;len>0; len--)
{
//如果这一位二进制的值为1,
//那么编号为number的结点必定在当前结点的右子树上
if(((1<<(len-1))& number)==1)
root=root.Rchild;
else
root=root.Lchild;
}
return root;
}
//
//方法功能: 查找二叉树中两个节点最近的共同父结点
//输入参数: root:根结点; node1与node2为二叉树中两个结点
//返回值: node1与node2最近的共同父结点
//
public static BiTNode FindParentNode(BiTNode root, BiTNode node1, BiTNode node2)
{
IntRef ref1=new IntRef();
ref1.Num=1;
IntRef ref2=new IntRef();
ref2.Num=1;
GetNo(root, node1, ref1);
GetNo(root, node2, ref2);
int num1=ref1.Num;
int num2=ref2.Num;
//找出编号为num1和num2的共同父结点
while(num1!=num2)
{
if(num1>num2)
num1/=2;
else
num2/=2;
}
//num1就是它们最近的公共父结点的编号, 通过结点编号找到对应的结点
return getNodeFromNum(root, num1);
}
}
算法性能分析: 这个方法的时间复杂度也为O(n),与方法一相比,在求解的过程中只用了个别的几个变量,因此,空间复杂度为O(l)。
方法三:后序遍历法 很多与二叉树相关的问题都可以通过对二叉树的遍历方法进行改装而求解。对于本题而言,可以通过对二叉树的后序遍历进行改编而得到。具体思路为:查找结点node1与结点node2的最近共同父结点可以转换为找到一个结点node,使得node1与node2分别位于结点node的左子树或右子树中。结点1与结点5的最近共同父结点为结点3,因为结点1位于结点3的左予树上,而结点5位于结点3的右子树上。实现代码如下:
public static BiTNode FindParentNode(BiTNode root, BiTNode node1, BiTNode node2)
{
if (null==root||root==node1||root==node2)
{
return root;
}
BiTNode lchild=FindParentNode(root.Lchild, node1, node2);
BiTNode rchild=FindParentNode(root.Rchild, node1, node2);
//root的左子树中没有结点node1和node2,那么一定在root的右子树上
if(null==lchild)
{
return rchild;
}
//root的右子树中没有结点node1和node2, 那么一定在root的左子树上
else if(null==rchild)
{
return lchild;
}
//node1与node2分别位于root的左子树与右子树上, root就是它们最近的共同父结点
else
{
return root;
}
}
把方法一中的FindParentNode替换为本方法的FindParentNode方法可以得到同样的输出结果。
算法性能分析: 这个方法与二叉树的后序遍历方法有着相同的时间复杂O(N)。
引申:如何计算二叉树中两个结点的距离 题目描述: 在没有给出父结点的条件下,计算二叉树中两个结点的距离。两个结点之间的距离是从一个结点到达另一个结点所需的最小的边数。例如:给出下面的二叉树:
Dist(4,5)=2, Dist(4,6)=4。
分析与解答: 对于给定的二叉树root,只要能找到两个结点n1与n2最低的公共父结点parent,那么就可以通过下面的公式计算出这两个结点的距离:
Dist(n1, n2)=Dist(root, n1)+Dist(root, n2)-2*Dist(root, parent)