IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> LeetCode 114. 二叉树展开为链表(一题三吃) -> 正文阅读

[数据结构与算法]LeetCode 114. 二叉树展开为链表(一题三吃)

题目描述

给定一个二叉树的根节点,将它展开为一个单链表

  • 展开后的单链表,同样使用TreeNode,其中right指针指向链表下一个节点,left指针始终为null
  • 展开后的单链表应该与二叉树的前序遍历的顺序相同

思路

前序遍历的访问节点的顺序为:根 -> 左 -> 右

则根节点访问后,应该先访问左子树,左子树访问完毕后,再访问右子树。

访问左子树时,同样按照 [根 -> 左 -> 右] 的顺序,左子树中第一个被访问的点应当是左子树的根节点。

所以根节点若存在左子树,则应当将左子树整个接到根节点的right指针上。那么根节点的右子树应当如何处理呢?我们知道右子树中第一个被访问的仍然是右子树的根节点。而只有当左子树全部访问完毕后,才会访问右子树。那么,应当将根节点的右子树,接到根节点左子树上最后一个被访问的点。

由于在左子树上,我们仍然是按照 [根 -> 左 -> 右] 的顺序来访问节点。所以左子树上,最后一个被访问的节点,是左子树上最右侧的节点,这个节点也是根节点的前驱结点

但是我们在代码实现时,不需要真正找到左子树上的最后一个被访问的节点,我们只需要从左子树上一直往右侧找,找到第一个不存在右儿子的节点last,然后将根节点的右子树,整个接到这个last节点的right指针上即可。(实际根节点左子树上最后一个被访问的节点,应当是last节点左子树上最后一个被访问的节点)

因为我们在左子树上,是一直往右侧找的,在找的过程中,只要存在右儿子,则右儿子所在的子树,其访问顺序一定是靠后的(因为前序遍历[根 -> 左 -> 右] 的访问顺序),而当找到一个不存在右儿子的节点时,这个节点所在的子树,一定是先访问往该节点的左子树,再访问其右子树,而我们后面进行展开时,也是按照前序遍历的规则来展开,这样就能保证,访问的相对顺序不变。
即,由于我们是按照前序遍历进行展开,在后面的处理中,我们能保证,先访问last节点,然后展开last节点的左子树,其左子树全部访问完毕后,再展开last节点的右子树,所以我们不需要一次性的将根节点的右子树接到其最终的位置 。留到后面进行处理即可。

根据上面的思路,给出我们的算法流程:

  1. 从根节点开始,判断当前节点是否存在左儿子

    • 若存在左儿子

      则在左子树上一直往右找,直到找到一个没有右儿子的节点last

      将根节点的右子树,整个接到last节点的right指针上

      将根节点的左子树,接到根节点的right指针上

      将根节点的left指针置空

    • 若不存在左儿子

      什么也不用做

  2. 当前节点往右走

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

扩展

既然题目要求按照前序遍历展开为链表,那么进一步,我们能不能按照中序和后序遍历的顺序展开呢?答案是可以的。

按照中序遍历展开

同样的,中序遍历的节点访问顺序是 [左 -> 根 -> 右] 。根据这个顺序,我们知道,根节点是在中间被访问的,根节点访问后访问右子树,则 根节点和右子树的关系先不用动。我们来看左子树。我们需要找到左子树上最后一个被访问的节点,这个节点的下一个节点就是根节点。由于访问顺序是[左 -> 根 -> 右],右子树仍然是后被访问的,所以我们仍然在左子树上,一直往右找,直到找到一个不存在右儿子的节点last,对于这个last节点所在的子树,由于访问顺序是 [左 -> 根 -> 右],而last节点不存在右儿子,则last节点就是最后被访问的节点了,也就是,此时找到的last,是左子树上真正的最后一个被访问的节点。那么我们需要把根节点,接到这个last节点的right指针上,然后把根节点的left指针置空,再更新当前节点为根节点的原左儿子。

当然,若根节点不存在左儿子,那么将当前节点往右走即可。

如此,我们的算法流程就是

从根节点开始,判断当前节点是否存在左子树

  • 若存在左子树

    则在左子树上一直往右侧找,直到找到一个不存在右儿子的节点last,这个节点是左子树上最后一个被访问的节点,于是将根节点,接到lastright指针上

    随后将根节点的left指针置空

    更新当前节点为根节点的原左儿子

  • 若不存在左子树

    更新当前节点为根节点的右儿子

class Solution {
    public void flatten(TreeNode root) {
        TreeNode newHead = null;
        // 中序展开
        while (root != null) {
            if (root.left == null) {
                root = root.right;
            } else {
                TreeNode last = root.left;
                TreeNode nextRoot = root.left;
                while (last.right != null) last = last.right;
                last.right = root;
                root.left = null;
                root = nextRoot;
                // 更新展开后的链表头
                newHead = root;
            }
        }

        // 验证, 看是否是中序遍历
        while (newHead != null) {
            System.out.printf("%d -> ", newHead.val);
            newHead = newHead.right;
        }
    }
}

按照后序遍历展开

类似的,后序遍历的访问顺序是 [左 -> 右 -> 根]。

根节点是最后访问的,所以我们需要将根节点,接到右子树上最后一个被访问的节点之后。右子树上最后一个被访问的节点,是右子树的根节点。所以就要把根节点,接到其右儿子的right指针上。但我们怎么维护右儿子原先的right指针呢?并且,若根节点存在左子树,我们还要将左子树的最后一个访问的节点,和右子树第一个访问的节点连接起来。想了很久感觉都是很难去实现的。

但是我们可以换一种思路,我们逆序,按照 [根 -> 右 -> 左] 的顺序来进行展开,展开完毕后再做一次链表翻转即可。这样的话,思路就和前序遍历展开差不多了。

我们需要在根节点的右子树上,找到最后一个被访问的点,然后将根节点的左子树,整个接到这个点上即可。

class Solution {
    public void flatten(TreeNode root) {
        // 后序展开
        // 根 -> 右 -> 左
        TreeNode head = root;
        while (root != null) {
            TreeNode l = root.left, r = root.right;
            if (l != null) {
                // 左子树不为空, 则需要接到右侧来
                if (r == null) root.right = l; // 右子树为空, 则直接接过来即可
                else {
                    // 右子树不为空, 找到右子树的最后一个被访问的节点, 与前序展开类似, 实际不需要找到真正最后一个被访问的
                    TreeNode last = r;
                    while (last.left != null) last = last.left; // 一直往左找
                    last.left = l; // 将根节点的左子树, 直接接到最后一个节点的左侧(注意是左侧, 等待后面的处理,因为根据我们的访问顺序, 左子树是最后被访问的)
                }
                root.left = null; // 左子树置空
            }
            // 往右走
            root = root.right;
        }

        // 翻转链表
        TreeNode cur = head, pre = null;
        while (cur != null) {
            TreeNode nextCur = cur.right;
            cur.right = pre;
            pre = cur;
            cur = nextCur;
        }
        
        // 翻转后的链表头
        head = pre;
        
        // 验证后序展开的顺序
        while (head != null) {
            System.out.printf("%d -> ", head.val);
            head = head.right;
        }
    }
}

小结

总结一下,此类题目都是考察了二叉树的遍历。对于一棵树,我们可以先将其切断,分成根节点,左子树,右子树,三部分来看。由于我们遍历时,实际每次只能处理当前的根节点,所以我们需要关注根节点和左右子树的相对访问顺序。然后根据需要,找到左子树,或者右子树上的第一个,或者最后一个被访问的节点,然后进行连接(注意,有时不需要找到真正的最后一个被访问的节点,也能保证答案的正确性,因为我们是按照既定的遍历顺序进行展开,只要我们的连接操作,不违反这个既定顺序,即可在后面的处理中,完成正确的展开(即,有时不能立即完成最终的顺序构造,而是生成了一些中间状态,只要保证这个中间状态不违反我们的既定遍历顺序,即可在后面的处理中对其进行正确构造))。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章           查看所有文章
加:2022-08-06 11:07:37  更:2022-08-06 11:12:18 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 21:14:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码