看到这个问题的时候,第一反应是
1.存下两个节点的路径,然后找最长公共前缀的最后一个节点就可以了,但是这样要额外的空间;
2.如果有父指针的话,就有点像寻找相交链表的第一个相交节点问题了;
3.用后序遍历,在一个节点先分别访问左子树和右子树,然后再访问自己,此时判断是否两个节点都已经访问过了,如果是,则此节点是两个节点的公共祖先,而由于后序遍历的递归过程,第一个满足此条件的节点自然就是最近的公共祖先节点,此时记录下该节点,然后退出就可以了。
代码如下:
注意很恶心的一点是每个节点必须新创建两个标志来表示是否在其子树中找到两个目标节点,不能公用其父节点的标志
否则的话会出现这种错误:比如一棵简单3个节点的树,根是R,左孩子是L,右孩子是R,求L,R的最近公共祖先---首先对R调用DFS,首先遍历左字数找到L,置标志L找到,然后递归遍历右子树,遍历到R的时候发现L也找到R也找到,于是以为R是公共祖先,输出!而实际祖先是R!
所以遍历到每个节点的时候要新建两个标识来表示两个目标节点是否在其子树中找到,同时要把这个信息传回给其父亲节点,代码如下:
int n1,n2; void dfs(tree root,int& lca,bool& n1F,bool& n2F) { if (lca!=-1 || !root) //if lca already found return; //lf1,lf2表示左子树中是否找到n1或n2 bool lf1=false,lf2=false; if ( root->lch ) dfs(root->lch,lca,lf1,lf2); bool rf1=false,rf2=false; if ( root->rch ) dfs(root->rch,lca,rf1,rf2); n1F=lf1||rf1; //n1是否在子树中找到 n2F=lf2||rf2; if ( root->val == n1 ) n1F=true; if ( root->val == n2 ) n2F=true; if ( n1F && n2F && lca==-1 ) //如果是第一个找到的 lca=root->val; }
另外在网上搜了一个tarjan算法,效率更高一些:
弄了好一会才弄明白,算法思想是这么的:当遍历到某一节点x的时候,首先遍历它的子树,可知它子树中的所有节点与x的最近公共祖先当然是x自己,然后x的兄弟节点以及兄弟节点的所有子树与x的公共祖先当然是x的父亲,因为x的遍历是从其父亲那里递归下来的,所以x的兄弟节点可能已经遍历过了,而除开这些节点与x的关系现在是不知道的,还要等继续回溯到某一个节点的时候才知道。
网上很多代码是针对图的,这里自己写个二叉树的放着:
void tarjan(tree root) { if ( !root || visited[root->num] ) return; visited[root->num]=1; if ( root->lch ) { tarjan(root->lch); unionFather(root->lch->num,root->num); } if ( root->rch ) { tarjan(root->rch); unionFather(root->rch->num,root->num); } for (int i=0;i< nodeNum ;i++) { int u=root->num; if ( (ask[u][i]||ask[i][u]) && visited[i] ) { lca[i][u]=lca[u][i]=findfa(i); } } }