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

夜伟小哥_1191

夜伟小哥_1191

2025-11-15

659人浏览

原创

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

本文深入探讨了如何通过移除单条边将二叉树分割成两个总和相等的子树问题。文章首先分析了常见递归解法中的逻辑错误,并提供了修正后的代码。接着,提出了一种更高效的自底向上计算子树和的算法,该算法通过一次遍历收集所有子树和,从而在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。

修正后的递归代码

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

修正点:

node-connect
node-connect

诊断Android、iOS及macOS配套应用的OpenClaw节点连接与配对故障,适用于二维码/设置码/手动连接失败或本地Wi-Fi问题时。

下载
  • 移除那些会同时切断多条边的条件判断。
  • 在递归调用 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。

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

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

node

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
treenode的用法
treenode的用法

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

2023.12.01

2061

7

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

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

2025.12.22

296

20

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

2026.01.06

337

22

C++ 数据结构与算法实现教程合集
C++ 数据结构与算法实现教程合集

以 C++ 为实现语言,系统讲解核心数据结构与算法,涵盖链表(单链表/双链表/环检测)、栈与队列(单调栈/优先队列)、二叉树(遍历/BST/AVL/红黑树)、哈希表(开地址法/链地址法)、图(邻接表/BFS/DFS/Dijkstra/拓扑排序)、常见排序算法(快排/归并/堆排/计数排序)的实现与复杂度分析,同时分享 LeetCode 刷题技巧、竞赛编程常用模板(二分/前缀和/滑动窗口/动态规划),帮助开发者夯实算法基础。

2026.05.09

352

25

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

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

2023.08.14

4496

4

AI视频生成软件推荐
AI视频生成软件推荐

本专题汇总了当前主流的AI视频生成软件推荐与排行榜单,涵盖seko、AniShort、剧云、Lovart、LiblibAI及立刻mv等热门工具。同时整理了各软件在文生视频、图生视频、时长限制、画质表现及免费额度等方面的差异对比,助您快速选对适合创作需求的AI视频生成工具。

2026.09.16

140

9

ai生成视频的工具免费版合集
ai生成视频的工具免费版合集

本专题汇总了当前免费AI生成视频工具的排行榜与推荐清单,涵盖seko、讯飞智作、AniShort及剧云、Lovart等多模型集成平台。同时整理了各工具的免费额度、输出时长、水印政策及适用场景差异,助您快速选择合适工具开启AI视频创作。

2026.09.16

60

10

Pandas时间序列分析与可视化报表
Pandas时间序列分析与可视化报表

本专题整理Pandas日期转换、时间索引、重采样、滚动窗口、时区处理、plot绘图、Styler表格样式和报表输出方法。

2026.09.16

60

23

Pandas数据筛选索引与清洗处理
Pandas数据筛选索引与清洗处理

本专题整理Pandas中的loc、iloc、条件筛选、query查询、缺失值处理、重复值删除、类型转换和字符串列清洗方法。

2026.09.16

40

25

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
WebSocket手册
WebSocket手册

共0课时 | 0人学习

HTML5/CSS3/JavaScript/ES6入门课程
HTML5/CSS3/JavaScript/ES6入门课程

共102课时 | 10.5万人学习

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

共162课时 | 27.6万人学习