【JavaSE与数据结构】数据结构与算法之二叉树(实践篇)

⭐️前面的话⭐️

本篇文章带大家认识数据结构——二叉树,树是一种非线性的数据结构,它是由有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。本文将从通过20多道二叉树力扣牛客题介绍二叉树的基本操作,创建以及应用,所以学习完本文,除了能够增加对二叉树结构的了解,还能掌握20多道在线编程题,如果对二叉树基本概念特点性质不清楚的请阅读博主的历史博文:树与二叉树(理论篇)
全文大约2.6w字,建议收藏哦!

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创,CSDN首发!
📆首发时间:🌴2022年1月28日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《Java核心技术》,📚《Java编程思想》,📚《Effective Java》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🙏作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!



题外话: 本篇文章为数据结构之二叉树的实践部分,是后续堆,优先队列,堆排序,红黑树等数据结构和算法的基础。
封面


1.二叉树的基本操作

1.1回顾二叉树基本理论

一棵树上所有结点的度均不大于 2 2 2,则这棵树是一棵二叉树,空树是一棵特殊的二叉树。
一棵树是二叉树,其子树均为二叉树,二叉树的储存表示方法常用孩子表示法和孩子双亲表示法进行储存。

// 孩子表示法
class Node { 
	int val; // 数据域 
	Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树 
	Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树 
}
// 孩子双亲表示法
class Node { 
	int val; // 数据域 
	Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树 
	Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树 
	Node parent; // 当前节点的根节点 
}

二叉树都是由一下几种基本形态复合而成:

2-2

满二叉树与完全二叉树是二叉树的两种特殊情况。
满二叉树: 在一棵二叉树中,除叶子结点外,其他所有结点的度均为2,则该树为满二叉树。
2-3

完全二叉树: 从根结点开始,每个非空结点按照层次依次递增,每层从左至右的顺序排列的二叉树,称为完全二叉树。换个说法,完全二叉树实际上是对应的满二叉树删除叶结点层最右边若干个结点得到的。

2-4
更多内容请参考:历史博文:树与二叉树(理论篇)

1.2二叉树的遍历

二叉树遍历是指按照一定次序访问树中所有结点,并且每个结点仅被访问一次的过程。
不论是前序遍历,中序遍历,还是后续遍历,二叉树的遍历所走的路径都是相同的,上述三者之间的区别只是获取根节点数据的时机不一样。
1-1
二叉树搜索路径:根->左子树->根->右子树,具体路径如上图。

二叉树结点结构:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */

1.2.1前序遍历

所谓前序遍历,也叫做先序遍历,是按照二叉树搜索路径先获取根结点数据,再获取左子树数据,最后获取右子树数据。

前序遍历三部曲:

  1. 访问根结点;
  2. 前序遍历左子树;
  3. 前序遍历右子树。

如下图这样一棵二叉树:
1-2
前序遍历的顺序是: A − > B − > D − > G − > C − > E − > F A->B->D->G->C->E->F A>B>D>G>C>E>F

在线练习前序遍历144. 二叉树的前序遍历

递归实现:
递归条件:如果结点root为空,则返回。
(1)获取并保存根结点数据。
(2)前序遍历左子树,并保存结果。
(3)前序遍历右子树,并保存结果。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> ret = new ArrayList<>();
        if (root == null) return ret;//递归条件
        ret.add(root.val);//(1)
        ret.addAll(preorderTraversal(root.left));//(2)
        ret.addAll(preorderTraversal(root.right));//(3)
        return ret;
    }
}

非递归实现:
(1)使用一个栈储存前序遍历的结点。
(2)获取并保存非空结点数据,并将结点入栈。
(3)遍历的结点为空时,将结点刷新为栈顶元素的右结点,并出栈。
(4)重复上述步骤,直到栈和结点都为空。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> ret = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();//(1)
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {    //(4)
            while (cur != null) {
                stack.push(cur);//(2)
                ret.add(cur.val);//(2)
                cur = cur.left;
            }
            TreeNode top = stack.pop();//(3)
            cur = top.right;//(3)
        }
        return ret;
    }
}

1.2.2中序遍历

所谓中序遍历,就是按照二叉树搜索路径先获取左子树数据,再获取根结点数据,最后获取右子树数据。

中序遍历三部曲:

  1. 中序遍历左子树;
  2. 访问根结点;
  3. 中序遍历右子树。

如下图这样一棵二叉树:
1-2
中序遍历的顺序是: D − > G − > B − > A − > E − > C − > F D->G->B->A->E->C->F D>G>B>A>E>C>F

在线练习中序遍历94. 二叉树的中序遍历

递归实现:
递归条件:如果结点root为空,则返回。
(1)中序遍历左子树,并保存结果。
(2)获取并保存根结点数据。
(3)中序遍历右子树,并保存结果。

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> ret = new ArrayList<>();
        if (root == null) return ret;
        ret.addAll(inorderTraversal(root.left));//(1)
        ret.add(root.val);//(2)
        ret.addAll(inorderTraversal(root.right));//(3)
        return ret;
    }
}

非递归实现:
(1)使用一个栈储存中序遍历的结点。
(2)按照二叉树搜索顺序,将非空结点入栈。
(3)遍历的结点为空时,获取并保存结点的数据,并将结点刷新为栈顶元素的右结点,并出栈。
(4)重复上述步骤,直到栈和结点都为空。

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> ret = new ArrayList<>();
        if (root == null) return ret;

        Stack<TreeNode> stack = new Stack<>();//(1)
        TreeNode cur = root;

        while (cur != null || !stack.isEmpty()) {       //(4)
            while (cur != null) {
                stack.push(cur);//(2)
                cur = cur.left;
            }
            TreeNode top = stack.pop();//(3)
            ret.add(top.val);//(3)
            cur = top.right;//(3)
        }
        return ret;
    }
}

1.2.3后序遍历

所谓后序遍历,就是按照二叉树搜索路径先获取右子树数据,再获取右子树数据,最后获取根结点数据。

后序遍历三部曲:

  1. 后序遍历左子树;
  2. 后序遍历右子树;
  3. 访问根结点。

如下图这样一棵二叉树:
1-2
后序遍历的顺序是: G − > D − > B − > E − > F − > C − > A G->D->B->E->F->C->A G>D>B>E>F>C>A

在线练习后序遍历145. 二叉树的后序遍历

递归实现:
递归条件:如果结点root为空,则返回。
(1)后序遍历左子树,并保存结果。
(2)后序遍历右子树,并保存结果。
(3)获取并保存根结点数据。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> ret = new ArrayList<>();
        if (root == null) return ret;    //递归条件
        ret.addAll(postorderTraversal(root.left));//(1)
        ret.addAll(postorderTraversal(root.right));//(2)
        ret.add(root.val);//(3)
        return ret;
    }
}

非递归实现:
后序遍历的非递归实现需相比于前序遍历,中序遍历,要注意一个点,那就是要防止右子树多次遍历,如果不注意这一点那很有可能造成死循环。比如,就按照前序中序的思路,仅仅只改变数据获取的顺序,你会发现会出现一些问题,思路如下:
(1)使用一个栈储存前序遍历的结点。
(2)获取并保存非空结点数据,并将结点入栈。
(3)遍历的结点为空时,获取栈顶元素,判断栈顶的元素的右子树是否为空(相当于判断该树的右子树是否还需要遍历,如果右子树为空,那么表示该树左右子树都遍历完成,只需获取并保存根结点数据,否则需要遍历右子树)。
(4)如果不为空,更新当前结点为该结点的右结点,如果为空,将结点出栈,并保存当前结点数据。
(5)重复上述步骤,直到栈和结点都为空。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> ret = new ArrayList<>();
        if (root == null) return ret;    

        Stack<TreeNode> stack = new Stack<>();//(1)
        TreeNode cur = root;
        
        while (cur != null || !stack.isEmpty()) {  //(5)
            while (cur != null) {
                stack.push(cur);                    //(2)
                cur = cur.left;
            }
            TreeNode top = stack.peek();
            if (top.right == null) {                //(3)
                ret.add(top.val);                   //(4)
                stack.pop();                        //(4)
            } else {                                //(3)
                cur = top.right;                    //(4)
            }
        }
        return ret;
    }
}

按照上述思路,得到上述代码,我们来运行看一看。
1-3
这就表示栈里面的元素太多了,使得超出了内存限制,大概率是死循环了。所以我们来分析分析思路的第(3)(4)点,先来看看这一段代码:

TreeNode top = stack.peek();
if (top.right == null) {                //(3)
	ret.add(top.val);                   //(4)
	stack.pop();                        //(4)
} else {                                //(3)
	cur = top.right;                    //(4)
}

我们试着模拟不难发现,当遍历完一个结点的右子树后,top又重新指向了该结点,然后又重复判断,得到的结果都是右子树不为空,最终一直对右子树重复遍历,为了解决这个问题,我们可以先标记遍历完后的右子树根结点,记为prev,当下一次top结点又回到这棵右子树的父结点,如果此时该结点的右子树为空或者prev与该结点右子树的根结点相同,就表示右子树已经遍历完成了,不需要再次遍历,将栈出栈,保存当前结点的数据,并将当前结点使用prev记录,以标识该结点已经遍历过。简单来说,还缺少了是否出栈的条件,这个条件一是该结点的右子树为空,二是该结点的右子树与上次标记的结点相同,只要满足上述两个条件之一,该结点的数据就可以获取,并标记已经完成遍历。

对上述思路进行优化得到:
(1)使用一个栈储存前序遍历的结点,定义二叉树结点prev,用来存放上一次遍历的右子树。
(2)获取并保存非空结点数据,并将结点入栈。
(3)遍历的结点为空时,获取栈顶元素,判断栈顶的元素的右子树是否为空和判断该元素的右子树是否与上一次遍历的右子树prev相等(相当于判断该树的右子树是否还需要遍历,如果右子树为空或者该结点的右子树与上一次遍历的结点相等,那么表示该树左右子树都遍历完成,只需获取并保存根结点数据和标识该结点已经被遍历,否则需要遍历右子树)。
(4)如果不为空或者prev与当前结点右子树不相同,更新当前结点为该结点的右结点,并使用prev记录该结点,如果为空或者prev与右子树根结点相同,将结点出栈,并保存当前结点数据和标记该结点已被遍历。
(5)重复上述步骤,直到栈和结点都为空。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> ret = new ArrayList<>();
        if (root == null) return ret;    

        Stack<TreeNode> stack = new Stack<>();//(1)
        TreeNode cur = root;
        TreeNode prev = null;						//用来标记最近一次已经遍历的结点
        while (cur != null || !stack.isEmpty()) {  //(5)
            while (cur != null) {
                stack.push(cur);                    //(2)
                cur = cur.left;
            }
            TreeNode top = stack.peek();
            if (top.right == null || top.right == prev) {      //(3)
                ret.add(top.val);                   //(4)
                stack.pop();                        //(4)
                prev = top;                         //(4)
            } else {                                //(3)
                cur = top.right;                    //(4)
            }
        }
        return ret;
    }
}

其实上述前中后序遍历的递归就是深度优先搜索(DFS)

1.3二叉树的结点个数

二叉树结点的泛型定义:

class TreeNode<E> {
    public E val;
    public TreeNode<E> left;
    public TreeNode<E> right;
    public TreeNode() {}
    public TreeNode(E val) {
        this.val = val;
    }
    public TreeNode(E val, TreeNode<E> left, TreeNode<E> right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

泛型二叉树前中后序遍历代码(思路与非泛型都一样):

    // 前序遍历
    List<E> preOrder(TreeNode<E> root) {
        if (root == null) return new ArrayList<E>();
        List<E> ret = new ArrayList<>();
        ret.add(root.val);
        ret.addAll(preOrder(root.left));
        ret.addAll(preOrder(root.right));
        return ret;
    }
    // 中序遍历
    List<E> inOrder(TreeNode<E> root) {
        if (root == null) return new ArrayList<E>();
        List<E> ret = new ArrayList<>(inOrder(root.left));
        ret.add(root.val);
        ret.addAll(inOrder(root.right));
        return ret;
    }
    // 后序遍历
    List<E> postOrde(TreeNode<E> root) {
        if (root == null) return new ArrayList<E>();
        List<E> ret = new ArrayList<>(postOrde(root.left));
        ret.addAll(postOrde(root.right));
        ret.add(root.val);
        return ret;
    }

1.3.1统计整棵二叉树的结点个数

遍历一遍二叉树,对二叉树非空结点计数,前中后序遍历均可。

// 获取树中节点的个数
    int size(TreeNode<E> root) {
        if (root == null) return 0;
        return 1 + size(root.left) + size(root.right);
    }

1.3.2统计二叉树叶子结点个数

递归思路:

  1. 如果结点为空,表示该树没有结点返回0,
  2. 如果结点的左右子树都为空,表示该结点为叶子结点,返回1。
  3. 一棵二叉树的叶子结点数为左右子树叶子结点数之和。
// 获取叶子节点的个数
    int getLeafNodeCount(TreeNode<E> root) {
        if (root == null) return 0;
        if (root.left == null && root.right == null) return 1;
        return getLeafNodeCount(root.left) + getLeafNodeCount(root.left);
    }

1.3.3统计二叉树第k层次的结点个数

当k=1时,表示第一层次的结点个数,结点个数为1,每递进一层,k就会减1,那么一棵二叉树第k层结点数为左子树,右子树第k-1层次的结点数之和。
递归思路:

  1. 如果结点为空,返回0,k为1,返回1。
  2. 一棵二叉树第k层结点数为左子树,右子树第k-1层次的结点数之和。
// 获取第K层节点的个数
    int getKLevelNodeCount(TreeNode<E> root, int k) {
        if (root == null || k <= 0) return 0;
        if (k == 1) return 1;//第一层结点数为1,每递归一层·k--
        return getKLevelNodeCount(root.left, k-1) + getKLevelNodeCount(root.right, k-1);
    }

1.4获取二叉树的深度

1.4.1最深深度

给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7]3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3

在线练习:
104. 二叉树的最大深度
剑指 Offer 55 - I. 二叉树的深度

🎉解题思路:

递归思路:

  1. 如果根结点为空,则这棵树的高度为0,返回0。
  2. 一棵二叉树的最深深度即为左右子树深度的最大值加上1。
class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
    }
}

1.4.2最浅深度

给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明:叶子节点是指没有子节点的节点

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:2

示例 2:

输入:root = [2,null,3,null,4,null,5,null,6]
输出:5

提示:

树中节点数的范围在 [ 0 , 1 0 5 ] [0, 10^5] [0,105]
− 1000 < = N o d e . v a l < = 1000 -1000 <= Node.val <= 1000 1000<=Node.val<=1000

在线练习:
111. 二叉树的最小深度

🎉解题思路:
对于最小深度首先要注意一点,当一个结点的左子树或者右子树为空时,最小深度并不是0,而是不为空的那颗子树的最小深度加上1。
比如,如图所示这样一棵树,它的最小深度是4,而不是1。
1-4
这道题可以使用递归,递归思路:

  1. 根结点为空,该树最小高度为0,返回0。
  2. 根结点的左右子树至少有一个是空,则该树的最小高度为另一棵子树的最小高度加1。
  3. 该根结点左右子树都不为空,则该树最小高度为左右子树最小高度的最小值加1。
class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) return 0;//1
        if (root.left == null) return minDepth(root.right) + 1;//2
        if (root.right == null) return minDepth(root.left) + 1;//2
        return Math.min(minDepth(root.left), minDepth(root.right)) + 1;//3
    }
}

1.5在二叉树中寻找目标值

使用二叉树前序遍历搜索即可,中序后序都可。

// 检测值为value的元素是否存在
    TreeNode<E> find(TreeNode<E> root, E val) {
        if (root == null) return null;
        if (root.val.equals(val)) return root;
        TreeNode<E> leftRet = find(root.left, val);
        TreeNode<E> rightRet = find(root.right, val);
        return leftRet != null ?  leftRet : rightRet;
    }

1.6层序遍历

1.6.1判断二叉树是否为完全二叉树

完全二叉树: 从根结点开始,每个非空结点按照层次依次递增,每层从左至右的顺序排列的二叉树,称为完全二叉树。换个说法,完全二叉树实际上是对应的满二叉树删除叶结点层最右边若干个结点得到的。

2-4

判断一棵树是否是完全二叉树,我们可以设计一个队列,从根节点开始,每次将根节点的左右孩子结点(包括空结点)依次入队,然后获得队列对头元素并出队,将出队这个结点的左右孩子结点(包括空结点)依次入队,以此类推,直到获取的结点为空,获取的结点为空后,结束上述操作,判断队列中的所有元素是否为空,如果为空,就表示这棵二叉树为完全二叉树。

// 判断一棵树是不是完全二叉树
    boolean isCompleteTree(TreeNode<E> root) {
        if (root == null) return true;
        Queue<TreeNode<E>> queue = new LinkedList<>();
        TreeNode<E> cur = root;
        while (cur != null) {
            queue.offer(cur.left);
            queue.offer(cur.right);
            cur = queue.poll();
        }
        while (!queue.isEmpty()) {
            if (queue.poll() != null) return false;
        }
        return true;
    }

1.6.2层序遍历

对于二叉树的层序遍历,它的实现思路与判断一棵二叉树是否是完全二叉树思路十分相似,都是使用队列来进行实现,但是入队时有一点不同,那就是如果结点为空,则不需要入队,直到最终队列和当前结点均为空时,表示遍历结束。

在线练习:102. 二叉树的层序遍历
对于上面这道题,层序遍历还需要将每层的元素分开,单独存入List中,为了解决这个问题,可以在每次获取出队元素前,先获取队列中元素个数,这个元素个数就是当前层次的元素个数,这样就能把每层的元素都分开了。
(1)记录当前队列元素个数,即二叉树每层的元素个数。
(2)将此层二叉树的结点的左右非空孩子结点存于队列中,并将该层二叉树结点的值存于顺序表中。
(3)将非空的子结点存入队列中。

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<>();
        if (root == null) {
            return ret;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();//(1)
            List<Integer> list = new ArrayList<>();//用与存放该层二叉树结点的值
            while (size > 0) {
                TreeNode cur = queue.poll();//(2)
                list.add(cur.val);
                //(3)
                if (cur.left != null) queue.offer(cur.left);
                if (cur.right != null) queue.offer(cur.right);
                size--;
            }
            ret.add(list);
        }
        return ret;
    }
}

像这种层序遍历的思路就是 广度优先搜索(BFS) 的思路。

2.二叉树的应用与创建

2.1二叉树的应用

2.1.1相同的二叉树

给你两棵二叉树的根节点 pq ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

img

输入:p = [1,2,3], q = [1,2,3]
输出:true

示例 2:

img

输入:p = [1,2], q = [1,null,2]
输出:false

示例 3:

img

输入:p = [1,2,1], q = [1,1,2]
输出:false

提示:

  • 两棵树上的节点数目都在范围 [0, 100]
  • -104 <= Node.val <= 104

🎉解题思路:
(1)如果两棵树的根结点都为空,则两棵树相同。
(2)如果两棵树的根结点有一个为空,则两棵树必然不相同。
(3)如果两棵树都不为空,则判断根结点的值是否相同,不相同则这两棵树必然不相同。
(4)如果两棵树根结点的值相同,则需要判断两棵树的左右子树是否相同,如果相同则这两棵树相同。

class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if (p == q && p == null) {
            return true;                                                            //(1)
        }
        if (p == null || q == null) {
            return false;                                                           //(2)
        }
        if (p.val != q.val) {
            return false;                                                           //(3)
        }else {
            return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);      //(4)
        }
    }
}

在线练习:
100. 相同的树

2.1.2另一棵树的子树

给你两棵二叉树 rootsubRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false
二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

示例 1:

img

输入:root = [3,4,5,1,2], subRoot = [4,1,2]
输出:true

示例 2:

img

输入:root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]
输出:false

提示:

  • root 树上的节点数量范围是 [1, 2000]
  • subRoot 树上的节点数量范围是 [1, 1000]
  • -104 <= root.val <= 104
  • -104 <= subRoot.val <= 104

🎉解题思路:
判断一棵树是否为另一棵树的子树我们可以基于判断两棵树是否相同去做。
(1)如果rootsubRoot的地址相同,则说明两棵树是同一棵树,那么subRoot肯定是root的子树。
(2)如果rootsubRoot有一棵树是空,那么subRoot必然不是root的子树。
(3)如果rootsubRoot相同,那么subRoot肯定是root的子树。
(4)如果root的子树含有与subRoot相同的树,那么subRoot肯定是root的子树。

class Solution {
	//判断两棵树是否相同
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if (p == q && q == null) return true;			
        if (p == null || q == null) return false;

        if (p.val != q.val) {
            return false;
        } 
        else {
            return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
        }
    }
    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if (root == subRoot) return true;
        if (root == null || subRoot == null) return false;
        //整棵树是否与subRoot相同
        if (isSameTree(root, subRoot)) return true;
        //subRoot是否是左子树的子树
        if (isSubtree(root.left, subRoot)) return true;
        //subRoot是否是右子树的子树
        if (isSubtree(root.right, subRoot)) return true;
        //上面都不满足,则subRoot不是该树的子树
        return false;
    }
}

在线练习:
572. 另一棵树的子树

2.1.3对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

示例 1:

img

输入:root = [1,2,2,3,4,4,3]
输出:true

示例 2:

img

输入:root = [1,2,2,null,3,null,3]
输出:false

提示:

  • 树中节点数目在范围 [1, 1000]
  • -100 <= Node.val <= 100

🎉解题思路:
判断一棵树是否是对称二叉树,其实就是判断这棵树的左右子树是否相同,如果相同则该二叉树对称,反之不对称,当然如果这棵二叉树是空树,那这棵树也是对称的。所以这又回到比较两棵树是否相等这个题目上去了。
(1)如果二叉树为空,返回true
(2)比较左右子树是否相等,相等返回true,否则返回false

class Solution {
    //(2)判断左右子树是否相同
    public boolean isSymmetricChild(TreeNode leftTree,TreeNode rightTree) {
        if (leftTree == rightTree && leftTree == null) {
            return true;
        }
        if (leftTree == null || rightTree == null) {
            return false;
        }
        if (leftTree.val != rightTree.val) {
            return false;
        }
        return isSymmetricChild(leftTree.left, rightTree.right) && isSymmetricChild(leftTree.right, rightTree.left);
    }

    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;       //(1)
        return isSymmetricChild(root.left,root.right);//(2)
    }
}

在线练习:
101. 对称二叉树

2.1.4镜像二叉树

所谓镜像二叉树,就是将二叉树的所有左右子树翻转一下。
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:

img

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

示例 2:

img

输入:root = [2,1,3]
输出:[2,3,1]

示例 3:

输入:root = []
输出:[]

提示:

  • 树中节点数目范围在 [0, 100]
  • -100 <= Node.val <= 100

🎉解题思路:
(1)如果该树为空树,返回null。
(2)交换左右子树的根结点。
(3)交换左子树的左右子树,交换右子树的左右子树。

class Solution {
    public TreeNode mirrorTree(TreeNode root) {
        if (root == null) return null;//(1)

        TreeNode tmp = root.left;
        root.left = root.right;//(2)
        root.right = tmp;
        mirrorTree(root.right);//(3)
        mirrorTree(root.left);//(3)
        return root;
    }
}

在线练习:
226. 翻转二叉树
剑指 Offer 27. 二叉树的镜像

2.1.5平衡二叉树

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:true

示例 2:

img

输入:root = [1,2,2,3,3,null,null,4,4]
输出:false

示例 3:

输入:root = []
输出:true

提示:

  • 树中的节点数在范围 [0, 5000]
  • -104 <= Node.val <= 104

🎉解题思路:
(1)如果树为空,则这棵树是平衡二叉树。
(2)获取该树左右子树的高度差的绝对值,如果大于1,则这棵树肯定不是平衡二叉树。
(3)如果高度差绝对值不大于1,再判断该树的左右子树是否都是平衡二叉树,如果是,则这一整棵树是平衡二叉树。

所以这道题最终回到了求二叉树的高度上来。

class Solution {
    //获取二叉树的高度
    public int maxDeep(TreeNode subtree) {
        if (subtree == null) {
            return 0;
        }
        return 1 + Math.max(maxDeep(subtree.left), maxDeep(subtree.right));
    }
    public boolean isBalanced(TreeNode root) {
        if (root == null) {
            return true;            //(1)
        }
        int ret = Math.abs(maxDeep(root.left) - maxDeep(root.right));       //(2)
        if (ret <= 1 && isBalanced(root.left) && isBalanced(root.right)) {  //(3)
            return true;
        }
        return false;
    }
}

在线练习:
110. 平衡二叉树
剑指 Offer 55 - II. 平衡二叉树
面试题 04.04. 检查平衡性

2.1.6二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

示例 2:

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例 3:

输入:root = [1,2], p = 1, q = 2
输出:1

提示:

  • 树中节点数目在范围 [2, 105] 内。
  • -109 <= Node.val <= 109
  • 所有 Node.val 互不相同
  • p != q
  • pq 均存在于给定的二叉树中。

🎉解题思路:
递归思路:
(1)如果为空树返回null。
(2)如果p,q结点的数据均与根结点root相同,则最近公共祖先为root。
(3)如果p,q都在左子树或右子树,则最近公共祖先也在左子树或右子树。
(4)如果p,q位于不同的左右子树上,则最近公共祖先为root。

对于(3)(4)两点,我们可以分别去左右子树找p,q结点,如果在左右子树均找到,说明思路(4)成立,如果在左右子树中只找到一个,就说明思路(3)成立,如果在左右子树都没有找到,就说明p,q在root这棵二叉树上没有公共祖先。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) {
            return null;                                            //(1)
        }
        if (q.val == root.val || p.val == root.val) {
            return root;                                            //(2)
        }
        TreeNode left = lowestCommonAncestor(root.left, p, q);     
        TreeNode right = lowestCommonAncestor(root.right, p, q);    
        if (left != null && right != null) {
            return root;                                            //(4)
        }else if (left != null) {
            return left;                                            //(3)
        } else if (right != null) {
            return right;                                           //(3)
        }else {
            return null;                                            //不存在最近祖先
        }
    }
}

非递归思路:
可以利用两个栈分别存储root->p, root->q的二叉树路径,然后将元素多的那一个栈内的元素出栈,直到与另一个栈的元素数量相等,最后两栈同时出栈,并比较栈顶元素,如果相等,则相等的那个结点就是p,q的最近公共祖先,直到栈为空,如果栈为空还没有找到,p,q在root这棵树上没有公共祖先。

(1)使用两个栈分别存储root->p, root->q的二叉树路径。
(2)寻找root->p, root->q的二叉树路径。
(3)调整元素数量较多的栈,使其元素数量与另一个栈相等。
(4)两栈同时出栈并比较栈顶元素,相等的元素结点即为最近公共结点。

class Solution {
    private boolean findPath(TreeNode root, TreeNode goal, Stack<TreeNode> stack) {
        if (root == null) return false;
        stack.push(root);//先将该结点入栈,再判断root->goal路径上是否含有该结点,若没有将该结点出栈
        if (root == goal) return true;
        if (findPath(root.left, goal, stack)) return true;//确定左子树路径
        if (findPath(root.right, goal, stack)) return true;//确定右子树路径
        stack.pop();//该节点左右子树都没goal结点路径,说明该结点也不是root->goal路径上的结点
        return false;//说明没有找到路径
    }
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) return null;

        Stack<TreeNode> stack1  = new Stack<>();                    //(1)
        Stack<TreeNode> stack2  = new Stack<>();                    //(1)
        findPath(root, p, stack1);                                  //(2)
        findPath(root, q, stack2);                                  //(2)

        int size1 = stack1.size();                  
        int size2 = stack2.size();
        if (size1 > size2) {
            int size = size1 - size2;
            while (size > 0) {
                stack1.pop();                                       //(3)
                size--;
            }
            while (!stack1.isEmpty() && !stack2.isEmpty()) {
                if (stack1.peek() == stack2.peek()) {
                    return stack1.pop();                            //(4)
                }
                stack1.pop();
                stack2.pop();
            }
        } else {
            int size = size2 - size1;
            while (size > 0) {
                stack2.pop();                                      //(3)
                size--;
            }
            while (!stack1.isEmpty() && !stack2.isEmpty()) {
                if (stack1.peek() == stack2.peek()) {
                    return stack1.pop();                            //(4)
                }
                stack1.pop();
                stack2.pop();
            }
        }
        return null;
    }
}

关于(2)寻找root->p, root->q的二叉树路径的思路(递归):
(1)如果目标的二叉树为空,那么肯定没有路径。
(2)创建一个栈,用于存放路径,先假设路径存在并把这条路径上的结点入栈,然后顺着这个路径寻找p或者q,如果没找到,将这条路径上的结点出栈,换另外一条路径寻找。
(3)根据(2)的需求,需设置函数返回值为boolean,以判断是否找到正确的路径。

private boolean findPath(TreeNode root, TreeNode goal, Stack<TreeNode> stack) {
        if (root == null) return false;						//(1)
        stack.push(root);									//(2)先将该结点入栈,再判断root->goal路径上是否含有该结点,若没有将该结点出栈
        if (root == goal) return true;
        if (findPath(root.left, goal, stack)) return true;	//(2)确定左子树路径
        if (findPath(root.right, goal, stack)) return true;	//(2)确定右子树路径
        stack.pop();										//(2)该节点左右子树都没goal结点路径,说明该结点也不是root->goal路径上的结点
        return false;										//(3)说明没有找到路径
}

在线练习:
235. 二叉搜索树的最近公共祖先
236. 二叉树的最近公共祖先
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
剑指 Offer 68 - II. 二叉树的最近公共祖先

2.2二叉树的创建

2.2.1根据字符串创建二叉树

编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。

输入描述:

输入包括1行字符串,长度不超过100。

输出描述:

可能有多组测试数据,对于每组数据, 输出将输入字符串建立二叉树后中序遍历的序列,每个字符后面都有一个空格。 每个输出结果占一行。

示例:

输入:

abc##de#g##f###

输出:

c b e g d f a 

🎉解题思路:
(1)注意题目需要多组输入。
(2)构建二叉树结点。
(3)读取字符串,根据前序遍历的顺序构建二叉树。遇到非#符号,创建一个结点并存入目标值,然后按照前序遍历的顺序构建该树的左子树,右子树,遇到#符号,相当于遇到二叉树的空,将字符串下标加1返回即可。
(4)使用一个成员变量,对字符串进行遍历,因为在递归的过程中,设置局部变量会被销毁。每构建一个二叉树应将该成员变量置为0
(5)中序遍历输出已经创建好的二叉树。

import java.util.*;
class TreeNode {                                //(2)
    public char val;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(char val) {
        this.val = val;
    }
}
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static int index = 0;                            //(4)
    public static TreeNode creatTree(String str) {
        TreeNode node = null;
        char val = str.charAt(index);                        //(3)
        if (val != '#') {
            node = new TreeNode(val);                        //(3)
            index++;
            node.left = creatTree(str);                        //(3)
            node.right = creatTree(str);                       //(3)
        } else {
            index++;
        }
        return node;
    }
    public static void inOrder(TreeNode root) {
        if (root == null) return;
        inOrder(root.left);
        System.out.print(root.val + " ");
        inOrder(root.right);
    }
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        while (in.hasNext()) { // 注意 while 处理多个 case
            String s = in.nextLine();                        //(1)
            index = 0;                                       //(4)
            TreeNode root = creatTree(s);                    //(2)
            inOrder(root);                                   //(5)
            System.out.println();
        }
    }
}

在线练习:
KY11 二叉树遍历

2.2.2根据前序遍历与中序遍历创建二叉树

根据前序遍历我们可以确定二叉树的根结点,前序遍历的顺序是根,左子树,右子树,所以创建树的时候,应该以根,左子树,右子树的顺序创建,根据中序遍历我们能够得到一个根结点的左右子树。

二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG.则二叉树根结点为___。
根据前序遍历结果,第一个遍历的结点为整棵二叉树的根结点,所以二叉树根结点为E,根据中序遍历能够确定该根结点的左右子树,所以HFIE的左子树中序遍历序列,JKGE右子树的中序遍历序列,以此类推能够确定整棵树所有的左子树与右子树。下图为根据前序遍历序列和中序遍历序列创建的二叉树:
2-2

尝试使用编程根据前序遍历与中序遍历创建二叉树:
给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的先序遍历inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例 1:

img

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

示例 2:

输入: preorder = [-1], inorder = [-1]
输出: [-1]

提示:

  • 1 <= preorder.length <= 3000
  • inorder.length == preorder.length
  • -3000 <= preorder[i], inorder[i] <= 3000
  • preorderinorder无重复 元素
  • inorder 均出现在 preorder
  • preorder 保证 为二叉树的前序遍历序列
  • inorder 保证 为二叉树的中序遍历序列

🎉解题思路:
(1)创建一棵二叉树的根结点。
(2)根据前序遍历数组找到根结点,通过该根节点取中序遍历数组确定该根节点的左右子树,该根结点左边的中序遍历序列为该结点的左子树,右边的中序遍历序列为该结点的右子树。
(3)通过分而治之的思想,去创建该树的左子树与右子树。
(4)假设遍历前序遍历数组的下标为pi,中序遍历数组的起始序列结点下标为is,结束序列结点下标为ie,所创建树的根结点为ri,则创建一棵树的左子树中序遍历序列下标范围为[is, ri-1],右子树中序遍历序列下标范围为[ri+1, ie],通过前序遍历的顺序去创建一棵二叉树的根,左子树和右子树。

class Solution {
    private int preIndex;                                                           //前序遍历序列数组下标,需使用成员变量
    private TreeNode creatTree(int[] preorder, int[] inorder, int instart, int inend) {
        if (instart > inend) return null;                                           //说明所创建结点为空
        TreeNode root = new TreeNode(preorder[preIndex]);                           //(1)
        int rootIndex = findOfInorder(inorder, instart, inend, preorder[preIndex]);//(2)找到中序遍历对应根节点的下标
        preIndex++;                                                                 //为下一个根结点的创建做准备
        //根据前序遍历顺序先找到root的左子树再找到root的右子树
        root.left = creatTree(preorder, inorder, instart, rootIndex - 1);           //(3)(4)
        root.right = creatTree(preorder, inorder, rootIndex + 1, inend);            //(3)(4)
        return root;
    }
    private int findOfInorder(int[] inorder, int instart, int inend, int key) {
        for (int i = instart; i <= inend; i++) {
            if (inorder[i] == key) return i;                                        //(2)
        }
        return -1;
    }
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        if (preorder == null || inorder == null) return null;
        return creatTree(preorder, inorder, 0, inorder.length - 1);
    }
}

在线练习:
105. 从前序与中序遍历序列构造二叉树

2.2.3根据中序遍历和后序遍历创建二叉树

根据后序遍历我们可以确定二叉树的根结点,但是要注意,后序遍历是最后才遍历根结点的,所以根结点在序列的末端,所以需要从右向左依次读取后续遍历的序列,得到根结点的顺序是整棵二叉树的根,右子树,左子树,所以根据后序遍历和中序遍历创建二叉树的顺序是根结点,右子树,左子树,根据中序遍历我们能够得到一个根结点的左右子树。

设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为_____。
2-3
根据二叉树中序遍历序列:badce,后序遍历序列:bdeca,能够确定二叉树序列,如上图。
所以二叉树前序遍历序列为abcde

尝试使用编程根据前序遍历与中序遍历创建二叉树:
给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

示例 1:

img

输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]

示例 2:

输入:inorder = [-1], postorder = [-1]
输出:[-1]

提示:

  • 1 <= inorder.length <= 3000
  • postorder.length == inorder.length
  • -3000 <= inorder[i], postorder[i] <= 3000
  • inorderpostorder 都由 不同 的值组成
  • postorder 中每一个值都在 inorder
  • inorder 保证是树的中序遍历
  • postorder 保证是树的后序遍历

🎉解题思路:
这题相对与根据前序遍历和中序遍历创建二叉树值改变了一点,也就是将前序遍历变为后续遍历,而我们知道可以根据后序遍历序列从右向左读取根,右子树,左子树,所以该题只需改变后序遍历数组遍历顺序和二叉树创建顺序。
(1)创建一棵二叉树的根结点。
(2)根据后序遍历数组从右向左找到根结点,通过该根节点取中序遍历数组确定该根结点的左右子树,该根结点左边的中序遍历序列为该结点的左子树,右边的中序遍历序列为该结点的右子树。
(3)通过分而治之的思想,去创建该树的右子树与左子树。
(4)假设 从右向左 遍历后序遍历数组的下标为pi,中序遍历数组的起始序列结点下标为is,结束序列结点下标为ie,所创建树的根结点为ri,则创建一棵树的左子树中序遍历序列下标范围为[is, ri-1],右子树中序遍历序列下标范围为[ri+1, ie],通过逆后序遍历的顺序去创建一棵二叉树的根,右子树和左子树。

class Solution {
    private int postIndex;                                                                                  //后序遍历序列数组下标,需使用成员变量                       
    private TreeNode creatTree(int[] inorder, int instart, int inend, int[] postorder) {
        if (instart > inend) return null;
        TreeNode root = new TreeNode(postorder[postIndex]);
        int rootIndex = findOfInorder(inorder, instart, inend, postorder[postIndex]);//(2)找到中序遍历对应根节点的下标
        postIndex--;                                                        //为创建下一个根结点做准备
        //根据后序遍历顺序先找到root的右子树再找到root的左子树
        root.right = creatTree(inorder, rootIndex + 1, inend, postorder);   //(3)(4)
        root.left = creatTree(inorder, instart, rootIndex - 1, postorder);  //(3)(4)
        return root;
    }
    private int findOfInorder(int[] inorder, int instart, int inend, int key) {
        for (int i = instart; i <= inend; i++) {
            if (inorder[i] == key) return i;                                //(2)
        }
        return -1;
    }
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        if (inorder == null || postorder == null) return null;
        postIndex = postorder.length - 1;                                   //从右向左遍历后序遍历数组
        return creatTree(inorder, 0, inorder.length - 1,postorder);
    }
}

在线练习:
106. 从中序与后序遍历序列构造二叉树

总结:
同时给定一棵二叉树的先序序列和中序序列唯一确定这棵二叉树。
同时给定一棵二叉树的中序序列和后序序列唯一确定这棵二叉树。
同时给定一棵二叉树的先序序列和后序序列不能唯一确定这棵二叉树。

2-6

2.2.4将二叉搜索树改为双链表

如果想要知道如何将搜索二叉树改造成双链表,得知道什么是二叉搜索树。
二叉搜索树是一种特殊的二叉树,又称二叉查找树,二叉排序树,它有几个特点:

  • 如果左子树存在,则左子树每个结点的值均小于根结点的值。
  • 如果右子树存在,则右子树每个结点的值均大于根结点的值。
  • 中序遍历二叉搜索树,得到的序列是依次递增的。
  • 二叉搜索树的左右子树均为二叉搜索树。

下面我们来看一道例题,来说明如何将二叉树转换为双链表:

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

为了让您更好地理解问题,以下面的二叉搜索树为例:

img

我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。

img
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

🎉解题思路:
首先根据题意我们需要将二叉搜索树转换为一个升序排列的循环双向链表,我们可以根据二叉搜索树中序遍历得到一个排序的序列这一特点,使用中序遍历进行遍历二叉树。
(1)使用一个引用prev(初始为null)用来保存中序遍历的前一个结点,使用引用head来记录二叉搜索树的最小结点,即双链表的头结点。
(2)如果prev为null,说明当前遍历的结点为二叉搜索树的最小结点(构造双链表的第一个结点),使用head记录该结点。
(3)如果prev不为null,说明当前遍历的结点在构造的双链表中存在前驱和后继,为了使改造对中序遍历影响最小,我们将二叉树结点的left作为链表的前驱,二叉树结点的right作为链表的后继,所以当prev不为null时,将prev的后继指向当前结点,当前结点的前驱置为prev。
(4)更新prev为当前结点,继续中序遍历剩下结点,重复上述操作,返回值为当前结点。
(5)双链表改造完成后,需要找到链表最后一个结点,将其与第一个结点连接,获得一个循环双链表。

class Solution {
    private Node prev;                              //记录前一个遍历结点
    private Node head;                              //记录头结点
    private Node inorderToList(Node root) {
        if (root == null) return null;
        //二叉搜索树中序遍历等有序序列,left指向前驱,right指向后继
        inorderToList(root.left);
        root.left = prev;                           //(3)
        if (prev != null) prev.right = root;        //(3)
        else head = root;                           //(2)
        prev = root;                                //(4)
        inorderToList(root.right);
        return root;                                //(4)
    }
    public Node treeToDoublyList(Node root) {
        if (root == null) return null;
        Node tail = inorderToList(root);//最终返回的结点为根结点
        while (tail.right != null) {
            tail = tail.right;                  //(5)
        }
        head.left = tail;                       //(5)
        tail.right = head;                      //(5)
        return head;
    }
}

在线练习:
剑指 Offer 36. 二叉搜索树与双向链表
JZ36 二叉搜索树与双向链表

2.2.5将二叉树改为单链表

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

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

示例 1:

img

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

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [0]
输出:[0]

提示:

  • 树中结点数在范围 [0, 2000]
  • -100 <= Node.val <= 100

进阶: 你可以使用原地算法(O(1) 额外空间)展开这棵树吗?

🎉解题思路:
(1)如果根结点的左子树为空,将根结点变为右子树根结点,直到该结点有左子树,找到左子树最右边的结点mostRight
(2)将右子树拼接到mostRight右边。
(3)将根结点的right指向左子树,根结点的leftnull,将根结点变为新右子树根结点,重复上述操作直到结点为空,这样二叉树就完全转换成单链表了。
2-2-5-1
2-2-5-2
2-2-5-3

class Solution {
    public void flatten(TreeNode root) {
        if (root == null) return;
        while (root != null) {
            if (root.left == null) {
                root = root.right;                      //(1)
            } else {
                TreeNode mostRight = root.left;
                while (mostRight.right != null) {
                    mostRight = mostRight.right;        //(1)找到左子树根结点的最右端
                }
                mostRight.right = root.right;           //(2)将右子树拼接到最右端
                root.right = root.left;                 //(2)
                root.left = null;                       //(2)
                root = root.right;                      //(3)
            }
        }
    }
}

在线练习:
114. 二叉树展开为链表

2.2.6根据二叉树创建字符串

你需要采用前序遍历的方式,将一个二叉树转换成一个由括号和整数组成的字符串。

空节点则用一对空括号 “()” 表示。而且你需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。

示例 1:

输入: 二叉树: [1,2,3,4]
       1
     /   \
    2     3
   /    
  4     

输出: "1(2(4))(3)"

解释: 原本将是“1(2(4)())(3())”,
在你省略所有不必要的空括号对之后,
它将是“1(2(4))(3)”。

示例 2:

输入: 二叉树: [1,2,3,null,4]
       1
     /   \
    2     3
     \  
      4 

输出: "1(2()(4))(3)"

解释: 和第一个示例相似,
除了我们不能省略第一个对括号来中断输入和输出之间的一对一映射关系。

🎉解题思路:
(1)按照前序遍历的顺序对二叉树进行遍历,遍历之前需要判断结点左右子树存在情况,根据不同情况添加不同的括号,可以使用StringBuilder对象来对字符串进行组装。
(2)首先在StringBuilder对象添加根结点数据,如果根结点的左子树不为空,加左括号,左子树数据,右括号,如果为空,就判断右子树是否存在,如果存在加上一对括号,不存在则什么都不用做。
(3)遍历完左子树后,考虑右子树是否为空,如果不为空,加左括号,加右子树数据,加右括号,如果为空什么都不用做。

class Solution {
    private void treeToString(TreeNode root, StringBuilder sb) {
        sb.append(root.val);                                //(1)

        if (root.left != null) {
            sb.append("(");
            treeToString(root.left, sb);                    //(2)
            sb.append(")");
        } else {
            if (root.right == null) return;                 //(2)
            sb.append("()");
        }

        if (root.right != null) {
            sb.append("(");
            treeToString(root.right, sb);                   //(3)
            sb.append(")");
        } else {
            return;                                         //(3)
        }
    }
    public String tree2str(TreeNode root) {
        if (root == null) return null;
        StringBuilder sb = new StringBuilder();             //(1)
        treeToString(root, sb);                             
        return sb.toString();
    }
}

在线练习:
606. 根据二叉树创建字符串

本文到这就该结束了,下期再见!


觉得文章写得不错的老铁们,点赞评论关注走一波!谢谢啦!

1-99

评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未见花闻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值