|
| 1 | +# 题目描述(中等难度) |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +把一个二叉树展开成一个链表,展开顺序如图所示。 |
| 6 | + |
| 7 | +# 解法一 |
| 8 | + |
| 9 | +可以发现展开的顺序其实就是二叉树的先序遍历。算法和 [94 题](<https://leetcode.wang/leetCode-94-Binary-Tree-Inorder-Traversal.html#%E8%A7%A3%E6%B3%95%E4%B8%89-morris-traversal>) 中序遍历的 Morris 算法有些神似,我们需要两步完成这道题。 |
| 10 | + |
| 11 | +1. 将左子树插入到右子树的地方 |
| 12 | +2. 将原来的右子树接到左子树的最右边节点 |
| 13 | +3. 考虑新的右子树的根节点,一直重复上边的过程,直到新的右子树为 null |
| 14 | + |
| 15 | +可以看图理解下这个过程。 |
| 16 | + |
| 17 | +```java |
| 18 | + 1 |
| 19 | + / \ |
| 20 | + 2 5 |
| 21 | + / \ \ |
| 22 | +3 4 6 |
| 23 | + |
| 24 | +//将 1 的左子树插入到右子树的地方 |
| 25 | + 1 |
| 26 | + \ |
| 27 | + 2 5 |
| 28 | + / \ \ |
| 29 | + 3 4 6 |
| 30 | +//将原来的右子树接到左子树的最右边节点 |
| 31 | + 1 |
| 32 | + \ |
| 33 | + 2 |
| 34 | + / \ |
| 35 | + 3 4 |
| 36 | + \ |
| 37 | + 5 |
| 38 | + \ |
| 39 | + 6 |
| 40 | + |
| 41 | + //将 2 的左子树插入到右子树的地方 |
| 42 | + 1 |
| 43 | + \ |
| 44 | + 2 |
| 45 | + \ |
| 46 | + 3 4 |
| 47 | + \ |
| 48 | + 5 |
| 49 | + \ |
| 50 | + 6 |
| 51 | + |
| 52 | + //将原来的右子树接到左子树的最右边节点 |
| 53 | + 1 |
| 54 | + \ |
| 55 | + 2 |
| 56 | + \ |
| 57 | + 3 |
| 58 | + \ |
| 59 | + 4 |
| 60 | + \ |
| 61 | + 5 |
| 62 | + \ |
| 63 | + 6 |
| 64 | + |
| 65 | + ...... |
| 66 | +``` |
| 67 | + |
| 68 | +代码的话也很好写,首先我们需要找出左子树最右边的节点以便把右子树接过来。 |
| 69 | + |
| 70 | +```java |
| 71 | +public void flatten(TreeNode root) { |
| 72 | + while (root != null) { |
| 73 | + //左子树为 null,直接考虑下一个节点 |
| 74 | + if (root.left == null) { |
| 75 | + root = root.right; |
| 76 | + } else { |
| 77 | + // 找左子树最右边的节点 |
| 78 | + TreeNode pre = root.left; |
| 79 | + while (pre.right != null) { |
| 80 | + pre = pre.right; |
| 81 | + } |
| 82 | + //将原来的右子树接到左子树的最右边节点 |
| 83 | + pre.right = root.right; |
| 84 | + // 将左子树插入到右子树的地方 |
| 85 | + root.right = root.left; |
| 86 | + root.left = null; |
| 87 | + // 考虑下一个节点 |
| 88 | + root = root.right; |
| 89 | + } |
| 90 | + } |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +# 解法二 |
| 95 | + |
| 96 | +题目中,要求说是`in-place`,之前一直以为这个意思就是要求空间复杂度是`O(1)`。偶然看见评论区 [StefanPochmann](https://leetcode.com/stefanpochmann) 大神的解释。 |
| 97 | + |
| 98 | + |
| 99 | + |
| 100 | +也就是说`in-place` 的意思可能更多说的是直接在原来的节点上改变指向,空间复杂度并没有要求。所以这道题也可以用递归解一下,参考 [这里](<https://leetcode.com/problems/flatten-binary-tree-to-linked-list/discuss/36977/My-short-post-order-traversal-Java-solution-for-share>) 。 |
| 101 | + |
| 102 | +```java |
| 103 | + 1 |
| 104 | + / \ |
| 105 | + 2 5 |
| 106 | + / \ \ |
| 107 | +3 4 6 |
| 108 | +``` |
| 109 | + |
| 110 | +利用递归的话,可能比解法一难理解一些。 |
| 111 | + |
| 112 | +题目其实就是将二叉树通过右指针,组成一个链表。 |
| 113 | + |
| 114 | +```java |
| 115 | +1 -> 2 -> 3 -> 4 -> 5 -> 6 |
| 116 | +``` |
| 117 | + |
| 118 | +我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们能不能利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。 |
| 119 | + |
| 120 | +先序遍历的顺序是`1 2 3 4 5 6`。 |
| 121 | + |
| 122 | +遍历到`2`,把`1`的右指针指向`2`。`1 -> 2 3 4 5 6`。 |
| 123 | + |
| 124 | +遍历到`3`,把`2`的右指针指向`3`。`1 -> 2 -> 3 4 5 6`。 |
| 125 | + |
| 126 | +... ... |
| 127 | + |
| 128 | +一直进行下去似乎就解决了这个问题。但现实是残酷的,原因就是我们把`1`的右指针指向`2`,那么`1`的原本的右孩子就丢失了,也就是`5` 就找不到了。 |
| 129 | + |
| 130 | +解决方法的话,我们可以逆过来进行。 |
| 131 | + |
| 132 | +我们依次遍历`6 5 4 3 2 1`,然后每遍历一个节点就将当前节点的右指针更新为上一个节点。 |
| 133 | + |
| 134 | +遍历到`5`,把`5`的右指针指向`6`。`6 <- 5 4 3 2 1`。 |
| 135 | + |
| 136 | +遍历到`4`,把`4`的右指针指向`5`。`6 <- 5 <- 4 3 2 1`。 |
| 137 | + |
| 138 | +... ... |
| 139 | + |
| 140 | +```java |
| 141 | + 1 |
| 142 | + / \ |
| 143 | + 2 5 |
| 144 | + / \ \ |
| 145 | +3 4 6 |
| 146 | +``` |
| 147 | + |
| 148 | +这样就不会有丢失孩子的问题了,因为更新当前的右指针的时候,当前节点的右孩子已经访问过了。 |
| 149 | + |
| 150 | +而`6 5 4 3 2 1`的遍历顺序其实变形的后序遍历,遍历顺序是右子树->左子树->根节点。 |
| 151 | + |
| 152 | +先回想一下后序遍历的代码 |
| 153 | + |
| 154 | +```java |
| 155 | +public void PrintBinaryTreeBacRecur(TreeNode<T> root){ |
| 156 | + if (root == null) |
| 157 | + return; |
| 158 | + |
| 159 | + PrintBinaryTreeBacRecur(root.right); |
| 160 | + PrintBinaryTreeBacRecur(root.left); |
| 161 | + System.out.print(root.data); |
| 162 | + |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +这里的话,我们不再是打印根节点,而是利用一个全局变量`pre`,更新当前根节点的右指针为`pre`,左指针为`null`。 |
| 167 | + |
| 168 | +```java |
| 169 | +private TreeNode pre = null; |
| 170 | + |
| 171 | +public void flatten(TreeNode root) { |
| 172 | + if (root == null) |
| 173 | + return; |
| 174 | + flatten(root.right); |
| 175 | + flatten(root.left); |
| 176 | + root.right = pre; |
| 177 | + root.left = null; |
| 178 | + pre = root; |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +相应的左孩子也要置为`null`,同样的也不用担心左孩子丢失,因为是后序遍历,左孩子已经遍历过了。和 [112 题](<https://leetcode.wang/leetcode-112-Path-Sum.html>) 一样,都巧妙的利用了后序遍历。 |
| 183 | + |
| 184 | +既然后序遍历这么有用,利用 [112 题](<https://leetcode.wang/leetcode-112-Path-Sum.html>) 介绍的后序遍历的迭代方法,把这道题也改一下吧。 |
| 185 | + |
| 186 | +```java |
| 187 | +public void flatten(TreeNode root) { |
| 188 | + Stack<TreeNode> toVisit = new Stack<>(); |
| 189 | + TreeNode cur = root; |
| 190 | + TreeNode pre = null; |
| 191 | + |
| 192 | + while (cur != null || !toVisit.isEmpty()) { |
| 193 | + while (cur != null) { |
| 194 | + toVisit.push(cur); // 添加根节点 |
| 195 | + cur = cur.right; // 递归添加右节点 |
| 196 | + } |
| 197 | + cur = toVisit.peek(); // 已经访问到最右的节点了 |
| 198 | + // 在不存在左节点或者右节点已经访问过的情况下,访问根节点 |
| 199 | + if (cur.left == null || cur.left == pre) { |
| 200 | + toVisit.pop(); |
| 201 | + /**************修改的地方***************/ |
| 202 | + cur.right = pre; |
| 203 | + cur.left = null; |
| 204 | + /*************************************/ |
| 205 | + pre = cur; |
| 206 | + cur = null; |
| 207 | + } else { |
| 208 | + cur = cur.left; // 左节点还没有访问过就先访问左节点 |
| 209 | + } |
| 210 | + } |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +# 总 |
| 215 | + |
| 216 | +其实两种解法就是从两种方向解决问题,解法一自顶向下,解法二自底向上。以前觉得后序遍历比较麻烦,没想到竟然连续遇到了后序遍历的应用。 |
0 commit comments