题目描述(中等难度)

114. Flatten Binary Tree to Linked List - 图1

把一个二叉树展开成一个链表,展开顺序如图所示。

解法一

可以发现展开的顺序其实就是二叉树的先序遍历。算法和 94 题 中序遍历的 Morris 算法有些神似,我们需要两步完成这道题。

  1. 将左子树插入到右子树的地方
  2. 将原来的右子树接到左子树的最右边节点
  3. 考虑新的右子树的根节点,一直重复上边的过程,直到新的右子树为 null

可以看图理解下这个过程。

  1. 1
  2. / \
  3. 2 5
  4. / \ \
  5. 3 4 6
  6. //将 1 的左子树插入到右子树的地方
  7. 1
  8. \
  9. 2 5
  10. / \ \
  11. 3 4 6
  12. //将原来的右子树接到左子树的最右边节点
  13. 1
  14. \
  15. 2
  16. / \
  17. 3 4
  18. \
  19. 5
  20. \
  21. 6
  22. //将 2 的左子树插入到右子树的地方
  23. 1
  24. \
  25. 2
  26. \
  27. 3 4
  28. \
  29. 5
  30. \
  31. 6
  32. //将原来的右子树接到左子树的最右边节点
  33. 1
  34. \
  35. 2
  36. \
  37. 3
  38. \
  39. 4
  40. \
  41. 5
  42. \
  43. 6
  44. ......

代码的话也很好写,首先我们需要找出左子树最右边的节点以便把右子树接过来。

  1. public void flatten(TreeNode root) {
  2. while (root != null) {
  3. //左子树为 null,直接考虑下一个节点
  4. if (root.left == null) {
  5. root = root.right;
  6. } else {
  7. // 找左子树最右边的节点
  8. TreeNode pre = root.left;
  9. while (pre.right != null) {
  10. pre = pre.right;
  11. }
  12. //将原来的右子树接到左子树的最右边节点
  13. pre.right = root.right;
  14. // 将左子树插入到右子树的地方
  15. root.right = root.left;
  16. root.left = null;
  17. // 考虑下一个节点
  18. root = root.right;
  19. }
  20. }
  21. }

解法二

题目中,要求说是in-place,之前一直以为这个意思就是要求空间复杂度是O(1)。偶然看见评论区 StefanPochmann 大神的解释。

114. Flatten Binary Tree to Linked List - 图2

也就是说in-place 的意思可能更多说的是直接在原来的节点上改变指向,空间复杂度并没有要求。所以这道题也可以用递归解一下,参考 这里

  1. 1
  2. / \
  3. 2 5
  4. / \ \
  5. 3 4 6

利用递归的话,可能比解法一难理解一些。

题目其实就是将二叉树通过右指针,组成一个链表。

  1. 1 -> 2 -> 3 -> 4 -> 5 -> 6

我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们能不能利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。

先序遍历的顺序是1 2 3 4 5 6

遍历到2,把1的右指针指向21 -> 2 3 4 5 6

遍历到3,把2的右指针指向31 -> 2 -> 3 4 5 6

… …

一直进行下去似乎就解决了这个问题。但现实是残酷的,原因就是我们把1的右指针指向2,那么1的原本的右孩子就丢失了,也就是5 就找不到了。

解决方法的话,我们可以逆过来进行。

我们依次遍历6 5 4 3 2 1,然后每遍历一个节点就将当前节点的右指针更新为上一个节点。

遍历到5,把5的右指针指向66 <- 5 4 3 2 1

遍历到4,把4的右指针指向56 <- 5 <- 4 3 2 1

… …

  1. 1
  2. / \
  3. 2 5
  4. / \ \
  5. 3 4 6

这样就不会有丢失孩子的问题了,因为更新当前的右指针的时候,当前节点的右孩子已经访问过了。

6 5 4 3 2 1的遍历顺序其实变形的后序遍历,遍历顺序是右子树->左子树->根节点。

先回想一下变形的后序遍历的代码

  1. public void PrintBinaryTreeBacRecur(TreeNode<T> root){
  2. if (root == null)
  3. return;
  4. PrintBinaryTreeBacRecur(root.right);
  5. PrintBinaryTreeBacRecur(root.left);
  6. System.out.print(root.data);
  7. }

这里的话,我们不再是打印根节点,而是利用一个全局变量pre,更新当前根节点的右指针为pre,左指针为null

  1. private TreeNode pre = null;
  2. public void flatten(TreeNode root) {
  3. if (root == null)
  4. return;
  5. flatten(root.right);
  6. flatten(root.left);
  7. root.right = pre;
  8. root.left = null;
  9. pre = root;
  10. }

相应的左孩子也要置为null,同样的也不用担心左孩子丢失,因为是后序遍历,左孩子已经遍历过了。和 112 题 一样,都巧妙的利用了后序遍历。

既然后序遍历这么有用,利用 112 题 介绍的后序遍历的迭代方法,把这道题也改一下吧。

  1. public void flatten(TreeNode root) {
  2. Stack<TreeNode> toVisit = new Stack<>();
  3. TreeNode cur = root;
  4. TreeNode pre = null;
  5. while (cur != null || !toVisit.isEmpty()) {
  6. while (cur != null) {
  7. toVisit.push(cur); // 添加根节点
  8. cur = cur.right; // 递归添加右节点
  9. }
  10. cur = toVisit.peek(); // 已经访问到最右的节点了
  11. // 在不存在左节点或者右节点已经访问过的情况下,访问根节点
  12. if (cur.left == null || cur.left == pre) {
  13. toVisit.pop();
  14. /**************修改的地方***************/
  15. cur.right = pre;
  16. cur.left = null;
  17. /*************************************/
  18. pre = cur;
  19. cur = null;
  20. } else {
  21. cur = cur.left; // 左节点还没有访问过就先访问左节点
  22. }
  23. }
  24. }

解法三

解法二中提到如果用先序遍历的话,会丢失掉右孩子,除了用后序遍历,还有没有其他的方法避免这个问题。在Discuss又发现了一种解法,参考 这里

为了更好的控制算法,所以我们用先序遍历迭代的形式,正常的先序遍历代码如下,

  1. public static void preOrderStack(TreeNode root) {
  2. if (root == null) {
  3. return;
  4. }
  5. Stack<TreeNode> s = new Stack<TreeNode>();
  6. while (root != null || !s.isEmpty()) {
  7. while (root != null) {
  8. System.out.println(root.val);
  9. s.push(root);
  10. root = root.left;
  11. }
  12. root = s.pop();
  13. root = root.right;
  14. }
  15. }

还有一种特殊的先序遍历,提前将右孩子保存到栈中,我们利用这种遍历方式就可以防止右孩子的丢失了。由于栈是先进后出,所以我们先将右节点入栈。

  1. public static void preOrderStack(TreeNode root) {
  2. if (root == null){
  3. return;
  4. }
  5. Stack<TreeNode> s = new Stack<TreeNode>();
  6. s.push(root);
  7. while (!s.isEmpty()) {
  8. TreeNode temp = s.pop();
  9. System.out.println(temp.val);
  10. if (temp.right != null){
  11. s.push(temp.right);
  12. }
  13. if (temp.left != null){
  14. s.push(temp.left);
  15. }
  16. }
  17. }

之前我们的思路如下:

题目其实就是将二叉树通过右指针,组成一个链表。

  1. 1 -> 2 -> 3 -> 4 -> 5 -> 6

我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们可以利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。

先序遍历的顺序是1 2 3 4 5 6

遍历到2,把1的右指针指向21 -> 2 3 4 5 6

遍历到3,把2的右指针指向31 -> 2 -> 3 4 5 6

… …

因为我们用栈保存了右孩子,所以不需要担心右孩子丢失了。用一个pre变量保存上次遍历的节点。修改的代码如下:

  1. public void flatten(TreeNode root) {
  2. if (root == null){
  3. return;
  4. }
  5. Stack<TreeNode> s = new Stack<TreeNode>();
  6. s.push(root);
  7. TreeNode pre = null;
  8. while (!s.isEmpty()) {
  9. TreeNode temp = s.pop();
  10. /***********修改的地方*************/
  11. if(pre!=null){
  12. pre.right = temp;
  13. pre.left = null;
  14. }
  15. /********************************/
  16. if (temp.right != null){
  17. s.push(temp.right);
  18. }
  19. if (temp.left != null){
  20. s.push(temp.left);
  21. }
  22. /***********修改的地方*************/
  23. pre = temp;
  24. /********************************/
  25. }
  26. }

解法一和解法三可以看作自顶向下的解决问题,解法二可以看作自底向上。以前觉得后序遍历比较麻烦,没想到竟然连续遇到了后序遍历的应用。先序遍历的两种方式自己也是第一次意识到,之前都是用的第一种正常的方式。

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情