0

0

二叉树等和分割:从递归修正到高效算法

DDD

DDD

发布时间:2025-11-15 12:59:24

|

639人浏览过

|

来源于php中文网

原创

二叉树等和分割:从递归修正到高效算法

本文深入探讨了如何通过移除单条边将二叉树分割成两个总和相等的子树问题。文章首先分析了常见递归解法中的逻辑错误,并提供了修正后的代码。接着,提出了一种更高效的自底向上计算子树和的算法,该算法通过一次遍历收集所有子树和,从而在O(N)时间复杂度内解决问题,显著提升了性能。

问题概述:二叉树等和分割

二叉树等和分割问题要求我们检查一棵给定的二叉树(至少包含一个节点)是否可以通过移除一条边,将其分割成两棵总和相等的子树。如果可以,函数应返回分割后每棵子树的总和;否则,返回0。这里需要注意的是,移除一条边后,原树会分成两部分,其中一部分必然是原树的一个子树,而另一部分则是原树剩余的部分。

例如,如果一棵树的总和为 S,那么分割后两部分的和都应为 S/2。这意味着,整个树的总和 S 必须是偶数。

递归尝试与常见陷阱

一种直观的思路是尝试遍历树中的每一个节点,并假设在某个节点与其父节点之间的边被移除。然后,我们需要计算被移除边所连接的两个部分的和,并判断它们是否相等。

原始代码尝试了这种递归方法,但存在几个关键问题:

  1. 不正确的断边逻辑: 原始代码中存在类似如下的条件判断:

    if leftsum+tree.value == rightsum+balancesum:
        return fullsum/2

    这个条件实际上等同于将 tree 节点及其左子树作为一个整体,与 tree 的右子树以及通过 balancesum 传入的父节点上方部分进行比较。这隐含地切断了 tree 与其父节点以及 tree 与其右子节点之间的两条边,这与问题描述中“移除一条边”的要求不符。正确的逻辑应是只移除 tree 与其左子节点或 tree 与其右子节点之间的边,或者 tree 与其父节点之间的边。

  2. balancesum 参数传递错误: 在递归调用中,balancesum 参数用于表示当前节点上方(即父节点方向)的总和。原始代码的递归调用:

    lefty = splitBinaryTree(tree.left, fullsum-rightsum)
    righty = splitBinaryTree(tree.right, fullsum-leftsum)

    这里的 fullsum-rightsum 实际上是当前节点 tree 的值加上其左子树的总和。当递归到 tree.left 时,其父节点上方(即 tree 及其右子树和 balancesum 的总和)应作为新的 balancesum 传递。正确的 balancesum 应该包含当前节点的值、其兄弟子树的总和以及从父节点继承的 balancesum。

修正后的递归代码

根据上述分析,我们可以对原始的递归方法进行修正。关键在于确保每次只考虑移除一条边,并且正确传递递归参数。

修正点:

英特尔AI工具
英特尔AI工具

英特尔AI与机器学习解决方案

下载
  • 移除那些会同时切断多条边的条件判断。
  • 在递归调用 splitBinaryTree(tree.left, ...) 时,传递给 tree.left 的 balancesum 应该是当前节点 tree.value 加上其右子树的总和 rightsum,再加上从父节点继承的 balancesum。对 tree.right 的递归调用同理。
  • 最终的返回逻辑可以简化。
# 辅助函数:计算以node为根的子树的总和
def icalculatesum(node):
    if node is None:
        return 0
    return node.value + icalculatesum(node.left) + icalculatesum(node.right)

# 修正后的二叉树分割函数
def splitBinaryTree_recursive_fixed(tree, balancesum=0):
    if tree is None:
        return 0

    # 计算当前子树的总和
    fullsum = icalculatesum(tree)

    # 如果当前子树的总和等于我们期望的平衡和,则找到一个分割点
    # 这对应于切断当前节点与其父节点之间的边
    if fullsum == balancesum and fullsum != 0: # 避免返回0作为有效分割
        return fullsum

    # 计算左右子树的总和(这里会重复计算,效率较低)
    leftsum = icalculatesum(tree.left)
    rightsum = icalculatesum(tree.right)

    # 递归检查左子树
    # 传递给左子树的balancesum应是:
    # 原始父节点传下来的balancesum + 当前节点的值 + 右子树的总和
    lefty = splitBinaryTree_recursive_fixed(tree.left, balancesum + tree.value + rightsum)

    # 递归检查右子树
    # 传递给右子树的balancesum应是:
    # 原始父节点传下来的balancesum + 当前节点的值 + 左子树的总和
    righty = splitBinaryTree_recursive_fixed(tree.right, balancesum + tree.value + leftsum)

    # 如果任一递归调用找到了有效分割,则返回该分割和
    if lefty != 0 or righty != 0:
        # 这里返回的是整个树的总和的一半,但因为我们已经检查了 fullsum == balancesum,
        # 且 fullsum 是当前子树的总和,如果找到分割,则意味着整个树的总和是 2 * fullsum
        # 所以直接返回 lefty 或 righty 即可,它们已经是目标值
        return lefty or righty

    return 0

# BinaryTree 类定义(与原问题一致)
class BinaryTree:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

说明: 尽管这段代码修正了逻辑错误,但由于 icalculatesum 函数在每次递归调用时都会重新计算子树的总和,导致大量的重复计算,其时间复杂度远高于 O(N)。对于大型树,这种方法效率低下。

高效算法:自底向上计算子树和

为了提高效率,我们可以采用自底向上的方法。问题的本质是寻找一个子树,其总和恰好是整棵树总和的一半。如果我们能一次性计算出所有可能子树的总和,并存储起来,那么就可以通过一次查找来解决问题。

核心思想:

  1. 一次遍历计算所有子树和:通过后序遍历(或类似的深度优先遍历),在访问完左右子节点后,计算当前节点的子树总和。
  2. 存储所有子树和:将计算得到的每个子树的总和存储在一个集合(Set)或列表中。
  3. 判断分割条件:计算整棵树的总和 total_sum。如果 total_sum 是偶数,并且 total_sum / 2 存在于我们存储的子树总和中,那么就找到了一个有效的分割点。

算法步骤:

  1. 定义一个辅助函数 getAllTreeSums(tree),它将递归地遍历树。
  2. 对于每个节点,首先递归调用左右子节点,获取它们的子树总和。
  3. 当前节点的子树总和等于 node.value + left_subtree_sum + right_subtree_sum。
  4. 将这个总和添加到一个全局列表或集合中,并返回当前节点的子树总和。
  5. 主函数 splitBinaryTree(tree) 调用 getAllTreeSums 来填充所有子树和的列表。
  6. 获取列表中的第一个元素,即整棵树的总和 total_sum。
  7. 检查 total_sum 是否为偶数,并且 total_sum // 2 是否存在于 getAllTreeSums 返回的列表中(除了 total_sum 本身,因为不能切断整个树)。

代码实现:

# BinaryTree 类定义(与原问题一致)
class BinaryTree:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

# 辅助函数:递归收集所有子树的总和
# 返回 [当前子树总和, ...所有子树总和]
def getAllTreeSums(tree, all_sums_list):
    if not tree:
        return 0 # 空树总和为0

    left_sum = getAllTreeSums(tree.left, all_sums_list)
    right_sum = getAllTreeSums(tree.right, all_sums_list)

    current_subtree_sum = tree.value + left_sum + right_sum
    all_sums_list.append(current_subtree_sum) # 收集当前子树的总和
    return current_subtree_sum

def splitBinaryTree(tree):
    if not tree:
        return 0

    all_sums = []
    total_tree_sum = getAllTreeSums(tree, all_sums) # 此时 all_sums 包含了所有子树的总和

    # 检查总和是否为偶数,且是否存在一半总和的子树
    # 注意:不能是整个树的总和本身,因为这意味着没有移除任何边
    if total_tree_sum % 2 == 0:
        target_sum = total_tree_sum // 2
        # 遍历 all_sums 列表,查找是否存在 target_sum
        # 排除 total_tree_sum 本身,因为我们不能切断整个树来得到一半
        for s in all_sums:
            if s == target_sum and s != total_tree_sum: # 确保不是整个树的总和
                return target_sum
    return 0

# 优化后的 getAllTreeSums 辅助函数,直接返回列表,更简洁
def _collect_subtree_sums(node, sums_set):
    if not node:
        return 0

    left_sum = _collect_subtree_sums(node.left, sums_set)
    right_sum = _collect_subtree_sums(node.right, sums_set)

    current_node_sum = node.value + left_sum + right_sum
    sums_set.add(current_node_sum)
    return current_node_sum

def splitBinaryTree_optimized(tree):
    if not tree:
        return 0

    # 使用集合存储子树和,方便 O(1) 查找
    subtree_sums = set()
    total_sum = _collect_subtree_sums(tree, subtree_sums)

    # 整个树的总和必须是偶数,并且一半的总和必须存在于某个子树中
    # 且这个子树不能是整个树本身(即不能是 total_sum)
    if total_sum % 2 == 0:
        target_half_sum = total_sum // 2
        # 如果 target_half_sum 存在于 subtree_sums 中,并且它不是整个树的总和
        # (即 target_half_sum != total_sum,这在 target_half_sum == total_sum 
        # 只有在 total_sum 为 0 的情况下才可能发生,但题目至少一个节点)
        # 更严谨的判断是 target_half_sum 必须是某个子树的和,而不能是整个树的和。
        # 由于我们收集的是所有子树的和,如果 target_half_sum == total_sum 
        # 且 total_sum != 0,则意味着整个树的总和是 0,这是不可能的。
        # 所以只需检查 target_half_sum 是否在集合中即可。
        # 如果 total_sum 是 0,则 target_half_sum 也是 0,此时 0 在集合中是有效的。
        if target_half_sum in subtree_sums:
            return target_half_sum
    return 0

效率分析:

  • 时间复杂度: _collect_subtree_sums 函数通过一次深度优先遍历访问每个节点一次,计算其子树总和并添加到集合中,这部分是 O(N)。最后在集合中查找目标和是 O(1)。因此,总时间复杂度为 O(N),其中 N 是树中节点的数量。
  • 空间复杂度: subtree_sums 集合最多存储 N 个子树的总和,因此空间复杂度为 O(N)

这种自底向上的方法避免了重复计算,显著提高了算法的效率。

总结与注意事项

  1. 理解问题约束:在处理树分割问题时,明确“移除一条边”是关键。这通常意味着分割后的一半是原树的某个子树,另一半是剩余部分。
  2. 避免重复计算:对于涉及计算子树属性(如总和、高度等)的问题,自底向上的递归(通常是后序遍历)结合记忆化或一次性收集所有结果,可以有效避免重复计算,将指数级或多项式时间复杂度降低到线性时间复杂度。
  3. 边界条件:始终考虑空树或单节点树等边界情况。
  4. 数据结构选择:使用 set 来存储所有子树总和,可以提供 O(1) 的平均查找时间,比在列表中查找更高效。
  5. 总和必须为偶数:如果整棵树的总和是奇数,则不可能将其分成两个总和相等的子树,可以直接返回0。

通过采用高效的自底向上策略,我们能够以最优的时间复杂度解决二叉树等和分割问题,这在处理大规模数据时尤为重要。

相关专题

更多
treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

529

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

7

2025.12.22

页面置换算法
页面置换算法

页面置换算法是操作系统中用来决定在内存中哪些页面应该被换出以便为新的页面提供空间的算法。本专题为大家提供页面置换算法的相关文章,大家可以免费体验。

389

2023.08.14

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

7

2025.12.31

php网站源码教程大全
php网站源码教程大全

本专题整合了php网站源码相关教程,阅读专题下面的文章了解更多详细内容。

4

2025.12.31

视频文件格式
视频文件格式

本专题整合了视频文件格式相关内容,阅读专题下面的文章了解更多详细内容。

7

2025.12.31

不受国内限制的浏览器大全
不受国内限制的浏览器大全

想找真正自由、无限制的上网体验?本合集精选2025年最开放、隐私强、访问无阻的浏览器App,涵盖Tor、Brave、Via、X浏览器、Mullvad等高自由度工具。支持自定义搜索引擎、广告拦截、隐身模式及全球网站无障碍访问,部分更具备防追踪、去谷歌化、双内核切换等高级功能。无论日常浏览、隐私保护还是突破地域限制,总有一款适合你!

7

2025.12.31

出现404解决方法大全
出现404解决方法大全

本专题整合了404错误解决方法大全,阅读专题下面的文章了解更多详细内容。

41

2025.12.31

html5怎么播放视频
html5怎么播放视频

想让网页流畅播放视频?本合集详解HTML5视频播放核心方法!涵盖<video>标签基础用法、多格式兼容(MP4/WebM/OGV)、自定义播放控件、响应式适配及常见浏览器兼容问题解决方案。无需插件,纯前端实现高清视频嵌入,助你快速打造现代化网页视频体验。

3

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
HTML5/CSS3/JavaScript/ES6入门课程
HTML5/CSS3/JavaScript/ES6入门课程

共102课时 | 6.6万人学习

前端基础到实战(HTML5+CSS3+ES6+NPM)
前端基础到实战(HTML5+CSS3+ES6+NPM)

共162课时 | 18.5万人学习

第二十二期_前端开发
第二十二期_前端开发

共119课时 | 12.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号