【以下内容为转载,部分注释为本人补注。原文地址:二叉树中两个节点的最近公共祖先 - eriol - ITeye博客】
求二叉树中任意两个节点的最近公共祖先也称为LCA问题(Lowest Common Ancestor)。
二叉查找树
如果该二叉树是二叉查找树,那么求解LCA十分简单。
基本思想为:从树根开始,该节点的值为t,如果t大于t1和t2,说明t1和t2都位于t的左侧,所以它们的共同祖先必定在t的左子树中,从t.left开始搜索;如果t小于t1和t2,说明t1和t2都位于t的右侧,那么从t.right开始搜索;如果t1<t< t2,说明t1和t2位于t的两侧,那么该节点t为公共祖先。
如果t1是t2的祖先,那么应该返回t1的父节点;同理,如果t2是t1的祖先,应该返回t2的父节点。
这里其实没有太大必要,直接返回t1 或者 t2即可。。。
- public int query(Node t1, Node t2, Node t) {
- int left = t1.value;
- int right = t2.value;
- Node parent = null;
- if (left > right) {
- int temp = left;
- left = right;
- right = temp;
- }
- while (true) {
- if (t.value < left) {
- parent = t;
- t = t.right;
- } else if (t.value > right) {
- parent = t;
- t = t.left;
- } else if (t.value == left || t.value == right) {
- return parent.value;
- } else {
- return t.value;
- }
- }
- }
其中,parent用于处理t1是t2的祖先(或t2是t1的祖先)的情况。
一般的二叉树
如果二叉树不是二叉查找树该怎么办呢?
1. 离线算法(Tarjan)
利用并查集优越的时空复杂度,可以实现O(n+q)的算法,q是查询次数。
Tarjan算法基于深度优先搜索。对于新搜索到的一个结点u,先创建由u构成的集合,再对u的每颗子树进行搜索,每搜索完一棵子树,这时候子树中所有的结点的最近公共祖先就是u了。
#include <iostream>
#include <stdio.h>
#define N 100
class Solution {
public:
int id[N], lcs[N][N], g[N][N];
void init() {
memset(id, -1, sizeof(id));
memset(g, 0, sizeof(g));
memset(lcs,0,sizeof(lcs));
g[1][2] = 1;
g[1][3] = 1;
g[2][4] = 1;
g[2][5] = 1;
g[3][6] = 1;
g[3][7] = 1;
g[7][8] = 1;
}
int find(int i) {
return id[i] == i ? id[i] : (id[i] = find(id[i]));
}
void merge(int i, int j) {
id[find(i)] = find(id[j]);
}
void dfs(int rt, int n) {
int i;
id[rt] = rt;
for (i = 1; i <= n; ++i)
if (g[rt][i] && id[i] == -1) {
dfs(i,n);
id[find(i)] = find(rt);
//id[find(rt)] = find(id[i]); //Not right!!!!
}
for (i = 1; i <= n; ++i)
if (id[i] != -1)
lcs[rt][i] = lcs[i][rt] = find(i);
}
};
int main() {
Solution s;
s.init();
s.dfs(1, 8);
return 0;
}
2. 在线算法(RMQ)
一个O(nlog2n)的预处理,O(1)的查询。
以下面一棵树为例:
(1)
/ \
(2) (7)
/ \ \
(3) (4) (8)
/ \
(5) (6)
step1:
按深度遍历树,记录访问顺序和相应的深度(2*n-1),及每个节点第一次出现的位置。
结点的访问顺序为:1 2 3 2 4 5 4 6 4 2 1 7 8 7
相应的深度为: 0 1 2 1 2 3 2 3 2 1 0 1 2 1 0
结点1-8第一次出现的次序为:1 2 3 5 6 8 12 13
step2:
查询3和6的公共祖先,考虑3和6第一次出现的位置为3和8,即寻找序列2 1 2 3 2 3中的最小值,最小值为1,对应的点位2,则3与6的最近公共祖先为2。
step3:
则对于给出的任意两个结点,找出它们第一次出现的位置i,j(i<j),在深度序列中查询最小值的下标k,depth[k]即为所求。显然,查询多次深度序列中的最小值的下标,自然而然就想到了RMQ。
- public class LCA {
- private final int MAX = 10;
- private int[] dfs = new int[2*MAX];
- private int[] depth = new int[2*MAX];
- private int[][] f;
- private int[] call = new int[MAX];
- private int len = 0;
- public void track(Node t, int d) {
- if (t == null)
- return;
- dfs[len] = t.value;
- depth[len] = d;
- call[t.value-1] = len;
- len++;
- track2(t.left, d+1);
- if (t.left != null) {
- dfs[len] = t.value;
- depth[len] = d;
- len++;
- }
- track2(t.right, d+1);
- if (t.right != null) {
- dfs[len] = t.value;
- depth[len] = d;
- len++;
- }
- }
- public void rmq() {
- int count = 1;
- while ((1 << count) <= len)
- count++;
- f = new int[len][count];
- count--;
- for (int i = 0; i < len; i++) {
- f[i][0] = i;
- }
- for (int j = 1; (1 << j) <= len; j++) {
- for (int i = 0; i+(1<<j)-1 < len; i++) {
- f[i][j] = depth[f[i][j-1]] < depth[f[i+(1<<j-1)][j-1]] ?
- f[i][j-1] : f[i+(1<<j-1)][j-1];
- }
- }
- }
- public int query(Node t1, Node t2) {
- int start = call[t1.value-1];
- int end = call[t2.value-1];
- if(start > end) {
- int temp = start;
- start = end;
- end = temp;
- }
- int count = 1;
- while ((1 << count) <= end - start + 1)
- count++;
- count--;
- int result = depth[f[start][count]] < depth[f[end-(1<<count)+1][count]] ?
- f[start][count] : f[end-(1<<count)+1][count];
- if (dfs[result] == t1.value || dfs[result] == t2.value) {
- int temp = depth[result];
- while (depth[result] >= temp)
- result--;
- }
- return dfs[result];
- }
- public static void main(String[] args) {
- Node n3 = new Node(3);
- Node n5 = new Node(5);
- Node n6 = new Node(6);
- Node n4 = new Node(4, n5, n6);
- Node n2 = new Node(2, n3, n4);
- Node n8 = new Node(8);
- Node n7 = new Node(7, null, n8);
- Node n1 = new Node(1, n2, n7);
- LCA l = new LCA();
- l.track(n1, 0);
- l.rmq();
- System.out.println(l.query(n5, n4));
- }
- }
- class Node {
- int value;
- Node left;
- Node right;
- public Node(int value, Node left, Node right) {
- this.value = value;
- this.left = left;
- this.right = right;
- }
- public Node(int value) {
- this.value = value;
- this.left = null;
- this.right = null;
- }
- }
3. 后序遍历
基本思想:如果这两个节点不在一条线上(即这两个节点不存在一个节点是另一个节点的祖先的情况),则它们必定分别在所求节点A的左子树和右子树上,后序遍历到第一个满足这个条件的节点就是所要求的节点A。否则,当这两个节点在一条线上,所求节点A则是这两个节点中深度最低的节点的父节点。
- bool lca(Node *root, int va, int vb, Node *&result, Node* parent)
- {
- // left/right 左/右子树是否含有要判断的两节点之一
- bool left = false, right = false;
- if (!result && root->left) left = lca(root->left,va,vb,result,root);
- if (!result && root->right) right = lca(root->right,va,vb,result,root);
- // mid 当前节点是否是要判断的两节点之一
- bool mid = false;
- if (root->data == va || root->data == vb) mid=true;
- if (!result && int(left + right + mid) == 2) { // 补注,如果left+right+mid等于2,说明,要么当前节点root就是要找的节点之一,并且另一节点在它的左或者右子树中;要么当前节点是的左右子树分别包含了要找的两个节点
- if (mid) result = parent; // 补注,如果当前节点是要找的节点之一,则其父节点就是公共节点
- else result = root; // 补注,否则当前节点就是公共节点
- }
- return left | mid | right ; // 补注,有一个满足就返回true
- }
- Node *query(Node *root,int va, int vb)
- {
- if (root == NULL) return NULL;
- Node *result = NULL;
- lca(root, va, vb,result, NULL);
- return result;
- }
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
}
N叉树:
class Node {
public:
int val;
vector<Node*> child;
};
Node* commonAncestor(Node* root, Node* p1, Node* p2) {
if (root == NULL || p1 == NULL || p2 == NULL) return root;
if (root == p1 || root == p2) return root;
int cnum = root->child.size(), count = 0;
Node* tmp = NULL;
for (int i = 0; i < cnum; ++i) {
tmp = commonAncestor(root->child[i], p1, p2);
if (tmp != NULL)
++count;
if (count == 2)
return root;
}
return tmp;
}
这个题递归坑的点在于if (root == NULL || p1 == NULL || p2 == NULL)递归是查p1或p2在树中的节点,但最终返回的commonAncestor,所以递归过程中只用了一个返回值来实现,如果用2个返回值会被绕晕。但这一个返回值假设p1或者p2不在树中,没法给出正确的返回。
如果p1或者p2不在树中,用个dict来非递归吧!!!