0

0

深入理解二叉树扁平化:原地转换为双向链表结构

花韻仙語

花韻仙語

发布时间:2025-12-04 13:44:37

|

514人浏览过

|

来源于php中文网

原创

深入理解二叉树扁平化:原地转换为双向链表结构

本文详细阐述如何将二叉树原地扁平化为一种类似双向链表的结构,其中节点的左右指针分别模拟链表的prev和next指针。通过递归辅助函数,文章深入解析了在遍历过程中如何正确地调整节点指针,以实现左-中-右的顺序连接,并避免常见的循环引用问题。此教程旨在提供一个清晰、高效的二叉树扁平化解决方案。

二叉树扁平化概述

二叉树扁平化是将一个标准的二叉树结构转换为一个类似双向链表的结构。在这个新的结构中,原二叉树的节点仍然存在,但它们的 left 和 right 指针被重新定义,分别扮演了双向链表中的 prev 和 next 角色。扁平化后的节点应遵循原二叉树的左-中-右(in-order)遍历顺序。此过程必须是“原地”(in-place)操作,即直接修改现有节点的指针,而不是创建新的节点或复制数据。最终,函数需要返回扁平化结构的最左侧节点(即原二叉树中in-order遍历的第一个节点)。

节点结构定义

首先,我们定义二叉树的节点结构,它包含一个值、一个左子节点和一个右子节点。

class BinaryTree:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

扁平化核心思想:递归与指针重定向

实现原地扁平化的关键在于使用递归辅助函数,它能够处理当前节点及其左右子树的扁平化,并将它们正确地连接起来。辅助函数通常需要返回扁平化子树的最左侧节点和最右侧节点,以便父节点能够将它们连接到自身。

初始尝试与常见陷阱

在尝试实现时,一个常见的误区是对辅助函数返回值的初始化。例如,考虑以下初始化方式:

# 假设这是某个辅助函数内部
leftmostofleft = node
rightmostofright = node
leftmostofright = node # 潜在问题
rightmostofleft = node # 潜在问题

这种初始化方式在某些情况下会导致错误。具体来说,如果一个节点没有右子节点,而我们将其 leftmostofright 初始化为 node,那么当执行 node.right = leftmostofright 时,就会导致 node.right = node,从而形成一个循环引用。这显然不是我们期望的双向链表结构,因为链表不应该有节点指向自身作为其下一个节点。因此,在没有实际子树的情况下,对应的“最左侧”或“最右侧”节点指针应该保持为 None,直到它们被实际的子树扁平化结果填充。

微软爱写作
微软爱写作

微软出品的免费英文写作/辅助/批改/评分工具

下载

优化后的递归辅助函数

为了避免上述问题并简化逻辑,我们可以采用一个更精炼的递归策略。核心思想是:

  1. 递归处理左子树:扁平化左子树,获取其扁平化后的最左侧和最右侧节点。
  2. 连接当前节点与左子树:将当前节点的 left 指针指向左子树扁平化后的最右侧节点,并建立从该最右侧节点到当前节点的反向连接。
  3. 递归处理右子树:扁平化右子树,获取其扁平化后的最左侧和最右侧节点。
  4. 连接当前节点与右子树:将当前节点的 right 指针指向右子树扁平化后的最左侧节点,并建立从该最左侧节点到当前节点的反向连接。
  5. 返回扁平化子树的整体最左侧和最右侧节点

以下是优化后的 helper 函数实现:

def flattenBinaryTree(root):
    # 如果根节点为空,则直接返回None
    if root is None:
        return None
    # 调用辅助函数进行扁平化,并返回扁平化结构的最左侧节点
    leftmost, _ = helper(root)
    return leftmost

def helper(node):
    # 递归终止条件:如果节点为空,则返回None, None
    if node is None:
        return None, None

    # 初始化当前节点为扁平化结构的最左和最右节点,
    # 假设它目前是独立的,没有左右子树连接
    leftmost_of_flattened_subtree = node
    rightmost_of_flattened_subtree = node

    # 1. 扁平化左子树
    # 如果存在左子节点
    if node.left:
        # 递归调用helper,获取左子树扁平化后的最左侧和最右侧节点
        # 注意:这里将返回的最左侧节点赋给 leftmost_of_flattened_subtree,
        # 因为整个扁平化结构的最左侧节点将来自最左侧的子树。
        # 同时,将左子树扁平化后的最右侧节点赋给 node.left,
        # 这样 node.left 现在指向了它在双向链表中的“前一个”节点。
        leftmost_of_flattened_subtree, node.left = helper(node.left)

        # 建立从左子树扁平化后的最右侧节点到当前节点的连接
        # 即:前一个节点的 right 指针指向当前节点
        node.left.right = node

    # 2. 扁平化右子树
    # 如果存在右子节点
    if node.right:
        # 递归调用helper,获取右子树扁平化后的最左侧和最右侧节点
        # 注意:这里将右子树扁平化后的最左侧节点赋给 node.right,
        # 这样 node.right 现在指向了它在双向链表中的“后一个”节点。
        # 同时,将返回的最右侧节点赋给 rightmost_of_flattened_subtree,
        # 因为整个扁平化结构的最右侧节点将来自最右侧的子树。
        node.right, rightmost_of_flattened_subtree = helper(node.right)

        # 建立从右子树扁平化后的最左侧节点到当前节点的反向连接
        # 即:后一个节点的 left 指针指向当前节点
        node.right.left = node

    # 返回当前节点所代表的扁平化子树的最左侧和最右侧节点
    return leftmost_of_flattened_subtree, rightmost_of_flattened_subtree

代码解析与注意事项

  1. flattenBinaryTree(root) 函数:这是对外暴露的接口。它首先处理根节点为空的情况,然后调用 helper 函数。helper 函数返回的是扁平化结构的最左侧节点和最右侧节点,但 flattenBinaryTree 只需要返回最左侧节点,因为它是链表的起点。
  2. helper(node) 函数
    • 基线条件:if node is None: return None, None。当遇到空节点时,没有可扁平化的内容,返回 None。
    • 初始化 leftmost_of_flattened_subtree 和 rightmost_of_flattened_subtree
      • 它们最初都指向 node 本身。这意味着如果 node 是一个叶子节点,那么它就是自身扁平化后的最左和最右节点。
      • 如果 node 有左子树,leftmost_of_flattened_subtree 会被更新为左子树扁平化后的最左节点。
      • 如果 node 有右子树,rightmost_of_flattened_subtree 会被更新为右子树扁平化后的最右节点。
    • 处理左子树 (if node.left:)
      • leftmost_of_flattened_subtree, node.left = helper(node.left):
        • 递归地扁平化左子树。
        • leftmost_of_flattened_subtree 被更新为左子树扁平化后的最左节点。这是因为整个扁平化序列的起点必然在当前节点的左子树中(如果存在)。
        • node.left 被赋值为 helper(node.left) 返回的第二个值,即左子树扁平化后的最右节点。这意味着 node.left 现在指向了它在扁平化链表中的“前一个”节点。
      • node.left.right = node:建立从左子树扁平化后的最右节点到当前节点的 right 指针连接。
    • 处理右子树 (if node.right:)
      • node.right, rightmost_of_flattened_subtree = helper(node.right):
        • 递归地扁平化右子树。
        • node.right 被赋值为 helper(node.right) 返回的第一个值,即右子树扁平化后的最左节点。这意味着 node.right 现在指向了它在扁平化链表中的“后一个”节点。
        • rightmost_of_flattened_subtree 被更新为右子树扁平化后的最右节点。这是因为整个扁平化序列的终点必然在当前节点的右子树中(如果存在)。
      • node.right.left = node:建立从右子树扁平化后的最左节点到当前节点的 left 指针连接。
    • 返回:return leftmost_of_flattened_subtree, rightmost_of_flattened_subtree。返回当前节点所代表的扁平化子树的整体最左侧和最右侧节点,供上一层递归调用使用。

这种方法巧妙地利用了Python的多重赋值特性,直接将递归调用返回的最左/最右节点赋值给对应的变量和当前节点的 left/right 指针,从而避免了中间变量的复杂性,并确保了指针的正确连接。

示例与测试

为了验证扁平化过程,我们需要一个辅助函数来插入节点和遍历扁平化后的链表。

# 假设 BinaryTree 类定义如上

# 扁平化主函数和辅助函数定义如上

# 用于测试的 BinaryTree 辅助方法
class BinaryTree(BinaryTree): # 继承以添加辅助方法
    def insert(self, values, i=0):
        if i >= len(values):
            return
        queue = [self]
        while len(queue) > 0:
            current = queue.pop(0)
            if current.left is None:
                current.left = BinaryTree(values[i])
                break
            queue.append(current.left)
            if current.right is None:
                current.right = BinaryTree(values[i])
                break
            queue.append(current.right)
        self.insert(values, i + 1)
        return self

    def leftToRightToLeft(self):
        nodes = []
        current = self
        # 从左到右遍历
        while current.right is not None:
            nodes.append(current.value)
            current = current.right
        nodes.append(current.value) # 添加最右侧节点

        # 从右到左遍历
        while current is not None:
            nodes.append(current.value)
            current = current.left
        return nodes

# 测试用例
if __name__ == "__main__":
    # 构建一个示例二叉树
    #       1
    #      / \
    #     2   3
    #    / \   \
    #   4   5   6
    #      / \
    #     7   8
    root = BinaryTree(1).insert([2, 3, 4, 5, 6])
    root.left.right.left = BinaryTree(7)
    root.left.right.right = BinaryTree(8)

    print("原始树结构(in-order 遍历):")
    # 模拟in-order遍历
    def in_order_traversal(node, result):
        if node is None:
            return
        in_order_traversal(node.left, result)
        result.append(node.value)
        in_order_traversal(node.right, result)

    in_order_result = []
    in_order_traversal(root, in_order_result)
    print(in_order_result) # 期望扁平化后的顺序:[4, 2, 7, 5, 8, 1, 6, 3]

    # 扁平化二叉树
    leftMostNode = flattenBinaryTree(root)

    # 验证扁平化后的链表
    flattened_values = leftMostNode.leftToRightToLeft()
    print("扁平化后从左到右再到左遍历结果:")
    print(flattened_values)

    # 期望结果 (leftToRightToLeft): [4, 2, 7, 5, 8, 1, 6, 3, 3, 6, 1, 8, 5, 7, 2, 4]
    expected = [4, 2, 7, 5, 8, 1, 6, 3, 3, 6, 1, 8, 5, 7, 2, 4]
    assert flattened_values == expected
    print("测试通过!")

总结

二叉树原地扁平化为双向链表结构是一个经典的递归问题,它要求我们深入理解二叉树的遍历顺序以及如何精确地操纵节点指针。通过一个返回子树最左和最右节点的辅助函数,我们可以高效地在in-order遍历过程中建立起前后节点间的双向连接。关键在于正确处理递归返回值的赋值,确保 node.left 和 node.right 最终指向其在扁平化链表中的 prev 和 next 节点,同时避免循环引用。这种方法不仅实现了原地操作,而且逻辑清晰,易于理解和维护。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

846

2023.08.22

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1877

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

656

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2382

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

47

2026.01.19

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

59

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

148

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

273

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

93

2026.03.04

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.5万人学习

Django 教程
Django 教程

共28课时 | 4.9万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.9万人学习

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

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