二叉树题目:二叉树展开为链表

news/2024/10/17 6:29:53/

文章目录

  • 题目
    • 标题和出处
    • 难度
    • 题目描述
      • 要求
      • 示例
      • 数据范围
      • 进阶
  • 解法一
    • 思路和算法
    • 代码
    • 复杂度分析
  • 解法二
    • 思路和算法
    • 代码
    • 复杂度分析
  • 解法三
    • 思路和算法
    • 代码
    • 复杂度分析
  • 后记

题目

标题和出处

标题:二叉树展开为链表

出处:114. 二叉树展开为链表

难度

3 级

题目描述

要求

给你二叉树的根结点 root \texttt{root} root,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode \texttt{TreeNode} TreeNode 类,其中右子结点指针指向链表中的下一个结点,左子结点指针总是 null \texttt{null} null
  • 展开后的单链表应该与二叉树前序遍历顺序相同。

示例

示例 1:

示例 1

输入: root = [1,2,5,3,4,null,6] \texttt{root = [1,2,5,3,4,null,6]} root = [1,2,5,3,4,null,6]
输出: [1,null,2,null,3,null,4,null,5,null,6] \texttt{[1,null,2,null,3,null,4,null,5,null,6]} [1,null,2,null,3,null,4,null,5,null,6]

示例 2:

输入: root = [] \texttt{root = []} root = []
输出: [] \texttt{[]} []

示例 3:

输入: root = [0] \texttt{root = [0]} root = [0]
输出: [0] \texttt{[0]} [0]

数据范围

  • 树中结点数目在范围 [0, 2000] \texttt{[0, 2000]} [0, 2000]
  • -100 ≤ Node.val ≤ 100 \texttt{-100} \le \texttt{Node.val} \le \texttt{100} -100Node.val100

进阶

你可以使用 O(1) \texttt{O(1)} O(1) 额外空间展开这棵树吗?

解法一

思路和算法

这道题要求按照二叉树前序遍历的顺序将二叉树展开为单链表。最直观的解法是对二叉树前序遍历并记录前序遍历的结点顺序,然后更改结点之间的指针指向。对于每个结点,将其左子结点指针设为 null \text{null} null,将其右子结点指针设为前序遍历顺序的后一个结点,其中前序遍历顺序的最后一个结点的右子结点指针也设为 null \text{null} null

代码

下面的代码为递归实现二叉树前序遍历的做法。

class Solution {List<TreeNode> traversal = new ArrayList<TreeNode>();public void flatten(TreeNode root) {preorder(root);int size = traversal.size();for (int i = 0; i < size; i++) {TreeNode node = traversal.get(i);node.left = null;node.right = i == size - 1 ? null : traversal.get(i + 1);}}public void preorder(TreeNode node) {if (node == null) {return;}traversal.add(node);preorder(node.left);preorder(node.right);}
}

下面的代码为迭代实现二叉树前序遍历的做法。

class Solution {public void flatten(TreeNode root) {List<TreeNode> traversal = new ArrayList<TreeNode>();Deque<TreeNode> stack = new ArrayDeque<TreeNode>();TreeNode node = root;while (!stack.isEmpty() || node != null) {while (node != null) {traversal.add(node);stack.push(node);node = node.left;}node = stack.pop().right;}int size = traversal.size();for (int i = 0; i < size; i++) {node = traversal.get(i);node.left = null;node.right = i == size - 1 ? null : traversal.get(i + 1);}}
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。前序遍历需要访问每个结点一次,展开为链表也需要访问每个结点一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。前序遍历的递归实现和迭代实现都需要栈空间,栈空间取决于二叉树的高度,最坏情况下二叉树的高度是 O ( n ) O(n) O(n)

解法二

思路和算法

解法一需要在前序遍历之后将二叉树展开为链表,因为展开为链表的过程会改变结点之间的指针关系,破坏二叉树的结构,导致丢失子结点的信息。

之所以会丢失子结点的信息,是因为在对左子树遍历时,没有存储右子结点的信息,在遍历完左子树之后才获得右子结点的信息。为了不丢失子结点的信息,可以修改前序遍历的迭代实现,在遍历左子树之前就获得右子结点的信息并存入栈内,此时可以在前序遍历的同时将二叉树展开为链表。

修改后的前序遍历的做法是,每次将当前访问的结点出栈,如果该结点的子结点不为空,则依次将右子结点和左子结点入栈。注意入栈顺序不可颠倒,因为左子结点先被访问因此需要先出栈。

展开为单链表的做法是,维护上一个访问的结点 prev \textit{prev} prev 和当前访问的结点 curr \textit{curr} curr,初始时 prev = null \textit{prev} = \text{null} prev=null。对于每个结点,当 prev ≠ null \textit{prev} \ne \text{null} prev=null 时,将 prev \textit{prev} prev 的左子结点指针设为 null \text{null} null,将 prev \textit{prev} prev 的右子结点指针设为 curr \textit{curr} curr。访问当前结点 curr \textit{curr} curr 之后,将 prev \textit{prev} prev 的值设为 curr \textit{curr} curr,继续访问下一个结点,直到遍历结束。

代码

class Solution {public void flatten(TreeNode root) {if (root == null) {return;}Deque<TreeNode> stack = new ArrayDeque<TreeNode>();stack.push(root);TreeNode prev = null;while (!stack.isEmpty()) {TreeNode curr = stack.pop();if (prev != null) {prev.left = null;prev.right = curr;}if (curr.right != null) {stack.push(curr.right);}if (curr.left != null) {stack.push(curr.left);}prev = curr;}}
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。每个结点都被访问一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。空间复杂度主要是栈空间,栈内元素个数不超过 n n n

解法三

思路和算法

使用常数空间实现二叉树展开为链表,则不能使用栈或者其他数据结构存储结点,只能改变结点之间的指针关系。

在遍历二叉树的过程中改变结点之间的指针关系,将二叉树展开为链表。对于每个结点,考虑子结点的情况。

  • 如果没有子结点,则该结点是叶结点,遍历结束。

  • 如果只有左子结点,则将左子树变成右子树,然后移动到右子结点继续展开剩下的结点。

  • 如果只有右子结点,则移动到右子结点继续展开剩下的结点。

  • 如果左子结点和右子结点都存在,则根据前序遍历顺序,遍历完左子树之后访问右子结点,因此需要找到左子树中最后被访问的结点,即当前结点的前驱结点,前驱结点为当前结点的左子树中的最右边的结点。找到当前结点的前驱结点之后,操作如下。

    1. 将当前结点的右子树移动到前驱结点的右子树的位置。

    2. 将当前结点的左子树移动到当前结点的右子树的位置。

    3. 将当前结点的左子结点设为空。

左子结点和右子结点都存在的情况的展开操作也适用于只有左子结点的情况,因此对于每个结点的展开操作如下。

  1. 如果当前结点的左子结点不为空,则执行上述展开操作。如果当前结点的左子结点为空,则不执行展开操作。

  2. 移动到当前结点的右子结点。

重复上述操作,直到当前结点变为空,此时二叉树展开为链表的操作结束。

考虑如下二叉树,共有 7 7 7 个结点,其前序遍历顺序是 [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] [1,2,3,4,5,6,7] [1,2,3,4,5,6,7]

图 1

从根结点 1 1 1 开始遍历。结点 1 1 1 的左子结点不为空,因此找到结点 1 1 1 的前驱结点 4 4 4,将以结点 5 5 5 为根结点的右子树移动到结点 4 4 4 的右子树的位置,然后将结点 1 1 1 的左子树(以结点 2 2 2 为根结点)移动到右子树的位置。

图 2

结点 2 2 2 的左子结点不为空,因此找到结点 2 2 2 的前驱结点 3 3 3,将以结点 4 4 4 为根结点的右子树移动到结点 3 3 3 的右子树的位置,然后将结点 2 2 2 的左子树(以结点 3 3 3 为根结点)移动到右子树的位置。

在这里插入图片描述

结点 3 3 3 和结点 4 4 4 的左子结点都为空,因此不执行展开操作。

结点 5 5 5 的左子结点不为空,因此找到结点 5 5 5 的前驱结点 6 6 6,将以结点 7 7 7 为根结点的右子树移动到结点 6 6 6 的右子树的位置,然后将结点 5 5 5 的左子树(以结点 6 6 6 为根结点)移动到右子树的位置。

图 4
结点 6 6 6 和结点 7 7 7 的左子结点都为空,因此不执行展开操作。

遍历结束,此时得到二叉树展开后的链表。

代码

class Solution {public void flatten(TreeNode root) {TreeNode node = root;while (node != null) {if (node.left != null) {TreeNode predecessor = node.left;while (predecessor.right != null) {predecessor = predecessor.right;}predecessor.right = node.right;node.right = node.left;node.left = null;}node = node.right;}}
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是二叉树的结点数。遍历和展开过程中,每个结点都被访问一次,寻找前驱结点的过程中,每个结点最多被访问一次,因此每个结点最多被访问两次。

  • 空间复杂度: O ( 1 ) O(1) O(1)

后记

读者也许已经发现,解法三和莫里斯遍历非常相似。和莫里斯遍历相比,这道题不需要将前驱结点的右指针指向当前结点,而是将当前结点的右子树移动到前驱结点的右子树的位置,因此解法三可以看成莫里斯遍历的简化版。


http://www.ppmy.cn/news/589378.html

相关文章

ModaHub魔搭社区:向量数据库MIlvus服务端配置(三)

目录 gpu 区域 logs 区域 metric_config 区域 gpu 区域 在该区域选择是否在 Milvus 里启用 GPU 用于搜索和索引创建。同时使用 CPU 和 GPU 可以达到资源的最优利用&#xff0c;在特别大的数据集里做搜索时性能更佳。 若要切换到 CPU-only 模式&#xff0c;只要将 enable 设…

Linux ACPI 高级电源管理状态

ACPI Power States 中定义了 G、S、D、C、P 5 个大的电力状态。 G状态 Global system state G 状态表示的是用户看到的整个系统的电力状态。 G0 运行模式。向硬件提供电源&#xff0c;软件可以运行的状态。 G1 停止模式。所谓的待机或休眠状态。 G2 软件为关闭状态&#xf…

金融场景下Java微服务图片压缩/加密等处理实战

目录导读 金融场景下Java微服务图片压缩/加密等处理实战1. 业务场景1.1 业务诉求1.2 业务分析 2. 技术分析2.1 技术预研2.2 处理问题汇总 3. 达成效果4. 编码解构 金融场景下Java微服务图片压缩/加密等处理实战 研究某项技术或者代码框架时&#xff0c;如果没有清晰的业务目标…

【Python爬虫与数据分析】进阶语法

目录 一、异常捕获 二、迭代器 三、拆包、聚合、映射 四、filter() 函数 五、匿名函数 六、闭包 七、装饰器 一、异常捕获 异常捕获可增强程序的健壮性&#xff0c;即程序在遇到遇到异常的时候并不会做中断处理&#xff0c;而是会将异常抛出&#xff0c;由程序员来分析…

红警3修改器无法连接服务器,红警3序列号修改器-不能加入游戏怎么办?红警3连局域网说cd-– 手机爱问...

2018-03-05 为什么我的红警不能联局域网 红警局域网联机的具体方法: 适用于原版红警、尤里复仇&#xff0c;及任何同样的扩展版。 第一步&#xff1a;安装IPX协议。 方法&#xff1a; 控制面板——网络连接(或网上邻居属性)——本地连接属性 ——在“此连接使用下列项目”中&am…

Eclipse 3.3 汉化包下载

Eclipse 是一款很好的IDE环境&#xff0c;功能完整而成熟。它使用 Java 语言开发&#xff0c;而且属于开源项目&#xff0c;网上充足的插件&#xff0c;保证了其强大的可扩展性。 Eclipse 的语言包也是以插件的形式来提供的。很可惜的是&#xff0c;从3.3版本开始&#xff0c;…

红警资源系列一 红警资源导出

XCC Mixer 1.46 解包mix文件&#xff0c;红警中比较重要的是ra2.mix&#xff0c;基本红警所有的资源都在这个包中。 对ra2.mix解包 双击可查看mix的包内容。 里面文件基本有以下两类 .shp 存储帧动画&#xff0c;比方说动员兵的每一个动作都在这个文件中&#xff0c;还有场景…

いもけんぴ 三作 汉化补丁

这几个程序 我已经完全逆向出全部源代码 所有汉化补丁 都在VS2010 下编译通过 能完全逆向出源代码 并修改成为自己的才叫真正的破解..... 支持 OS:Windows XP/VISTA/7 其中 Windows XP 需要安装 .net Framework 2.0 或者3.0系列 显卡需支持OpenGL 非简体中文系统注意&#xff…