二叉树复习总览

================================================================

在复习总览部分,尽可能文字写思路,梳理过程,尽可能是力扣最好的解法。

随想录+力扣题解的基础上,包括hot100的二叉树题目。

================================================================

二叉树基础

满二叉树

除了最后一层无任何子节点外,每一层上的所有节点都有两个子节点。

完全二叉树

若二叉树的深度为 h,除第 h 层外,其它各层的结点数都达到最大个数,第 h 层所有的叶子结点都连续集中在最左边,这就是完全二叉树。

二叉搜索树

左子树上的所有节点都小于它的根节点,右子树上的所有节点大于它的根节点。

二叉搜索树上一个有序树。

平衡二叉搜索树(AVL树,adelson-velsky and landis)

左右两个子树的高度差不超过1的二叉搜索树。

226. 翻转二叉树 【HOT100】

把每个节点的左右节点交换就好了。左右节点交换时,它们的子树自然会一起被带着交换。

class Solution:
    def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        if not root:
            return
        root.left, root.right = root.right, root.left
        self.invertTree(root.left)
        self.invertTree(root.right)
        return root

Python 的“多重赋值”机制允许直接交换两个变量而不需要额外的临时变量。

树的遍历

深度优先遍历:先往深走,遇到叶子节点再往回走。
- 前序遍历(中左右)
- 中序遍历(左中右)
- 后序遍历(左右中)

94. 二叉树的中序遍历【HOT100】

class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        return self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)

比想象中的难写,一定要确定自己能写出来而不只是懂原理。

广度优先遍历:一层一层遍历。

102. 二叉树的层序遍历 【HOT100】

class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if not root:
            return []
        # 用双端队列,先进先出
        next_level = collections.deque([root])
        result = []
        while next_level:
            level = []
            for _ in range(len(next_level)):
                # 处理本节点
                cur = next_level.popleft()
                level.append(cur.val)
                # 如果有子节点,把子节点压入nextlevel
                if cur.left:
                    next_level.append(cur.left)
                if cur.right:
                    next_level.append(cur.right)
            result.append(level)
        return result

重新整理过代码和注释。层序遍历,其实二叉树基本只看了递归,所以对层序遍历不是很熟。

通常实现层序遍历时,会使用deque双端队列,因为它可以popleft,满足二叉树层序遍历所需要的先进先出。

==-*——

在Python里我们会使用deque双端队列来处理层序遍历,因为它可以popleft,实现先进先出。先进先出就可以自然地逐层地访问每一层的节点。

我们需要一个while循环,只要双端队列next_level不为空,就代表还有下一层的节点需要处理。

具体上是用len(next_level)次的For循环处理目前next_level里的每个节点,把这一层的节点取出加入到列表level,把节点的子节点,全部放到next_level。

每一次while循环的结尾就是说明处理完了一整层,把level列表加入到result列表。

最后返回result列表就可以了。

==-*——

107. 二叉树的层序遍历 II

这一题是要从最下面一层往最上面一层遍历。那么在102的基础上,return result[::-1]就可以了。

199. 二叉树的右视图 【HOT100】

在102的基础上,每层只把level.pop(),也就是每层的最后一个加入到result就好。

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

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
        index = {x: i for i, x in enumerate(inorder)}

        def dfs(pre_l: int, pre_r: int, in_l: int, in_r: int) -> Optional[TreeNode]:
            if pre_l == pre_r:  # 空节点
                return None
            left_size = index[preorder[pre_l]] - in_l  # 左子树的大小
            left = dfs(pre_l + 1, pre_l + 1 + left_size, in_l, in_l + left_size)
            right = dfs(pre_l + 1 + left_size, pre_r, in_l + 1 + left_size, in_r)
            return TreeNode(preorder[pre_l], left, right)

        return dfs(0, len(preorder), 0, len(inorder))  # 左闭右开区间

高度深度相关

104. 二叉树的最大深度【HOT100】

深度:任意一个节点到根节点的距离(根节点的深度为1)

高度:任意一个节点到最远的叶子结点的距离(根节点的高度为层数)

求高度解法:

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        leftheight = self.maxDepth(root.left)
        rightheight = self.maxDepth(root.right)
        height = 1 + max(leftheight, rightheight)
        return height

==-*——

最大深度就是根节点的高度。所以这道题转换为求高度会更简单。

核心思路:一个节点的高度,就是左边子树的高度和右边子树的高度中大的那个再+1

==-*——

直接求深度解法:

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        if root.left and not root.right:
            return 1 + self.maxDepth(root.left)
        if root.right and not root.left:
            return 1 + self.maxDepth(root.right)
        return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))

==-*——

相同核心思路:一个节点的高度,就是左边子树的高度和右边子树的高度中大的那个再+1

111.二叉树的最小深度,答案就是这个直接求深度解法,把max换成min。所以两个都要掌握。

==-*——

101. 对称二叉树【HOT100】

我记得这一题,是先比较内侧的节点是否一致,root.left.right == root.right.left

然后比较外侧的节点是否一致,root.left.left == root.right.right

class Solution:
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        def isMirror(left: Optional[TreeNode], right: Optional[TreeNode]) -> bool:
            # 若两边都为空,对称
            if not left and not right:
                return True
            # 若一边为空一边不为空,不对称
            if not left or not right:
                return False
            # 两边的值相同,且继续判断子节点是否镜像
            return (left.val == right.val) and isMirror(left.left, right.right) and isMirror(left.right, right.left)

        # 根节点为空,返回 True;否则调用 isMirror 判断左右子树是否镜像
        return isMirror(root.left, root.right) if root else True

必须使用辅助函数的原因在于:对称性检查本质上是二叉树的左右子树的递归比较,需要同时传入左右两个子树的节点进行镜像比较,而主函数 isSymmetric 本身只接收根节点 root。因此,辅助函数 isMirror(left, right) 用于分别传入左右子树进行递归的镜像对比判断。

==-*——

因为检查对称性需要传入两个节点作为参数,所以新开一个需要输入两个节点的isMirror辅助函数可以让逻辑更清晰,符合“单一责任原则”。

对于每对节点,首先判断高度相同,然后比较val值是否相同,最后继续判断子节点是否镜像。

==-*——

110. 平衡二叉树

平衡二叉树:左右两个子树的高度差最多是1。

class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        return False if self.getHeight(root) == -1 else True
    
    def getHeight(self,node) -> int:
        if not node:
            return 0
        lefth = self.getHeight(node.left)
        righth = self.getHeight(node.right)
        if lefth == -1 or righth == -1:
            return -1
        if abs(lefth - righth) > 1:
            return -1
        
        return 1 + max(lefth,righth)

==-*——

因为需要每次记录当前节点的高度,所以重新开一个辅助函数,每次返回int变量节点高度。如果左右不平衡了,就直接返回-1。

每一次递归的逻辑:

如果节点为空,返回0。

如果节点不为空,获取节点左子树的高度,右子树的高度。①如果左子树或右子树的高度为-1,直接返回-1。②如果一个节点的左右子树高度差大于1,返回-1。③否则,返回1+子树里最高的高度

==-*——

543. 二叉树的直径【HOT100】

class Solution:
    def diameterOfBinaryTree(self, root: TreeNode) -> int:
        self.max_diameter = 0
        
        def depth(node):
            if not node:
                return 0
            left_depth = depth(node.left)
            right_depth = depth(node.right)
            # 更新最大直径
            self.max_diameter = max(self.max_diameter, left_depth + right_depth)
            # 返回节点的最大深度
            return max(left_depth, right_depth) + 1
        
        depth(root)
        return self.max_diameter

543 题 关注的是 每个节点的最大左右子树深度之和,通过递归遍历所有节点的左右子树来更新最大直径。

路径相关

257. 二叉树的所有路径

比之前写换了个思路,这个是力扣排名最快的。这题稍微有点难。

class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        # 初始化结果列表
        res = []
        # 深度优先搜索递归函数
        def dfs(node, path):
            # 将当前节点的值加入路径中
            path.append(str(node.val))
            # 如果当前节点是叶子节点,将路径添加到结果列表
            if node.left is None and node.right is None:
                res.append("->".join(path))
            else:
                # 递归遍历左子节点和右子节点
                if node.left:
                    dfs(node.left, path)
                if node.right:
                    dfs(node.right, path)
            # 回溯,移除路径中的当前节点
            path.pop()
        # 特殊情况:根节点为空
        if root:
            dfs(root, [])
        return res

回溯模板

递归结束条件:当前节点是叶子节点,那就有一条路径了

递归过程:递归左右子节点

path.pop()就是回溯的过程。

112. 路径总和

class Solution:
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        if not root:
            return False
        # 叶子节点,且总和等于目标值
        if not root.left and not root.right and targetSum == root.val:
            return True
        # 不是叶子结点或还没找到目标值,检查它的子节点 
        left_has_path = self.hasPathSum(root.left, targetSum - root.val)
        right_has_path = self.hasPathSum(root.right, targetSum - root.val)

        return left_has_path or right_has_path

上一题是求所有路径,这道题是求是否有一条路径的总和为目标值。题目看起来很像,所以放在一起看。但是他们的解题思路差别很大。

  • 257题 需要遍历所有路径,故采用 回溯,记录路径以获取所有可能的路径。
  • 112题 只需找到一条符合条件的路径,因此采用 递归/DFS 即可,无需回溯,因为一旦找到目标路径就可以直接返回。

437. 路径总和 III【HOT100】

这题是求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

本题的前置题目,560. 和为 K 的子数组,是本题在数组上的情况。本题做法和 560 题是一样的,前缀和+哈希表。

class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
        ans = 0
        cnt = defaultdict(int)
        cnt[0] = 1

        def dfs(node: Optional[TreeNode], s: int) -> None:
            if node is None:
                return
            nonlocal ans
            s += node.val
            ans += cnt[s - targetSum]
            cnt[s] += 1
            dfs(node.left, s)
            dfs(node.right, s)
            cnt[s] -= 1  # 恢复现场

        dfs(root, 0)
        return ans

-=- 目前到day17 -=-

补充二叉树题目

层序遍历相关

637. 二叉树的层平均值

凡是每一层要做什么的,都先考虑BFS能不能解决。

class Solution:
    def averageOfLevels(self, root: Optional[TreeNode]) -> List[float]:
        ans = []
        queue = deque([root])
        while queue:
            s = 0
            length = len(queue)
            for _ in range(length):
                node = queue.popleft()
                s += node.val
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            ans.append(s / length)
        return ans

在102的基础上,在每一行,把每个节点的值累加。在每次while循环的结尾,把累加值除于这行节点个数得到平均值,加入到结果列表。

429. N 叉树的层序遍历

# 如果有子节点,把子节点压入nextlevel
for child in cur.children:
    next_level.append(child)

N叉树和二叉树的区别,只有处理子节点时不一样。

只是把if cur.right/ if cur.left的部分改成了for child in cur.children。这个cur.children是这题里面定义的数据结构。

简单递归

404. 左叶子之和

class Solution:
    def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
        if root is None:
            return 0
        value = 0
        if root.left and not root.left.left and not root.left.right:
            value = root.left.val
        return value + self.sumOfLeftLeaves(root.left) + self.sumOfLeftLeaves(root.right)

左叶子要通过父节点判断是左子节点,然后判断是叶子节点。

每次递归要先初始化value = 0,因为return的时候可能需要传这个0。没有初始化的话,会因为找不到变量报错。

==-*——

左叶子之和,就是每次判断当前节点、当前节点的左右子节点是否为左叶子,是的话就把值累加。

==-*——

222. 完全二叉树的节点个数

class Solution:
    def countNodes(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
        return 1 + self.countNodes(root.left) + self.countNodes(root.right)

所有二叉树的节点个数都可以这样算。

先进先出列表

513. 找树左下角的值

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

==-*——

用了一个先进先出列表。在while循环里,只要列表不为空,每次把最前面的节点取出,然后把它的右子节点加到列表里,然后把它的左子节点加到列表里。这样处理,while循环结束后,最后留下的肯定是最底层最左边的节点。

==-*——

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值