标签:
作为一个工程党,在各路竞赛大神的面前总会感到自己实力的捉急。大神总是能够根据问题的不同,轻而易举地给出问题的解法,然而我这种渣渣只能用所谓的”直观方法“聊以自慰,说多了都是泪啊。However,正视自己理论方面的不足,迎头赶上还是必要的,毕竟要真正踏入业界,理论知识是不能少的啊。(比如各种语言的Hash Map,它们的核心可都是红黑树啊)
既然助教要求博文要直观,通俗易懂,那就让我们递归这种方法开始。方法一:递归法
按照题目的要求,如果某两个节点具有同一个公共祖先的话,那么会存在两种情况:要么其中一个就是公共祖先,而另一个在它的子树里;要么两个节点分别在公共节点的左右子树中。(什么,两个节点在公共节点的同侧子树中?那样的话某侧的直接子节点不就也成公共节点了么?)这样,我们就可以如下设计自己的程序:
class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { //Tail end of the tree, nothing found if (!root) return NULL; //p or q found, return non-NULL value as signal if ((root==p)||(root==q)) return root; //Find p or q on left and right branch TreeNode* r_left = this->lowestCommonAncestor(root->left, p, q); TreeNode* r_right = this->lowestCommonAncestor(root->right, p, q); //p and q found respectively on two branches, return root as result if (r_left && r_right) return root; //Only one branch contains target node, return non-NULL value as signal else if (r_left) return r_left; else return r_right; } };
该程序采用递归方式执行。首先,针对传入的节点而言,如果它是空节点,表示已经达到了树的末端,但没有找到p或者q,于是返回null表示没有找到。如果root就是p或者q,则表示我们找到p或q了,返回p或q表示在当前递归路径上找到了p或者q。对于递归过程中间经过的路径而言,如果左右分支都有返回节点,那么根据上面的分析,皆大欢喜,root就是我们要找的结果。如果左右中只有一个分支返回了非null的signal,那么就返回找到的节点,表示我这个分支上还是有找到节点的。程序中当然也隐含了两边分支都没找到节点,同时返回null的情况,这时返回上一层的必然是null(即表示没找到)。
显然,在最倒霉的情况下,该方法有可能需要访问到所有节点,如果以n代表节点个数的话,最大复杂度可达O(n)。
方法二:遍历法
再想想看,遍历整个树也不失为一种不错的做法。在遍历树节点的过程中,我们可以维护一个包含有逐级节点的栈,分别表示从当前节点一直往上到根的路径,在找到p与q时比较两个栈,那么最小公共祖先就很容易找到了。(理论上不需要刻意维护一个栈的,因为函数调用(递归)本身就有调用栈,但是这个无关紧要的问题偷偷懒我想并无大碍吧)
1 class Solution { 2 public: 3 bool Traverse(TreeNode* root, TreeNode* target, vector<TreeNode*>& stack) 4 { stack.push_back(root); 5 6 if (root==target) 7 return true; 8 else 9 { bool result; 10 11 if ((root->left)&&(Traverse(root->left,target,stack))) 12 return true; 13 else if ((root->right)&&(Traverse(root->right,target,stack))) 14 return true; 15 stack.pop_back(); 16 return false; 17 } 18 } 19 20 TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) 21 { vector<TreeNode*> stack_p, stack_q; 22 unsigned int min_stack_size; 23 unsigned int i = 0; 24 TreeNode* result = NULL; 25 26 Traverse(root,p,stack_p); 27 Traverse(root,q,stack_q); 28 29 min_stack_size = min(stack_p.size(),stack_q.size()); 30 while ((i<min_stack_size)&&(stack_p[i]==stack_q[i])) 31 { result = stack_p[i]; 32 i++; 33 } 34 35 return result; 36 } 37 };
这个方法的复杂度也是O(n),在Leetcode上执行速度貌似和上一个方法差不多。
方法三:建立反向索引
现在让我们思考一种情况,如果我们要多次对同一棵树查询最小公共祖先呢?显然在这种情况下,每次调用前两种方法中任一种进行,并不是太经济。这时候我们可以对已有的树进行扩充,为每一个节点建立指向其父节点的反向索引,这样对任两个节点查询最小公共祖先就会变得有效率的多。
限于Leetcode限定了树的节点的数据结构,并且C++运行时不能够扩充数据类型的成员(所以动态语言大法好),这里就不贴代码了。简要思路就是首先遍历整棵树,除去根节点之外,为其它所有节点建立指向父节点的指针。然后从两个给定节点向上查询,分别构成两个前驱序列(说的很玄其实跟上一问得到两个栈是完全一样的),再找最小公共祖先。
建立这样一个反向索引的复杂度为O(n),所以对只运行一次的情况这不是经济的做法,然而多次的情况下,该方法的复杂度往往会比前两种低。若令树的层数为m,则每次查询复杂度为O(m),只要树别丧心病狂到长得像链表(换言之,比较”平衡“,m不超过几倍log(n)),方法三的优势还是能够体现的。
小建(yi)议(yin)
Coding Jump实在木有存在的必要,为何不用Github Classroom来布置作业呢?什么,你说没有办法自动判作业?Travis CI这种自动构建工具可以办到啊,做个Web Hook,每当有人提交作业就触发Travis CI编译跑样例,然后输出测试结果登分嘛。
标签:
原文地址:http://www.cnblogs.com/lqf-96/p/lowest-common-ancestor-of-a-binary-tree.html