东哥手把手带你刷二叉树(第三期)

接前文 手把手带你刷二叉树(第一期) 和 手把手带你刷二叉树(第二期),本文继续来刷二叉树。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;
}

这样,这道题就彻底解决了,题目自己算不上难,可是思路拆解下来仍是挺有启发性的吧?

_____________

相关文章
相关标签/搜索