数据结构问题集锦 - 最小公共祖先问题

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

时间: 2024-12-17 14:03:11

数据结构问题集锦 - 最小公共祖先问题的相关文章

最小公共祖先lca

3.神秘国度的爱情故事 题目要求:某个太空神秘国度中有很多美丽的小村,从太空中可以想见,小村间有路相连,更精确一点说,任意两村之间有且仅有一条路径.小村 A 中有位年轻人爱上了自己村里的美丽姑娘.每天早晨,姑娘都会去小村 B 里的面包房工作,傍晚 6 点回到家.年轻人终于决定要向姑娘表白,他打算在小村 C 等着姑娘路过的时候把爱慕说出来.问题是,他不能确定小村 C 是否在小村 B 到小村 A 之间的路径上.你可以帮他解决这个问题吗? 输入要求:输入由若干组测试数据组成.每组数据的第 1 行包含一

最小公共祖先

对二叉查找树找到两个节点的最小公共祖先:可以根据二叉查找树的性质:左子树的节点值比根节点的值小,右子树的节点值比根节点值大 public class BinarySearchTree<T extends Comparable> { BinaryTreeNode<T> root; public BinarySearchTree(BinaryTreeNode<T> root) { this.root = root; } public BinaryTreeNode<T&

二叉树的最小公共祖先问题

今天做了两个二叉树的题目, 挺简单的, 不用调试, 直接在提交框上敲完提交, 直接就通过了. 第一个题目是求二叉查找树的公共祖先, 因为是排序的, 所以很好做. 具体思路如下: 1 如果两个节点都比当前节点小, 那公共祖先必然是当前节点的左子树上, 所以递归左子树; 2 如果两个节点都比当前节点大, 那么同上, 递归右子树; 3 如果两个节点有一个为当前节点, 则当前节点为公共节点; 如果两个节点分别比当前节点小和大, 那么当前节点必为公共节点. 具体代码如下: class Solution {

LeetCode -- 查找最小公共祖先

在一棵二叉树中, 查找两个节点的最近的公共祖先.由于本题没有涉及到批量查询,因此考虑一般解法即可,如果涉及批量,可考虑Tarjan算法. 思路:1. 先序遍历2. 判断查找的两节点和当前节点的关系3. 根据是否为空的情况返回不同节点 要注意的地方是判断节点是否相等,本题使用了C++语言,直接判断指针本身了 /** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * Tree

最近公共祖先(LCA)问题

描述 对于有根树T的两个节点u和v,最近公共祖先LCA(T,u,v)表示一个节点x满足x是u,v的公共祖先且x的深度尽可能大. 算法 求解LCA问题主要有三种解法,分别是暴力搜索,Tanjar算法,最后一种是转化为RMQ问题,用DFS+ST算法来求解 暴力搜索 如果数据量不大的时候可以采用暴力搜索法.先将节点u的祖先节点全部标记出来,然后顺着节点v沿着父亲节点的方向向上遍历,直到遍历到一个被标记的节点,这个节点即为所求节点.或者分别获取u,v到根节点的路径P1,P2,可以将这两条路径看做两个两个

LCA(最近公共祖先)——离线 Tarjan 算法

一.梳理概念 定义:对于有根树T的两个结点u.v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u.v的祖先且x的深度尽可能大. 通俗地讲,最近公共祖先节点,就是两个节点在这棵树上深度最大的公共的祖先节点,即两个点在这棵树上距离最近的公共祖先节点. 提示:父亲节点也是祖先节点,节点本身也是它的祖先节点. 给出一棵树,如图所示: 由上面的定义可知:3和5的最近公共祖先为1,5和6的最近公共祖先为2,2和7的最近公共祖先为2, 6和7的最近公共祖先为4. 二.繁文缛节 注意注意注意!!!尚

亚马逊 在线测试题目 amazon (变种的)三叉树的最近公共祖先问题

题目意思就是找一棵按上面链接所示的树对应的上面的两个点的最小公共祖先(LCP,Least Common Father),按照比较大小来依次返回自己的父亲节点就行了.具体看代码:getfather(a)函数是找父亲的代码 #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int maxDepth = 2

二叉树中两个结点最近的公共祖先汇总

一.若二叉树为搜索二叉树 原题链接:https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/#/description Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BST. According to the definition of LCA on

数据结构作业——sights(最短路/最近公共祖先)

sights Description 美丽的小风姑娘打算去旅游散心,她走进了一座山,发现这座山有 n 个景点,由于山路难修,所以施工队只修了最少条的路,来保证 n 个景点联通,娇弱的小风姑娘不想走那么长的山路, 所以打算乘坐专用的交通工具. 有的景点之间有路,乘坐交通工具需要花费一定的金额.由于到达景区之前已经花了一部分钱了,现在可爱的小风姑娘站在景点 1,即根景点.按原计划她要去编号为 m 的景点,导游告诉她到景点 m 总共要花的钱(包括来之前花的钱) .然而善变的小风姑娘突然想去景点 y(直