題目:二叉樹的結點定義如下:
struct TreeNode
{
int m_nvalue;
TreeNode* m_pLeft;
TreeNode* m_pRight;
};
輸入二叉樹中的兩個結點,輸出這兩個結點在數中最低的共同父結點。
分析:求數中兩個結點的最低共同結點是面試中經常出現的一個問題。這個問題至少有兩個變種。
第一變種是二叉樹是一種特殊的二叉樹:尋找二叉樹。也就是樹是排序過的,位於左子樹上的結點都比父結點小,而位於右子樹的結點都比父結點大。我們只需要從根結點開始和兩個結點進行比較。如果當前結點的值比兩個結點都大,則最低的共同父結點一定在當前結點的左子樹中。如果當前結點的值比兩個結點都小,則最低的共同父結點一定在當前結點的右子樹中。
第二個變種是樹不一定是二叉樹,每個結點都有一個指標指向它的父結點。於是我們可以從任何一個結點出發,得到一個到達樹根結點的單向鏈表。因此這個問題轉換為兩個單向鏈表的第一個公用結點。我們在本面試題系列的第35題討論了這個問題。
現在我們回到這個問題本身。所謂共同的父結點,就是兩個結點都出現在這個結點的子樹中。因此我們可以定義一函數,來判斷一個結點的子樹中是不是包含了另外一個結點。這不是件很難的事,我們可以用遞迴的方法來實現:
/////////////////////////////////////////////////////////////////////////////////// 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),時間效率要比第一種方法好很多。但同時我們也要注意到,這種思路需要兩個鏈表來儲存路徑,空間效率比不上第一個方法。
本文已經收錄到《劍指Offer——名企面試官精講典型編程題》一書中,有改動,書中的分析講解更加詳細。歡迎關注。
博主何海濤對本部落格文章享有著作權。網路轉載請註明出處http://zhedahht.blog.163.com/。整理出版物請和作者聯絡。