樹中兩結點的最低公共祖先(C++實現)
題目是,輸入兩個樹結點,求它們的最低公共祖先
首先,要說明的是,這是一組題目,根據劍指Offer上所講的,這道題可能會分好幾種情況,因此,如果在面試時候遇到,我們需要和麵試官溝通,而不是一上來就寫程式碼。
1. 如果給定樹是二叉搜尋樹
二叉搜尋樹(又叫二叉排序樹),中序遍歷是可以得到有序序列的,因此,我們可以利用這個性質來解題。
假設給定的兩個樹結點為4,1,根據圖示,顯然3為最低公共祖先;
假設給定的兩個樹結點為4,3,根據圖示,顯然3為最低公共祖先;
假設給定的兩個樹結點為2,1,根據圖示,顯然2為最低公共祖先;
假設給定的兩個樹結點為6,8,根據圖示,顯然7為最低公共祖先;
假設給定的兩個樹結點為8,1,根據圖示,顯然5為最低公共祖先;
因此我們發現,公共祖先的值都是在給定兩個樹結點的範圍之內。
所以,我們可以由根結點遍歷,如果當前結點的值比兩結點大,需要到當前結點的左子樹中找,如果當前結點的值比兩結點小,需要到當前結點的右子樹中找,僅噹噹前結點的值在兩結點的範圍內(等於也算)。
程式碼比較簡單,如下:
Node* FindLowestCommonAncestor(Node* root, Node* n1, Node* n2)
{
//引數有一個為空返回NULL
if (root == NULL || n1 == NULL || n2 == NULL)
return NULL;
if (root->data > n1-> data && root->data > n2->data)
return FindLowestCommonAncestor(root->left, n1, n2);
else if (root->data < n1->data && root->data < n2->data)
return FindLowestCommonAncestor(root->right, n1, n2);
else
return root;
}
2. 如果給定樹由指向父節點的指標
假設我們給定的結點為G,E,顯然由圖示,最低公共祖先為B。
由於有了指向父結點的指標,因此我們可以由G,E出發,找到最低公共祖先,所以此題其實本質是兩個連結串列的第一個公共結點。
連結串列求交問題不難,有不少方法,比如可以先把G,E到根節點A所經過的所有結點壓棧,此時有兩個序列,
GDBA,EBA,兩棧棧頂相同Pop,最後一個相同的結點即為所求結果,這樣的演算法需要藉助兩個棧(其他類似容器也可以)。
在這裡,我給出一種不需要藉助容器的演算法,分別由G,E遍歷到根節點得到各自的長度,4和3,讓長度長一點的那個連結串列先走一步,比如G先走到D,此時序列為DBA和EBA,同時走,遇到相同的結點即為所求結果。
程式碼如下:
Node* FindLowestCommonAncestor(Node* root, Node* n1, Node* n2)
{
//引數有一個為空返回NULL
if (root == NULL || n1 == NULL || n2 == NULL)
return NULL;
Node* p1 = n1;
Node* p2 = n2;
int count1 = 0; //求n2到根節點的長度
int count2 = 0; //求n2到根節點的長度
while (p1 != NULL)
{
++count1;
p1 = p1->next;
}
while (p2 != NULL)
{
++count2;
p2 = p2->next;
}
if (count1 >= count2)
{
int n = count1 - count2;
p1 = n1;
p2 = n2;
while (n--)
{
p1 = p1->next;
}
}
else
{
int n = count2 - count1;
p1 = n1;
p2 = n2;
while (n--)
{
p2 = p2->next;
}
}
while (p1 != p2)
{
p1 = p1->next;
p2 = p2->next;
}
return p1;
}
3. 如果給定樹是普通的二叉樹
我們往往可能遇到更一般的情況,也就是給定樹只是普通的二叉樹。那麼這道題如何解?
我們還是以之前的圖來看,不過需要注意的是此時沒有指向父節點的指標。
3. 1一般解法
其實,我們可以類比第一種情況(二叉搜尋樹)的解法,我們舉例來分析:
假設給定樹結點為D,E,求最低公共結點。我們由根節點A出發,D,E都在A的左子樹中,於是我們遍歷到B,此時D,E分別在B的左右子樹中,B即為所求答案。
假設給定樹結點為E,H,我們由A結點出發,發現E,H都在它的左子樹中,於是我們遍歷到B,發現他們都在B的右子樹中,於是我們遍歷到了E,此時E為所求答案。
因此,我們發現,如果給定的兩個樹結點都在當前結點的左子樹裡面,最低公共祖先必在左子樹中,如果給定的兩個樹結點都在當前結點的右子樹裡面,最低公共祖先必在右子樹中,如果不滿足這兩個條件,當前結點即為最低公共祖先。
思路有了,我們需要完成程式碼
//判斷某個結點在當前樹中
bool IsInTheBinaryTree(Node* root, Node* n)
{
if (root == NULL || n == NULL)
return false;
if (root == n)
return true;
bool found = IsInTheBinaryTree(root->left, n);
if (!found)
found = IsInTheBinaryTree(root->right, n);
return found;
}
Node* FindLowestCommonAncestor(Node* root, Node* n1, Node* n2)
{
//引數有一個為空返回NULL
if (root == NULL || n1 == NULL || n2 == NULL)
return NULL;
//都在左子樹,結果必在左子樹中
if (IsInTheBinaryTree(root->left, n1) && IsInTheBinaryTree(root->left, n2))
return FindLowestCommonAncestor(root->left, n1, n2);
//都在右子樹,結果必在右子樹中
else if (IsInTheBinaryTree(root->right, n1) && IsInTheBinaryTree(root->right, n2))
return FindLowestCommonAncestor(root->right, n1, n2);
else
return root;
}
3. 2更快的解法
我們已經完成基本功能了,然而我們仔細分析下程式碼發現,我們判斷某個結點在樹中遞迴了一次樹,求公共結點又遞迴了一次樹,這樣子會對某些結點重複遍歷很多次,效率比較低,能否有一個更好的演算法?
為了解決這個問題,我們需要儲存從根節點到兩個給定樹結點的路徑,這樣子由變成了求連結串列的最後公共結點了。還是拿上面的圖舉例:
求G,E的最低公共祖先。
由A到G的路徑為:ABDG
由A到E的路徑為:ABE
如果看作是連結串列,我們只需要求最後一個公共交點了,當然藉助其他類似容器也是同樣的道理,這裡我使用vector儲存路徑。
bool GetPath(Node* root, Node* n, vector<Node*>& v)
{
if (root == NULL || n == NULL)
return false;
if (root == n)
{
v.push_back(root);
return true;
}
v.push_back(root);
bool found = false;
found = GetPath(root->left, n, v);
if (!found)
found = GetPath(root->right, n, v);
if (!found)
v.pop_back();
return found;
}
Node* FindLowestCommonAncestor(Node* root, Node* n1, Node* n2)
{
//引數有一個為空返回NULL
if (root == NULL || n1 == NULL || n2 == NULL)
return NULL;
vector<Node*> v1;
bool hasPath1 = GetPath(root, n1, v1);
vector<Node*> v2;
bool hasPath2 = GetPath(root, n2, v2);
if (hasPath1 && hasPath2)
{
int i = 0;
while (v1[i] == v2[i])
{
++i;
}
return v1[i - 1];
}
else
{
return NULL;
}
}