Day20 算法学习|二叉树06 继续递归

题目 654.最大二叉树

问题描述

给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:

二叉树的根是数组中的最大元素。
左子树是通过数组中最大值左边部分构造出的最大二叉树。
右子树是通过数组中最大值右边部分构造出的最大二叉树。
通过给定的数组构建最大二叉树,并且输出这个树的根节点。

在这里插入图片描述

解题思路

  • 最大二叉树是一种特殊的二叉树,它的规则是:树的根是数组nums中的最大元素,左子树是通过数组中最大值左边部分构造出的最大二叉树,右子树是通过数组中最大值右边部分构造出的最大二叉树。
  • 这个定义本身就是递归的,这就提示我们可以使用递归的方式来解决这个问题。因为每次我们都是从数组中找出最大的元素作为根节点,然后将数组分割成两部分来递归构造左子树和右子树。
  • 基于这个理解,我们可以将问题分解为以下几个步骤:
    。找到数组中的最大值和其索引:这是最直观的步骤,我们需要遍历数组找到最大值,因为它将成为二叉树的根节点。
    。创建新节点:找到最大值后,我们创建一个新的树节点,将其值设为找到的最大值。
    。递归构造左右子树:最大值将数组分为两部分,左边的部分和右边的部分。我们需要递归地将这两部分分别构造成最大二叉树,并将它们设置为刚才创建的节点的左子树和右子树。注意,递归的基线条件是当数组只剩下一个元素时,直接返回一个新的树节点。
  • 举例:
    >      6
/       3         5
\       /2  01

这个树是通过以下步骤构造出来的:

找到数组中的最大值6和其索引3。

构建值为6的节点。

使用数组[3, 2, 1]递归构建左子树。

使用数组[0, 5]递归构建右子树。

返回当前构建的树的根节点。

代码

  • 递归
lass Solution:
    def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode:
        if len(nums) == 1:
            return TreeNode(nums[0])
        node = TreeNode(0)
        # 找到数组中最大的值和对应的下标
        maxValue = 0
        maxValueIndex = 0
        for i in range(len(nums)):
            if nums[i] > maxValue:
                maxValue = nums[i]
                maxValueIndex = i
        node.val = maxValue
        # 最大值所在的下标左区间 构造左子树
        if maxValueIndex > 0:
            new_list = nums[:maxValueIndex]
            node.left = self.constructMaximumBinaryTree(new_list)
        # 最大值所在的下标右区间 构造右子树
        if maxValueIndex < len(nums) - 1:
            new_list = nums[maxValueIndex+1:]
            node.right = self.constructMaximumBinaryTree(new_list)
        return node
        

  • 使用下标
class Solution:
    def traversal(self, nums: List[int], left: int, right: int) -> TreeNode:
        if left >= right:
            return None
        maxValueIndex = left
        for i in range(left + 1, right):
            if nums[i] > nums[maxValueIndex]:
                maxValueIndex = i
        root = TreeNode(nums[maxValueIndex])
        root.left = self.traversal(nums, left, maxValueIndex)
        root.right = self.traversal(nums, maxValueIndex + 1, right)
        return root

    def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode:
        return self.traversal(nums, 0, len(nums))


  • 使用切片

class Solution:
    def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode:
        if not nums:
            return None
        max_val = max(nums)
        max_index = nums.index(max_val)
        node = TreeNode(max_val)
        node.left = self.constructMaximumBinaryTree(nums[:max_index])
        node.right = self.constructMaximumBinaryTree(nums[max_index+1:])
        return node
        

复杂度分析 递归

  • 时间复杂度O(n)

在这个问题中,我们首先遍历了整个数组找到最大值(这个操作的时间复杂度为O(n)),然后我们对最大值左边和右边的子数组递归地进行了同样的操作。因此,时间复杂度是取决于每次划分的子数组的大小。

在最坏的情况下,如果输入数组是升序或者降序,那么每次递归时我们都需要处理长度减少1的数组,这样时间复杂度就是O(n2)。这是因为第一次找最大值需要遍历n个元素,第二次需要遍历n-1个元素,依此类推,总共需要遍历的次数就是n*(n+1)/2,所以是O(n2)。

在最好的情况下,如果每次都能将数组均匀地划分成两部分,那么时间复杂度就是O(n log n)。

  • 空间复杂度

在这个问题中,递归的深度取决于输入数组的情况。在最坏的情况下,如果数组是升序或者降序,那么递归的深度就是n,所以空间复杂度是O(n)。

另一方面,我们也需要存储结果的二叉树,而二叉树中的节点数量等于输入数组的大小,也就是n。所以,总的空间复杂度也是O(n)。

所以总结起来,这个问题的时间复杂度在O(n^2)到O(n log n)之间,空间复杂度是O(n)。

题目 617.合并二叉树

问题描述

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。

你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
在这里插入图片描述
合并必须从两个树的根节点开始

解题思路

  • 如果两棵树的相应节点都存在,那么将这两个节点的值相加作为新树的节点值,如果只有一个节点存在,那么直接将这个节点作为新树的节点。
  • 对于递归,我们首先需要确定基线条件,也就是最简单的情况。在这个问题中,基线条件就是至少有一个节点不存在的情况。
  • 然后我们需要编写递归的主体部分。在这个问题中,我们需要对两个节点的值进行相加,然后递归地处理左子树和右子树。

代码

  • 递归
class Solution:
    def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
        # 递归终止条件: 
        #  但凡有一个节点为空, 就立刻返回另外一个. 如果另外一个也为None就直接返回None. 
        if not root1: 
            return root2
        if not root2: 
            return root1
        # 上面的递归终止条件保证了代码执行到这里root1, root2都非空. 
        root1.val += root2.val # 中
        root1.left = self.mergeTrees(root1.left, root2.left) #左
        root1.right = self.mergeTrees(root1.right, root2.right) # 右
        
        return root1 # ⚠️ 注意: 本题我们重复使用了题目给出的节点而不是创建新节点. 节省时间, 空间.

  • 迭代
class Solution:
    def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode:
        if not root1:
            return root2
        if not root2:
            return root1

        queue = deque()
        queue.append((root1, root2))

        while queue:
            node1, node2 = queue.popleft()
            node1.val += node2.val

            if node1.left and node2.left:
                queue.append((node1.left, node2.left))
            elif not node1.left:
                node1.left = node2.left

            if node1.right and node2.right:
                queue.append((node1.right, node2.right))
            elif not node1.right:
                node1.right = node2.right

        return root1



复杂度分析 递归

  • 时间复杂度O(n)

  • 空间复杂度O(n)

空间复杂度主要由递归调用栈的深度决定。在最坏的情况下,树可能完全不平衡,例如每个节点都只有左子节点,此时递归的深度为n,所以空间复杂度为O(n)。在最好的情况下,树完全平衡,高度为log(n),因此空间复杂度为O(log(n))。但是在没有其他信息的情况下,我们通常考虑最坏的情况,所以空间复杂度是O(n)。

题目 700.二叉搜索树中的搜索

问题描述

给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
在这里插入图片描述

解题思路

  • 当你要找一本特定的书时,你不需要浏览每一本书。首先,你会看书架中间的书,如果这本书就是你要找的,那么你就已经找到了。如果这本书比你要找的书要大,那么你就知道你要找的书肯定在左边,于是你就转向左边的书架(进行左子树搜索)。同样,如果这本书比你要找的书要小,你就知道你要找的书在右边,于是转向右边的书架(进行右子树搜索)。
  • 当然,如果图书馆里没有你要找的书(如果值不存在于树中),那么你最终会走到图书馆的尽头(空节点),这时就应该返回没有找到(返回None)。
  • 在这个过程中,因为图书馆的书是有序排列的,所以你不需要查看所有的书,这大大提高了搜索的效率。这就是二叉搜索树的优点,也是这个算法之所以有效的原因。
  • 为什么需要返回值?因为我们要把找到的节点返回给调用者,如果找不到,返回None。这样,每次递归调用都会返回一个结果,即:在当前节点以下的子树中是否找到了目标节点。如果找到了,就立即返回该节点,这样可以提高效率,避免不必要的搜索。

代码

  • 递归
class Solution:
    def searchBST(self, root: TreeNode, val: int) -> TreeNode:
        # 为什么要有返回值: 
        #   因为搜索到目标节点就要立即return,
        #   这样才是找到节点就返回(搜索某一条边),如果不加return,就是遍历整棵树了。

        if not root or root.val == val: 
            return root

        if root.val > val: 
            return self.searchBST(root.left, val)

        if root.val < val: 
            return self.searchBST(root.right, val)

  • 迭代

class Solution:
    def searchBST(self, root: TreeNode, val: int) -> TreeNode:
        while root:
            if val < root.val: root = root.left
            elif val > root.val: root = root.right
            else: return root
        return None

复杂度分析

  • 时间复杂度O(log n)/O(n)

    在最好的情况下,树是平衡的,搜索的时间复杂度为O(log n),其中n为树中节点的数量。在最坏的情况下,树是一个线性链(比如,每个节点都只有右孩子或只有左孩子),此时搜索的时间复杂度为O(n)。

  • 空间复杂度 O(log n)/O(n)

    空间复杂度主要取决于递归的深度,即树的高度。在最坏的情况下,树的高度为n,所以空间复杂度为O(n)。在最好的情况下,树是平衡的,高度为log(n),因此空间复杂度为O(log n)。

题目 98.验证二叉搜索树

问题描述

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
在这里插入图片描述

解题思路

这道题目的核心是理解二叉搜索树的性质:对于任何节点,它的值都大于其左子树所有节点的值,同时小于其右子树所有节点的值。此外,如果对二叉搜索树进行中序遍历,我们将得到一个严格升序的序列。因此,我们可以通过这两个性质来判断一棵树是否是二叉搜索树。

此代码中通过递归中序遍历,并且在每次访问节点时,都会检查该节点的值是否大于前一个节点的值(前一个节点的值存储在self.maxVal中)。如果是,则继续递归遍历;如果不是,则立即返回False。

陷阱

在这里插入图片描述
节点10大于左节点5,小于右节点15,但右子树里出现了一个6 这就不符合了!

代码

  • 递归法(版本一)利用中序递增性质,转换成数组

class Solution:
    def __init__(self):
        self.vec = []

    def traversal(self, root):
        if root is None:
            return
        self.traversal(root.left)
        self.vec.append(root.val)  # 将二叉搜索树转换为有序数组
        self.traversal(root.right)

    def isValidBST(self, root):
        self.vec = []  # 清空数组
        self.traversal(root)
        for i in range(1, len(self.vec)):
            # 注意要小于等于,搜索树里不能有相同元素
            if self.vec[i] <= self.vec[i - 1]:
                return False
        return True

  • 递归法(版本二)设定极小值,进行比较
class Solution:
    def __init__(self):
        self.maxVal = float('-inf')  # 因为后台测试数据中有int最小值

    def isValidBST(self, root):
        if root is None:
            return True

        left = self.isValidBST(root.left)
        # 中序遍历,验证遍历的元素是不是从小到大
        if self.maxVal < root.val:
            self.maxVal = root.val
        else:
            return False
        right = self.isValidBST(root.right)

        return left and right


  • 递归法(版本三)直接取该树的最小值
class Solution:
    def __init__(self):
        self.pre = None  # 用来记录前一个节点

    def isValidBST(self, root):
        if root is None:
            return True

        left = self.isValidBST(root.left)

        if self.pre is not None and self.pre.val >= root.val:
            return False
        self.pre = root  # 记录前一个节点

        right = self.isValidBST(root.right)
        return left and right



  • 迭代法
class Solution:
    def isValidBST(self, root):
        stack = []
        cur = root
        pre = None  # 记录前一个节点
        while cur is not None or len(stack) > 0:
            if cur is not None:
                stack.append(cur)
                cur = cur.left  # 左
            else:
                cur = stack.pop()  # 中
                if pre is not None and cur.val <= pre.val:
                    return False
                pre = cur  # 保存前一个访问的结点
                cur = cur.right  # 右
        return True

复杂度分析(最小值递归)

  • 时间复杂度O(n)

  • 空间复杂度O(log(n))和O(n)之间

在这种情况下,考虑最坏的情况,也就是这棵树是一条线(线性结构),也就是说每个节点除了根节点都只有一个子节点。在这种情况下,树的高度等于节点的数量,记作n。所以在这种情况下,递归调用的栈空间复杂度为O(n)。

在最好的情况下,如果这棵树是一个完全平衡的二叉树,那么树的高度为log(n),在这种情况下,空间复杂度为O(log(n))。

总的来说,空间复杂度会在O(log(n))和O(n)之间,取决于树的形状。当然,如果我们没有任何关于树形状的额外信息,那么在分析空间复杂度时,通常会考虑最坏的情况,也就是O(n)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值