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


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

讀完本文,你不僅學會了算法套路,還可以順便去 LeetCode 上拿下如下題目:

652.尋找重復的子樹

-----------

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

從前兩篇文章的閱讀量來看,大家還是能夠通過二叉樹學習到 框架思維 的。但還是有不少讀者有一些問題,比如如何判斷我們應該用前序還是中序還是后序遍歷的框架

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

看題,這是力扣第 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,歡迎標星!

相關推薦:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM