目录
前期准备
二叉树
概念描述
(1)二叉树
- 每个节点最多有两个子树:在二叉树中,每个节点最多有两个子节点,通常被称为左子节点和右子节点。
- 子树有左右之分:节点的两个子树被严格地区分为左子树和右子树,它们的顺序不能随意颠倒。
- 树的层数可以不一致:在一个二叉树中,不同的节点处的子树高度可以不同,即树的各个部分的深度可以不一致。
展示:
A
/ \
B C
/ \ / \
D E F G
(2)二叉树的几种特殊形态包括:
- 完全二叉树:除了最后一层外,其他各层的节点都达到了最大数目,并且最后一层的节点都连续地紧挨在左边。
- 满二叉树:所有的层都达到了最大节点数目的二叉树,也就是说这是一个深度为k且有2^k - 1个节点的二叉树。
- 平衡二叉树(AVL树):任何节点的两个子树的高度最大差别为1,这样是为了防止树的高度过大,导致查询效率下降
- 二叉搜索树(BST,Binary Search Tree):对于每个节点来说,其左子树上的所有节点的值都小于它,而右子树上的所有节点的值都大于它,这样的性质使得二叉搜索树加快了查找的速度
构建
构建二叉树的树节点,
public class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
几种遍历
(1)前序遍历(Pre-order Traversal):访问顺序为根节点 -> 左子树 -> 右子树。首先访问当前节点(根节点),然后递归地对左子树进行前序遍历,最后递归地对右子树进行前序遍历。
访问A(根节点)
对节点B(左子树)应用前序遍历:
访问B
对节点D(左子树)应用前序遍历(访问D)
对节点E(右子树)应用前序遍历(访问E)
对节点C(右子树)应用前序遍历:
访问C
对节点F(左子树)应用前序遍历(访问F)
对节点G(右子树)应用前序遍历(访问G)
遍历结果:A, B, D, E, C, F, G
(2)中序遍历(In-order Traversal):访问顺序为左子树 -> 根节点 -> 右子树。首先递归地对左子树进行中序遍历,然后访问当前节点(根节点),最后递归地对右子树进行中序遍历。
对节点B(左子树)应用中序遍历:
对节点D(左子树)应用中序遍历(访问D)
访问B
对节点E(右子树)应用中序遍历(访问E)
访问A(根节点)
对节点C(右子树)应用中序遍历:
对节点F(左子树)应用中序遍历(访问F)
访问C
对节点G(右子树)应用中序遍历(访问G)
遍历结果:D, B, E, A, F, C, G
(3)后序遍历(Post-order Traversal):访问顺序为左子树 -> 右子树 -> 根节点。首先递归地对左子树进行后序遍历,然后递归地对右子树进行后序遍历,最后访问当前节点(根节点)。
对节点B(左子树)应用后序遍历:
对节点D(左子树)应用后序遍历(访问D)
对节点E(右子树)应用后序遍历(访问E)
访问B
对节点C(右子树)应用后序遍历:
对节点F(左子树)应用后序遍历(访问F)
对节点G(右子树)应用后序遍历(访问G)
访问C
访问A(根节点)
遍历结果:D, E, B, F, G, C, A
(4)层序遍历(Level-order Traversal or Breadth-first Traversal):逐层从上到下,每一层从左到右访问所有节点。通常使用队列来实现。
访问第1层:A
访问第2层:B, C
访问第3层:D, E, F, G
遍历结果:A, B, C, D, E, F, G
这四种遍历方式各有其应用场景,
前序遍历:适合用来复制二叉树。
中序遍历:在二叉搜索树中会按照顺序访问节点,常用于排序任务。
后序遍历:适合用来执行先操作子节点,再操作父节点的任务,如树的删除操作。
层序遍历:能够按照树的层次结构逐层遍历,常用于寻找最短路径或进行分层次的分析。
一、几种遍历
题目
题解
我们首先实现了buildTree函数,这个函数基于TreeNode类创建节点并连接成我们所描述的特定二叉树。之后是四种不同的遍历方法。
前序遍历函数首先访问根节点,然后递归地对左子树进行前序遍历,接着是右子树。
中序遍历函数首先递归地对左子树进行中序遍历,然后访问根节点,最后递归地对右子树进行中序遍历。
后序遍历函数首先递归地对左子树进行后序遍历,然后是右子树,最后访问根节点。
层序遍历使用Queue(队列)来实现。首先将根节点放入队列,然后在队列不为空的情况下,不断地从队列中取出节点进行访问,并将该节点的左右子节点(如果有的话)依次放入队列。
最后,在main函数中我们构造了树并调用这些遍历方法,打印出每种遍历的结果。
当运行这个程序时,它将输出构造的二叉树按照前序、中序、后序和层序遍历的节点顺序。
public class TreeUtils {
public static void main(String[] args) {
TreeNode root = buildTree();
System.out.print("前序遍历");
PreOrder(root);
System.out.println();
System.out.print("中序遍历");
InOrder(root);
System.out.println();
System.out.print("后序遍历");
PostOrder(root);
System.out.println();
System.out.print("层序遍历");
LevelOrder(root);
}
public static TreeNode buildTree(){
TreeNode root = new TreeNode(3,
new TreeNode(9, null, null),
new TreeNode(20, new TreeNode(15, null, null), new TreeNode(7, null, null)));
return root;
}
public static void PreOrder(TreeNode node){
if(node == null) return;
System.out.print(node.val + " ");
PreOrder(node.left);
PreOrder(node.right);
}
public static void InOrder(TreeNode node){
if(node == null) return;
InOrder(node.left);
System.out.print(node.val + " ");
InOrder(node.right);
}
public static void PostOrder(TreeNode node){
if(node == null) return;
PostOrder(node.left);
PostOrder(node.right);
System.out.print(node.val + " ");
}
public static void LevelOrder(TreeNode root){
if(root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()){
TreeNode node = queue.poll();
System.out.print(node.val + " ");
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
}
}
二、104二叉树的最大深度
题目
给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
提示:
树中节点的数量在 [0, 104] 区间内。
-100 <= Node.val <= 100
题解
分析这个问题, 我们可以采用Divide-and-Conquer分治法思想,将原二叉树分解为矮一层的左右子树, 通过计算子树的最大深度, 以获取原树的最大深度, 即可得到问题的解。
我们直接套用分治法的步骤来寻找解决问题的方法。
按照分治法的思想, 分成三步来解决问题:
- devide:将原二叉树分解为两个子树, 左二叉树和右二叉树
- conquer:递归地求解左二叉树的最大深度和右二叉树的最大深度
- combine:原始二叉树的最大深度等于左子树最大深度与右子树最大深度的最大值 + 1
按照上面分治法的分析过程,直接写出代码很简单。
/*
* 二叉树的最大深度
* */
public static int maxDeepth(TreeNode root){
System.out.print("二叉树的最大深度:");
return root == null ? 0 : countDeepth(root);
}
public static int countDeepth(TreeNode node){
if(node == null){
return 0;
} else {
return 1 + Math.max(countDeepth(node.left), countDeepth(node.right));
}
}
三、110平衡二叉树
题目
给定一个二叉树,判断它是否是平衡二叉树。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
示例 3:
输入:root = []
输出:true
提示:
树中的节点数在范围 [0, 5000] 内
-104 <= Node.val <= 104
题解
方法一
判断一棵树是不是平衡二叉树,就要判断每个节点是不是平衡的二叉树,即递归遍历每个节点。思路如下:
- 判断当前节点的左树和右树的高度差的绝对值是否不超过 1
- 看左树是否平衡
- 看右树是否平衡
class Solution {
//求高度(最大深度就是高度)
public int maxDepth(TreeNode root) {
if(root == null) return 0;
//为了防止递归时间过长,把leftHeight和rightHeight定义出来
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
return leftHeight > rightHeight ?
leftHeight +1 : rightHeight +1;
}
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
//求左树以及右树的高度
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
int ret = Math.abs(leftHeight-rightHeight);
//同时满足当前节点是平衡的,且其左右子树都是平衡的,才是平衡二叉树
return ret <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
}
此解法存在一定的问题:时间复杂度太高了,时间复杂度是O(N * N)。
调用isBalanced求高度,maxDepth函数的时间复杂度是O(N)。从上往下,每个节点都需要求高度,共N个节点,所以时间复杂度是O(N * N)。
方法二
可以发现,判断一棵二叉树是否是平衡二叉树,都得依赖于求高度的函数maxDepth。
那么,我们可以思考,第一种解法是从上往下求高度,换个角度,从下往上走,每次返回节点的高度,这样做是否能让时间复杂度降低。可以知道,这样做只需要遍历一次二叉树,而且遍历的过程中,每次求完节点的高度,如果发现左树和右树的高度差的绝对值超过 1,就认为不是平衡二叉树,直接return false结束。
将时间复杂度变为O(N)。也就是说,每次求高度的同时就判断是否平衡。
class Solution {
public boolean isBalanced(TreeNode root) {
return height(root) != -1;
}
public int height(TreeNode node){
if(node==null){
return 0;
}
int l = height(node.left);
int r = height(node.right);
if(l == -1 || r == -1 || l - r < -1 || l - r > 1){
return -1;
}
return 1 + Math.max(l, r);
}
}
或者
class Solution {
public boolean isBalanced(TreeNode root) {
return height(root) >= 0;
}
public int height(TreeNode node){
if(node == null) return 0;
int l = height(node.left);
int r = height(node.right);
// if(l == -1 || r == -1 || l - r < -1 || l - r > 1){
// return -1;
// }
// return 1 + Math.max(l ,r);
if(l >= 0 && r >= 0 && Math.abs(l-r) <= 1){
return 1 + Math.max(l ,r);
} else{
return -1;
}
}
}
此时,时间复杂度都是O(N)。
四、543二叉树的直径
题目
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
示例 1:
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
示例 2:
输入:root = [1,2]
输出:1
提示:
树中节点数目在范围 [1, 104] 内
-100 <= Node.val <= 100
题解
根据题目描述,我们要获得二叉树中任意两个节点的最大直径。那么如何确定哪两个节点是值得去进行计算的?或者那两个节点我们应该去进行计算。以一个3个节点
的子树为例,分为:根节点(rootNode
)、左子节点(leftNode
)和右子节点(rightNode
),那么leftNode
到rootNode
的距离和rootNode
到rightNode
的距离其实没有必要参与最大直径的计算,因为leftNode
到rightNode
的距离一定倾向是最大直径。所以,我们得出一个结论:
可能的最大直径 = leftNode到rootNode的距离 + rootNode到rightNode的距离;
那么,因为二叉树也并不只有3个节点,如果节点很多的话,那么这个二叉树的层级也就会越深,那么下面我们其实如果能找到leftNode到rootNode距离的最大值(或最深路径)以及找到rootNode到rightNode距离的最大值(或最深路径),那么相加必然就是本题所要求解的最大直径了。
那么针对树形结构的解题,最常用的方式就是递归算法了,从叶子节点开始统计,一直统计到根节点,并且每次都要进行直径的计算和比较,当遍历到根节点时,最大直径也就计算出来了。
以上就是本题的解题思路,为了便于大家更加深入的理解,下面我们以输入root = [1,2,3,4,5]
为例,看一下是如何进行最大直径计算的(图中省略了根节点的深度和直径的计算,大家自行脑补即可),请见下图所示:
所以需要引入一个全局变量,记录每一个节点的最大直径。
class Solution {
int ans = 0;
public int diameterOfBinaryTree(TreeNode root) {
if(root == null || root.left == null && root.right == null){
return 0;
}
depth(root);
return ans;
}
public int depth(TreeNode root){ // 计算其深度(即从该节点到叶节点的最长路径)
if(root == null){
return 0;
}
// 递归求两子树的最大深度
int left = depth(root.left); // 左子树最大深度
int right = depth(root.right); // 右子树最大深度
ans = Math.max(left + right, ans); // 更新一个全局变量来记录遍历到目前为止的最大直径
return Math.max(left, right) + 1; // 在每次递归调用中,计算并返回节点的最大深度
}
}
时间复杂度是O(N),其中 N 为二叉树的节点数,即遍历一棵二叉树的时间复杂度,每个结点只被访问一次。
空间复杂度:O(Height),其中 Height 为二叉树的高度。由于递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,而递归的深度显然为二叉树的高度,并且每次递归调用的函数里又只用了常数个变量,所以所需空间复杂度为 O(Height)。