東哥手把手帶你刷二叉樹(第三期)
讀完本文,你不僅學會了算法套路,還可以順便去 LeetCode 上拿下如下題目:
-----------
接前文 手把手帶你刷二叉樹(第一期) 和 手把手帶你刷二叉樹(第二期),本文繼續來刷二叉樹。
從前兩篇文章的閱讀量來看,大家還是能夠通過二叉樹學習到 框架思維 的。但還是有不少讀者有一些問題,比如如何判斷我們應該用前序還是中序還是后序遍歷的框架?
那么本文就針對這個問題,不貪多,給你掰開揉碎只講一道題。還是那句話,根據題意,思考一個二叉樹節點需要做什么,到底用什么遍歷順序就清楚了。
看題,這是力扣第 652 題「尋找重復子樹」:
函數簽名如下:
List<TreeNode> findDuplicateSubtrees(TreeNode root);
我來簡單解釋下題目,輸入是一棵二叉樹的根節點 root
,返回的是一個列表,里面裝着若干個二叉樹節點,這些節點對應的子樹在原二叉樹中是存在重復的。
說起來比較繞,舉例來說,比如輸入如下的二叉樹:
首先,節點 4 本身可以作為一棵子樹,且二叉樹中有多個節點 4:
類似的,還存在兩棵以 2 為根的重復子樹:
那么,我們返回的 List
中就應該有兩個 TreeNode
,值分別為 4 和 2(具體是哪個節點都無所謂)。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種算法套路后投再入題海就如魚得水了。
這題咋做呢?還是老套路,先思考,對於某一個節點,它應該做什么。
比如說,你站在圖中這個節點 2 上:
如果你想知道以自己為根的子樹是不是重復的,是否應該被加入結果列表中,你需要知道什么信息?
你需要知道以下兩點:
1、以我為根的這棵二叉樹(子樹)長啥樣?
2、以其他節點為根的子樹都長啥樣?
這就叫知己知彼嘛,我得知道自己長啥樣,還得知道別人長啥樣,然后才能知道有沒有人跟我重復,對不對?
好,那我們一個一個來看,先來思考,我如何才能知道以自己為根的二叉樹長啥樣?
其實看到這個問題,就可以判斷本題要使用「后序遍歷」框架來解決:
void traverse(TreeNode root) {
traverse(root.left);
traverse(root.right);
/* 解法代碼的位置 */
}
為什么?很簡單呀,我要知道以自己為根的子樹長啥樣,是不是得先知道我的左右子樹長啥樣,再加上自己,就構成了整棵子樹的樣子?
如果你還繞不過來,我再來舉個非常簡單的例子:計算一棵二叉樹有多少個節點。這個代碼應該會寫吧:
int count(TreeNode root) {
if (root == null) {
return 0;
}
// 先算出左右子樹有多少節點
int left = count(root.left);
int right = count(root.right);
/* 后序遍歷代碼位置 */
// 加上自己,就是整棵二叉樹的節點數
int res = left + right + 1;
return res;
}
這不就是標准的后序遍歷框架嘛,和我們本題在本質上沒啥區別對吧。
現在,明確了要用后序遍歷,那應該怎么描述一棵二叉樹的模樣呢?我們前文 序列化和反序列化二叉樹 其實寫過了,二叉樹的前序/中序/后序遍歷結果可以描述二叉樹的結構。
所以,我們可以通過拼接字符串的方式把二叉樹序列化,看下代碼:
String traverse(TreeNode root) {
// 對於空節點,可以用一個特殊字符表示
if (root == null) {
return "#";
}
// 將左右子樹序列化成字符串
String left = traverse(root.left);
String right = traverse(root.right);
/* 后序遍歷代碼位置 */
// 左右子樹加上自己,就是以自己為根的二叉樹序列化結果
String subTree = left + "," + right + "," + root.val;
return subTree;
}
我們用非數字的特殊符 #
表示空指針,並且用字符 ,
分隔每個二叉樹節點值,這屬於序列化二叉樹的套路了,不多說。
注意我們 subTree
是按照左子樹、右子樹、根節點這樣的順序拼接字符串,也就是后序遍歷順序。你完全可以按照前序或者中序的順序拼接字符串,因為這里只是為了描述一棵二叉樹的樣子,什么順序不重要。
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種算法套路后投再入題海就如魚得水了。
這樣,我們第一個問題就解決了,對於每個節點,遞歸函數中的 subTree
變量就可以描述以該節點為根的二叉樹。
現在我們解決第二個問題,我知道了自己長啥樣,怎么知道別人長啥樣?這樣我才能知道有沒有其他子樹跟我重復對吧。
這很簡單呀,我們借助一個外部數據結構,讓每個節點把自己子樹的序列化結果存進去,這樣,對於每個節點,不就可以知道有沒有其他節點的子樹和自己重復了么?
初步思路可以使用 HashSet
記錄子樹,代碼如下:
// 記錄所有子樹
HashSet<String> memo = new HashSet<>();
// 記錄重復的子樹根節點
LinkedList<TreeNode> res = new LinkedList<>();
String traverse(TreeNode root) {
if (root == null) {
return "#";
}
String left = traverse(root.left);
String right = traverse(root.right);
String subTree = left + "," + right+ "," + root.val;
if (memo.contains(subTree)) {
// 有人和我重復,把自己加入結果列表
res.add(root);
} else {
// 暫時沒人跟我重復,把自己加入集合
memo.add(subTree);
}
return subTree;
}
但是呢,這有個問題,如果出現多棵重復的子樹,結果集 res
中必然出現重復,而題目要求不希望出現重復。
為了解決這個問題,可以把 HashSet
升級成 HashMap
,額外記錄每棵子樹的出現次數:
// 記錄所有子樹以及出現的次數
HashMap<String, Integer> memo = new HashMap<>();
// 記錄重復的子樹根節點
LinkedList<TreeNode> res = new LinkedList<>();
/* 主函數 */
List<TreeNode> findDuplicateSubtrees(TreeNode root) {
traverse(root);
return res;
}
/* 輔助函數 */
String traverse(TreeNode root) {
if (root == null) {
return "#";
}
String left = traverse(root.left);
String right = traverse(root.right);
String subTree = left + "," + right+ "," + root.val;
int freq = memo.getOrDefault(subTree, 0);
// 多次重復也只會被加入結果集一次
if (freq == 1) {
res.add(root);
}
// 給子樹對應的出現次數加一
memo.put(subTree, freq + 1);
return subTree;
}
這樣,這道題就完全解決了,題目本身算不上難,但是思路拆解下來還是挺有啟發性的吧?
_____________
我的 在線電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 算法倉庫 已經獲得了 70k star,歡迎標星!
相關推薦: