652. Find Duplicate Subtrees
给定root,查找树内的重复子树,返回所有重复子树列表,列表去重
一些有意思的东西
一个问题引出了很多有意思的东西。
- 元组序列化,python中可以方便使用的技巧,和string序列化互补
- 性能测试,timeit工具包
- 对象编号,借用dict工具对复杂对象编号,常用的技巧
- defaultdict的使用,一个好用的字典
- frozenset和tuple的hash原理,前者是缓存了hash,后者是计算,和元素的内容有关,无缓存
v 1
思路:
后序遍历描述树的样子(满树后序唯一),map记录其他树的样子
-
时间复杂度:
递归树深度与树的平衡性有关,最差情况下深度为N,对于每一层的所有分支,算法复杂度为O(N)——来自hash函数计算,它和子树的节点数量有关,左右子树结合和为N。所以,这个算法是明显的O(N^2)算法。理想情况下,我们可以期待它O(logN N)。
-
空间复杂度:
N个节点进入dict,O(N)
-
节点。
"""
version 1
-
运行时间:
Runtime: 52 ms, faster than 81.61% of Python online submissions for Find Duplicate Subtrees.
Memory Usage: 23.2 MB, less than 62.54% of Python online submissions for Find Duplicate Subtrees.
"""
class Solution(object):
def findDuplicateSubtrees(self, root):
sel, vis = {}, set()
def trav(root):
if not root: return '#'
left = trav(root.left)
right = trav(root.right)
cur = left + ',' + right + ',' + str(root.val)
if cur in vis:
sel[cur] = root
else:
vis.add(cur)
return cur
trav(root)
return list(sel.values())
v2
思路:
改进version 1提到的重复hash问题。这个问题有点意思,比较容易绕晕,其实核心是,子树的hash值能否缓存,直接作为该节点的hash值的计算——这样就不用每次都计算子树hash了。
对比下面两个版本,tuple实现和frozenset实现,核心的区别是,frozenset会缓存hash值,而它的hash值计算是取出所有元素的hash值进行计算,而tuple的hash和元素内容相关,没有缓存。可以参考frozenset的hash实现。
-
空间复杂度:
关于该版本的空间复杂度,由于dict需要保存每一个子树的所有节点,对于每一层所有节点子树节点的和为N,因而又是和树的深度有关,最差O(N^2),否则O(N logN)
-
时间复杂度:
看起来每次都能在O(1)的时间内计算完成hash值,但隐藏的隐患来自hash函数本身,它只是摊还分析意义上的O(1),在极端情况下,不断出现hash冲突,则还是O(N)
重复hash:
效率低的原因之一是hash重复计算,例如对于root,即使子树的hash已经计算完成,仍然需要计算整颗数的所有节点
'''
version 2
-
-
ref:
version2 源码,StefanPochmann的大佬,分析非常精彩。 https://leetcode.com/problems/find-duplicate-subtrees/discuss/106016/O(n)-time-and-space-lots-of-analysis
frozenset hash函数:https://stackoverflow.com/questions/20832279/python-frozenset-hashing-algorithm-implementation
'''
class Solution(object):
def findDuplicateSubtrees(self, root):
def tuplify(root):
if root:
tuple = root.val, tuplify(root.left), tuplify(root.right)
trees[tuple].append(root)
return tuple
trees = collections.defaultdict(list)
tuplify(root)
return [roots[0] for roots in trees.values() if roots[1:]]
class Solution(object):
def findDuplicateSubtrees(self, root):
def convert(root):
if root:
result = frozenset([(root.val, convert(root.left), convert(root.right))])
trees[result].append(root)
return result
trees = collections.defaultdict(list)
convert(root)
return [roots[0] for roots in trees.values() if roots[1:]]
V3
思路:
解决上述问题,其实有个常用的技巧,编号。以下的实现极为精彩。
'''
version 3
-
ref:
https://leetcode.com/problems/find-duplicate-subtrees/discuss/106016/O(n)-time-and-space-lots-of-analysis
'''
class Solution(object):
def findDuplicateSubtrees(self, root, heights=[]):
def getid(root):
if root:
id = treeid[root.val, getid(root.left), getid(root.right)]
trees[id].append(root)
return id
trees = collections.defaultdict(list)
# 不声明默认值
treeid = collections.defaultdict()
# 将它绑定它自己的size函数,每次添加元素的时候,默认等于一个新的编号
treeid.default_factory = treeid.__len__
getid(root)
return [roots[0] for roots in trees.values() if roots[1:]]
REF
- version2 3 源码,StefanPochmann,分析得尤其精彩。 https://leetcode.com/problems/find-duplicate-subtrees/discuss/106016/O(n)-time-and-space-lots-of-analysis
- frozenset hash函数:https://stackoverflow.com/questions/20832279/python-frozenset-hashing-algorithm-implementation