Leetcode刷题笔记17:二叉树4(110平衡二叉树-257二叉树的所有路径-404左叶子之和)

105 阅读7分钟

导语

leetcode刷题笔记记录,本篇博客记录二叉树部分的题目,主要题目包括:

  • 110 平衡二叉树
  • 257 二叉树的所有路径
  • 404 左叶子之和

知识点

平衡二叉树

平衡二叉树(Balanced Binary Tree)是一种特殊的二叉树,其中任意两个子树的高度差(或深度差)都不超过1。简而言之,这意味着树的左侧和右侧子树的大小相对均衡,不会有一侧明显比另一侧深很多。

平衡二叉树的主要优点是它能够保证树的操作(如添加、删除和查找)具有相对均衡的时间复杂度,即O(log n),其中n是树中的节点数。这是因为树保持了较低的高度,不会出现最坏情况下的链状结构,这样的结构会导致操作的时间复杂度变为O(n)。其中最著名的平衡二叉树类型是AVL树红黑树。这两种类型的树都在插入和删除操作后通过特定的旋转和调整保持其平衡性。

  • AVL树是最早的自平衡二叉查找树。在AVL树中,任何节点的两个子树的高度最多差1。当在AVL树中进行插入或删除时,如果这一条件被破坏,它会通过旋转来自平衡。
  • 红黑树则使用颜色标记(红或黑)和一系列特定的属性和旋转规则来维持平衡。虽然红黑树的平衡不如AVL树严格(它允许的高度差是对数级别的),但在某些操作上,红黑树通常更为高效,因为它涉及的旋转操作比AVL树少。

平衡二叉树主要用于那些需要快速插入、删除和查找操作的数据结构,例如某些数据库索引和高级的数据结构库中。

回溯法

回溯算法是一种试探性的算法,用于解决决策问题,如寻找所有可能的组合、排列或表示等。回溯算法的核心思想是从一组可能的选项中选择一个,然后递归地(或迭代地)尝试从中选择更多。如果当前选择的序列不满足我们的需求或已经确定不能产生一个有用的解,那么它将撤销/回溯上一步或几步的选择,然后尝试其他选项。其中的一些关键词定义如下:

  1. 选择:每一步都需要在一组选项中做出选择。
  2. 约束:每次做出选择时,都会有一些约束限制可选的选择。
  3. 目标:我们试图找到一个或多个到达目标的解决方案。

回溯算法可以被视为一种深度优先搜索策略,但与传统的深度优先搜索不同,回溯算法在确定当前选择不可能产生一个有用的解时,会放弃当前选择,返回(即回溯)到上一个选择点,然后继续尝试其他选项。这种策略通常用于解决如组合、排列、图的颜色化、八皇后问题等需要尝试多种选择并寻找所有可能解的问题。

Leetcode 110 平衡二叉树

题目描述

给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

示例 1:

输入: root = [3,9,20,null,null,15,7]
输出: true

解法

使用递归法求解这个问题,那么需要来确定递归中的三个关键因素:

  • 递归传入的参数:只需要传入一个节点即可,函数返回该节点的高度;
  • 递归的终止条件,这里有几个设置:
    • 若节点None,则返回0;
    • 若当前节点对应的子树已经不是平衡二叉树了,则直接返回-1;
    • 因为若一个树的某个子树已经不是平衡二叉树时,这个树也肯定不会是平衡二叉树了。
  • 递归函数设计:采用后序遍历的方式,首先获取左右子树的高度,然后判断左右子树是否已经不是平衡二叉树,若都是的话则判断高度差值是否大于1,若满足以上条件,则直接返回-1,否则需要返回当前节点的左右子树的高度中的最大值再加1.

整体函数设计如下:

class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        if not root:
            return True
        right_height = self.getHeight(root.right)
        left_height = self.getHeight(root.left)
        if left_height == -1 or right_height == -1:
            return False
        else:
            return abs(right_height-left_height) < 2
        
    def getHeight(self, node):
        if node is None:
            return 0
        left_height = self.getHeight(node.left)
        right_height = self.getHeight(node.right)
        if right_height == -1 or left_height == -1 or abs(right_height-left_height)>1:
            return -1
        else:
            return max(left_height, right_height) + 1

Leetcode 257 二叉树的所有路径

题目描述

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。

示例 1:

输入: root = [1,2,3,null,5]
输出: ["1->2->5","1->3"]

解法

本题需要使用回溯的思想,使用前序遍历进行递归,这里截一张代码随想录的示意图:

image.png

使用两个list来存数据,一个存所经历路径的值,另一个存结果。 具体代码如下:

class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        result, path = [], []
        if not root:
            return result
        self.traversal(root, path, result)

        return result

    def traversal(self, cur, path, result):
        path.append(cur.val)
        if not cur.left and not cur.right: # 是叶子节点
            s_ptah = "->".join(map(str, path))
            result.append(s_ptah)
            return None
        if cur.left:
            self.traversal(cur.left, path, result)
            path.pop()
        if cur.right:
            self.traversal(cur.right, path, result)
            path.pop()

需要注意的是,在遍历左、右完成后,分别要进行pop(),这个就是“回溯”的核心思想所在。

Leetcode 404 左叶子之和

题目描述

给定二叉树的根节点 root ,返回所有左叶子之和。

示例 1:

输入: root = [3,9,20,null,null,15,7] 
输出: 24 
解释: 在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24

解法

层次遍历

使用层次遍历可以比较容易理解这道题目,只需要注意判断左叶子时,应该是由当前左叶子节点的父节点来判断的,所以其实层次遍历法可以进行一步剪枝的操作,即最后一层的叶子节点没必要入队(因为叶子节点肯定没有自己的左叶子了)。具体代码如下:

from collections import deque

class Solution:
    def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
        queue = deque()
        ans = 0
        if root:
            queue.append(root)
        while queue:
            size = len(queue)
            for _ in range(size):
                node = queue.popleft()
                if node.left is None and node.right is None:
                    continue
                if node.left is not None:
                    queue.append(node.left)
                    if node.left.left is None and node.left.right is None:
                        ans += node.left.val
                if node.right is not None:
                    queue.append(node.right)
        return ans

递归法

使用递归来解决这道题目时同样需要注意,判断节点为左叶子节点时,必须处理的是“父节点”,所以递归的终止条件中,需要考虑到这一变化,具体代码如下:

class Solution:
    def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
        # 先写递归终止条件
        if root is None:
            return 0
        if root.left is None and root.right is None:
            return 0
        
        left_num = self.sumOfLeftLeaves(root.left)
        if root.left is not None and root.left.left is None and root.left.right is None:
            left_num = root.left.val
        right_num = self.sumOfLeftLeaves(root.right)

        return left_num + right_num

这里不太好理解的一个地方是

        if root.left is not None and root.left.left is None and root.left.right is None:
            left_num = root.left.val

这是因为我们如果处理的节点就是一个左叶子节点的父节点,那么递归他的左子树时,回被当做只有一个叶子节点的情况给返回0(即下面的这个逻辑中,

        if root.left is None and root.right is None:
            return 0

所以,必须要单独考虑这种情况。最后,我们返回的是左子树的左叶子节点和加上右子树的左叶子节点和。