LCA (Lowerest Common Ancestor)问题是指任意指定二叉树中的两个节点,求他们的最近共同祖先节点。例如在下图中,节点 7和4的LCA为2,6和4的LCA为5,2和8的LCA为3,等等。
我们用下列C语言数据结构表示树节点:
typedef struct Node Node; struct Node { int val; struct Node *left; struct Node *right; };
我们可以从数组创建二叉树,数组包含根据BFS(Breadth First Search)遍历树结构所获得的数值序列,如果某个节点的子节点为空但该子节点不是最后一个节点,则在数组中用NullVal表示。在下列代码中,函数CreateTreeFromArray() 用于从上述数组创建二叉树,ReleaseTree()用于回收二叉树空间:
1 #define NullVal (0xBADFACE) 2 3 4 static Node* 5 NewNode(int val) 6 { 7 Node* p = malloc(sizeof(Node)); 8 if (p != NULL) { 9 p->val = val; 10 p->left = p->right = NULL; 11 } 12 13 return p; 14 } 15 16 static Node* 17 CreateTreeFromArray(int* nums, int count, int pos) 18 { 19 if (nums == NULL || pos >= count || nums[pos] == NullVal) { 20 return NULL; 21 } 22 23 Node *p = NewNode(nums[pos]); 24 int leftPos = 2 * pos + 1; 25 int rightPos = 2 * pos + 2; 26 27 if (leftPos < count) { 28 p->left = CreateTreeFromArray(nums, count, leftPos); 29 } 30 31 if (rightPos < count) { 32 p->right = CreateTreeFromArray(nums, count, rightPos); 33 } 34 35 return p; 36 } 37 38 39 static void 40 ReleaseTree(Node *root) 41 { 42 if (root == NULL) { 43 return; 44 } 45 46 ReleaseTree(root->left); 47 ReleaseTree(root->right); 48 49 root->left = root->right = NULL; 50 free(root); 51 }
在创建好数据结构后,我们可以开始解决LCA问题,假设: 1)二叉树根节点为root; 2)指定的两个节点为n1和n2; 3)二叉树中没有重复数据。
以下给出三种解决方式并粗略提供时(空)间复杂度分析。
方案一. 在树结构中添加Parent字段,则二叉树中除根结点外每一个节点都可以找到其父节点。我们的方法是从n1开始向上回溯父节点直到根节点,在此过程中把访问到的每个节点的parent字段置为NULL. 然后从n2开始向上回溯父节点直到无法回溯,则最后访问的节点即为所求的LCA节点。
该方案中,parent字段既用于回溯到父节点,也用于标记当前节点是否被访问过,如被访问过,则parent字段为NULL. 二叉树结点变为:
typedef struct Node Node; struct Node { int val; struct Node *parent; struct Node *left; struct Node *right; };
二叉树构建函数CreateTreeFromArray()稍作修改如下,NewNode() 和 ReleaseTree()函数也应做细微修改,不再赘述。
1 Node* 2 CreateTreeFromArray(int* nums, int count, int pos, Node *parent) 3 { 4 if (nums == NULL || pos >= count || nums[pos] == NullVal) { 5 return NULL; 6 } 7 8 Node *p = NewNode(nums[pos]); 9 if (p == NULL) { 10 return NULL: 11 } 12 13 p->parent = parent; 14 15 int leftPos = 2 * pos + 1; 16 int rightPos = 2 * pos + 2; 17 if (leftPos < count) { 18 p->left = CreateTreeFromArray(nums, count, leftPos, p); 19 } 20 21 if (rightPos < count) { 22 p->right = CreateTreeFromArray(nums, count, rightPos, p); 23 } 24 25 return p; 26 }
LCA求解代码:
1 static Node* 2 GetNode(Node* root, int val) 3 { 4 if (root == NULL) { 5 return NULL; 6 } 7 8 if (root->val == val) { 9 return root; 10 } 11 12 Node *p = GetNode(root->left, val); 13 return (p == NULL ? GetNode(root->right, val) : p); 14 } 15 16 17 static int 18 Lca(Node *root, int n1, int n2) 19 { 20 Node *p1 = GetNode(root, n1); 21 Node *p2 = GetNode(root, n2); 22 23 if ((p1 == NULL && p2 == NULL) || (p1 == NULL || p2 == NULL)) { 24 /* data not existing in tree */ 25 return NullVal; 26 } else if (p1 == p2) { 27 /* n1 == n2 */ 28 return n1; 29 } 30 31 Node* parent = NULL; 32 while (p1 != NULL) { 33 parent = p1->parent; 34 p1->parent = NULL; 35 p1 = parent; 36 } 37 38 while (p2 != NULL) { 39 parent = p2->parent; 40 if (parent == NULL) { 41 return p2->val; 42 } 43 p2 = parent; 44 } 45 46 return NullVal; 47 }
空间复杂度:除每个节点增加一个parent字段没有其他额外开销,所以空间复杂度为O(n)
时间复杂度:LCA求解中最主要的操作为从两个节点回溯直至根节点,平均复杂度为O(logn),最坏复杂度O(n) (二叉树深度为n, 且LCA节点为根节点)
此方案最大的缺点是较大的空间开销以及改变了原有的数据结构,尤其是后者,在很多情况下是不允许的。下面我们来看一下,不改变现有数据结构的情况下如何求解LCA问题。
方案二. 从根节点到二叉树中任一节点所经过的所有节点构成了一条路径,相应的我们可以求出从根节点分别到n1和n2的两条路经,依次比较这两条路径上的节点,找到第一对不相等节点,那他们前面的节点即为所求的LCA节点:
我们定义了一个Path结构类型用以存储从root到n1/n2的路径:
typedef struct Path Path; struct Path { Node *node; struct Path *next; };
函数GetPath()用于获取root到n1/n2的路径, 函数Lca()用于求解LCA节点:
1 static Path* 2 NewPath(Node *root) { 3 Path *p = malloc(sizeof(Path)); 4 if (p) { 5 p->node = root; 6 p->next = NULL; 7 } 8 return p; 9 } 10 11 12 static bool 13 GetPath(Node* root, Path *path, int val) 14 { 15 if (root == NULL) { 16 return false; 17 } 18 19 /* Add current node into path */ 20 path->next = NewPath(root); 21 if (path->next == NULL) { 22 return false; 23 } 24 25 /* Find the last node of the path, no further check is needed */ 26 if (root->val == val) { 27 return true; 28 } 29 30 /* Check sub trees */ 31 if (GetPath(root->left, path->next, val) || 32 GetPath(root->right, path->next, val)) { 33 return true; 34 } 35 36 /* 37 * Target data is not in the tree rooted at current node, remove it from 38 * path 39 */ 40 free(path->next); 41 path->next = NULL; 42 return false; 43 } 44 45 46 static void 47 ReleasePath(Path *path) 48 { 49 while (path) { 50 Path *next = path->next; 51 path->node = NULL; 52 path->next = NULL; 53 free(path); 54 path = next; 55 } 56 } 57 58 59 static int 60 Lca(Node *root, int n1, int n2) 61 { 62 Path path1, *p1; 63 Path path2, *p2; 64 65 if (n1 == n2) { 66 return n1; 67 } 68 69 /* Get path from root to n1/n2 */ 70 GetPath(root, &path1, n1); 71 GetPath(root, &path2, n2); 72 73 /* Find the first two different nodes on two paths */ 74 p1 = &path1; 75 p2 = &path2; 76 while (p1->next && p2->next && 77 p1->next->node->val == p2->next->node->val) { 78 p1 = p1->next; 79 p2 = p2->next; 80 } 81 82 /* The node before the first two different nodes is the LCA node */ 83 int ans = p1->node->val; 84 ReleasePath(path1.next); 85 ReleasePath(path2.next); 86 return ans; 87 }
空间复杂度:此方案需要两个链表用以存储root到n1/n2的路径,故空间复杂度为O(logn)
时间复杂度: 获取root到n1/n2的路径时,GetPath()平均遍历logN个节点,之后两条路径上的节点比较操作次数不超过路径长度,故时间复杂度为O(logn)
方案三. 如果要查找的两个节点一个位于当前节点的左子树,一个位于当前节点的右子树,则当前节点即为LCA节点。具体代码如下:
1 static Node * 2 Lca(Node *root, int n1, int n2) 3 { 4 if (root == NULL) { 5 return NULL; 6 } 7 8 if (root->val == n1 || root->val == n2) { 9 return root; 10 } 11 12 Node *p1 = Lca(root->left, n1, n2); 13 Node *p2 = Lca(root->right, n1, n2); 14 15 if (p1 && p2) { 16 return root; 17 } 18 19 return (p1==NULL ? p2 : p1); 20 }
空间复杂度:此方案没有额外开销,故空间复杂度为O(1)
时间复杂度: 最坏情况下,Lca()要递归遍历所有节点,故时间复杂度为O(n)
输出结果:
stephenw@stephenw-devbox1:~/cTest$ ./lca LCA(7, 4) = 2 LCA(6, 4) = 5 LCA(2, 8) = 3