你们好,我是Johngo!python
这篇文章是「讲透树」系列的第 4 篇文章,也是「树」专题中非自顶向下这类题目的一个复盘总结。git
前 3 讲的连接地址在这里了:github
讲透树1 | 树的基础遍历专题 mp.weixin.qq.com/s/nTB41DvE7…算法
讲透树2 | 树的遍历复盘专题 mp.weixin.qq.com/s/MkCF5TaR1…markdown
讲透树3 | 自顶向下类别题目复盘专题 mp.weixin.qq.com/s/9U4P5zZIF…oop
不一样的类型已经都进行了各自的总结,相信在后面记忆不太清晰的时候,返回头来看看,这些文档又会和思惟激起灵魂碰撞!post
一块儿刷题的小伙伴们,复盘仍是要唠叨一句,记录思路,在记录的过程当中,又一次深入体会!好比说ui
相信在后面记忆不太清晰的时候,返回头来看看,这些文档又会和思惟激起灵魂碰撞!spa
直观的先看看本文的所处的一个进度code
相比较于「自顶向下」的题目,「非自顶向下」的题目相比较下来不太适合用 BFS 来解决,很是适合于用 DFS 来解决。并且相较于「自顶向下」的题目,「非自顶向下」的题目难度会大一点。
关于 BFS 的解题思路对于「自顶向下」这类型题目是很是友好的,思路清晰。可查看这里mp.weixin.qq.com/s/9U4P5zZIF…
本篇文章涉及到的题目
687.最长同值路径:leetcode-cn.com/problems/lo…
124.二叉树中的最大路径和:leetcode-cn.com/problems/bi…
543.二叉树的直径:leetcode-cn.com/problems/di…
652.寻找重复的子树:leetcode-cn.com/problems/fi…
236.二叉树的最近公共祖先:leetcode-cn.com/problems/lo…
235.二叉搜索树的最近公共祖先:leetcode-cn.com/problems/lo…
本篇着重说「非自顶向下」的思路以及涉及到的 LeetCode 题目。
在这里总结一句,其实就是下面三种基础递归遍历的变形,这个变形来源于题目的要求,但本质都是递归遍历。
这里我再一次把三种二叉树递归的代码贴出来:
二叉树的先序遍历
def pre_order_traverse(self, head):
if head is None:
return
print(head.value, end=" ")
self.pre_order_traverse(head.left)
self.pre_order_traverse(head.right)
复制代码
二叉树的中序遍历
def in_order_traverse(self, head):
if head is None:
return
self.in_order_traverse(head.left)
print(head.value, end=" ")
self.in_order_traverse(head.right)
复制代码
二叉树的后续遍历
def post_order_traverse(self, head):
if head is None:
return
self.post_order_traverse(head.left)
self.post_order_traverse(head.right)
print(head.value, end=" ")
复制代码
使人整洁的舒服...
使人难以理解的不舒服...
对的,就是这种整洁的代码,在处理起二叉树的问题来,着实是游刃有余的。
LeetCode687.最长同值路径
题目连接:leetcode-cn.com/problems/lo…
GitHub解答:github.com/xiaozhutec/…
这就是一个后续的递归遍历,后续递归遍历完,不进行结点的打印,而是进行结点值和相关孩子结点的比对判断:
def longestUnivaluePath_dfs(self, root):
self.length = 0
def dfs(root):
if not root:
return 0
left_len = dfs(root.left)
right_len = dfs(root.right)
left_tag = right_tag = 0
if root.left and root.left.val == root.val:
left_tag = left_len + 1
if root.right and root.right.val == root.val:
right_tag = right_len + 1
# max(最大长度, 左子树最大长度+右子树最大长度)
self.length = max(self.length, left_tag + right_tag)
return max(left_tag, right_tag)
dfs(root)
return self.length
复制代码
看到了吧,其实就是在left_len = dfs(root.left)
和 right_len = dfs(root.right)
以后,进行当前结点和左右孩子结点的结点值比对,若是相同了,很显然是 +1
。
另外,就题论题。
下面会介绍一种很重要的思路,在不少题目中都会遇到。
这个题目有很关键的一点是self.length = max(self.length, left_tag + right_tag)
,因为题目不要求必定通过根结点。那么,此时若是左孩子的结点值和根结点的结点值以及右孩子的结点值和根结点的结点值都相同,此时会是下面的一种状况。
谈论红色框内的结点:
灰色结点的左孩子length=1
,灰色结点的右孩子length=0
;
再往上层看,灰色结点 5 的左孩子也是 5,那么left_tag=2
,灰色结点 5 的右孩子也是 5,那么right_tag=1
;
因此,length = max(length, left_tag + right_tag)
,那么,获得的结果是length=3
,也就是黑色所示的粗边。
以上,利用后续遍历的递归思想,就能够把问题解决了!
强调:上面的思路常常会遇到,很重要!
再看一个例子:
LeetCode124.二叉树中的最大路径和
题目连接:leetcode-cn.com/problems/bi…
GitHub解答:github.com/xiaozhutec/…
从二叉树中找出一个最大的路径,就是找到一个连续结点值最大的一个路径。
能够参考上一个题目的思路,依然采用后续遍历的思路,进行解决。
def maxPathSum(self, root):
self.length = float("-inf")
def dfs(root):
if not root:
return 0
left_len = max(dfs(root.left), 0) # 只有贡献值大于 0 的,才会选取对应的子结构
right_len = max(dfs(root.right), 0)
inner_max = left_len + root.val + right_len
self.length = max(self.length, inner_max) # 计算当前结点所在子树的最大路径
return max(left_len, right_len) + root.val # 返回当前结点左右子结构的最大路径
dfs(root)
return self.length
复制代码
思路依然仍是比较轻松的吧,后续遍历的典型变形。
看这句self.length = max(self.length, inner_max)
,是否是和上一题殊途同归,也是比较上一层递归的 length
和本层中 inner_max
做比较,找出最大值。
两点注意
- 每次递归,须要计算 length 的值,以保证每次递归获得最大路径和
- 每次递归的返回值必定是左子树和右子树中的最大值+当前结点值
再来看一道题目:
LeetCode543.二叉树的直径
题目连接:leetcode-cn.com/problems/di…
GitHub解答:github.com/xiaozhutec/…
题目要求返回一颗二叉树的直径,其实仍是一个后续递归遍历。
依然仍是采用上述「LeetCode687」中介绍的重要思路进行求解。
def diameterOfBinaryTree(self, root):
self.path_length = 0
def dfs(root):
if not root:
return 0
left_len = dfs(root.left)
right_len = dfs(root.right)
left_tag = right_tag = 0
if root.left:
left_tag = left_len + 1
if root.right:
right_tag = right_len + 1
self.path_length = max(self.path_length, left_tag + right_tag)
return max(left_tag, right_tag)
dfs(root)
return self.path_length
复制代码
依然是后续遍历,递归调用后,采用max(self.path_length, left_tag + right_tag)
,比较上一层返回的 length
和左右子树中的最大值。
依然是两点:
每次递归计算 length 的值,找当前结点最长路径
返回左右子树的最大路径长度
上面说了三个题目,大体思路都相同,不一样的是一个题目计算路径和、两个题目是计算路径长度。
比较重要的那个点,在「LeetCode687」中介绍的重要思路,这个是最重要的一环,务必理解加以运用!
见得太多,用的也太多!
最后我们再看两个题目:
LeetCode236.二叉树的最近公共祖先
题目连接:leetcode-cn.com/problems/lo…
GitHub解答:github.com/xiaozhutec/…
这个是给定两个结点,求解他们的最近公共祖先,也是一个递归的问题。
def lowestCommonAncestor(self, root, p, q):
if not root or root == p or root == q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if not left: return right
if not right: return left
return root
复制代码
其实说白了仍是递归的思路进行求解,不过这里须要注意四种状况:
1.无左孩子 and 无右孩子,直接返回根结点
2.无左孩子 and 有右孩子,直接返回根结点
3.有左孩子 and 无右孩子,直接返回根结点
4.有左孩子 and 有右孩子,继续递归遍历
因此,仍是递归的变形进行解决,惟一有一点就是须要深刻思考,深刻体会。
LeetCode235.二叉搜索树的最近公共祖先
题目连接:leetcode-cn.com/problems/lo…
GitHub解答:github.com/xiaozhutec/…
这个题目是上一个题目的变形,难度下降,由于要考察的二叉树由通常结构变为了二叉搜索树。
所以,利用搜索二叉树的性质,稍进行判断就能够解决。
def lowestCommonAncestor(self, root, p, q):
if p.val < root.val and q.val < root.val:
return self.lowestCommonAncestor(root.left, p, q)
if p.val > root.val and q.val > root.val:
return self.lowestCommonAncestor(root.right, p, q)
return root.val
复制代码
是否是很简单整洁的代码。
第一个if
,就是若是p
和q
都小于当前结点,那么直接到左子树进行递归调用;
第二个if
,就是若是p
和q
都大于当前结点,那么直接到右子树进行递归调用;
若是都不知足,即就是所须要的答案。
想一想看,是否是?利用搜索二叉树的结构,其实思路很清晰简单。
好了,「树」这一阶段的刷题已经进入尾声,但我理解在第一轮刷题完毕以后,还会继续回来从新来过。
下一阶段就到了「动态规划」的一个刷题时间窗口了,递归和动态规划都是出了名的比较难学,可是它们的思想深刻贯彻整个算法的各个节点。因此,坚持把这几部份内容都进行一个总结,让你们把思路逐渐清晰起来!
代码和本文的文档都在 github.com/xiaozhutec/… star。谢过你们!