東哥手把手帶你刷二叉樹(第三期)

接前文 手把手帶你刷二叉樹(第一期) 和 手把手帶你刷二叉樹(第二期),本文繼續來刷二叉樹。git

從前兩篇文章的閱讀量來看,你們仍是可以經過二叉樹學習到 框架思惟 的。但仍是有很多讀者有一些問題,好比如何判斷咱們應該用前序仍是中序仍是後序遍歷的框架算法

那麼本文就針對這個問題,不貪多,給你掰開揉碎只講一道題。仍是那句話,根據題意,思考一個二叉樹節點須要作什麼,到底用什麼遍歷順序就清楚了數據結構

看題,這是力扣第 652 題「尋找重複子樹」:框架

d526dd7a26c2965e4d4bd0fe52d3a63a.jpg

函數簽名以下:ide

List<TreeNode> findDuplicateSubtrees(TreeNode root);

我來簡單解釋下題目,輸入是一棵二叉樹的根節點 root,返回的是一個列表,裏面裝着若干個二叉樹節點,這些節點對應的子樹在原二叉樹中是存在重複的。函數

提及來比較繞,舉例來講,好比輸入以下的二叉樹:學習

113d2049113ebda8fa83cd3f253248d9.jpg

首先,節點 4 自己能夠做爲一棵子樹,且二叉樹中有多個節點 4:spa

78803e6abcdeb85728213f6ba013669e.jpg

相似的,還存在兩棵以 2 爲根的重複子樹:3d

8e6b3b80725665984687f0d78fddf00f.jpg

那麼,咱們返回的 List 中就應該有兩個 TreeNode,值分別爲 4 和 2(具體是哪一個節點都無所謂)。指針

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。

這題咋作呢?仍是老套路,先思考,對於某一個節點,它應該作什麼

好比說,你站在圖中這個節點 2 上:

4e5925ca1f06a3313e1074c216e31b46.jpg

若是你想知道以本身爲根的子樹是否是重複的,是否應該被加入結果列表中,你須要知道什麼信息?

你須要知道如下兩點

一、以我爲根的這棵二叉樹(子樹)長啥樣

二、以其餘節點爲根的子樹都長啥樣

這就叫知己知彼嘛,我得知道本身長啥樣,還得知作別人長啥樣,而後才能知道有沒有人跟我重複,對不對?

好,那咱們一個一個來看,先來思考,我如何才能知道以本身爲根的二叉樹長啥樣

其實看到這個問題,就能夠判斷本題要使用「後序遍歷」框架來解決:

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;
}

這樣,這道題就徹底解決了,題目自己算不上難,可是思路拆解下來仍是挺有啓發性的吧?

_____________

相關文章
相關標籤/搜索