有一個普通二叉樹,AB分別爲兩個子節點,求AB最近(深度最大)的公共父節點。
此題仍然是一個老題,有着多種解決方法,本文針對其中三種方法來進行分析總結。
這三種方法分別是:遞歸法,tarjan離線算法,RMQ在線算法。
遞歸法
遞歸法比較直觀簡單,思路如下:
- 首先判定當前節點root是否是A節點或者B節點,若是的話直接返回該節點
若不是,分別對root節點的左右子樹進行遞歸查找最小公共父節點,若左右子樹都返回了節點,那麼表示當前節點就是最小公共父節點,若只有其中一個子樹返回了結果,那麼就返回該結果節點。
參考代碼如下:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root)
return NULL;
if(root == p || root == q)
return root;
TreeNode* left = lowestCommonAncestor(root->left,p,q);
TreeNode* right = lowestCommonAncestor(root->right,p,q);
if(left == NULL) return right;
if(right==NULL) return left;
return root;
}
如上圖中我們要查找節點4,3的最小公共父節點,那麼上述代碼的執行過程如下
- 判斷1是否4,3節點不是,查詢1的左子樹和右子樹
- 1的左子樹中先判斷2是否4,3節點不是,查找2的左右子樹
- 2的左子樹 顯然返回結果4,2的右子樹返回結果NULL,顯然2的左右子樹並沒有都返回樹中的節點,因此2不是4,3的最小公共父節點,因此以2位根節點的1的左子樹調用方法返回的是4.
- 1的右子樹判斷3節點是否是4,3節點,發現是的,直接返回3節點。
- 1的左右子樹的遞歸調用都返回了結果,因此1就是節點4,3的最小公共父節點。
Tarjan離線算法
對於該算法,常常會有一個給定的樹T和一個給定的查詢集合P={(u,v)},我們需要確定P中每對的最小公共祖先。
算法爲何說是離線的?因爲針對所有的查詢,我們都是在算法的一次執行過程中找到。
算法思路:
- 根節點root開始搜索,每次遞歸搜索所有的子樹
- 當搜索到節點u時,創建一個由u本身組成的集合,這個集合的祖先爲u自己。然後遞歸搜索u的所有兒子節點。每個兒子節點遞歸完畢之後,將兒子節點所在的集合和u所在的集合合併,再把該集合的祖先設定爲節點u。
- 每個節點的所有孩子都遞歸完畢後,將該節點設定爲已經訪問過,開始遍歷所有u的查詢,若發現查詢的另外一個節點v也是標示爲訪問過的話,那麼節點u和v的最小公共父節點即爲節點v所在集合的祖先節點。
上述所有操作都是使用並查集高效完成的。因此時間複雜度,O(n)深度優先搜索所有節點的時間,搜索每個節點時會遍歷這個節點相關的所有查詢。如果總的查詢個數爲q,則總的複雜度爲O(n+q)。
還是上圖的樹,我們從節點1開始,創建集合{1},然後遞歸到1的第一個孩子節點2,創建集合{2},之後繼續深度遞歸到節點4,創建集合{4},完成節點4的訪問後,將4設定爲已訪問,集合{4}與集合{2}合併,得到{4,2},將他們的祖先設爲2,然後訪問節點5,創建集合{5},完成之後將集合{2,4}和{5}合併,得到{2,4,5}然後將該集合的祖先設爲2,若此時(5,4)是一個查詢,那麼就可以得到他們的最小公共祖先爲2。之後合併{1}和{2,4,5}得到{1,2,4,5},祖先爲1,訪問3,繼續訪問6,完成之後,若(6,2)是一個查詢,那麼其最小公共祖先爲1。剩下的操作類似。
僞代碼如下(參考算法導論):
LCA(u)
MAKE-SET(u)
Find-Set(u).ancestor = u
for each child v of u in T
LCA(u)
Union(u,v)
Find-Set(u).ancestor = v
u.color= Black
for each node v such that (u,v) belongs to P
if v.color ==Black
print "The LCA of "u "and" v"is" Find-Set(v).ancestor
RMQ在線算法
無論是一個詢問還是很多個詢問,使用離線算法都是只需要做一次深度優先搜索就可以了的。所以離線算法針對一次性較多查詢的話比較實惠,但是每次只來一個查詢,這時候多次調用離線算法就不值當了。在線算法處理這種情況比較合適。
之前文章裏寫過RMQ在線算法的原理了,這裏就不加多說,本文主要介紹如何把RMQ算法和最小公共父節點問題結合起來。
算法思路:
LCA集合RMQ主要是通過DFS(深度優先搜索)完成。每次經過某一個點——無論是從它的父親節點進入這個點,還是從它的兒子節點返回這個點,都按順序記錄下來,還是上面的樹,DFS之後順序爲:1-2-4-2-5-2-1-3-6-3-1。那麼要找到樹上兩個節點的最近公共祖先,無非就是找到這兩個節點最後一次出現在數組中的位置所囊括的一段區間中深度最小的那個點,並且我們假定我們的樹中每個父親節點的標號都比孩子節點小,那麼目標就轉換爲求該區間中標號最小的那個節點。
部分代碼如下:
int ind[N];//用來存儲每個節點在生成路徑中最後一次出現的位置
vector<int> road;//存儲DFS路徑
void dfs(int s)
{
road.push_back(s);
for(int i = 0; i < V[s].size();i++){
dfs(V[s][i]);
road.push_back(s);
}
}
得到路徑之後,那麼每個節點在生成路徑中最後一次出現的位置可以用如下方式得到:
for(int i = 0; i < road.size();i++)
{
ind[road[i]] = i + 1;
}
進行上述操作之後,對於每一對查詢(u,v),我們再利用RMQ區間查詢的算法輸入ind[u],ind[v]就能得到其最小公共父節點了。
總結
本文總結了三種不同求解最小公共父節點的方法,本人曾在微軟面試中被問及此題,答得並不是很好,若是能在面試中答出多種方法,那麼一定會有大大的加分。