一道算法面试题(Lowerest Common Ancestor) 的三种解法

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 }
View Code

在创建好数据结构后,我们可以开始解决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 }
View Code

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 }
View Code

空间复杂度:除每个节点增加一个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 }
View Code

空间复杂度:此方案需要两个链表用以存储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

猜你喜欢

转载自www.cnblogs.com/wangwenzhi2019/p/10681037.html
今日推荐