数据结构问题集锦

作为一个工程党,在各路竞赛大神的面前总会感到自己实力的捉急。大神总是能够根据问题的不同,轻而易举地给出问题的解法,然而我这种渣渣只能用所谓的”直观方法“聊以自慰,说多了都是泪啊。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编译跑样例,然后输出测试结果登分嘛。

原文地址:https://www.cnblogs.com/lqf-96/p/lowest-common-ancestor-of-a-binary-tree.html