LeetCode 记录-寻找重复的子树
我的解法
本题我没有想出一个自己觉得高效可行的解法,但还是记录一下自己的思路,用来查漏补缺。
思路
首先,我看到这是树的题目,而且是中等难度的题目,估计是使用 DFS 或者 BFS 算法就可以解决。
然后我去看示例。想找到合适的逻辑在一遍 DFS 或者 BFS 里找到所有重复的子树。但我始终没想到如何快速的对比两颗子树是否相同。
本来是想两边递归去对比结构和值,但觉得这样时间复杂度实在太高,不太合理。所以就放弃自己做了。
官方解法 1:使用序列化进行唯一标识
思路
一种容易想到的方法是将每一棵子树都「序列化」成一个字符串,并且保证:
相同的子树会被序列化成相同的子串; 不同的子树会被序列化成不同的子串。 那么我们只要使用一个哈希表存储所有子树的序列化结果,如果某一个结果出现了超过一次,我们就发现了一类重复子树。
序列化一棵二叉树的方法可以参考「剑指 Offer 37. 序列化二叉树」的官方题解,这里也给出两种常用的序列化方法:
第一种方法是使用层序遍历的方法进行序列化,例如示例 1 中的二叉树可以序列化为:
1,2,3,4,null,2,4,null,null,4
这也是力扣平台上测试代码时输入一棵二叉树的默认方法。
第二种方法是使用递归的方法进行序列化。我们可以将一棵以 x 为根节点值的子树序列化为:
x(左子树的序列化结果)(右子树的序列化结果)
左右子树分别递归地进行序列化。如果子树为空,那么序列化结果为空串。示例 1 中的二叉树可以序列化为:
1(2(4()())())(3(2(4()())())(4()()))
下面的代码使用的是第二种方法。我们需要使用一个哈希映射 seen 存储序列到子树的映射。如果在计算序列时,通过 seen 查找到了已经出现过的序列,那么就可以把对应的子树放到哈希集合 repeat 中,这样就可以保证同一类的重复子树只会被存储在答案中一次。
使用序列化来表示一棵树的唯一性是我没有想到的。这样就可以在一边 dfs 里面去生成所有子树的唯一标识,并判断出重复的子树。
代码
const findDuplicateSubtrees = (root) => {
const seen = new Map();
const repeat = new Set();
const dfs = (node) => {
if (!node) {
return "";
}
let sb = "";
sb += node.val;
sb += "(";
if (node.left) {
sb += dfs(node.left);
}
sb += ")(";
if (node.right) {
sb += dfs(node.right);
}
sb += ")";
if (seen.has(sb)) {
repeat.add(seen.get(sb));
} else {
seen.set(sb, node);
}
return sb;
};
dfs(root);
return [...repeat];
};
复杂度分析
时间复杂度
O(),其中 n 指树中节点的数目。在最坏的情况下,树呈现链状的结构,而每一个节点都会在其左右子树序列的基础上最多再增加 9 个字符(两组括号以及节点本身的值)。那么所有子树的序列长度之和为。构造出这些序列就需要的时间。
说实话,我不太理解这里的时间复杂度为什么是 O(),我理解它官方解释这个序列长之和,是将每颗子树都重新调用了一边 dfs,等于是每颗子树都重新计算了一边。但这里其实并没有。我考虑最坏的情况,就是树呈现向右的链状结构,当在 root 调用了 dfs 后,在中间运行sb+=dfs(node.right)的时候,会一层层往下,直到最后一层(leaf)。然后从 leaf 开始返回,前面一层都是在子树序列的基础上加至多 9 个字符,所以我理解时间复杂度应该是 O()。
空间复杂度
O(),即为哈希表需要使用的空间
我觉得是 O(n)。 但也不太确定,等后续有时间再深入研究一下。
官方解法 2:使用三元组进行唯一表示
思路
通过方法一中的递归序列化技巧,我们可以进一步进行优化。
我们可以用一个三元组直接表示一棵子树,即 ,它们分别表示:
根节点的值为 x;
左子树的序号为 l;
右子树的序号为 r。
这里的「序号」指的是:每当我们发现一棵新的子树,就给这棵子树一个序号,用来表示其结构。那么两棵树是重复的,当且仅当它们对应的三元组完全相同。
使用「序号」的好处在于同时减少了时间复杂度和空间复杂度。方法一的瓶颈在于生成的序列会变得非常长,而使用序号替换整个左子树和右子树的序列,可以使得每一个节点只使用常数大小的空间。
代码
var findDuplicateSubtrees = function (root) {
const seen = new Map();
const repeat = new Set();
let idx = 0;
const dfs = (node) => {
if (!node) {
return 0;
}
const stx = [node.val, dfs(node.left), dfs(node.right)].toString();
if (seen.has(stx)) {
const pair = seen.get(stx);
repeat.add(pair[0]);
return pair[1];
} else {
seen.set(stx, [node, ++idx]);
return idx;
}
};
dfs(root);
return [...repeat];
};
时间复杂度
O(),其中 n 是书中节点的数目。
我理解,方法二和方法一的时间复杂度应该是一样的,都是采用递归的方式生成唯一序列号。
空间复杂度
O(),即为哈希表需要使用的空间