在上个月头条二面中,被问到求树中任意两个结点的最低公共祖先这个问题,寒假刷《剑指offer》又看到这个题目,遂在此简单总结一下。对于这个开放性问题,我们需要和面试官沟通题目的具体要求,比如题目中谈及的树是普通树、二叉树还是二叉搜索树,树结点的定义是否包含父结点信息,是否对时间复杂度和空间复杂度有要求等等。所以下面我将该问题细分为三个小问题:求二叉搜索树中任意两个结点的最低公共祖先、求二叉树(含父结点信息)中任意两个结点的最低公共祖先、求二叉树(不含父结点信息)中任意两个结点的最低公共祖先。
1. 求二叉搜索树中任意两个结点的最低公共祖先
对于这个问题,我们可以充分利用二叉搜索树的性质(若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值)。于是我们产生了这样的思路:若两结点的值均小于当前结点的值,则这两结点的最低公共祖先只可能出现在该结点的左子树上;若两结点的值均大于当前结点的值,则这两结点的最低公共祖先只可能出现在该结点的右子树上;若不满足以上两种情况,则有一个结点的值小于当前结点,另一个根结点的值大于当前结点,则该结点为最低公共祖先。
代码实现:
#include<iostream> using namespace std; // 定义树结点 struct TreeNode { int val; struct TreeNode * left; struct TreeNode * right; TreeNode(int x):val(x), left(NULL), right(NULL){} }; // 创建一棵二叉树 TreeNode * CreateBinaryTree() { int val; cin>>val; if(val==-1) return NULL; TreeNode * root = new TreeNode(val); root->left = CreateBinaryTree(); root->right = CreateBinaryTree(); return root; } // 前序遍历二叉树 void PreOrder(TreeNode * root) { if(root!=NULL) cout<<root->val<<ends; if(root->left!=NULL) PreOrder(root->left); if(root->right!=NULL) PreOrder(root->right); } TreeNode * CommonAncestorBinarySearch(TreeNode * root, TreeNode * node1, TreeNode * node2) // 二叉搜索树的最低公共祖先 { if(root==NULL||node1==NULL||node2==NULL) return NULL; // 树为空或者查找的结点也有一个为空,返回空指针 TreeNode * p = root; while(p) { if(node1->val<p->val && node2->val<p->val) // 两结点都小于当前结点,则最低公共祖先在该结点的左子树上 p = p->left; else if(node1->val>p->val && node2->val>p->val) // 两结点都大于当前结点,则最低公共祖先在该结点的右子树上 p = p->right; else break; } return p; } int main() { TreeNode * root = NULL; root = CreateBinaryTree(); // 创建一个二叉树 // 二叉搜索树的测试数据:5 2 1 -1 -1 4 3 -1 -1 -1 7 6 -1 -1 9 8 -1 -1 10 -1 -1 PreOrder(root); // 前序遍历二叉树 cout<<endl; cout<<root->left->left->val<<ends<<root->left->right->left->val<<endl; TreeNode * result = CommonAncestorBinarySearch(root, root->left->left, root->left->right->left); if(result) cout<<result->val<<endl; else cout<<"Not Found!"<<endl; return 0; }
2. 求二叉树(含父结点信息)中任意两个结点的最低公共祖先
该问题和上个问题一样,要充分利用题目中包含的信息。由于题目所给二叉树是包含指向父结点的信息的,因此我们可以利用该信息找到树中任意一结点的路径(从当前结点出发,终止于根结点)。因此我们的解题思路是:首先找出两个结点的路径(这样就把问题转换成“两个链表的第一个公共结点”问题),然后求这两条路径的第一个公共结点即可(考虑到时间复杂度,这里采用O(n)的算法)。
代码实现:
#include<iostream> using namespace std; struct TreeNode { int val; struct TreeNode * parent; struct TreeNode * left; struct TreeNode * right; TreeNode(int x):val(x), parent(NULL), left(NULL), right(NULL){} }; // 创建一棵二叉树 TreeNode * CreateBinaryTree() { int val; cin>>val; if(val==-1) return NULL; TreeNode * root = new TreeNode(val); root->left = CreateBinaryTree(); if(root->left!=NULL) root->left->parent = root; root->right = CreateBinaryTree(); if(root->right!=NULL) root->right->parent = root; return root; } // 从当前结点开始遍历到根节点 void Travel(TreeNode * root) { if(root==NULL) return ; TreeNode * p = root; while(p!=NULL) { cout<<p->val<<ends; p = p->parent; } } TreeNode * CommonAncestorBinary(TreeNode * root, TreeNode * node1, TreeNode * node2) // 二叉的最低公共祖先(含父结点) { if(root==NULL||node1==NULL||node2==NULL) return NULL; // 树为空或者查找的结点也有一个为空,返回空指针 TreeNode * p1 = node1; TreeNode * p2 = node2; int len1 = 0; int len2 = 0; /*统计路径长度*/ while(p1) { len1++; p1 = p1->parent; } while(p2) { len2++; p2 = p2->parent; } /*长的先走几步*/ p1 = node1; p2 = node2; if(len1>len2) { p1 = p1->parent; len1--; } if(len2>len1) { p2 = p2->parent; len2--; } /*同步走,结点相同即为最低公共祖先*/ while(p1&&p2) { if(p1==p2) break; p1 = p1->parent; p2 = p2->parent; } return p1; } int main() { TreeNode * root = NULL; root = CreateBinaryTree(); // 创建一个二叉树 // 二叉树的测试数据:5 2 1 -1 -1 4 3 -1 -1 -1 7 6 -1 -1 9 8 -1 -1 10 -1 -1 Travel(root->left->right->left); // 从该结点开始遍历至根结点 cout<<endl; cout<<root->left->left->val<<ends<<root->left->right->left->val<<endl; TreeNode * result = CommonAncestorBinary(root, root->left->left, root->left->right->left); if(result) cout<<result->val<<endl; else cout<<"Not Found!"<<endl; return 0; }
3. 求二叉树(不含父结点信息)中任意两个结点的最低公共祖先
对于该问题,明显比前两个问题复杂一些。但是只要我们能够分析出复杂之处并进行正确处理,也就可以想出不错的思路。与问题2而相比,问题3复杂之处就是二叉树结点的定义中不包含父结点信息,我们无法直接获得二叉树任意一结点的路径。接下来,我们会想到如果我间接获得任意一结点的路径呢?这里就需要开辟额外的空间来存放任意一结点的路径(从根结点开始,终止于该结点),关于“求某一结点的路径”问题,可以根据递归遍历二叉树的算法改进而来。得到两个结点的路径之后,从根结点开始同时遍历这两条路径,第一个不同结点的上一结点即为最低公共祖先。
如果第一次拿到这个题目,可能不会立刻想到上面的解法。一般可以想到时间复杂度为O(n^2)的思路:从根节点开始遍历,如果当前结点的左结点是这两个结点的公共祖先,则进一步遍历左子树找到最低公共祖先;如果当前结点的右结点是这两个结点的公共祖先,则进一步遍历右子树找到最低公共祖先;若不满足以上两种情况,则只需要判断当前结点是否是这两个结点的祖先,若是则该结点为最低公共祖先,反之则不存在!因此,这种思路的重点变成了如何判断某个结点A是否为另一结点B的祖先。很简单,从A结点出发遍历子树(该子树可以看做以A结点为根结点的二叉树),若找到B结点则可认为A结点为B结点的祖先,若找不到则不是!
代码实现:
#include<iostream> using namespace std; #include<vector> struct TreeNode { int val; struct TreeNode * left; struct TreeNode * right; TreeNode(int x):val(x), left(NULL), right(NULL){} }; // 创建一棵二叉树 TreeNode * CreateBinaryTree() { int val; cin>>val; if(val==-1) return NULL; TreeNode * root = new TreeNode(val); root->left = CreateBinaryTree(); root->right = CreateBinaryTree(); return root; } // 前序遍历二叉树 void PreOrder(TreeNode * root) { if(root!=NULL) cout<<root->val<<ends; if(root->left!=NULL) PreOrder(root->left); if(root->right!=NULL) PreOrder(root->right); } bool IsAncestor(TreeNode * root, TreeNode * node) // 判断当前子树是否包含node结点 { if(root==NULL||node==NULL) return false; if(root==node) return true; return IsAncestor(root->left, node) || IsAncestor(root->right, node); } TreeNode * CommonAncestorBinary(TreeNode * root, TreeNode * node1, TreeNode * node2) // 二叉树的最低公共祖先(不含父节点),O(n^2)时间复杂度 { if(root==NULL||node1==NULL||node2==NULL) return NULL; // 树为空或者查找的结点也有一个为空,返回空指针 if(IsAncestor(root->left,node1) && IsAncestor(root->left,node2)) // 左结点是公共祖先 { return CommonAncestorBinary(root->left,node1,node2); } else if(IsAncestor(root->right,node1) && IsAncestor(root->right,node2)) // 右结点是公共祖先 { return CommonAncestorBinary(root->right,node1,node2); } else { if(IsAncestor(root,node1)&&IsAncestor(root,node2)) return root; else return NULL; } } bool FindPath(TreeNode * root, TreeNode * node, vector<TreeNode*> &path) { if(root==NULL) return false; path.push_back(root); if(root==node) // 遍历到该结点 return true; bool left = false; bool right = false; left = FindPath(root->left,node,path); if(!left) { right = FindPath(root->right,node,path); } if(left==false&&right==false) path.pop_back(); return (left||right); } TreeNode * CommonAncestorBinary2(TreeNode * root, TreeNode * node1, TreeNode * node2) // 二叉树的最低公共祖先(不含父节点),O(n)时间复杂度 { if(root==NULL||node1==NULL||node2==NULL) return NULL; // 树为空或者查找的结点也有一个为空,返回空指针 vector<TreeNode*> path1; bool is_path1 = FindPath(root, node1, path1); //找到node1的路径 vector<TreeNode*> path2; bool is_path2 = FindPath(root, node2, path2); //找到node2的路径 if(is_path1==true&&is_path2==true) { for(int i=0;i<path1.size()||i<path2.size();i++) { if(path1[i]!=path2[i]) break; } if(i>0&&(i<path1.size()||i<path2.size())) return path1[i-1]; else return NULL; } else return NULL; } int main() { TreeNode * root = NULL; root = CreateBinaryTree(); // 创建一个二叉树 // 二叉搜索树的测试数据:5 2 1 -1 -1 4 3 -1 -1 -1 7 6 -1 -1 9 8 -1 -1 10 -1 -1 PreOrder(root); // 前序遍历二叉树 cout<<endl; cout<<root->left->left->val<<ends<<root->left->right->left->val<<endl; /*bool is = IsAncestor(root->left->left, root->right); if(is) cout<<root->val<<" is the ancestor of "<<root->left->left->val<<endl; else cout<<root->val<<" is not the ancestor of "<<root->left->left->val<<endl;*/ TreeNode * result = CommonAncestorBinary2(root, root->left->left, root->left->right->left); if(result) cout<<result->val<<endl; else cout<<"Not Found!"<<endl; return 0; }
4. 总结
二叉树的遍历:前序遍历、中序遍历、后序遍历、广度遍历非常重要! 递归与非递归两种方式都要会!以上算法都是基于这些遍历的变形,如果还不清楚的话,恐怕面试的时候会悲伤到变形吧!