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

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

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

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

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

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

递归尝试与常见陷阱

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

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

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

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

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

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

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

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

修正后的递归代码

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

修正点:

移除那些会同时切断多条边的条件判断。在递归调用 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)。对于大型树,这种方法效率低下。

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

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

核心思想:

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

算法步骤:

定义一个辅助函数 getAllTreeSums(tree),它将递归地遍历树。对于每个节点,首先递归调用左右子节点,获取它们的子树总和。当前节点的子树总和等于 node.value + left_subtree_sum + right_subtree_sum。将这个总和添加到一个全局列表或集合中,并返回当前节点的子树总和。主函数 splitBinaryTree(tree) 调用 getAllTreeSums 来填充所有子树和的列表。获取列表中的第一个元素,即整棵树的总和 total_sum。检查 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_sumdef 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_sumdef 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)

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

总结与注意事项

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

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

以上就是二叉树等和分割:从递归修正到高效算法的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1380114.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月14日 21:30:48
下一篇 2025年12月14日 21:31:04

相关推荐

  • 处理Pandas中带嵌入双引号的制表符分隔文件:实现精确读写回溯

    在使用Pandas处理制表符分隔文件(TSV)时,我们经常会遇到一些非标准格式,其中一个常见且棘手的问题是字段值内部包含未转义的双引号,而整个字段又被双引号包裹。例如,一个字段可能是 `”Series 48SL–5 WEDGE–LOK, 2-56UNC-2B, 5.00″, …

    好文分享 2025年12月14日
    000
  • 在FastAPI中优雅地管理和监控外部服务的启动与关闭

    本文详细阐述了如何在fastapi应用中启动并监控外部服务(如java服务)的生命周期。通过结合`asyncio.subprocess_shell`、自定义`asyncio.subprocessprotocol`以及fastapi的`lifespan`事件,我们能够实现对外部服务启动日志的实时监听、…

    2025年12月14日
    000
  • Python多线程如何实现任务队列 Python多线程生产者消费者模型

    答案:使用Python多线程和queue.Queue可实现生产者-消费者模型,生产者生成任务并放入队列,消费者从队列取出任务处理,通过put和get的阻塞机制保证线程安全,生产者结束后向队列发送None作为结束信号,消费者接收到后退出,配合task_done和join确保所有任务完成,适用于爬虫、日…

    2025年12月14日
    000
  • Python range 函数:实现包含终止值的迭代

    本文详细介绍了python `range` 函数在迭代时如何包含终止值的问题。通过修改 `range` 函数的第二个参数,即将其设置为 `stop + 1`,可以轻松实现对指定范围内的所有数字(包括起始和终止值)进行遍历和处理,从而解决默认 `range` 函数不包含终止值的特性,提高代码的灵活性和…

    2025年12月14日
    000
  • 使用Selenium自动化展开动态下拉菜单并高效提取子分类链接

    本教程详细阐述如何利用selenium处理动态网页中的下拉菜单,通过识别并迭代点击展开图标,实现所有子菜单的完全展开。随后,指导读者如何从展开后的页面结构中精准提取所需的子分类链接,并提供完整的python代码示例及实用的注意事项,旨在提升网页数据抓取的效率和准确性。 使用Selenium自动化展开…

    2025年12月14日
    000
  • Dask DataFrame groupby 模式(Mode)聚合的实现指南

    本教程详细阐述了如何在 dask dataframe 中对分组数据执行模式(mode)聚合。由于 dask 不直接提供 `groupby.agg` 的模式函数,文章通过自定义 `dask.dataframe.aggregation` 类,实现 `chunk`、`agg` 和 `finalize` 阶…

    2025年12月14日
    000
  • 处理Pandas中带嵌入双引号的制表符分隔文件:实现精确往返读写

    本文探讨了在pandas中处理特殊制表符分隔文件(tsv)的挑战,特别是当字段被双引号包围且内部包含未转义的双引号时。我们将介绍三种策略:利用python内置`csv`模块进行手动解析、实现自定义`decode/encode`函数以确保文件内容的精确往返,以及结合正则表达式预处理与pandas进行读…

    2025年12月14日
    000
  • Python跨目录导入模块与包管理深度解析

    本文深入探讨了python中跨目录导入模块时常见的`importerror`问题,详细阐述了python的包结构、模块搜索机制及正确的执行上下文。通过分析独立包与子包两种场景,并提供相应的代码示例和执行方法,旨在帮助开发者理解如何构建可维护的python项目结构,并强调将可执行脚本与可重用包分离的最…

    2025年12月14日
    000
  • 使用Pandas处理Excel数据:合并跨行单元格以优化表格结构

    本教程旨在指导如何使用python pandas库处理非标准格式的excel数据。当数据逻辑上属于同一记录但物理上分散在两行时,我们将学习一种迭代方法,将特定列的跨行数据合并到单个单元格(列表形式)中。此过程有助于将原始的非规范化数据转换为更适合分析和表格展示的结构,提高数据可用性。 在日常数据处理…

    2025年12月14日
    000
  • ROS2 Python节点中导入外部Python模块的最佳实践

    本文旨在解决在ROS2 Python节点中,因尝试导入位于非ROS2包目录下的Python模块而导致的`ModuleNotFoundError`。核心解决方案是利用Python的`sys.path.append()`方法,在运行时动态扩展Python解释器的模块搜索路径,从而成功加载外部Python…

    2025年12月14日
    000
  • Python属性与+=操作符:深入理解其工作机制及陷阱规避

    本文深入探讨了python中对属性使用`+=`等原地操作符时的工作机制。揭示了该操作不仅会调用底层对象的`__iadd__`方法,还会隐式地尝试将`__iadd__`的返回值重新赋值给该属性,从而触发属性的setter方法。文章将通过具体示例分析这一行为带来的潜在陷阱,并提供修改setter的解决方…

    2025年12月14日
    000
  • Python中如何优化随机事件的角色生成与属性管理

    本文旨在探讨并解决在Python中处理随机事件(如游戏角色生成)时常见的代码冗余和维护难题。通过引入面向对象编程和数据驱动的设计模式,我们将展示如何将重复的条件逻辑重构为更简洁、可扩展且易于维护的代码结构,从而有效管理不同角色的属性和行为,避免重复代码和潜在的逻辑错误。 1. 传统条件逻辑的挑战 在…

    2025年12月14日
    000
  • 确保GitHub Actions构建使用正确的发布标签版本:常见问题与解决方案

    本文旨在解决github actions在构建python包时,版本号与发布标签不匹配的问题。核心在于理解github actions如何处理发布事件,以及确保在创建发布标签时,`setup.py`文件中的版本号已正确更新并提交。通过调整标签创建流程,可以有效避免构建失败,确保每次发布都使用与标签一…

    2025年12月14日
    000
  • 如何为Python Slack Bolt Socket模式应用实现代码热重载

    本文详细介绍了如何在开发阶段为Python Slack Bolt Socket模式应用实现代码自动重载功能。通过将Slack Bolt应用与FastAPI框架结合,并利用Uvicorn的–reload选项,开发者可以在代码修改后自动重启应用,显著提升开发效率。文章提供了完整的代码示例和运…

    2025年12月14日
    000
  • Twilio WhatsApp API:从沙盒到生产环境的消息发送指南

    在使用twilio whatsapp api进行开发测试时,开发者常遇到无法向twilio沙盒外部号码发送消息的问题,即使控制台显示消息已创建且无错误。本文旨在阐明这一现象的根本原因——twilio沙盒环境的测试性质,并提供解决方案:要实现向任意whatsapp号码发送消息,必须完成whatsapp…

    2025年12月14日
    000
  • 解决Keras GAN图像维度不匹配:生成器训练中的常见陷阱

    本文深入探讨了在使用Keras构建生成对抗网络(GAN)进行图像着色时,生成器训练过程中常见的图像维度不匹配问题。通过分析生成器输出与目标标签形状的差异,文章提供了加载彩色图像、将其尺寸调整至与生成器输出精确匹配的解决方案,并强调了在深度学习模型训练中数据预处理和形状一致性的重要性。 在构建基于深度…

    2025年12月14日
    000
  • Python子类中实现无副作用的队列判空方法

    本文旨在探讨如何在Python中为队列的子类实现一个高效且无副作用的`isempty`方法。我们将深入分析在继承场景下,调用父类方法可能引发的状态管理问题,特别是当父类方法(如`get`)会修改队列状态时。教程将详细讲解`QueueError`的正确继承、`super()`关键字的恰当使用,以及如何…

    2025年12月14日
    000
  • Python从PDF饼图(及类似图表)中提取数据的专业指南

    本教程详细介绍了如何使用Python从PDF文档中的饼图(或其他类似图表)中提取数据。核心方法是将PDF页面转换为图像,随后利用图像处理库(如OpenCV)识别并分析图表元素。文章涵盖了从PDF到图像的转换工具安装、图像预处理、轮廓检测以及初步的数据分析方法,旨在提供一个清晰、可操作的流程,帮助开发…

    2025年12月14日
    000
  • TensorFlow中变量初始化与优化机制详解

    本文深入探讨了tensorflow中`tf.variable`的初始化及其在模型训练中的作用。通过一个多项式回归的例子,解释了即使变量被初始化为零,它们也会在优化器的驱动下,根据损失函数和训练数据迭代更新为非零值,从而实现模型参数的学习。文章强调了优化器在机器学习模型训练中的核心地位。 Tensor…

    2025年12月14日
    000
  • python中geopy怎么用

    geopy用于地理编码和逆地理编码,支持多种服务如Nominatim;需设置user_agent,遵守请求限制,建议生产环境使用付费API。 geopy 是一个 Python 第三方库,用于地理编码(将地址转为经纬度)和逆地理编码(将经纬度转为地址)。它支持多种服务,比如 Google Maps、O…

    2025年12月14日
    000

发表回复

登录后才能评论
关注微信