struct TreeNode { int m_nvalue; TreeNode* m_pLeft; TreeNode* m_pRight; };
分析:求数中两个结点的最低共同结点是面试中经常出现的一个问题,这个问题至少有两个变种:
第一变种是二叉树是一种特殊的二叉树:查找二叉树,也就是树是排序过的,位于左子树上的结点都比父结点小,而位于右子树的结点都比父结点大。我们只需要从根结点开始和两个结点进行比较:如果当前结点的值比两个结点都大,则最低的共同父结点一定在当前结点的左子树中;如果当前结点的值比两个结点都小,则最低的共同父结点一定在当前结点的右子树中。
第二个变种是树不一定是二叉树,每个结点都有一个指针指向它的父结点。于是我们可以从任何一个结点出发,得到一个到达树根结点的单向链表,因此这个问题转换为两个单向链表的第一个公共结点。
现在我们回到这个问题本身。所谓共同的父结点,就是两个结点都出现在这个结点的子树中。因此我们可以定义一函数,来判断一个结点的子树中是不是包含了另外一个结点。这不是件很难的事,我们可以用递归的方法来实现:
// If the tree with head pHead has a node pNode, return true. Otherwise return false. bool HasNode(TreeNode* pHead, TreeNode* pNode) { if(pHead == pNode) return true; bool has = false; if(pHead->m_pLeft != NULL) has = HasNode(pHead->m_pLeft, pNode); if(!has && pHead->m_pRight != NULL) has = HasNode(pHead->m_pRight, pNode); return has; }
我们可以从根结点开始,判断以当前结点为根的树中左右子树是不是包含我们要找的两个结点。如果两个结点都出现在它的左子树中,那最低的共同父结点也出现在它的左子树中。如果两个结点都出现在它的右子树中,那最低的共同父结点也出现在它的右子树中。如果两个结点一个出现在左子树中,一个出现在右子树中,那当前的结点就是最低的共同父结点。基于这个思路,我们可以写出如下代码:
// Find the last parent of pNode1 and pNode2 in a tree with head pHead TreeNode* LastCommonParent_1(TreeNode* pHead, TreeNode* pNode1, TreeNode* pNode2) { if(pHead == NULL || pNode1 == NULL || pNode2 == NULL) return NULL; // check whether left child has pNode1 and pNode2 bool leftHasNode1 = false; bool leftHasNode2 = false; if(pHead->m_pLeft != NULL) { leftHasNode1 = HasNode(pHead->m_pLeft, pNode1); leftHasNode2 = HasNode(pHead->m_pLeft, pNode2); } if(leftHasNode1 && leftHasNode2) { if(pHead->m_pLeft == pNode1 || pHead->m_pLeft == pNode2) return pHead; return LastCommonParent_1(pHead->m_pLeft, pNode1, pNode2); } // check whether right child has pNode1 and pNode2 bool rightHasNode1 = false; bool rightHasNode2 = false; if(pHead->m_pRight != NULL) { if(!leftHasNode1) rightHasNode1 = HasNode(pHead->m_pRight, pNode1); if(!leftHasNode2) rightHasNode2 = HasNode(pHead->m_pRight, pNode2); } if(rightHasNode1 && rightHasNode2) { if(pHead->m_pRight == pNode1 || pHead->m_pRight == pNode2) return pHead; return LastCommonParent_1(pHead->m_pRight, pNode1, pNode2); } if((leftHasNode1 && rightHasNode2) || (leftHasNode2 && rightHasNode1)) return pHead; return NULL; }
接着我们来分析一下这个方法的效率。函数HasNode的本质就是遍历一棵树,其时间复杂度是O(n)(n是树中结点的数目)。由于我们根结点开始,要对每个结点调用函数HasNode。因此总的时间复杂度是O(n2)。
我们仔细分析上述代码,不难发现我们判断以一个结点为根的树是否含有某个结点时,需要遍历树的每个结点。接下来我们判断左子结点或者右结点为根的树中是否含有要找结点,仍然需要遍历。第二次遍历的操作其实在前面的第一次遍历都做过了。由于存在重复的遍历,本方法在时间效率上肯定不是最好的。
前面我们提过如果结点中有一个指向父结点的指针,可以把问题转化为求两个链表的共同结点,现在我们可以想办法得到这个链表。我们在本面试题系列的第4题中分析过如何得到一条中根结点开始的路径。我们在这里稍作变化即可:
// Get the path form pHead and pNode in a tree with head pHead bool GetNodePath(TreeNode* pHead, TreeNode* pNode, std::list<TreeNode*>& path) { if(pHead == pNode) return true; path.push_back(pHead); bool found = false; if(pHead->m_pLeft != NULL) found = GetNodePath(pHead->m_pLeft, pNode, path); if(!found && pHead->m_pRight) found = GetNodePath(pHead->m_pRight, pNode, path); if(!found) path.pop_back(); return found; }
由于这个路径是从跟结点开始的,最低的共同父结点就是路径中的最后一个共同结点:
// Get the last common Node in two lists: path1 and path2 TreeNode* LastCommonNode ( const std::list<TreeNode*>& path1, const std::list<TreeNode*>& path2 ) { std::list<TreeNode*>::const_iterator iterator1 = path1.begin(); std::list<TreeNode*>::const_iterator iterator2 = path2.begin(); TreeNode* pLast = NULL; while(iterator1 != path1.end() && iterator2 != path2.end()) { if(*iterator1 == *iterator2) pLast = *iterator1; iterator1++; iterator2++; } return pLast; }
有了前面两个子函数之后,求两个结点的最低共同父结点就很容易了。我们先求出从根结点出发到两个结点的两条路径,再求出两条路径的最后一个共同结点。代码如下:
// Find the last parent of pNode1 and pNode2 in a tree with head pHead TreeNode* LastCommonParent_2(TreeNode* pHead, TreeNode* pNode1, TreeNode* pNode2) { if(pHead == NULL || pNode1 == NULL || pNode2 == NULL) return NULL; std::list<TreeNode*> path1; GetNodePath(pHead, pNode1, path1); std::list<TreeNode*> path2; GetNodePath(pHead, pNode2, path2); return LastCommonNode(path1, path2); }
这种思路的时间复杂度是O(n),时间效率要比第一种方法好很多。但同时我们也要注意到,这种思路需要两个链表来保存路径,空间效率比不上第一个方法。