1、左边是BFS,按照层进行搜索;
2、图右边是DFS,先一路走到底,然后再回头搜索。
在这个策略中,我们从根延伸到某一片叶子,然后再返回另一个分支。根据根节点,左节点,右节点的相对顺序,DFS
还可以分为前序,中序,后序。
![82da7dbaabf745520257017ac4eb7cda.png](https://img-blog.csdnimg.cn/img_convert/82da7dbaabf745520257017ac4eb7cda.png)
BFS的实现
BFS使用队列,把每个还没有搜索到的点依次放入队列,然后再弹出队列的头部元素当做当前遍历点。BFS总共有两个模板:
- 如果不需要确定当前遍历到了哪一层,BFS模板如下。
while queue 不空:
cur = queue.pop()
for 节点 in cur的所有相邻节点:
if 该节点有效且未访问过:
queue.push(该节点)
2.如果要确定当前遍历到了哪一层,BFS模板如下。
这里增加了level表示当前遍历到二叉树中的哪一层了,也可以理解为在一个图中,现在已经走了多少步了。size表示在当前遍历层有多少个元素,也就是队列中的元素数,我们把这些元素一次性遍历完,即把当前层的所有元素都向外走了一步。
level = 0
while queue 不空:
size = queue.size()
while (size --) {
cur = queue.pop()
for 节点 in cur的所有相邻节点:
if 该节点有效且未被访问过:
queue.push(该节点)
}
level ++;
102. 二叉树的层序遍历
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7],
3
/
9 20
/
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
一、迭代实现
广度优先遍历是按层层推进的方式,遍历每一层的节点。题目要求的是返回每一层的节点值,所以这题用广度优先来做非常合适。
广度优先需要用队列作为辅助结构,我们先将根节点放到队列中,然后不断遍历队列。
![06419af914e01e84c3be2cd67e66ab72.png](https://img-blog.csdnimg.cn/img_convert/06419af914e01e84c3be2cd67e66ab72.png)
首先拿出根节点,如果左子树/右子树不为空,就将他们放入队列中。第一遍处理完后,根节点已经从队列中拿走了,而根节点的两个孩子已放入队列中了,现在队列中就有两个节点 2 和 5。
![0621b110a89a00e962972a45cd860b00.png](https://img-blog.csdnimg.cn/img_convert/0621b110a89a00e962972a45cd860b00.png)
第二次处理,会将 2 和 5 这两个节点从队列中拿走,然后再将 2 和 5 的子节点放入队列中,现在队列中就有三个节点 3,4,6。
![8512b87aea1efdbbad4fdb83fb0320ca.png](https://img-blog.csdnimg.cn/img_convert/8512b87aea1efdbbad4fdb83fb0320ca.png)
我们把每层遍历到的节点都放入到一个结果集中,最后返回这个结果集就可以了。
时间复杂度: O(n)
空间复杂度:O(n)
bfs解法:
class Solution(object):
def levelOrder(self, root):
"""
:type root: TreeNode
:rtype: List[List[int]]
"""
if not root:
return []
res = []
queue = [root]
while queue:
# 获取当前队列的长度,这个长度相当于 当前这一层的节点个数
size = len(queue)
tmp = []
# 将队列中的元素都拿出来(也就是获取这一层的节点),放到临时list中
# 如果节点的左/右子树不为空,也放入队列中
for _ in range(size):
r = queue.pop(0)
tmp.append(r.val)
if r.left:
queue.append(r.left)
if r.right:
queue.append(r.right)
res.append(tmp)
return res
二、递归实现
DFS
本题使用 DFS 同样能做。由于题目要求每一层的节点都是从左到右遍历,因此递归时也要先递归左子树、再递归右子树。
DFS 做本题的主要问题是: DFS 不是按照层次遍历的。为了让递归的过程中同一层的节点放到同一个列表中,在递归时要记录每个节点的深度 level。递归到新节点要把该节点放入 level 对应列表的末尾。
当遍历到一个新的深度 level,而最终结果 res 中还没有创建 level 对应的列表时,应该在 res 中新建一个列表用来保存该 level 的所有节点。
把这个二叉树的样子调整一下,摆成一个田字形的样子。田字形的每一层就对应一个 res 。
![489b7a85a15e57d6c1db74dbe6ea1be9.png](https://img-blog.csdnimg.cn/img_convert/489b7a85a15e57d6c1db74dbe6ea1be9.png)
按照深度优先的处理顺序,会先访问节点 1,再访问节点 2,接着是节点 3。
之后是第二列的 4 和 5,最后是第三列的 6。
每次递归的时候都需要带一个 level(表示当前的层数),也就对应那个田字格子中的第几行,如果当前行对应的 res 不存在,就加入一个空 res进去。
动态演示如下:
![b7cf794a42c802e1ca557f5bc533254c.png](https://img-blog.csdnimg.cn/img_convert/b7cf794a42c802e1ca557f5bc533254c.png)
根节点为第0层:
class Solution(object):
def levelOrder(self, root):
res = []
self.level(root, 0, res)
return res
def level(self, root, level, res):
if not root:
return
if len(res) == level: #只有在新的深度 level才会加[]
res.append([])
res[level].append(root.val)
if root.left:
self.level(root.left,level+1,res)
if root.right:
self.level(root.right, level + 1, res)
根节点为第1层:
class Solution(object):
def levelOrder(self, root):
if not root:
return []
res = []
def dfs(index,r):
# 假设res是[ [1],[2,3] ], index是3,就再插入一个空list放到res中
if len(res) < index:
res.append([])
# 将当前节点的值加入到res中,index代表当前层,假设index是3,节点值是99
# res是[ [1],[2,3] [4] ],加入后res就变为 [ [1],[2,3] [4,99] ]
res[index-1].append(r.val)
if r.left:
dfs(index+1,r.left)
if r.right:
dfs(index+1,r.right)
dfs(1,root)
return res
104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。示例:
给定二叉树 [3,9,20,null,null,15,7]
,
3
/
9 20
/
15 7
返回它的最大深度 3 。
一、递归实现(DFS)
很多二叉树的题目,用递归写起来就非常简单,这道题就是。
如果我们知道了左子树和右子树的最大深度
而左子树和右子树的最大深度又可以以同样的方式进行计算。
再来分析下递归的两个条件
- 递归终止条件:当节点为空时返回
- 再次递归计算 max( 左节点最大高度,右节点最大高度)+1
终止条件很好理解,节点为空了,就返回0,也就是高度为0。关键是第二句,这句可能不好理解。
我们看下面这个图,假设节点左边节点这一坨的高度是x,右边节点那一坨的高度是y
![b1a9f741a0b43fe5a7b8697c98550568.png](https://img-blog.csdnimg.cn/img_convert/b1a9f741a0b43fe5a7b8697c98550568.png)
我们需要比较X和Y的值谁大,也就是谁的高度更高,假设X这一坨更高。当我们得到了X的值后,还需要 +1。
+1的原因是,我们只得到了X的高度,但是整个树是由根节点,一坨X和一坨Y组成的。所以为了求得整个树的高度,还需要在X的基础上,再加上1,也就多加一个节点(根节点)。
动画演示:
![882c49543498eb9cfc588c4597704df6.gif](https://img-blog.csdnimg.cn/img_convert/882c49543498eb9cfc588c4597704df6.gif)
代码实现:
class Solution:
def maxDepth(self, root):
if root is None:
return 0
else:
left_height = self.maxDepth(root.left)
right_height = self.maxDepth(root.right)
return max(left_height,right_height)+1
复杂度分析
时间复杂度:
空间复杂度:
二、迭代实现
DFS实现,从上往下遍历,然后求得最深的一个节点的高度,就是整个树的高度了。
![77531f1c520f71708241b3fd2b63effe.png](https://img-blog.csdnimg.cn/img_convert/77531f1c520f71708241b3fd2b63effe.png)
上图中,我们假设每个节点都有一个附加参数key,key就是节点的高度,当遍历完整个树后,就可以求得最大的那个key了。
动画演示:
![9e5f2bb55c888a177aa15724cadc00e6.gif](https://img-blog.csdnimg.cn/img_convert/9e5f2bb55c888a177aa15724cadc00e6.gif)
DFS实现,从上往下遍历,得到最大的路径的深度,用栈实现(后进先出)
DFS与BFS有两点不同:
- 最后得到的深度不一定是最大深度,所以要用max判断(有多条从上往下的路径)
- DFS(先序遍历)节点右孩子先入栈,左孩子再入栈
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if root is None:
return 0
stack = [(1, root)]
depth = 0
while stack:
cur_dep, node = stack.pop()
depth = max(depth, cur_dep)
if node.right:
stack.append((cur_dep+1,node.right))
if node.left:
stack.append((cur_dep+1,node.left))
return depth
还可以是BFS实现,就是每一层从左到右扫描节点,每层节点都扫描完了再进入下一层,用队列实现(先进先出)
class Solution:
def maxDepth(self, root: TreeNode) -> int:
# BFS
if root is None:
return 0
queue = [(1, root)]
while queue:
depth, node = queue.pop(0)
if node.left:
queue.append((depth+1,node.left))
if node.right:
queue.append((depth+1,node.right))
return depth
230. 二叉搜索树中第K小的元素
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
说明:
你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/
1 4
2
输出: 1
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/
3 6
/
2 4
/
1
输出: 3
进阶:
如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化 kthSmallest 函数?
为了解决这个问题,可以使用 BST 的特性:BST 的中序遍历是升序序列。
方法一:递归(DFS)
算法:
通过构造 BST 的中序遍历序列,则第 k-1
个元素就是第 k
小的元素。
![5f3c50ceb9b57f8efbf8bedc2b0b3f3c.png](https://img-blog.csdnimg.cn/img_convert/5f3c50ceb9b57f8efbf8bedc2b0b3f3c.png)
class Solution:
def kthSmallest(self, root, k):
"""
:type root: TreeNode
:type k: int
:rtype: int
"""
def inorder(r):
return inorder(r.left) +[r.val] +inorder(r.right) if r else []
return inorder(root)[k-1]
复杂度分析
时间复杂度:O(N),遍历了整个树。
空间复杂度:O(N),用了一个数组存储中序序列。
方法二:迭代(DFS)
算法:
在栈的帮助下,可以将方法一的递归转换为迭代,这样可以加快速度,因为这样可以不用遍历整个树,可以在找到答案后停止。
![fd8ab1887115ce077909accb775d40db.png](https://img-blog.csdnimg.cn/img_convert/fd8ab1887115ce077909accb775d40db.png)
class Solution:
def kthSmallest(self, root, k):
"""
:type root: TreeNode
:type k: int
:rtype: int
"""
stack = []
while True:
while root:
stack.append(root)
root = root.left
root = stack.pop()
k -=1
if not k:
return root.val
root = root.right
复杂度分析
时间复杂度:O(H+k),其中 H 指的是树的高度,由于我们开始遍历之前,要先向下达到叶,当树是一个平衡树时:复杂度为
空间复杂度:O(H+k)。当树是一个平衡树时:
124.二叉树中的最大路径和
给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
示例 1:
输入:[1,2,3]
1
/
2 3
输出:6
示例 2:
输入:[-10,9,20,null,null,15,7]
-10
/
9 20
/
15 7
输出:42
方法一:递归(DFS)
限制条件:路径上的点不能重复使用(下图(a)(b))
比如: 1-2-4-8 和 1-2-5-9 就只能选择其一,因为 1-2 重复利用了
![ee2e8968ccabf1de4b9fcd4dd185c784.png](https://img-blog.csdnimg.cn/img_convert/ee2e8968ccabf1de4b9fcd4dd185c784.png)
(a)、(b) 以 节点1 作为 起始节点 的其中 2 条路径。(c) 以 节点2 作为 起始节点 的所有路径; (d) 以节点3 作为 起始节点 的路径
首先,考虑实现一个简化的函数
具体而言,该函数的计算如下。
- 空节点的最大贡献值等于 0。
- 非空节点的最大贡献值等于节点值与其子节点中的最大贡献值之和(对于叶节点而言,最大贡献值等于节点值)
例如,考虑如下二叉树。
-10
/
9 20
/
15 7
叶节点 9、15、7的最大贡献值分别为9、15、7。
得到叶节点的最大贡献值之后,再计算非叶节点的最大贡献值。节点 20 的最大贡献值等于
上述计算过程是递归的过程,因此,对根节点调用函数 DFS,即可得到每个节点的最大贡献值。
根据函数 DFS 得到每个节点的最大贡献值之后,如何得到二叉树的最大路径和?
对于二叉树中的一个节点,该节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值,如果子节点的最大贡献值为正,则计入该节点的最大路径和,否则不计入该节点的最大路径和。维护一个全局变量 maxSum 存储最大路径和,在递归过程中更新 maxSum 的值,最后得到的 maxSum 的值即为二叉树中的最大路径和。
我们用题目的示例来描述一下这个算法过程:(理解如何递归很关键)
> 1. 从 dfs(-10) 开始,
1.1 dfs(9):
1.1.1 左孩子为空;贡献为 0
1.1.2 右孩子为空,贡献为 0
1.1.3 更新 res = max (-∞,(9 + 0 + 0)) = 9
1.1.4 返回 dfs(9) = 9 + max(左孩子贡献,右孩子贡献)) = 9
1.2 dfs(20)
1.2.1 dfs(15):
1.2.1.1 左孩子为空;贡献为0
1.2.1.2 右孩子为空,贡献为0
1.2.1.3 更新 res = max(9, 15 + 0 + 0) = 15
1.2.1.4 返回 dfs(15) = 15 + 0 = 15
1.2.2 dfs(7):
1.2.2.1 左孩子为空;贡献为 0
1.2.2.2 右孩子为空,贡献为 0
1.2.2.3 更新 res = max(15, 7 + 0 + 0) = 15
1.2.2.4 返回 dfs(7) = 7 + 0 = 7
1.2.3 更新 res = max (15, 20 + dfs(15) + dfs(7) ) = 42
1.2.4 返回dfs(20) = 20 + max(15, 7) = 35
1.3 更新 res = max(42, -10 + dfs(9) + dfs(20) ) = max(42, 34) = 42
1.4 返回 dfs(-10) = -10 + max(9, 35) = 25 (当然这一步就没啥用了,已经有最终res)
所以最大路径和 res = 42
关键就是区分:
1. 当前节点最大路径和计算:以当前节点为起点的所有路径和
2. 当前节点对上一层的贡献:只能选择当前节点的最大的一条路径作为贡献,因为路径节点不可重复
代码:
class Solution:
def __init__(self):
self.maxSum = float("-inf")
def maxPathSum(self, root: TreeNode) -> int:
def maxGain(node):
if not node:
return 0
# 递归计算左右子节点的最大贡献值
# 只有在最大贡献值大于 0 时,才会选取对应子节点
leftGain = max(maxGain(node.left), 0)
rightGain = max(maxGain(node.right), 0)
# 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
priceNewpath = node.val + leftGain + rightGain
# 更新答案
self.maxSum = max(self.maxSum,priceNewpath)
# 返回节点的最大贡献值
return node.val + max(leftGain, rightGain)
maxGain(root)
return self.maxSum
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
self.max_path_sum = float('-inf')
def dfs(node):
if not node: # 边界情况
return 0
left = dfs(node.left) # 对左右节点dfs
right = dfs(node.right)
cur_max = max(
node.val,
node.val + left,
node.val + right,
)
# 更新全局变量
self.max_path_sum = max(self.max_path_sum, cur_max, node.val + left + right)
return cur_max
dfs(root)
return self.max_path_sum
235.二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
![e3b47187db8df97c21571fe05c14edea.png](https://img-blog.csdnimg.cn/img_convert/e3b47187db8df97c21571fe05c14edea.png)
示例 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 为不同节点且均存在于给定的二叉搜索树中。
方法一:两次遍历
思路与算法
注意到题目中给出的是一棵「二叉搜索树」,因此我们可以快速地找出树中的某个节点以及从根节点到该节点的路径,例如我们需要找到节点 p:
我们从根节点开始遍历;
- 如果当前节点就是 p,那么成功地找到了节点;
- 如果当前节点的值大于 p 的值,说明 p 应该在当前节点的左子树,因此将当前节点移动到它的左子节点;
- 如果当前节点的值小于 p 的值,说明 p 应该在当前节点的右子树,因此将当前节点移动到它的右子节点。
对于节点 q 同理。在寻找节点的过程中,我们可以顺便记录经过的节点,这样就得到了从根节点到被寻找节点的路径。
当我们分别得到了从根节点到 p 和 q 的路径之后,我们就可以很方便地找到它们的最近公共祖先了。显然,p 和 q 的最近公共祖先就是从根节点到它们路径上的「分岔点」,也就是最后一个相同的节点。因此,如果我们设从根节点到 p 的路径为数组
那么对应的节点就是「分岔点」,即 p 和 q 的最近公共祖先就是 「分岔点」.
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
def getPath(root:TreeNode,target:TreeNode) -> List[TreeNode]:
path = list()
node = root
while node !=target:
path.append(node)
if target.val < node.val:
node = node.left
else:
node = node.right
path.append(node)
return path
path_p = getPath(root,p)
path_q = getPath(root,q)
ancestor = None
for u , v in zip(path_p,path_q):
if u ==v:
ancestor = u
else:
break
return ancestor
方法二:一次遍历
根据二叉搜索树的特性,我们可以发现,这里其中有一定的规律:
- 如果 p 和 q 节点值均小于根节点值时,那么此时应该在根的左子树中找答案(如示例 2);
- 如果 p 和 q 节点值均大于根节点值时,那么应该在根的右子树中找答案。
上面两种情况,p 和 q 节点值要么都小于根节点值,要么都大于根节点值。其他情况该如何处理?
这里先罗列下可能的三种情况:
- p 和 q 分布在根节点的左右子树中;(如示例 1)
- p 是 q 的父节点,且 p 为根节点;
- q 是 p 的父节点,且 q 为根节点。
我们先说下这三者的结论,三者最终返回的都是根节点。
第一种情况可能比较好理解,这里 p 和 q 分布在根的左右子树,两者往上追溯祖先节点即是根节点。
第二种情况,这里先看下题目中的一个信息,如下:
- 一个节点也可以是它自己的祖先。
- 因为 p 是 q 的父节点,那么 p 可作为 q 的祖先,而 p 也可作为自身祖先。那么 p 就是 p 和 q 的最近公共祖先。此时 p 为根节点,那么返回的结果即是根节点。
第三种情况,与第二种情况同理。
这里,我们以示例 2 为例,以图示来理解下上面所述的分析:
![a12ef845b4261c2726c633d63e6f0609.png](https://img-blog.csdnimg.cn/img_convert/a12ef845b4261c2726c633d63e6f0609.png)
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
ancestor = root
while True:
if p.val<ancestor.val and q.val<ancestor.val :
ancestor = ancestor.left
elif p.val>ancestor.val and q.val>ancestor.val :
ancestor = ancestor.right
else:
break
return ancestor
递归(dfs)
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
def dfs(root):
nonlocal res, flag
if not root or flag:
return
if q.val < root.val and p.val < root.val:
dfs(root.left)
elif q.val > root.val and p.val > root.val:
dfs(root.right)
else:
res = root
flag = True
flag = False
res = TreeNode()
dfs(root)
return res
236.二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
![98695ce172620a6bb561bc13e87f0bf9.png](https://img-blog.csdnimg.cn/img_convert/98695ce172620a6bb561bc13e87f0bf9.png)
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉树中。
方法一:递归
思路和算法
我们递归遍历整棵二叉树,定义
其中
再来看第二条判断条件,这个判断条件即是考虑了 x 恰好是 p 节点或 q 节点且它的左子树或右子树有一个包含了另一个节点的情况,因此如果满足这个判断条件亦可说明 x 就是我们要找的最近公共祖先。
你可能会疑惑这样找出来的公共祖先深度是否是最大的。其实是最大的,因为我们是自底向上从叶子节点开始更新的,所以在所有满足条件的公共祖先中一定是深度最大的祖先先被访问到,且由于
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
res = []
def backtrack(node):
if not node:
return False
l_son = backtrack(node.left)
r_son = backtrack(node.right)
if (l_son and r_son) or ((l_son or r_son) and (node.val == p.val or node.val == q.val)):
res.append(node)
return True
return l_son or r_son or (node.val == q.val or node.val == p.val)
backtrack(root)
return res[0]
复杂度分析
时间复杂度:O(N),其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,因此时间复杂度为 O(N)。
空间复杂度:O(N) ,其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O(N)。力扣空间复杂度:O(N) ,其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O(N)。
方法二:存储父节点
思路
我们可以用哈希表存储所有节点的父节点,然后我们就可以利用节点的父节点信息从 p 结点开始不断往上跳,并记录已经访问过的节点,再从 q 节点开始不断往上跳,如果碰到已经访问过的节点,那么这个节点就是我们要找的最近公共祖先。
算法
- 从根节点开始遍历整棵二叉树,用哈希表记录每个节点的父节点指针。
- 从 p 节点开始不断往它的祖先移动,并用数据结构记录已经访问过的祖先节点。
- 同样,我们再从 q 节点开始不断往它的祖先移动,如果有祖先已经被访问过,即意味着这是 p 和 q 的深度最深的公共祖先,即 LCA 节点。
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
visited = set()
parent = {}
def backtrack(node):
if not node:
return
if node.left:
parent[node.left.val] = node
backtrack(node.left)
if node.right:
parent[node.right.val]= node
backtrack(node.right)
backtrack(root)
while p:
visited.add(p)
p = parent.get(p.val)
while q:
if q in visited:
return q
q = parent.get(q.val)
复杂度分析
时间复杂度:O(N),其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,从 p 和 q 节点往上跳经过的祖先节点个数不会超过 N,因此总的时间复杂度为 O(N)。
空间复杂度:O(N) ,其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O(N),哈希表存储每个节点的父节点也需要 O(N) 的空间复杂度,因此最后总的空间复杂度为 O(N)。
后序遍历 DFS
考虑通过递归对二叉树进行后序遍历,当遇到节点 p 或 q 时返回。从底至顶回溯,当节点 p, q 在节点 root的异侧时,节点 root 即为最近公共祖先,则向上返回 root。
递归解析:
终止条件:
当越过叶节点,则直接返回 null ;
当 root等于 p, q,则直接返回 root;
递推工作:
开启递归左子节点,返回值记为 left;
开启递归右子节点,返回值记为 right ;
返回值: 根据 left 和 right ,可展开为四种情况;
1、当 left 和 right 同时为空 :说明 root的左 / 右子树中都不包含 p,q,返回 null ;
2、当 left 和 right 同时不为空 :说明 p, q 分列在 root的 异侧 (分别在 左 / 右子树),因此 root为最近公共祖先,返回 root ;
3、当 left为空 ,right 不为空 :p,q都不在 root的左子树中,直接返回 right 。具体可分为两种情况:
p,q其中一个在 root 的 右子树 中,此时 right 指向 p(假设为 p );
p,q 两节点都在 root 的 右子树 中,此时的 right指向 最近公共祖先节点 ;
4、当 left不为空 , right为空 :与情况 3. 同理;
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
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 and not right:return
if not left: return right
if not right: return left
return root
复杂度分析:
时间复杂度 O(N) : 其中 N 为二叉树节点数;最差情况下,需要递归遍历树的所有节点。
空间复杂度 O(N) : 最差情况下,递归深度达到 NN ,系统使用 O(N) 大小的额外空间。
class Solution(object):
def __init__(self):
self.result = TreeNode(float("inf"))
def lowestCommonAncestor(self, root, p, q):
"""
:type root: TreeNode
:type p: TreeNode
:type q: TreeNode
:rtype: TreeNode
: 思路: 深度优先,从叶节点开始判断当前分支是不是包含目标节点,包含则返回True,如果左右子分支都返回True或者当前节点是目标节点,且有一个分支返回True
"""
def dfs(root):
if not root:
return False
left_result = dfs(root.left)
right_result = dfs(root.right)
if root.val == p.val or root.val == q.val: # 当前节点未目标节点
if left_result or right_result: # 左右分支有一个True
self.result = root
return True
if left_result and right_result: # 左右分支都为True
self.result = root
return True
if left_result or right_result:
return True
dfs(root)
return self.result