# 手把手带你刷二叉树(三期)

按着前两篇文章的思路,接下来我们应该思考一个问题:

如何判断我们应该用前序还是中序还是后序遍历的框架? 那么本文就针对这个问题,用一个例子来讲清楚, 根据题意,思考一个二叉树节点需要做什么,到底用什么遍历顺序就清楚了。

# 寻找重复子树

这是 LeetCode 第 652 题:寻找重复子树 (opens new window)

img

我来简单解释一下题目,输入的是一棵二叉树的根节点 root,返回的是一个列表,里面装着若干个二叉树节点,这些节点对应的子树在原二叉树中是重复存在的。

举个例子来说,比如输入如下二叉树:

img

首先节点 4 本身可以作为一棵子树,且二叉树中有多个节点 4:

img

类似的,还存在着两棵以 2 为根的重复子树:

img

那么我们返回的列表中就应该有两个 TreeNode,值分别为 4 和 2(具体是哪个节点无所谓)。

这种题目该怎么做呢? 还是老套路,先思考,对于某个节点,它应该做什么。

比如你站在图中这个 节点 2 上:

img

如果你想知道以自己为根的子树是不是重复的,是否应该被加入结果列表中,你需要知道什么信息?

你需要知道以下两点:

  • 以我为根的这棵二叉树(子树)长啥样?
  • 以其他节点为根的子树都长啥样

这叫知己知彼,我得知道自己是啥样,还有别人是啥样,才能知道有没有人和自己重复。

# 以我为根的二叉树长啥样

其实看到这个问题就可以判断本题要用「后序遍历」框架来解决:

function traverse(root: TreeNode): void {
  traverse(root.left)
  traverse(root.right)
  // 解法代码位置
}

为什么?因为我们要知道以自己为根的子树长啥样,是不是得先知道左右子树长啥样,再加上自己就构成了一棵完整的子树,综上这不就是一个「后序遍历」框架吗。

我们都知道,在 JS 中的不同的引用变量即使长得一模一样,它们之间也是不相等的,因此我们需要将引用变量转换成基本变量,这就是「二叉树的序列化与反序列化」。

所以,我们可以通过拼接字符串的方式把二叉树序列化:

function traverse(root: TreeNode): string {
  // 对于空节点,我们可以用一个特殊的字符串来表示
  if(!root) return '#'
  
  // 将左右子树序列化成字符串
  const left = traverse(root.left)
  const right = traverse(root.right)
  // 后序遍历代码位置
  // 左右子树加上自己就是以自己为根的二叉树序列化结果
  const subTree = `${left},${right},${root.val}`
  return subTree
}

这样,我们第一个问题就解决了,对于每个节点,递归函数中的 subTree 变量就可以描述以该节点为根的二叉树。

# 以其他节点为根的二叉树长啥样

非常简单,我们只需要把每一次序列化之后的结果 memo 起来,然后在通过判断是否有相同的,即可判断出是否有重复:

function findDuplicateSubtrees(root: TreeNode): TreeNode[] {
  const memo = new Map<string, number>()
  const res: TreeNode[] = []
  
  function traverse(root: TreeNode | null): string {
    if(!root) return '#'
    
    const left = traverse(root.left)
    const right = traverse(root.right)
    
    const subTree = `${left},${right},${root.val}`
    const count = memo.get(subTree) || 0
    if (count === 1) {
      res.push(root)
    }
    memo.set(subTree, count + 1)

    return subTree
  }
  
  traverse(root)
  
  return res
}