【算法思想·二叉搜索树】后序篇

本文参考labuladong算法笔记[二叉搜索树心法(后序篇) | labuladong 的算法笔记]

1、概述

本文是承接 东哥带你刷二叉树(纲领篇) 的第五篇文章,主要讲二叉树后序位置的妙用,复述下前文关于后序遍历的描述:

前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。

那么换句话说,一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了

其实二叉树的题目真的不难,无非就是前中后序遍历框架来回倒嘛,只要你把一个节点该做的事情安排好,剩下的抛给递归框架即可。

但是对于有的题目,不同的遍历顺序时间复杂度不同。尤其是这个后序位置的代码,有时候可以大幅提升算法效率。

我们再看看后序遍历的代码框架:

def traverse(root):
    if not root:
        return
    traverse(root.left)
    traverse(root.right)
    # 后序代码的位置
    # 在这里处理当前节点

看这个代码框架,你说什么情况下需要在后序位置写代码呢?

如果当前节点要做的事情需要通过左右子树的计算结果推导出来,就要用到后序遍历

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

给你一棵以 root 为根的 二叉树 ,请你返回 任意 二叉搜索子树的最大键值和。

二叉搜索树的定义如下:

  • 任意节点的左子树中的键值都 小于 此节点的键值。
  • 任意节点的右子树中的键值都 大于 此节点的键值。
  • 任意节点的左子树和右子树都是二叉搜索树。

示例 1:

输入:root = [1,4,3,2,4,2,5,null,null,null,null,null,null,4,6]
输出:20
解释:键值为 3 的子树是和最大的二叉搜索树。

示例 2:

输入:root = [4,3,null,1,2]
输出:2
解释:键值为 2 的单节点子树是和最大的二叉搜索树。

示例 3:

输入:root = [-4,-2,-5]
输出:0
解释:所有节点键值都为负数,和最大的二叉搜索树为空。

示例 4:

输入:root = [2,1,3]
输出:6

示例 5:

输入:root = [5,4,8,3,null,6,3]
输出:7

提示:

  • 每棵树有 1 到 40000 个节点。
  • 每个节点的键值在 [-4 * 10^4 , 4 * 10^4] 之间。

【题目分析】

题目会给你输入一棵二叉树,这棵二叉树的子树中可能包含二叉搜索树对吧,请你找到节点之和最大的那棵二叉搜索树,返回它的节点值之和。

二叉搜索树(简写作 BST)的性质详见基础知识章节 二叉树基础,简单说就是「左小右大」,对于每个节点,整棵左子树都比该节点的值小,整棵右子树都比该节点的值大。

比如题目给了这个例子:

如果输入这棵二叉树,算法应该返回 20,也就是图中绿圈的那棵子树的节点值之和,因为它是一棵 BST,且节点之和最大。

那有的读者可能会问,输入的是一棵普通二叉树,有没有可能其中不存在 BST?

不会的,因为按照 BST 的定义,任何一个单独的节点肯定是 BST,也就是说,再不济,二叉树最下面的叶子节点肯定都是 BST。

比如说如果输入下面这棵二叉树:

两个叶子节点 1 和 2 就是 BST,比较一下节点之和,算法应该返回 2。

好了,到这里,题目应该解释地很清楚了,下面我们来分析一下这道题应该怎么做。

【思路分析】

刚才说了,二叉树相关题目最核心的思路是明确当前节点需要做的事情是什么

那么我们想计算子树中 BST 的最大和,站在当前节点的视角,需要做什么呢

1、我肯定得知道左右子树是不是合法的 BST,如果下面的这俩儿子有一个不是 BST,以我为根的这棵树肯定不会是 BST,对吧。

2、如果左右子树都是合法的 BST,我得瞅瞅左右子树加上自己还是不是合法的 BST 了。因为按照 BST 的定义,当前节点的值应该大于左子树的最大值,小于右子树的最小值,否则就破坏了 BST 的性质。

3、因为题目要计算最大的节点之和,如果左右子树加上我自己还是一棵合法的 BST,也就是说以我为根的整棵树是一棵 BST,那我需要知道我们这棵 BST 的所有节点值之和是多少,方便和别的 BST 争个高下,对吧。

根据以上三点,站在当前节点的视角,需要知道以下具体信息

1、左右子树是否是 BST。

2、左子树的最大值和右子树的最小值。

3、左右子树的节点值之和。

只有知道了这几个值,我们才能满足题目的要求,现在可以尝试用伪码写出算法的大致逻辑:

class Solution:
    
    def __init__(self):
        # 全局变量,记录 BST 最大节点之和
        self.maxSum = 0

    def maxSumBST(self, root):
        self.traverse(root)
        return self.maxSum

    # 遍历二叉树
    def traverse(self, root):
        if root is None:
            return

        # ******* 前序遍历位置 *******
        # 判断左右子树是不是 BST
        if self.isBST(root.left) and self.isBST(root.right):
            # 计算左子树的最大值和右子树的最小值
            leftMax = self.findMax(root.left)
            rightMin = self.findMin(root.right)
            # 判断以 root 节点为根的树是不是 BST
            if root.val > leftMax and root.val < rightMin:
                # 如果条件都符合,计算当前 BST 的节点之和
                leftSum = self.findSum(root.left)
                rightSum = self.findSum(root.right)
                rootSum = leftSum + rightSum + root.val
                # 计算 BST 节点的最大和
                self.maxSum = max(self.maxSum, rootSum)
        # **************************

        # 二叉树遍历框架,遍历子树节点
        self.traverse(root.left)
        self.traverse(root.right)

    # 计算以 root 为根的二叉树的最大值
    def findMax(self, root):
        pass

    # 计算以 root 为根的二叉树的最小值
    def findMin(self, root):
        pass

    # 计算以 root 为根的二叉树的节点和
    def findSum(self, root):
        pass

    # 判断以 root 为根的二叉树是否是 BST
    def isBST(self, root):
        pass

这个代码逻辑应该是不难理解的,代码在前序遍历的位置把之前的分析都实现了一遍。

其中有四个辅助函数比较简单,我就不具体实现了,其中只有判断合法 BST 的函数稍有技术含量,前文 二叉搜索树操作集锦 写过,这里就不展开了。

稍作分析就会发现,这几个辅助函数都是递归函数,都要遍历输入的二叉树,外加 traverse 函数本身的递归,可以说是递归上加递归,所以这个解法的复杂度是非常高的

具体来说,每一个辅助方法都是二叉树遍历函数,时间复杂度是 O(N),而 traverse 遍历框架会在每个节点上都把这些辅助函数调用一遍,所以总的时间复杂度是 O(N^2)

但是根据刚才的分析,像 leftMaxrootSum 这些变量又都得算出来,否则无法完成题目的要求。

【思路优化】

我们希望既算出这些变量,又避免辅助函数带来的额外复杂度,鱼和熊掌全都要,可以做到吗?

其实是可以的,只要把前序遍历变成后序遍历,让 traverse 函数把辅助函数做的事情顺便做掉

你仔细想想,如果我知道了我的左右子树的最大值,那么把我的值和它们比较一下,就可以推导出以我为根的这整棵二叉树的最大值。根本没必要再遍历一遍所有节点,对吧?求最小节点的值和节点的和也是一样的道理。

这就是我在前文 手把手带你刷二叉树(纲领篇) 所讲的后序遍历位置的妙用。

当然,正如前文所讲,如果要利用函数的返回值,就不建议使用 traverse 这个函数名了,我们想计算最大值、最小值和所有节点之和,不妨叫这个函数 findMaxMinSum 好了。

其他代码不变,我们让 findMaxMinSum 函数做一些计算任务,返回一个大小为 4 的 int 数组,我们暂且称它为 res,其中:

res[0] 记录以 root 为根的二叉树是否是 BST,若为 1 则说明是 BST,若为 0 则说明不是 BST;

res[1] 记录以 root 为根的二叉树所有节点中的最小值;

res[2] 记录以 root 为根的二叉树所有节点中的最大值;

res[3] 记录以 root 为根的二叉树所有节点值之和。

对于当前节点,如果分别对左右子树计算出了这 4 个值,只需要简单的运算,就可以推导出以当前节点为根的二叉树的这 4 个值,避免了重复遍历。

【python】

class Solution:
    
    def __init__(self):
        # 记录 BST 最大节点之和
        self.maxSum = 0

    def maxSumBST(self, root):
        self.findMaxMinSum(root)
        return self.maxSum

    # 计算以 root 为根的二叉树的最大值、最小值、节点和
    def findMaxMinSum(self, root):
        # base case
        if root is None:
            return [1, float('inf'), float('-inf'), 0]
        
        # 递归计算左右子树
        left = self.findMaxMinSum(root.left)
        right = self.findMaxMinSum(root.right)

        # ******* 后序遍历位置 *******
        # 通过 left 和 right 推导返回值
        # 并且正确更新 maxSum 变量
        res = [0, 0, 0, 0]
        if left[0] == 1 and right[0] == 1 and root.val > left[2] and root.val < right[1]:
            # 以 root 为根的二叉树是 BST
            res[0] = 1
            # 对于叶子结点,需要这么写才能得到自己作为叶子的最小值
            res[1] = min(left[1], root.val)
            # 对于叶子结点,需要这么写才能得到自己作为叶子的最大值
            res[2] = max(right[2], root.val)
            # 计算以 root 为根的这棵 BST 所有节点之和
            res[3] = left[3] + right[3] + root.val
            # 更新全局变量
            self.maxSum = max(self.maxSum, res[3])
            
        return res

这样,这道题就解决了,findMaxMinSum 函数在遍历二叉树的同时顺便把之前辅助函数做的事情都做了,避免了在递归函数中调用递归函数,时间复杂度只有 O(N)。

总结

1、考虑到本题要求子树问题,那自然是用[分解问题]思路,同时采用后序遍历,因为只有后序遍历才能拿到左右子树的信息。

2、对于单个树节点,需要掌握①自身是否是BST ②左子树的最小值 ③右子树的最大值 ④自身子树所有节点和,因此我们用一个数组res来记录这些信息。

3、递归终止条件:对于空节点来说,其本身得算一个BST,其节点和自然是0,但其节点最小值和最大值应设为float('inf')和float('-inf'),以便后续做比较时方便赋值。

4、后序位置:判断一个节点是否为BST后,依次刷新该节点的res数组信息,并比较该节点值之和与全局max_sum。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值