深入二叉樹兩個(gè)結(jié)點(diǎn)的最低共同父結(jié)點(diǎn)的詳解
更新時(shí)間:2013年05月23日 18:21:23 作者:
本篇文章是對(duì)二叉樹兩個(gè)結(jié)點(diǎn)的最低共同父結(jié)點(diǎn)進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
題目:二叉樹的結(jié)點(diǎn)定義如下:
struct TreeNode
{
int m_nvalue;
TreeNode* m_pLeft;
TreeNode* m_pRight;
};
輸入二叉樹中的兩個(gè)結(jié)點(diǎn),輸出這兩個(gè)結(jié)點(diǎn)在數(shù)中最低的共同父結(jié)點(diǎn)。
分析:求數(shù)中兩個(gè)結(jié)點(diǎn)的最低共同結(jié)點(diǎn)是面試中經(jīng)常出現(xiàn)的一個(gè)問題。這個(gè)問題至少有兩個(gè)變種。
第一變種是二叉樹是一種特殊的二叉樹:查找二叉樹。也就是樹是排序過的,位于左子樹上的結(jié)點(diǎn)都比父結(jié)點(diǎn)小,而位于右子樹的結(jié)點(diǎn)都比父結(jié)點(diǎn)大。我們只需要從根結(jié)點(diǎn)開始和兩個(gè)結(jié)點(diǎn)進(jìn)行比較。如果當(dāng)前結(jié)點(diǎn)的值比兩個(gè)結(jié)點(diǎn)都大,則最低的共同父結(jié)點(diǎn)一定在當(dāng)前結(jié)點(diǎn)的左子樹中。如果當(dāng)前結(jié)點(diǎn)的值比兩個(gè)結(jié)點(diǎn)都小,則最低的共同父結(jié)點(diǎn)一定在當(dāng)前結(jié)點(diǎn)的右子樹中。
第二個(gè)變種是樹不一定是二叉樹,每個(gè)結(jié)點(diǎn)都有一個(gè)指針指向它的父結(jié)點(diǎn)。于是我們可以從任何一個(gè)結(jié)點(diǎn)出發(fā),得到一個(gè)到達(dá)樹根結(jié)點(diǎn)的單向鏈表。因此這個(gè)問題轉(zhuǎn)換為求兩個(gè)單向鏈表的第一個(gè)公共結(jié)點(diǎn)。
現(xiàn)在我們回到這個(gè)問題本身。所謂共同的父結(jié)點(diǎn),就是兩個(gè)結(jié)點(diǎn)都出現(xiàn)在這個(gè)結(jié)點(diǎn)的子樹中。因此我們可以定義一函數(shù),來判斷一個(gè)結(jié)點(diǎn)的子樹中是不是包含了另外一個(gè)結(jié)點(diǎn)。這不是件很難的事,我們可以用遞歸的方法來實(shí)現(xiàn):
/*
// 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;
}
我們可以從根結(jié)點(diǎn)開始,判斷以當(dāng)前結(jié)點(diǎn)為根的樹中左右子樹是不是包含我們要找的兩個(gè)結(jié)點(diǎn)。如果兩個(gè)結(jié)點(diǎn)都出現(xiàn)在它的左子樹中,那最低的共同父結(jié)點(diǎn)也出現(xiàn)在它的左子樹中。如果兩個(gè)結(jié)點(diǎn)都出現(xiàn)在它的右子樹中,那最低的共同父結(jié)點(diǎn)也出現(xiàn)在它的右子樹中。如果兩個(gè)結(jié)點(diǎn)一個(gè)出現(xiàn)在左子樹中,一個(gè)出現(xiàn)在右子樹中,那當(dāng)前的結(jié)點(diǎn)就是最低的共同父結(jié)點(diǎn)?;谶@個(gè)思路,我們可以寫出如下代碼:
/*
// 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;
}
接著我們來分析一下這個(gè)方法的效率。函數(shù)HasNode的本質(zhì)就是遍歷一棵樹,其時(shí)間復(fù)雜度是O(n)(n是樹中結(jié)點(diǎn)的數(shù)目)。由于我們根結(jié)點(diǎn)開始,要對(duì)每個(gè)結(jié)點(diǎn)調(diào)用函數(shù)HasNode。因此總的時(shí)間復(fù)雜度是O(n^2)。
我們仔細(xì)分析上述代碼,不難發(fā)現(xiàn)我們判斷以一個(gè)結(jié)點(diǎn)為根的樹是否含有某個(gè)結(jié)點(diǎn)時(shí),需要遍歷樹的每個(gè)結(jié)點(diǎn)。接下來我們判斷左子結(jié)點(diǎn)或者右結(jié)點(diǎn)為根的樹中是否含有要找結(jié)點(diǎn),仍然需要遍歷。第二次遍歷的操作其實(shí)在前面的第一次遍歷都做過了。由于存在重復(fù)的遍歷,本方法在時(shí)間效率上肯定不是最好的。
前面我們提過如果結(jié)點(diǎn)中有一個(gè)指向父結(jié)點(diǎn)的指針,我們可以把問題轉(zhuǎn)化為求兩個(gè)鏈表的共同結(jié)點(diǎn)?,F(xiàn)在我們可以想辦法得到這個(gè)鏈表。我們?cè)谶@里稍作變化即可:
/*
// 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;
}
由于這個(gè)路徑是從跟結(jié)點(diǎn)開始的。最低的共同父結(jié)點(diǎn)就是路徑中的最后一個(gè)共同結(jié)點(diǎn):
/*
// 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;
}
有了前面兩個(gè)子函數(shù)之后,求兩個(gè)結(jié)點(diǎn)的最低共同父結(jié)點(diǎn)就很容易了。我們先求出從根結(jié)點(diǎn)出發(fā)到兩個(gè)結(jié)點(diǎn)的兩條路徑,再求出兩條路徑的最后一個(gè)共同結(jié)點(diǎn)。代碼如下:
/*
// 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);
}
這種思路的時(shí)間復(fù)雜度是O(n),時(shí)間效率要比第一種方法好很多。但同時(shí)我們也要注意到,這種思路需要兩個(gè)鏈表來保存路徑,空間效率比不上第一個(gè)方法。
復(fù)制代碼 代碼如下:
struct TreeNode
{
int m_nvalue;
TreeNode* m_pLeft;
TreeNode* m_pRight;
};
輸入二叉樹中的兩個(gè)結(jié)點(diǎn),輸出這兩個(gè)結(jié)點(diǎn)在數(shù)中最低的共同父結(jié)點(diǎn)。
分析:求數(shù)中兩個(gè)結(jié)點(diǎn)的最低共同結(jié)點(diǎn)是面試中經(jīng)常出現(xiàn)的一個(gè)問題。這個(gè)問題至少有兩個(gè)變種。
第一變種是二叉樹是一種特殊的二叉樹:查找二叉樹。也就是樹是排序過的,位于左子樹上的結(jié)點(diǎn)都比父結(jié)點(diǎn)小,而位于右子樹的結(jié)點(diǎn)都比父結(jié)點(diǎn)大。我們只需要從根結(jié)點(diǎn)開始和兩個(gè)結(jié)點(diǎn)進(jìn)行比較。如果當(dāng)前結(jié)點(diǎn)的值比兩個(gè)結(jié)點(diǎn)都大,則最低的共同父結(jié)點(diǎn)一定在當(dāng)前結(jié)點(diǎn)的左子樹中。如果當(dāng)前結(jié)點(diǎn)的值比兩個(gè)結(jié)點(diǎn)都小,則最低的共同父結(jié)點(diǎn)一定在當(dāng)前結(jié)點(diǎn)的右子樹中。
第二個(gè)變種是樹不一定是二叉樹,每個(gè)結(jié)點(diǎn)都有一個(gè)指針指向它的父結(jié)點(diǎn)。于是我們可以從任何一個(gè)結(jié)點(diǎn)出發(fā),得到一個(gè)到達(dá)樹根結(jié)點(diǎn)的單向鏈表。因此這個(gè)問題轉(zhuǎn)換為求兩個(gè)單向鏈表的第一個(gè)公共結(jié)點(diǎn)。
現(xiàn)在我們回到這個(gè)問題本身。所謂共同的父結(jié)點(diǎn),就是兩個(gè)結(jié)點(diǎn)都出現(xiàn)在這個(gè)結(jié)點(diǎn)的子樹中。因此我們可以定義一函數(shù),來判斷一個(gè)結(jié)點(diǎn)的子樹中是不是包含了另外一個(gè)結(jié)點(diǎn)。這不是件很難的事,我們可以用遞歸的方法來實(shí)現(xiàn):
復(fù)制代碼 代碼如下:
/*
// 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;
}
我們可以從根結(jié)點(diǎn)開始,判斷以當(dāng)前結(jié)點(diǎn)為根的樹中左右子樹是不是包含我們要找的兩個(gè)結(jié)點(diǎn)。如果兩個(gè)結(jié)點(diǎn)都出現(xiàn)在它的左子樹中,那最低的共同父結(jié)點(diǎn)也出現(xiàn)在它的左子樹中。如果兩個(gè)結(jié)點(diǎn)都出現(xiàn)在它的右子樹中,那最低的共同父結(jié)點(diǎn)也出現(xiàn)在它的右子樹中。如果兩個(gè)結(jié)點(diǎn)一個(gè)出現(xiàn)在左子樹中,一個(gè)出現(xiàn)在右子樹中,那當(dāng)前的結(jié)點(diǎn)就是最低的共同父結(jié)點(diǎn)?;谶@個(gè)思路,我們可以寫出如下代碼:
復(fù)制代碼 代碼如下:
/*
// 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;
}
接著我們來分析一下這個(gè)方法的效率。函數(shù)HasNode的本質(zhì)就是遍歷一棵樹,其時(shí)間復(fù)雜度是O(n)(n是樹中結(jié)點(diǎn)的數(shù)目)。由于我們根結(jié)點(diǎn)開始,要對(duì)每個(gè)結(jié)點(diǎn)調(diào)用函數(shù)HasNode。因此總的時(shí)間復(fù)雜度是O(n^2)。
我們仔細(xì)分析上述代碼,不難發(fā)現(xiàn)我們判斷以一個(gè)結(jié)點(diǎn)為根的樹是否含有某個(gè)結(jié)點(diǎn)時(shí),需要遍歷樹的每個(gè)結(jié)點(diǎn)。接下來我們判斷左子結(jié)點(diǎn)或者右結(jié)點(diǎn)為根的樹中是否含有要找結(jié)點(diǎn),仍然需要遍歷。第二次遍歷的操作其實(shí)在前面的第一次遍歷都做過了。由于存在重復(fù)的遍歷,本方法在時(shí)間效率上肯定不是最好的。
前面我們提過如果結(jié)點(diǎn)中有一個(gè)指向父結(jié)點(diǎn)的指針,我們可以把問題轉(zhuǎn)化為求兩個(gè)鏈表的共同結(jié)點(diǎn)?,F(xiàn)在我們可以想辦法得到這個(gè)鏈表。我們?cè)谶@里稍作變化即可:
復(fù)制代碼 代碼如下:
/*
// 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;
}
由于這個(gè)路徑是從跟結(jié)點(diǎn)開始的。最低的共同父結(jié)點(diǎn)就是路徑中的最后一個(gè)共同結(jié)點(diǎn):
復(fù)制代碼 代碼如下:
/*
// 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;
}
有了前面兩個(gè)子函數(shù)之后,求兩個(gè)結(jié)點(diǎn)的最低共同父結(jié)點(diǎn)就很容易了。我們先求出從根結(jié)點(diǎn)出發(fā)到兩個(gè)結(jié)點(diǎn)的兩條路徑,再求出兩條路徑的最后一個(gè)共同結(jié)點(diǎn)。代碼如下:
復(fù)制代碼 代碼如下:
/*
// 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);
}
這種思路的時(shí)間復(fù)雜度是O(n),時(shí)間效率要比第一種方法好很多。但同時(shí)我們也要注意到,這種思路需要兩個(gè)鏈表來保存路徑,空間效率比不上第一個(gè)方法。
您可能感興趣的文章:
- 平衡二叉樹AVL操作模板
- 平衡二叉樹的實(shí)現(xiàn)實(shí)例
- 二叉樹的非遞歸后序遍歷算法實(shí)例詳解
- 二叉樹先序遍歷的非遞歸算法具體實(shí)現(xiàn)
- 二叉樹先根(先序)遍歷的改進(jìn)
- c語言版本二叉樹基本操作示例(先序 遞歸 非遞歸)
- python二叉樹遍歷的實(shí)現(xiàn)方法
- python二叉樹的實(shí)現(xiàn)實(shí)例
- C++二叉樹結(jié)構(gòu)的建立與基本操作
- 二叉樹遍歷 非遞歸 C++實(shí)現(xiàn)代碼
- 先序遍歷二叉樹的遞歸實(shí)現(xiàn)與非遞歸實(shí)現(xiàn)深入解析
- PHP Class&Object -- 解析PHP實(shí)現(xiàn)二叉樹
- PHP Class&Object -- PHP 自排序二叉樹的深入解析
- 如何在二叉樹中找出和為某一值的所有路徑
- 探討:C++實(shí)現(xiàn)鏈?zhǔn)蕉鏄?用非遞歸方式先序,中序,后序遍歷二叉樹)
- 深入理解二叉樹的非遞歸遍歷
- 深入遍歷二叉樹的各種操作詳解(非遞歸遍歷)
- 二叉樹前序遍歷的非遞歸算法
相關(guān)文章
C++實(shí)現(xiàn)多源最短路徑之Floyd算法示例
這篇文章主要介紹了C++實(shí)現(xiàn)多源最短路徑之Floyd算法,結(jié)合實(shí)例形式分析了多源最短路徑之Floyd算法的原理、實(shí)現(xiàn)方法及核心代碼,需要的朋友可以參考下2017-08-08一盤王者的時(shí)間用C語言實(shí)現(xiàn)三子棋
相信我們都玩過三子棋,規(guī)則很簡單,但想用c語言做出這個(gè)游戲,事實(shí)上也是比較簡單的,下面通過c語言進(jìn)行對(duì)五子棋的分析2022-02-02QT使用QComBox和QLineEdit實(shí)現(xiàn)模糊查詢功能
模糊查詢是指根據(jù)用戶輸入的文本,在下拉框的選項(xiàng)中進(jìn)行模糊匹配,并動(dòng)態(tài)地顯示匹配的選項(xiàng),本文將使用QComBox和QLineEdit實(shí)現(xiàn)模糊查詢功能,需要的可以參考下2023-11-11epoll多路復(fù)用的一個(gè)實(shí)例程序(C實(shí)現(xiàn))
這篇文章主要為大家詳細(xì)介紹了epoll多路復(fù)用的一個(gè)實(shí)例程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08C++圖形界面開發(fā)Qt教程:嵌套圓環(huán)示例
這篇文章主要介紹了C++實(shí)現(xiàn)圖形界面開發(fā)Qt教程,涉及坐標(biāo)函數(shù)的應(yīng)用及圖形界面程序設(shè)計(jì),需要的朋友可以參考下,希望能給你帶來幫助2021-08-08C語言實(shí)現(xiàn)linux網(wǎng)卡檢測(cè)精簡版
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)linux網(wǎng)卡檢測(cè)的精簡版,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06