二叉树等和分割:从递归错误到高效算法实践

二叉树等和分割:从递归错误到高效算法实践

本文深入探讨了如何通过移除一条边将二叉树分割成两个和相等的子树。文章首先分析并纠正了在递归实现中常见的逻辑错误,包括不正确的边缘判断和递归参数传递问题。随后,介绍了一种更高效的算法,通过一次遍历自底向上收集所有子树和,从而在O(N)时间复杂度内解决该问题,并提供了详细的代码示例和实现解析。

问题描述

给定一个至少包含一个节点的二叉树,判断是否可以通过移除一条边将其分割成两个和相等的二叉树。如果可能,返回分割后每个子树的和;否则,返回0。我们不需要返回被移除的边。

例如,对于一个总和为32的二叉树,如果能找到一条边将其分割成两个总和均为16的子树,则返回16。

初始递归方法的分析与修正

在解决这类问题时,一种直观的思路是采用递归方法遍历树的每个节点,并尝试以该节点为根的子树作为其中一个分割点。然而,这种方法容易引入一些逻辑错误和效率问题。

原始实现中的常见错误

考虑以下用户尝试的递归实现:

# 辅助函数:计算子树的总和def icalculatesum(node):    if node is None:        return 0    return node.value + icalculatesum(node.left) + icalculatesum(node.right)def splitBinaryTree(tree, balancesum=0):    if tree is None:        return 0    fullsum = icalculatesum(tree) # 重复计算    # 错误1:这些条件可能导致同时移除两条边    # 例如:if leftsum + tree.value == rightsum + balancesum    # 这意味着将 tree 及其左子树作为一个整体,同时从其父节点和右子节点断开    # 这不符合移除“一条”边的要求    leftsum = icalculatesum(tree.left) # 重复计算    rightsum = icalculatesum(tree.right) # 重复计算    # 错误2:递归调用中的 balancesum 参数传递不正确    # lefty = splitBinaryTree(tree.left, fullsum - rightsum)    # righty = splitBinaryTree(tree.right, fullsum - leftsum)    # 这里的 fullsum - rightsum 并没有正确考虑当前节点 tree.value    # 导致 balancesum 无法正确代表“另一半”子树的和    # ... 其他条件判断 ...    # if lefty != 0 or righty !=0:    #     return fullsum/2    # return 0

上述代码存在以下主要问题:

重复计算子树和: icalculatesum 函数在每次递归调用中都会从头开始计算子树的总和。这导致了大量的重复计算,使得算法效率低下(最坏情况下时间复杂度可能达到 O(N^2))。不正确的分割判断: 像 if leftsum + tree.value == rightsum + balancesum: 这样的条件,实际上是在尝试移除两条边(将当前节点与其父节点以及其右子节点断开),这与问题要求“移除一条边”不符。正确的逻辑应该是移除与子节点相连的一条边。递归参数传递错误: 在 splitBinaryTree(tree.left, fullsum – rightsum) 这样的递归调用中,balancesum 期望代表的是“另一半”子树的总和。然而,fullsum – rightsum 并未包含当前节点 tree.value,导致计算偏差。正确的 balancesum 应该包括当前节点的值和其兄弟子树的值,以及从父节点继承的 balancesum。

修正后的递归实现

针对上述问题,我们可以对递归逻辑进行修正。关键在于:

balancesum 的正确含义: 在递归到子节点时,balancesum 应该代表当前节点及其兄弟子树,加上从父节点传下来的所有值的总和。避免重复计算: 虽然下面的修正版仍然会重复计算 icalculatesum,但它修正了逻辑错误。更优的方案会在后续介绍。简化判断: 许多显式的 if 条件判断实际上可以在递归的深层被捕获,因此可以简化。

class BinaryTree:    def __init__(self, value, left=None, right=None):        self.value = value        self.left = left        self.right = right# 辅助函数:计算子树的总和def icalculatesum(node):    if node is None:        return 0    return node.value + icalculatesum(node.left) + icalculatesum(node.right)def splitBinaryTree_corrected(tree, balancesum=0):    if tree is None:        return 0    fullsum = icalculatesum(tree) # 当前子树的总和    # 如果当前子树的和等于期望的 balancesum,则找到了一个分割点    # 此时,fullsum 就是其中一个子树的和,另一个子树的和就是 balancesum    # 如果 fullsum == balancesum,意味着整个树可以被分割成两半,每半的和为 fullsum    if fullsum == balancesum:        return fullsum    leftsum = icalculatesum(tree.left)    rightsum = icalculatesum(tree.right)    # 递归调用左子树:    # 传递给左子树的 balancesum 应该包含:    # 1. 当前节点的值 (tree.value)    # 2. 当前节点的右子树的总和 (rightsum)    # 3. 从父节点继承的 balancesum (即除了当前子树之外的其余部分的总和)    # 这三部分加起来,就是如果从当前节点和左子树的连接处断开,    # “另一半”树的总和。    lefty = splitBinaryTree_corrected(tree.left, balancesum + tree.value + rightsum)    # 递归调用右子树:    # 传递给右子树的 balancesum 应该包含:    # 1. 当前节点的值 (tree.value)    # 2. 当前节点的左子树的总和 (leftsum)    # 3. 从父节点继承的 balancesum    righty = splitBinaryTree_corrected(tree.right, balancesum + tree.value + leftsum)    # 如果任一子树的递归调用返回非零值(即找到了分割点),则返回该值    return lefty or righty

这个修正后的版本解决了递归参数传递的逻辑错误。然而,它仍然存在效率问题,因为 icalculatesum 会在每个节点被多次调用。

更高效的算法:单次遍历收集子树和

解决重复计算问题的最佳方法是采用一次深度优先遍历(DFS),自底向上地计算每个子树的总和,并将这些和存储起来。这样,每个节点只会被访问一次,从而达到 O(N) 的时间复杂度。

算法思路

自底向上计算所有子树和: 遍历整个二叉树,对于每个节点,递归地计算其左右子树的和,然后加上自身的值,得到以该节点为根的子树的总和。在计算过程中,将所有遇到的子树的总和存储到一个列表中。检查分割条件: 在收集完所有子树和后,我们得到整个树的总和(列表中的第一个元素)。如果整个树的总和是偶数,并且其一半的值存在于我们收集到的子树和列表中,那么就意味着可以进行等和分割。

代码实现

# 定义二叉树节点(与问题描述一致)class BinaryTree:    def __init__(self, value, left=None, right=None):        self.value = value        self.left = left        self.right = right# 辅助函数:递归收集所有子树的总和# 返回一个列表,其中第一个元素是当前子树的根节点总和,# 后面跟着左右子树的所有子和。def getAllTreeSums(tree):    if not tree:        return [0] # 空树的总和为0    # 递归获取左右子树的所有子和    left_sums = getAllTreeSums(tree.left)    right_sums = getAllTreeSums(tree.right)    # 当前子树的总和 = 自身值 + 左子树根节点总和 + 右子树根节点总和    current_tree_sum = tree.value + left_sums[0] + right_sums[0]    # 将当前子树的总和以及左右子树的所有子和合并返回    # current_tree_sum 放在列表首位    return [current_tree_sum] + left_sums + right_sumsdef splitBinaryTree_optimized(tree):    # 收集所有子树的总和    tree_sums = getAllTreeSums(tree)    # 整个树的总和是列表的第一个元素    total_sum = tree_sums[0]    # 如果总和是偶数,并且其一半的值存在于所有子树和的集合中    # 那么就找到了一个分割点    if total_sum % 2 == 0 and (total_sum // 2) in tree_sums:        return total_sum // 2    # 否则,无法进行等和分割    return 0

算法解析与优势

getAllTreeSums 函数:这是一个典型的深度优先搜索(DFS)后序遍历。它首先递归处理左右子树,然后结合子树的结果计算当前节点的值。left_sums[0] 和 right_sums[0] 分别代表左右子树的根节点(即整个左右子树)的总和。通过将所有子树和收集到一个列表中,我们避免了重复计算。每个节点的值只被加到其祖先节点的和中一次。splitBinaryTree_optimized 函数:在获取了所有可能的子树和之后,问题转化为一个简单的查找操作。首先检查 total_sum % 2 == 0,因为如果总和是奇数,不可能分割成两个相等的整数和。然后,通过 (total_sum // 2) in tree_sums 检查是否存在一个子树的总和恰好等于总和的一半。如果存在,这意味着我们可以从该子树的根节点与父节点之间的边断开,从而实现等和分割。时间复杂度: getAllTreeSums 函数遍历每个节点一次,因此时间复杂度为 O(N),其中 N 是树中的节点数。in tree_sums 操作在最坏情况下(列表)是 O(N),如果转换为 set 则为 O(1) 平均。整体时间复杂度为 O(N)。空间复杂度: 存储所有子树和的列表 tree_sums 需要 O(N) 的空间。递归调用的深度在最坏情况下(倾斜树)也需要 O(N) 的空间。

总结

二叉树等和分割问题是一个经典的树形结构问题。从最初的递归尝试中,我们学习到在处理树的递归问题时,需要特别注意:

参数传递的准确性: 确保递归函数接收的参数能够正确反映当前子问题所需的状态。避免重复计算: 通过自底向上或记忆化搜索等技术,可以显著提高算法的效率。清晰的分割逻辑: 严格按照问题定义来判断分割条件,避免引入歧义或错误的逻辑。

最终,通过采用一次高效的深度优先遍历来收集所有子树和,我们能够以最优的 O(N) 时间复杂度解决此问题,提供了一个既健壮又高效的解决方案。

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

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

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
解决Keras模型中Ellipsis对象序列化错误的教程
上一篇 2025年12月14日 21:04:02
Python中高精度计算(1-1/x)^y:大数场景下的策略
下一篇 2025年12月14日 21:04:13

相关推荐

  • Golang goroutine与channel调试技巧

    使用go run -race检测数据竞争,结合runtime.NumGoroutine监控协程数量,通过pprof分析阻塞调用栈,利用select超时避免永久阻塞,有效排查goroutine泄漏、死锁和数据竞争问题。 Go语言的goroutine和channel是并发编程的核心,但它们也带来了调试上…

    2026年5月10日
    000
  • 深入理解 Express.js 中 next() 参数的作用与中间件机制

    本文深入探讨 express.js 中间件函数中的 `next()` 参数。它负责将控制权传递给请求-响应周期中的下一个中间件或路由处理程序。文章将详细解释 `next()` 的工作原理、中间件的注册与执行顺序,以及不正确使用 `next()` 可能导致请求挂起的风险,并通过代码示例和实际应用场景,…

    2026年5月10日
    000
  • Golang空接口如何应用在项目中

    空接口可用于接收任意类型值,常见于日志函数、通用数据结构、JSON动态解析及配置驱动逻辑,提升代码灵活性,但需配合类型断言确保安全,避免滥用以降低维护成本。 空接口 interface{} 在 Go 语言中是一个非常灵活的类型,它可以存储任何类型的值。虽然它牺牲了一部分类型安全,但在实际项目中合理使…

    2026年5月10日
    100
  • JavaScript 高效判断页面所有复选框状态的技巧与实践

    本文旨在提供一套高效且专业的javascript方法,用于判断网页中所有复选框的选中状态。我们将探讨如何利用`array.some()`快速确定是否有未选中的复选框(进而判断是否全部选中),以及如何使用`array.filter()`统计选中和未选中的复选框数量。通过优化dom元素选择和数组操作,提…

    2026年5月10日
    000
  • OSMnx中interpolate_points函数详解及街道细分与图构建实践

    本文详细介绍了osmnx库中`utils_geo.interpolate_points`函数的使用方法,特别是其返回的python生成器类型。我们将学习如何处理生成器输出,并提供一个完整的教程,演示如何利用此函数将现有街道几何体细分为更小的线段,进而构建一个精细化的网络图,以支持更细粒度的空间分析。…

    2026年5月10日
    000
  • 如何安全有效地从外部网页获取HTML元素数据并应用于自身页面

    本教程旨在解决如何在不同域名下,通过javascript获取并使用另一个网页的html元素数据。文章将深入探讨同源策略的限制,并提供两种主要解决方案:使用` 在现代Web开发中,有时我们需要从外部网站获取特定的HTML内容或属性值,并将其整合到我们自己的网页中。例如,从XYZ.COM/B.html页…

    2026年5月10日
    000
  • Go语言Cgo代码GDB调试失效:Go 1.1版本下的挑战与官方进展

    本文探讨了go语言程序中cgo代码在使用gdb进行调试时遇到的挑战,特别指出go 1.1版本中存在的变量值显示异常问题。该问题是一个已知的官方缺陷(go issue 5221),导致在cgo交互部分gdb调试功能失效,而go 1.0版本则无此问题。文章将通过示例代码重现该现象,并阐述其根源及官方的解…

    2026年5月10日
    000
  • 使用SMTP.js发送邮件:客户端集成、常见问题与最佳实践指南

    本文深入探讨了使用SMTP.js库在前端发送邮件时可能遇到的问题,特别是与Elastic Email集成时的挑战。我们将分析代码中常见的异步处理错误、条件函数定义陷阱,并提供修正后的代码示例和最佳实践。重点强调了正确处理Promise链、确保函数可访问性以及客户端邮件发送的安全考量,帮助开发者构建更…

    2026年5月10日
    000
  • JS如何操作HTML元素_DOM编程核心方法【教程】

    必须掌握操作HTML元素的核心DOM方法:一、通过ID获取单个元素;二、通过类名获取元素集合;三、通过标签名获取元素集合;四、通过CSS选择器获取元素;五、为元素绑定事件监听器;六、创建并插入新元素;七、替换或删除现有元素。 如果您希望使用JavaScript动态修改网页内容、响应用户交互或构建交互…

    2026年5月10日
    000
  • Golang如何提升TCP长连接处理效率_Golang TCP长连接处理性能优化实践详解

    答案:通过非阻塞I/O、单Goroutine双工模型、sync.Pool对象复用、TCP_NODELAY优化及高效心跳管理,结合系统调优,可显著提升Golang百万级TCP长连接处理效率。 在高并发网络服务场景中,TCP长连接的处理效率直接影响系统的吞吐能力和资源消耗。Golang凭借其轻量级Gor…

    2026年5月10日
    000
  • PHP动态网页数据库备份恢复_PHP动态网页MySQL数据库备份教程

    答案:PHP动态网页的MySQL数据库备份与恢复需通过定期导出SQL文件并安全存储来保障数据安全,核心方法包括使用mysqldump命令行工具实现高效灵活的自动化备份,利用phpMyAdmin图形化工具进行手动导出导入以降低操作门槛,以及通过PHP脚本调用系统命令将备份过程集成到应用中;恢复时可采用…

    2026年5月10日
    000
  • 如何在不暴露密钥的情况下,在客户端创建 Stripe Payment Link

    本文介绍了在纯静态网站环境下,如何利用 Stripe Payment Link 实现商品售卖,并着重讨论了在不暴露 Stripe 密钥的前提下,客户端创建 Payment Link 的可行性。分析了直接在客户端使用密钥的风险,并提出了预先生成 Payment Link 或使用后端服务动态生成 Pay…

    2026年5月10日
    000
  • Windows用Prettier一键格式化乱码HTML代码

    首先确保HTML文件保存为UTF-8编码,使用文本编辑器另存为UTF-8格式;其次在命令行执行chcp 65001切换至UTF-8代码页后再运行Prettier;接着在VS Code中设置files.encoding为utf8并启用files.autoGuessEncoding;最后可通过Node.…

    2026年5月10日
    000
  • php怎么截取网页_php抓取网页内容的几种方法

    file_get_contents适用于静态页抓取,但受限于allow_url_fopen且无法执行JS;2. cURL支持自定义请求头、Cookie等,适合处理复杂HTTP请求;3. Guzzle作为现代PHP项目推荐方案,具备良好扩展性与异步支持;4. 动态渲染内容需借助Puppeteer或Se…

    2026年5月10日
    000
  • html函数如何实现动态内容显示 html函数在网页交互中的核心应用

    JavaScript函数通过操作DOM实现动态内容更新与交互,如显示时间、实时搜索、增删元素及加载数据,使网页具备动态功能。 HTML 本身没有“函数”的概念,它是一种标记语言,用于定义网页结构。真正实现动态内容显示和交互功能的是 JavaScript。通常所说的“HTML函数”其实是 JavaSc…

    2026年5月10日
    000
  • JavaScript解释器_javascript代码执行

    JavaScript通过引擎解析执行,先语法分析生成AST,再编译为字节码或机器码,最后执行;执行时创建上下文并入栈,同步代码直接运行,异步任务由API处理后回调入队,事件循环在调用栈空时将回调推入执行;此机制解释了变量提升、暂时性死区及宏任务与微任务执行顺序差异。 JavaScript代码的执行依…

    2026年5月10日
    000
  • C#如何处理异常?C# try-catch-finally最佳实践与常见错误规避

    正确使用 try-catch-finally 应捕获具体异常、用 finally 或 using 释放资源、避免空 catch 和裸抛异常,确保异常日志记录并保留堆栈跟踪,提升代码健壮性与可维护性。 在C#中,异常处理是保障程序稳定运行的重要机制。正确使用 try-catch-finally 结构不…

    2026年5月10日
    000
  • HTMLAMP怎么做_加速移动页面实现教程

    答案:HTML AMP通过规范标签、禁用自定义JS、引入AMP JS库和缓存技术提升移动页面加载速度,需遵循AMP HTML标准并验证有效性,有助于SEO但非万能,未来将更开放并与PWA等融合。 HTML AMP 旨在加速移动页面加载速度,提升用户体验。简单来说,它通过限制某些 HTML 功能,并采…

    2026年5月10日
    000
  • 理解PHP服务器端请求与浏览器开发者工具的限制

    当PHP脚本使用file_get_contents等函数发起服务器端请求时,这些请求直接在服务器上执行,而非通过浏览器。因此,浏览器开发者工具的网络活动面板无法捕获和显示这些内部的服务器间通信,因为它仅监控浏览器自身发出的网络请求,对服务器内部处理过程无感知。 客户端请求与服务器端请求的本质区别 在…

    2026年5月10日
    000
  • Flet应用中正确显示AlertDialog对话框的指南

    本文旨在指导flet开发者如何正确显示`alertdialog`对话框。针对在`usercontrol`中直接设置`dlg_modal.open = true`和调用`self.update()`无法显示对话框的常见问题,文章详细阐述了其原因,并提供了使用`e.page.show_dialog_as…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信