【灵神高频面试题合集09-13】二叉树、二叉搜索树

基础算法精讲·题目汇总:灵茶山艾府 - 【基础算法精讲】- GitHub

视频:灵茶山艾府的个人空间-灵茶山艾府个人主页-哔哩哔哩视频


09 递归 数学归纳法 栈

课程讲解

在做二叉树题目的时候,往往需要用到递归

104. 二叉树的最大深度

定义:根节点到最远叶子节点的最长路径上的节点数

如何思考递归

写循环就是在重复执行同样一份代码,每次循环都是加在同一个变量(放在循环外)

而递归处理的问题是有嵌套关系的,需要把计算结果返给它的上一级问题,以此类推(类似数学归纳法)

  • 从原问题出发,把问题不断分解成更小的子问题 ------ 递
  • 不断递下去总会有尽头,即边界条件,返回答案 ------ 归

本题中的递归条件就是空节点,可以直接返回0作为答案

【递归写法】
python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if root is None:
            return 0
        left_depth = self.maxDepth(root.left)
        right_depth = self.maxDepth(root.right)
        return max(left_depth, right_depth) + 1
  • 时间复杂度:O(n),每个节点都遍历了一次
  • 空间复杂度:O(n)。return 的时候怎么知道要 return 到哪个节点上去,需要一个数据结构,支持最后放进去的节点最先出来,即栈。最坏情况下,二叉树只有左儿子,没有右儿子,就变成了链状结构,此时栈的大小就是 O(n)

另一种写法:还可以把路径上的节点个数也传下去

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        res = 0
        def f(node, cnt):   # cnt为经过的节点个数
            if node is None:
                return
            cnt += 1   # 每到达一个节点都要+1
            nonlocal res
            res = max(res, cnt)
            f(node.left, cnt)
            f(node.right, cnt)
        f(root, 0)
        return res

# 下面这种写法本人更习惯,思路是一样的
class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        self.res = 0
        self.dfs(root, 0)
        return self.res

    def dfs(self, node, cnt):
        if not node:
            return
        cnt += 1
        self.res = max(self.res, cnt)
        self.dfs(node.left, cnt)
        self.dfs(node.right, cnt)
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
【层序遍历写法】
python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if root is None:
            return 0
        ans = []
        q = deque([root])
        while q:
            vals = []
            for _ in range(len(q)):
                node = q.popleft()
                vals.append(node.val)
                if node.left:
                    q.append(node.left)
                if node.right:
                    q.append(node.right)
            ans.append(vals)
        return len(ans)

课后作业

111. 二叉树的最小深度

404. 左叶子之和

112. 路径总和

129. 求根节点到叶节点数字之和

1448. 统计二叉树中好节点的数目

987. 二叉树的垂序遍历


10 相同 对称 平衡 右视图

课程讲解

100. 相同的树

怎么定义相同?根节点必须相同、左子树和右子树必须相同

递归的边界条件:有一棵为空就无法递归,如果两个节点都是空,则返回True,否则返回False

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
        if p is None or q is None:
            return p is q
        return p.val == q.val and self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

101. 对称二叉树

把这棵树拆成左右两棵树

  • 看根节点是否相同
  • 然后递归左边的左子树和右边的右子树,以及左边的右子树和右边的左子树,看是否相同
python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    # 复用上一题的函数
    def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
        if p is None or q is None:
            return p is q
        return p.val == q.val and self.isSameTree(p.left, q.right) and self.isSameTree(p.right, q.left)   # diff

    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        return self.isSameTree(root.left, root.right)
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

110. 平衡二叉树

**高度平衡:**左子树和右子树高度差的绝对值不超过1。如下图,左子树的高度为3,右子树的高度为1,则不平衡

通过递归得到左右两棵子树的高度,算一下高度差的绝对值是否小于等于1。如果在递归过程中发现这棵树是不平衡的,可以返回-1(表示这棵树不平衡),就不再继续递归了

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        return self.get_height(root) != -1
        
    def get_height(self, node):
        if node is None:
            return 0
        left_height = self.get_height(node.left)
        if left_height == -1:
            return -1
        right_height = self.get_height(node.right)
        if right_height == -1 or abs(left_height - right_height) > 1:
            return -1
        return max(left_height, right_height) + 1
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

199. 二叉树的右视图

先递归右子树,再递推左子树

在递归的同时记录一个节点个数或者说递归深度,如果递归深度等于答案长度,那么这个节点就需要记录到答案中;否则就相当于被右边挡住了,不记录到答案中

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
        self.ans = []
        self.dfs(root, 0)
        return self.ans
        
    def dfs(self, node, depth):
        if node is None:
            return
        if depth == len(self.ans):
            self.ans.append(node.val)
        self.dfs(node.right, depth+1)
        self.dfs(node.left, depth+1)
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

课后作业

965. 单值二叉树

951. 翻转等价二叉树

226. 翻转二叉树

617. 合并二叉树

2331. 计算布尔二叉树的值

508. 出现次数最多的子树元素和

1026. 节点与其祖先之间的最大差值

1372. 二叉树中的最长交错路径

1080. 根到叶路径上的不足节点


11 二叉搜索树 前序 中序 后序

课程讲解

98. 验证二叉搜索树

三种做法:

  • 前序遍历:先判断,再递归
  • 中序遍历:大于上一个节点
  • 后序遍历:先递归,再判断

**二叉搜索树定义:**对于一个节点来说,它的左子树所有节点值都小于它的值,它的右子树所有节点值都大于它的值。同时,它的左子树和右子树也必须是二叉搜索树

【前序遍历写法】

递归时需要传入节点开区间的范围,先判断节点值是否在开区间内,然后再往下递归

  • 如果往左边递归,就把开区间的右边界更新为节点值
  • 如果往右边递归,就把开区间的左边界更新为节点值
  • 这种先访问节点值,再递归左右子树的做法叫【前序遍历】
  • 特别注意,根节点(函数调用的入口)再往上是没有节点的,这种情况下需要传入负无穷和正无穷
python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    # def check(self, node, left, right):

    def isValidBST(self, root: Optional[TreeNode], left=-inf, right=inf) -> bool:
        if root is None:
            return True
        x = root.val
        return left < x < right and self.isValidBST(root.left, left, x) and self.isValidBST(root.right, x, right)
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
【中序遍历写法】

**【中序遍历】**如果按照递归左子树访问节点值,再递归右子树这样的顺序去遍历,就可以得到一个严格递增数组

  • 对于二叉搜索树来说,按照左子树 -> 节点值 -> 右子树这样的顺序遍历
  • 遍历完左子树,就可以看当前的节点值是否 > 上一个节点的值
  • 然后把当前节点值记录下来,和下一个节点值比较大小
python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    pre = -inf   # 第一个节点没有上一个节点,初始化为-inf
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        if root is None:
            return True
        if not self.isValidBST(root.left):    # 递归左子树
            return False
        if root.val <= self.pre:
            return False
        self.pre = root.val
        return self.isValidBST(root.right)   # 递归右子树
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
【后序遍历写法】

前序遍历是把节点值的范围往下传,还可以把节点值的范围往上传。需要先遍历左右子树,再判断节点值,这就是**【后序遍历】**

  • 最小值和最大值都需要返回
  • 为了简化代码逻辑,可以递归到空节点,返回 正无穷到负无穷
  • 如果中途发现它不是一棵二叉搜索树,可以返回 负无穷到正无穷(表示False)
python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        def dfs(node):
            if node is None:
                return inf, -inf
            l_min, l_max = dfs(node.left)    # 递归左子树
            r_min, r_max = dfs(node.right)   # 递归右子树
            x = node.val
            if x <= l_max or x >= r_min:    # 不是一棵二叉搜索树
                return -inf, inf
            return min(l_min, x), max(r_max, x)
        return dfs(root)[1] != inf
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

课后作业

700. 二叉搜索树中的搜索

938. 二叉搜索树的范围和

530. 二叉搜索树的最小绝对差

2476. 二叉搜索树最近节点查询

501. 二叉搜索树中的众数

230. 二叉搜索树中第 K 小的元素

1373. 二叉搜索子树的最大键值和

105. 从前序与中序遍历序列构造二叉树

106. 从中序与后序遍历序列构造二叉树

889. 根据前序和后序遍历构造二叉树

1110. 删点成林


12 最近公共祖先

课程讲解

一个节点也可以是它自己的祖先

236. 二叉树的最近公共祖先

找p和q两个节点的最近公共祖先

  • 对于当前节点是空节点的情况,直接返回空节点
  • 如果当前节点是p或q,不需要继续递归寻找了
  • 以上情况之外,就递归左右子树
    • 如果左右子树都找到了,那么最近公共祖先就是当前节点
    • 如果只有左子树找到了,那么最近公共祖先肯定在左子树中,所以返回递归左子树后的结果就行
    • 右子树同理
python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        if root is None or root is p or root is q:
            return root
        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)
        if left and right:
            return root
        if left:
            return left
        return right
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

235. 二叉搜索树的最近公共祖先

可以利用二叉搜索树的性质:左子树的节点值都比当前节点值小,右子树的节点值都比当前节点值大

  • 如果p和q的节点值都小于当前节点值,说明p和q都在左子树中,所以最近公共祖先也在左子树中,递归左子树就好了
  • 如果p和q的节点值都大于当前节点值,说明p和q都在右子树中,所以最近公共祖先也在右子树中,递归右子树就好了
  • 如果p和q分别在左右子树中,那么最近公共祖先就是当前节点
  • 当前节点是p或者q,那么最近公共祖先也是当前节点
  • 无论什么情况,当前节点都不会是空节点(题目说了p和q一定在给定的二叉搜索树中),因此不需要判断当前节点是空节点
python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        x = root.val
        if p.val < x and q.val < x:
            return self.lowestCommonAncestor(root.left, p, q)
        if p.val > x and q.val > x:
            return self.lowestCommonAncestor(root.right, p, q)
        return root
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)。题目没有说这棵树是平衡树,在最坏情况下,这棵树会退化成一条链

课后作业

1123. 最深叶节点的最近公共祖先


13 层序遍历 BFS 队列

二叉树的层序遍历,也叫广度优先搜索

  • 递归相当于是从根节点出发,径直往下走
  • 层序遍历是一行一行地遍历

计算机在执行递归的时候,会用一个栈来维护入栈出栈的过程,这些操作计算机都帮你完成了,所以代码写起来很简洁;

而层序遍历需要手动编写入队出队的代码,相比递归,层序遍历需要写更多代码

课程讲解

102. 二叉树的层序遍历

【双数组写法】

用两个数组 cur 和 nxt

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if root is None:
            return []
        ans = []
        cur = [root]
        while cur:
            nxt = []
            vals = []
            for node in cur:
                vals.append(node.val)
                if node.left:
                    nxt.append(node.left)
                if node.right:
                    nxt.append(node.right)
            cur = nxt
            ans.append(vals)
        return ans
  • 时间复杂度:O(n)。每个节点只会遍历一次
  • 空间复杂度:O(n)。由于在满二叉树(每一层都填满)的情况下,最后一层有大约 n/2 个节点
【队列写法】

每一层要循环多少次,其实就是队列的长度。在循环开始时获取队列的长度,然后循环这么多次就可以得到下一层的节点了

Python里可以用双端队列 deque 表示队列

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if root is None:
            return []
        ans = []
        q = deque([root])
        while q:
            vals = []
            for _ in range(len(q)):
                node = q.popleft()
                vals.append(node.val)
                if node.left:
                    q.append(node.left)
                if node.right:
                    q.append(node.right)
            ans.append(vals)
        return ans

103. 二叉树的锯齿形层序遍历

只需要把偶数层的数组翻转一下就好了

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if root is None:
            return []
        ans = []
        q = deque([root])
        # 需要一个布尔值来表示当前是否为偶数层
        even = False    # 一开始是奇数层,所以初始化为False
        while q:
            vals = []
            for _ in range(len(q)):
                node = q.popleft()
                vals.append(node.val)
                if node.left:
                    q.append(node.left)
                if node.right:
                    q.append(node.right)
            ans.append(vals[::-1] if even else vals)   # 偶数层就翻转一下,奇数层就不变
            even = not even
        return ans
  • 时空复杂度都是 O(n)

513. 找树左下角的值

最底层最左边的节点值

做法1:返回最后一层的第一个节点

做法2:把层序遍历改成从右到左遍历 (只需要改变一下节点入队的顺序即可,先把右孩子入队,再把左孩子入队;出队也是右孩子先出队,然后左孩子再出队),那么最后一个出队的节点值就是答案

python 复制代码
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
        q = deque([root])
        while q:
            # 只需要知道最后一个节点即可,因此此处不用再写二重循环
            node = q.popleft()
            # 每一层从右到左
            if node.right:
                q.append(node.right)
            if node.left:
                q.append(node.left)
        return node.val   # 最后一个节点就是答案
  • 时空复杂度都是 O(n)

课后作业

107. 二叉树的层序遍历 II

104. 二叉树的最大深度

111. 二叉树的最小深度

2583. 二叉树中的第 K 大层和

199. 二叉树的右视图

116. 填充每个节点的下一个右侧节点指针

117. 填充每个节点的下一个右侧节点指针 II

1302. 层数最深叶子节点的和

1609. 奇偶树

2415. 反转二叉树的奇数层

2641. 二叉树的堂兄弟节点 II

相关推荐
皆圥忈1 小时前
磁盘物理结构与文件系统基础讲解
linux·算法
数据仓库搬砖人1 小时前
用 LangGraph 从零搭一个客服 Agent:多轮对话 + 工具调用全流程
算法
GuWenyue1 小时前
告别JS类型坑!Ts为什么在ai时代逐渐成为"第一"语言
前端·算法·typescript
子琦啊1 小时前
哈希与前缀和
算法·哈希算法
xqqxqxxq1 小时前
树结构技术学习笔记
数据结构·笔记·学习
Deep-w1 小时前
【MATLAB】基于离散 LQR 的车辆横向轨迹跟踪控制方法研究
开发语言·算法·matlab
Peter·Pan爱编程2 小时前
23. 算法库:用算法代替手写循环
c++·人工智能·算法
小欣加油2 小时前
leetcode2161 根据给定数字划分数组
数据结构·c++·算法·leetcode·职场和发展
雨落在了我的手上2 小时前
Java数据结构(四):List的介绍
数据结构