子树结构
需要检查子树结构的题都需要一个 helper 函数,带着两个 root,递归解决。 如果默认没给,就自己写一个。
子树类问题如果出现不连续,或者需要多个子树信息的时候,自定义 SubtreeTuple 是最合适的选择。
subtree size (int);
在 size / count 这种非负情况下,还可以把符号当 flag 用;
subtree min/max (int);
这种递归结构中先处理完 left / right 再来汇总结果的,其实就是 post-order traversal. 这点在 search 类 dfs 中也很常见,比如安卓解锁,数解锁方式数量的做法。
Tree 类问题另一个递归转迭代的思路就是,观察下递归是 pre-order, in-order 还是 post-order,然后对应的靠 stack 保存状态,模拟整个过程即可。
Trivial problem.
public class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == q) return true;
if(p == null || q == null) return false;
if(p.val != q.val) return false;
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}迭代写法,思路很简单,就是按照同一个顺序做 DFS (pre-order),每步上检查下元素值和 stack 大小就行,如果两个树一样,那么在迭代过程中所有的状态也都应该是一样的。
这个写法的子树访问路线以及顺序,和递归的写法是完全一样的。也就是说,其实这个迭代写法的思路,是完全建立在递归写法的代码上:
递归中先对两个 root 判断 (中)
然后递归处理两棵子树:
先左,后右;
这就是 pre-order 嘛。
这题和上一题非常像,都是给你两个 root,去判断他们的结构,考虑到要做 symmetric tree 所以每次递归的时候参数是 p.left 和 q.right ,而不是每次都同一方向。
Bonus : 用迭代。
这题用迭代的写法和思路,思路像 google onsite 面经里出现过的,交替输出 kth level 的节点。 Queue(deque) 的写法很好写,空间优化上就需要用 push 顺序相反的两个 stack 了。
具体思路参考了下论坛,我当初的写法是用两个 queue 存两个 level,对于每个 level 用类似 two pointer 的方式去检查元素,不过因为 queue 的大小一直变动,而且 queue 的 implementation 直接 access by index 也不太方便,写起来很麻烦。
比较好的思路是根据题意,直接把每个 level 拆成两个 queue,像 segment tree 似的,一个 queue 对应左子树,一个 queue 对应右子树,插入的时候,如果 q1 放左节点,q2 就放右节点,vice versa. 如果结果正确的话,两个 queue 里面的元素完全一样。
同样一题,用 stack 省空间的做法,其实和两个 queue 的思路基本完全一样。。
两种做法都是把树 traverse 了一遍,只不过顺序不同。有的时候我觉得,各种 tree 的 traversal,其实就像花式 for loop 一个 array 一样。
这题的双 stack 代码和 Same Tree 基本一样,只是 push 顺序不同而已。其代码的相似与不同可以追溯到各自的递归解法中,Tree 类问题递归结构是迭代写法的指引。
这题和 Binary Tree Maximum Path Sum 的联系非常密切,要一起研究。
这题因为执着于用一个 helper 函数同时做 “验证BST” 和 “数子树大小”的工作,做了很多次失败提交。
需要传递的信息太多就自定义 SubtreeTuple
同时这题的定义也稍微有点模糊,正确定义是:如果整棵树都是 BST,那么返回 tree size; 反之返回左右子树的最大 size ,而不考虑 root. 这个 “不考虑root” 稍微有点歧义,因为如果右子树不是 BST,左子树是 BST,并且 root.val 大于左子树的情况下,按理讲算上 root 也是一个 BST 的,只是这题我们不考虑而已。
假如我们只用一个返回 int 的函数来层层递归,需要处理这些问题:
正解的的子树很可能和 root 以及上层的 node 不是连续的;
如果某个子树不是 BST (size = 0),也意味着上层的所有 node 都不能包含这个子树;
给定 root,要验证这个 root 是否在合理的左右子树极值区间内;
这些问题都不是一个 int 就能完美解决的。
时间复杂度 O(n log n),一次检查 BST/getSize 为O(n),最多重复调用 O(log n) 次
自定义 SubtreeTuple 的写法:
自底向上连续传递的只有 size,代表这个 tree 下面最大的 BST subtree size.
size 的绝对值代表以这个 node 为 tree root 的最大 BST subtree 大小;
size 的符号代表到底是不是 BST.
当我们有一个一定非负的变量时(在这里是 size),符号就成了 boolean 一样的可利用信息。
时间复杂度 O(n),每个 node 只访问一次,没有重复递归调用。
对于 subtree 特征以及 flag 的处理上,和上一道题可以用完全一样的套路。
唯一的不同在于,我们要返回的是总数,而不是 max ,所以要左右加一起才行。
如果一个 Tree 是 complete tree,那所有的 subtree 也都是 complete tree.
直接扫肯定 TLE .. 要利用好 complete tree 的定义和性质。
参考了一下论坛之后,写了这个解居然也 TLE,都 O(log n * log n) 了
能 AC 的代码是论坛上的这个:
如果一个 Tree 是 complete tree,那所有的 subtree 也都是 complete tree.
1 << l 其实包含了每一层上加一(root) 的步骤。
按照这个代码的执行方式,每一次的左右子树必定有一个是 prefect tree,于是可以根据 depth 决定下一步处理哪棵。
(G) 面经
http://www.1point3acres.com/bbs/forum.php?mod=viewthread&tid=197372&highlight=google
Given a binary tree, find if there are two subtrees that are the same. (i.e. the tree structures are the same; the values on all corresponding nodes are the same). You should find the largest subtree and don’t use brute force.
贴个 hx 的思路,仔细讨论和思考之后觉得靠谱。
假设identical的子树share一个group id. NULL的group id是0.其它子树的group id按照group在后序遍历中第一次出现的时间顺序决定。 这样我们可以一边后序遍历子树,一边建两个hashmap:
(root value, group id of left, group id of right) -> group id of root group id -> subtrees of this group
这样我们做一遍后序遍历同时维护层数最高、subtree数量>=2的group id就行了。
这个其实跟用整棵子树的serialization作为key做法差不多,只是对key作了压缩。
如果只是要 size 的话,一个 hashmap 就够。

按着这个思路试着写了一下,附带两个 test case,返回的是最大 subtree 的 size.
第一个 comment 掉的 test case 和图里的一样,返回 4 .
第二个是左右完全一样的 binary tree,中间缺一个 leaf node,返回 6.
Last updated
Was this helpful?