235. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T
的两个结点 p
、q
,最近公共祖先表示为一个结点 x
,满足 x
是 p
、q
的祖先且 x
的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉搜索树中。
思路
做过二叉树:236. 二叉树的最近公共祖先题目的同学应该知道,利用回溯从底向上搜索,遇到一个节点的左子树里有p
,右子树里有q
,那么当前节点就是最近公共祖先。
那么本题是二叉搜索树,二叉搜索树是有序的,那得好好利用一下这个特点。
在有序树里,如果判断一个节点的左子树里有p
,右子树里有q
呢?
因为是有序树,所以 如果 中间节点是 q
和 p
的公共祖先,那么 中节点的数值 一定是在 [p, q]
区间的。即 中节点 > p && 中节点 < q
或者 中节点 > q && 中节点 < p
。
那么只要从上到下去遍历,遇到 当前节点是数值在[p, q]
区间中则一定可以说明该节点就是p
和 q
的公共祖先。 那问题来了,一定是最近公共祖先吗?
如图,我们从根节点搜索,第一次遇到 当前节点是数值在[q, p]
区间中,即 节点5
,此时可以说明 q
和 p
一定分别存在于 节点 5的左子树
,和右子树
中。
此时节点5
是不是最近公共祖先? 如果 从节点5
继续向左遍历,那么将错过成为p
的祖先, 如果从节点5
继续向右遍历则错过成为q
的祖先。
所以当我们从上向下去递归遍历,第一次遇到 当前节点是数值在[q, p]
区间中,那么当前就是 q
和p
的最近公共祖先。
理解这一点,本题就很好解了。
而递归遍历顺序,本题就不涉及到 前中后序了(这里没有中节点的处理逻辑,遍历顺序无所谓了)。
如图所示:p
为节点6
,q
为节点9
可以看出直接按照指定的方向,就可以找到节点8
,为最近公共祖先,而且不需要遍历整棵树,找到结果直接返回!
递归法
递归三部曲如下:
1.确定递归函数返回值以及参数
参数就是当前节点,以及两个结点 p
、q
。返回值是要返回最近公共祖先,所以是*TreeNode
。和题目所给函数一致,所以不用额外定义辅助函数了。
代码如下:
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {}
2.确定终止条件
遇到空返回就可以了,此外如果找到p
或者q
了也可以返回,代码如下:
if root == nil {
return nil
}
if root == p || root == q {
return root
}
其实都不需要遇到空这个终止条件,因为题目中说了p
、q
为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。
3.确定单层递归的逻辑
在遍历二叉搜索树的时候就是寻找区间(p.Val, q.Val)
(注意这里是左开右开)
那么如果 root.Val
大于 p.Val
,同时 root.Val
大于q.Val
,那么就应该向左遍历(说明目标区间在左子树上)。
如果 root.Val
小于 p.Val
,同时 root.Val
小于 q.Val
,那么就应该向右遍历(目标区间在右子树)。
需要注意的是此时不知道p
和q
谁大,所以两个都要判断
剩下的情况,就是当前节点在区间中,那么当前节点就是最近公共祖先了,直接返回当前节点。
代码如下:
if root.Val > p.Val && root.Val > q.Val { // 公共祖先在左子树
return lowestCommonAncestor(root.Left,p,q)
}
if root.Val < p.Val && root.Val < q.Val { // 公共祖先在右子树
return lowestCommonAncestor(root.Right,p,q)
}
return root // 当前节点就是公共祖先
细心的同学会发现,在这里调用递归函数的地方,把递归函数的返回值继续直接return
了。
在二叉树:236. 二叉树的最近公共祖先中,如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树。
搜索一条边的写法:
if (递归函数(root.Left)) {return XX}
if (递归函数(root.Right)) {return XX}
搜索整个树写法:
left = 递归函数(root.Left) // 左
right = 递归函数(root.Right) // 右
left与right的逻辑处理 // 中
本题就是标准的搜索一条边的写法,遇到递归函数的返回值,如果不为空,可以立刻返回,不需要再去关心右子树的情况了。
那么整体递归Go
代码如下:
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == nil {
return nil
}
if root == p || root == q {
return root
}
if root.Val > p.Val && root.Val > q.Val {
return lowestCommonAncestor(root.Left,p,q)
}
if root.Val < p.Val && root.Val < q.Val {
return lowestCommonAncestor(root.Right,p,q)
}
return root
}
迭代法
利用其有序性,迭代的方式还是比较简单的,解题思路在递归中已经分析了。
迭代代码如下:
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
for root != nil {
if root.Val > p.Val && root.Val > q.Val {
root = root.Left
} else if root.Val < p.Val && root.Val < q.Val {
root = root.Right
} else {
return root
}
}
return nil
}
灵魂拷问:是不是又被简单的迭代法感动到痛哭流涕?
总结
对于二叉搜索树的最近祖先问题,其实要比普通二叉树公共祖先问题简单的多。
不用使用回溯,二叉搜索树自带方向性,可以方便的从上向下查找目标区间,遇到目标区间内的节点,直接返回。
最后给出了对应的迭代法,二叉搜索树的迭代法甚至比递归更容易理解,也是因为其有序性(自带方向性),按照目标区间找就行了。