递归_leetcode.114.二叉树展开为链表

41 篇文章 0 订阅
7 篇文章 0 订阅

题目

给定一个二叉树,原地将它展开为一个单链表。

例如,给定二叉树

    1
   / \
  2   5
 / \   \
3   4   6

将其展开为

1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6

分析

本题需要将二叉树转化为列表,对于二叉树的题目,无非就以下几种解题思路:

  • 先序遍历(深度优先搜索)
  • 中序遍历(深度优先搜索)(尤其二叉搜索树)
  • 后序遍历(深度优先搜索)
  • 层序遍历(广度优先搜索)(尤其按照层来解决问题的时候)
  • 序列化与反序列化(结构唯一性问题)

解法一:暴力(常规思路)

可以发现展开的顺序其实就是二叉树的先序遍历,我们需要两步完成这道题。

  • 将左子树插入到右子树的地方
  • 将原来的右子树接到左子树的最右边节点
  • 考虑新的右子树的根节点,一直重复上边的过程,直到新的右子树为 null
  • 可以看图理解下这个过程。
    1
   / \
  2   5
 / \   \
3   4   6

//将 1 的左子树插入到右子树的地方
    1
     \
      2         5
     / \         \
    3   4         6        
//将原来的右子树接到左子树的最右边节点
    1
     \
      2          
     / \          
    3   4  
         \
          5
           \
            6
            
 //将 2 的左子树插入到右子树的地方
    1
     \
      2          
       \          
        3       4  
                 \
                  5
                   \
                    6   
        
 //将原来的右子树接到左子树的最右边节点
    1
     \
      2          
       \          
        3      
         \
          4  
           \
            5
             \
              6         
  
  ......
	public void flatten(TreeNode root) {
		while(root != null) {
			//左子树为空直接考虑下一个节点
			if(root.left == null) {
				root = root.right;
			}else {
				//找左子树最右边的节点
				TreeNode pre = root.left;
				while(pre.right != null) {
					pre = pre.right;
				}
				//将原来的右子树接到左子树的最右边的节点
				pre.right = root.right;
				//将左子树插入到右子树的地方
				root.right = root.left;
				root.left = null;
				//考虑下一个节点
				root = root.right;
			}
		}
	}

解法二: 深度递归搜索

其实是分为三步:

  • 首先将根节点的左子树变成链表
  • 其次将根节点的右子树变成链表
  • 最后将变成链表的右子树放在变成链表的左子树的最右边

这就是一个递归的过程,递归的一个非常重要的点就是:不去管函数的内部细节是如何处理的,我们只看其函数作用以及输入与输出。对于函数flatten来说:

  • 函数作用:将一个二叉树,原地将它展开为链表
  • 输入:树的根节点
  • 输出:无
	public void flatten(TreeNode root) {
		if(root == null) {
			return;
		}
		//将根节点的左子树编程连表
		flatten(root.left);
		//将根节点的右子树变成链表
		flatten(root.right);
		TreeNode temp = root.right;
		//讲述的右边换成左边的链表
		root.right = root.left;
		//记得将左边置空
		root.left = null;
		//找到树的最右边的点
		while(root.right != null) {
			root = root.right;
		}
		//把右边的链表连接到刚才树得最右边的节点
		root.right = temp;
	}

解法三:先序遍历递归

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

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

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

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

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

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

… …

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

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

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

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

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

… …

    1
   / \
  2   5
 / \   \
3   4   6

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

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

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

public void PrintBinaryTreeBacRecur(TreeNode<T> root){
    if (root == null)
        return;
    
    PrintBinaryTreeBacRecur(root.right);
    PrintBinaryTreeBacRecur(root.left); 
    System.out.print(root.data);
    
} 

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

private TreeNode pre = null;

public void flatten(TreeNode root) {
    if (root == null)
        return;
    flatten(root.right);
    flatten(root.left);
    root.right = pre;
    root.left = null;
    pre = root;
}

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

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

public void flatten(TreeNode root) { 
    Stack<TreeNode> toVisit = new Stack<>();
    TreeNode cur = root;
    TreeNode pre = null;

    while (cur != null || !toVisit.isEmpty()) {
        while (cur != null) {
            toVisit.push(cur); // 添加根节点
            cur = cur.right; // 递归添加右节点
        }
        cur = toVisit.peek(); // 已经访问到最右的节点了
        // 在不存在左节点或者右节点已经访问过的情况下,访问根节点
        if (cur.left == null || cur.left == pre) {
            toVisit.pop(); 
            /**************修改的地方***************/
            cur.right = pre;
            cur.left = null;
            /*************************************/
            pre = cur;
            cur = null;
        } else {
            cur = cur.left; // 左节点还没有访问过就先访问左节点
        }
    } 
}

解法四: 先序遍历迭代

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

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

public static void preOrderStack(TreeNode root) {
    if (root == null) { 
        return;
    }
    Stack<TreeNode> s = new Stack<TreeNode>();
    while (root != null || !s.isEmpty()) {
        while (root != null) {
            System.out.println(root.val);
            s.push(root);
            root = root.left;
        }
        root = s.pop();
        root = root.right;
    }
}

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

public static void preOrderStack(TreeNode root) {
    if (root == null){
        return;
    }
    Stack<TreeNode> s = new Stack<TreeNode>();
    s.push(root);
    while (!s.isEmpty()) {
        TreeNode temp = s.pop();
        System.out.println(temp.val);
        if (temp.right != null){
            s.push(temp.right);
        }
        if (temp.left != null){
            s.push(temp.left);
        }
    }
}

之前我们的思路如下:

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

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

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

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

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

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

… …

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

public void flatten(TreeNode root) { 
    if (root == null){
        return;
    }
    Stack<TreeNode> s = new Stack<TreeNode>();
    s.push(root);
    TreeNode pre = null;
    while (!s.isEmpty()) {
        TreeNode temp = s.pop(); 
        /***********修改的地方*************/
        if(pre!=null){
            pre.right = temp;
            pre.left = null;
        }
        /********************************/
        if (temp.right != null){
            s.push(temp.right);
        }
        if (temp.left != null){
            s.push(temp.left);
        } 
        /***********修改的地方*************/
        pre = temp;
        /********************************/
    }
}


题解来自@windiang

最后,不经历风雨,怎能在计算机的大山之顶看见彩虹呢! 无论怎样,相信明天一定会更好!!!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值