110.平衡二叉树
给定一个二叉树,判断它是否是 平衡二叉树
一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
示例 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
Related Topics
-
树
-
深度优先搜索
-
二叉树
思路
咋眼一看这道题目和104.二叉树的最大深度 (opens new window)很像,其实有很大区别。
这里强调一波概念:
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
但leetcode中强调的深度和高度很明显是按照节点来计算的,如图:
关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。
因为求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)
有的同学一定疑惑,为什么104.二叉树的最大深度 (opens new window)中求的是二叉树的最大深度,也用的是后序遍历。
那是因为代码的逻辑其实是求的根节点的高度,而根节点的高度就是这棵树的最大深度,所以才可以使用后序遍历。
递归法
- 明确递归函数的参数和返回值
参数:当前传入节点。 返回值:以当前传入节点为根节点的树的高度。
那么如何标记左右子树是否差值大于1呢?
如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。
所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。
代码如下:
private int getHeight(TreeNode root) {
}
- 明确终止条件
递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0
if (root == null) {
return 0;
}
- 明确单层递归的逻辑
如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。
分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。
代码如下:
//获得左子树的高度
int leftDepth = getHeight(root.left);
//获得左子树的高度
int rightDepth = getHeight(root.right);
//如果左子树或者右子树的左右叶子节点出现了高度差大于-1时.返回-1
if (leftDepth == -1 || rightDepth == -1) {
return -1;
}
//如果左节点与右节点高度差大于1时,返回-1;
if (Math.abs(leftDepth - rightDepth) > 1) { // 中
return -1;
} else {
//返回以当前节点为根节点的树的最大高度
return 1+Math.max(leftDepth,rightDepth);
}
完全的递归遍历如下
public boolean isBalanced(TreeNode root) {
int res = getHeight(root);
return res == -1 ? false : true;
}
/**
* @Description 获得当前节点左右子树高度差
* @Param
* @Return {@link int}
* @Author 君君
* @Date 2024/7/5 14:33
*/
private int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
//获得左子树的高度
int leftDepth = getHeight(root.left);
//获得左子树的高度
int rightDepth = getHeight(root.right);
//如果左子树或者右子树的左右叶子节点出现了高度差大于-1时.返回-1
if (leftDepth == -1 || rightDepth == -1) {
return -1;
}
//如果左节点与右节点高度差大于1时,返回-1;
if (Math.abs(leftDepth - rightDepth) > 1) { // 中
return -1;
} else {
//返回以当前节点为根节点的树的最大高度
return 1+Math.max(leftDepth,rightDepth);
}
}
迭代法
在104.二叉树的最大深度 (opens new window)中我们可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。
本题的迭代方式可以先定义一个函数,专门用来求高度。
这个函数通过栈模拟的后序遍历找每一个节点的高度(其实是通过求传入节点为根节点的最大深度来求的高度)
cur节点的最大深度,就是cur的高度
/**
* 层序遍历,求结点的高度
*/
public int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
int depth = 0;
while (!deque.isEmpty()) {
int size = deque.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode poll = deque.poll();
if (poll.left != null) {
deque.offer(poll.left);
}
if (poll.right != null) {
deque.offer(poll.right);
}
}
}
return depth;
}
然后再用栈来模拟后序遍历,遍历每一个节点的时候,再去判断左右孩子的高度是否符合,代码如下:
/**
* @Description 迭代法(后序遍历,遍历每一个节点,获得每一个节点的深度)
* @Param root
* @Return {@link boolean}
* @Author 君君
* @Date 2024/7/5 14:57
*/
public boolean isBalanced(TreeNode root) {
if (root == null)
return true;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty())
{
TreeNode node = stack.peek(); // 中
if(node!=null)
{
stack.pop();
stack.push(node);
stack.push(null);
if (Math.abs(getHeight(node.left) - getHeight(node.right)) > 1) {
return false;
}
if (node.right!=null) stack.push(node.right); // 右(空节点不入栈)
if (node.left!=null) stack.push(node.left); // 左(空节点不入栈)
}
else{
stack.pop();
stack.pop();
}
}
return true;
}
整体代码如下:
/**
* 层序遍历,求结点的高度
*/
public int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
int depth = 0;
while (!deque.isEmpty()) {
int size = deque.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode poll = deque.poll();
if (poll.left != null) {
deque.offer(poll.left);
}
if (poll.right != null) {
deque.offer(poll.right);
}
}
}
return depth;
}
/**
* @Description 迭代法(后序遍历,遍历每一个节点,获得每一个节点的深度)
* @Param root
* @Return {@link boolean}
* @Author 君君
* @Date 2024/7/5 14:57
*/
public boolean isBalanced(TreeNode root) {
if (root == null)
return true;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty())
{
TreeNode node = stack.peek(); // 中
if(node!=null)
{
stack.pop();
stack.push(node);
stack.push(null);
if (Math.abs(getHeight(node.left) - getHeight(node.right)) > 1) {
return false;
}
if (node.right!=null) stack.push(node.right); // 右(空节点不入栈)
if (node.left!=null) stack.push(node.left); // 左(空节点不入栈)
}
else{
stack.pop();
stack.pop();
}
}
return true;
}
时间复杂度O(N^2)
空间复杂度O(N)
当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。
虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。
例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!
因为对于回溯算法已经是非常复杂的递归了,如果再用迭代的话,就是自己给自己找麻烦,效率也并不一定高。
257.二叉树的所有路径
给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]
示例 2:
输入:root = [1]
输出:["1"]
提示:
- 树中节点的数目在范围
[1, 100]
内 -100 <= Node.val <= 100
Related Topics
- 树
- 深度优先搜索
- 字符串
- 回溯
- 二叉树
递归法
- 递归函数参数以及返回值
要传入根节点,记录每一条路径的path,和存放结果集的res,这里递归不需要返回值,代码如下:
private void traversal(TreeNode root,List<Integer> paths,List<String> res){
...
}
- 确定递归终止条件
在写递归的时候都习惯了这么写:
if (cur == NULL) {
终止处理逻辑
}
但是本题的终止条件这样写会很麻烦,因为本题要找到叶子节点,就开始结束的处理逻辑了(把路径放进result里)。
那么什么时候算是找到了叶子节点? 是当 cur不为空,其左右孩子都为空的时候,就找到叶子节点。
所以本题的终止条件是:
if(root.left==null&&root.right==null){
StringBuilder sb = new StringBuilder();// StringBuilder用来拼接字符串,速度更快
for (int i = 0; i < paths.size() - 1; i++) {
sb.append(paths.get(i)).append("->");
}
sb.append(paths.get(paths.size() - 1));
res.add(sb.toString());
return;
}
- 确定单层递归逻辑
因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中。
//已经确定当前节点不会为空
paths.add(root.val);
然后是递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了。
所以递归前要加上判断语句,下面要递归的节点是否为空,
此时还没完,递归完,要做回溯啊,因为path 不能一直加入节点,它还要删节点,然后才能加入新的节点。
//向左边遍历
if(root.left!=null)
{
//此时向左递归
traversal(root.left,paths,res);
//向左递归已经执行完毕,进行回溯
paths.remove(paths.size()-1);
}
//向右边遍历
if(root.right!=null)
{
//此时向右递归
traversal(root.right,paths,res);
//向右递归已经执行完毕,进行回溯
paths.remove(paths.size()-1);
}
回溯和递归是一一对应的,有一个递归,就要有一个回溯,这么写的话相当于把递归和回溯拆开了, 一个在花括号里,一个在花括号外。
所以回溯要和递归永远在一起,世界上最遥远的距离是你在花括号里,而我在花括号外!
整体代码如下
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();
if (root == null) {
return res;
}
List<Integer> paths = new ArrayList<>();// 作为结果中的路径
traversal(root,paths,res);
return res;
}
/**
* @Description 使用递归法+回溯进行路径的记录
* @Param root 当前节点
* @Param paths 当前路径,当回溯到上一个节点时,要将paths的最后一个数字去掉
* @Param res 结果值,只有在找到目标值时才进行操作
* @Return
* @Author 君君
* @Date 2024/7/6 3:18
*/
private void traversal(TreeNode root,List<Integer> paths,List<String> res){
//已经确定当前节点不会为空
paths.add(root.val);
//终止条件,已经找到了无叶子节点
if(root.left==null&&root.right==null){
StringBuilder sb = new StringBuilder();// StringBuilder用来拼接字符串,速度更快
for (int i = 0; i < paths.size() - 1; i++) {
sb.append(paths.get(i)).append("->");
}
sb.append(paths.get(paths.size() - 1));
res.add(sb.toString());
return;
}
//向左边遍历
if(root.left!=null)
{
//此时向左递归
traversal(root.left,paths,res);
//向左递归已经执行完毕,进行回溯
paths.remove(paths.size()-1);
}
//向右边遍历
if(root.right!=null)
{
//此时向右递归
traversal(root.right,paths,res);
//向右递归已经执行完毕,进行回溯
paths.remove(paths.size()-1);
}
}
时间复杂度O(N)
空间复杂度O(N)
迭代法
/**
* @Description 迭代法
* @Param root
* @Return {@link List< String>}
* @Author 君君
* @Date 2024/7/6 3:36
*/
public List<String> binaryTreePaths(TreeNode root){
List<String> res = new ArrayList<>();
if(root==null) {
return res;
}
Stack<String> paths = new Stack<>();
paths.push(root.val+"");
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()&&!paths.isEmpty()){
//获得当前路径
String path = paths.pop();
//得到当前节点
TreeNode temp =stack.pop();
//找到叶子节点,将当前路径加入到结果集中
if(temp.left==null&&temp.right==null) {
res.add(path);
}
//将右节点入栈(不为空)
if(temp.right!=null) {
//paths.
stack.push(temp.right);
paths.push(path + "->" + temp.right.val);
}
//将左节点入栈(不为空)
if(temp.left!=null) {
stack.push(temp.left);
paths.push(path + "->" + temp.left.val);
}
}
return res;
}
时间复杂度O(N)
空间复杂度O(N)
404.左叶子之和
给定二叉树的根节点 root
,返回所有左叶子之和。
示例 1:
输入: root = [3,9,20,null,null,15,7]
输出: 24
解释: 在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24
示例 2:
输入: root = [1]
输出: 0
提示:
- 节点数在
[1, 1000]
范围内 -1000 <= Node.val <= 1000
Related Topics
- 树
- 深度优先搜索
- 广度优先搜索
- 二叉树
思路
首先要注意是判断左叶子,不是二叉树左侧节点,所以不要上来想着层序遍历。
因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点
大家思考一下如下图中二叉树,左叶子之和究竟是多少?
其实是0,因为这棵树根本没有左叶子!
但看这个图的左叶子之和是多少?
但看这个图的左叶子之和是多少?
相信通过这两个图,大家对最左叶子的定义有明确理解了。
那么判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。
如果该节点的左节点不为空,该节点的左节点的左节点为空,该节点的左节点的右节点为空,则找到了一个左叶子
if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) {
左叶子节点处理逻辑
}
递归法
递归的遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和。
递归三部曲:
- 确定递归函数的参数和返回值
判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int
使用题目中给出的函数就可以了。
private int sum(TreeNode node){
}
- 确定终止条件
如果遍历到空节点,那么左叶子值一定是0
if(node==null)
{
return 0;
}
注意,只有当前遍历的节点是父节点,才能判断其子节点是不是左叶子。 所以如果当前遍历的节点是叶子节点,那其左叶子也必定是0,那么终止条件为:
if(node==null)
{
return 0;
}
//其实这个也可以不写,如果不写不影响结果,但就会让递归多进行了一层。
if (node.left == null && node.right== null)
return 0;
- 确定单层递归的逻辑
当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。
//求左子树
int leftValue = sumOfLeftLeaves(node.left);
//此时左子树就是一个左叶子
if(node.left!=null&&node.left.left==null&&node.left.right==null){
leftValue = node.left.val;
}
//求右子树
int rightValue = sumOfLeftLeaves(node.right);
int sum = leftValue+rightValue;
return sum;
整体代码
/**
* @Description 求当前节点的左
* @Param node
* @Return {@link int}
* @Author 君君
* @Date 2024/7/6 4:23
*/
private int sum(TreeNode node)
{
if(node==null)
{
return 0;
}
//其实这个也可以不写,如果不写不影响结果,但就会让递归多进行了一层。
if (node.left == null && node.right== null)
return 0;
//求左子树
int leftValue = sumOfLeftLeaves(node.left);
//此时左子树就是一个左叶子
if(node.left!=null&&node.left.left==null&&node.left.right==null){
leftValue = node.left.val;
}
//求右子树
int rightValue = sumOfLeftLeaves(node.right);
int sum = leftValue+rightValue;
return sum;
}
时间复杂度O(N)
空间复杂度O(N)
迭代法
本题迭代法使用前中后序都是可以的,只要把左叶子节点统计出来,就可以了
/**
* @Description 迭代法
* @Param root
* @Return {@link int}
* @Author 君君
* @Date 2024/7/6 4:34
*/
public int sumOfLeftLeaves(TreeNode root) {
int res = 0;
if (root == null) return 0;
Stack<TreeNode> stack = new Stack<> ();
stack.push(root);
while(!stack.isEmpty())
{
TreeNode temp=stack.pop();
//找到叶子节点,对res进行处理
if (temp.left != null && temp.left.left == null && temp.left.right == null) {
res += temp.left.val;
}
if (temp.right != null) stack.add(temp.right);
if (temp.left != null) stack.add(temp.left);
}
return res;
}
时间复杂度O(N)
空间复杂度O(N)
222.完全二叉树的节点个数
给你一棵 完全二叉树 的根节点 root
,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h
层,则该层包含 1~ 2h
个节点。
示例 1:
输入:root = [1,2,3,4,5,6]
输出:6
示例 2:
输入:root = []
输出:0
示例 3:
输入:root = [1]
输出:1
提示:
- 树中节点的数目范围是
[0, 5 * 104]
0 <= Node.val <= 5 * 104
- 题目数据保证输入的树是 完全二叉树
**进阶:**遍历树来统计节点是一种时间复杂度为 O(n)
的简单解决方案。你可以设计一个更快的算法吗?
Related Topics
- 位运算
- 树
- 二分查找
- 二叉树
思路
本篇给出按照普通二叉树的求法以及利用完全二叉树性质的求法。
完全二叉树的节点个数.html#普通二叉树)普通二叉树
首先按照普通二叉树的逻辑来求。
这道题目的递归法和求二叉树的深度写法类似, 而迭代法,二叉树:层序遍历登场! (opens new window)遍历模板稍稍修改一下,记录遍历的节点数量就可以了。
递归遍历的顺序依然是后序(左右中)。
递归
- 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回以该节点为根节点二叉树的节点数量,所以返回值为int类型。
private int getNodesNum(TreeNode node){
}
- 确定终止条件:如果为空节点的话,就返回0,表示节点数为0。
if (node == null) {
return 0;
}
- 确定单层递归的逻辑:先求它的左子树的节点数量,再求右子树的节点数量,最后取总和再加一 (加1是因为算上当前中间节点)就是目前节点为根节点的节点数量。
int leftNum = getNodesNum(node.left);
int rightNum = getNodesNum(node.right);
//当前节点的所有节点数
int sum = leftNum + rightNum + 1;
return sum;
完整代码
public int countNodes(TreeNode root) {
return getNodesNum(root);
}
/**
* @Description 传入当前节点, 返回当前节点下边的节点数
* @Param node
* @Return {@link int}
* @Author 君君
* @Date 2024/7/6 4:41
*/
private int getNodesNum(TreeNode node) {
if (node == null) {
return 0;
}
int leftNum = getNodesNum(node.left);
int rightNum = getNodesNum(node.right);
//当前节点的所有节点数
int sum = leftNum + rightNum + 1;
return sum;
}
时间复杂度:O(n)
空间复杂度:O(log n),算上了递归系统栈占用的空间
迭代法
那么只要模板少做改动,加一个变量result,统计节点数量就可以了
/**
* @Description 迭代法(层序遍历)
* @Param root
* @Return {@link int}
* @Author 君君
* @Date 2024/7/6 4:53
*/
public int countNodes(TreeNode root) {
int res = 0;
if(root==null)
{
return res;
}
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
while(!deque.isEmpty())
{
int size = deque.size();
for (int i = 0; i < size; i++) {
TreeNode temp = deque.poll();
res++;
if(temp.right!=null)
{
deque.offer(temp.right);
}
if(temp.left!=null)
{
deque.offer(temp.left);
}
}
}
return res;
}
时间复杂度:O(n)
空间复杂度:O(n)
完全二叉树
以上方法都是按照普通二叉树来做的,对于完全二叉树特性不了解的同学可以看这篇 关于二叉树,你该了解这些! (opens new window),这篇详细介绍了各种二叉树的特性。
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
大家要自己看完全二叉树的定义,很多同学对完全二叉树其实不是真正的懂了。
我来举一个典型的例子如题:
完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。
对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。
对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。
完全二叉树(一)如图:
完全二叉树(二)如图:
可以看出如果整个树不是满二叉树,就递归其左右孩子,直到遇到满二叉树为止,用公式计算这个子树(满二叉树)的节点数量。
这里关键在于如何去判断一个左子树或者右子树是不是满二叉树呢?
在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树。如图:
在完全二叉树中,如果递归向左遍历的深度不等于递归向右遍历的深度,则说明不是满二叉树,如图:
那有录友说了,这种情况,递归向左遍历的深度等于递归向右遍历的深度,但也不是满二叉树,如题:
如果这么想,大家就是对 完全二叉树理解有误区了,以上这棵二叉树,它根本就不是一个完全二叉树!
判断其子树是不是满二叉树,如果是则利用公式计算这个子树(满二叉树)的节点数量,如果不是则继续递归,那么 在递归三部曲中,第二部:终止条件的写法应该是这样的:
if (root == null) return 0;
TreeNode left = root.left;
TreeNode right = root.right;
int leftDepth = 0;
int rightDepth = 0;
//求左子树深度
while (left != null) {
left = left.left;
leftDepth++;
}
//求右子树深度
while (right != null) {
right = right.right;
rightDepth++;
}
//此时为满二叉树
if (leftDepth == rightDepth) {
// 注意(2<<1) 相当于2^2,所以leftDepth初始为0
return (2 << leftDepth) - 1;
}
递归三部曲,第三部,单层递归的逻辑:(可以看出使用后序遍历)
int leftTreeNum = countNodes(root.left); // 左
int rightTreeNum = countNodes(root.right); // 右
int result = leftTreeNum + rightTreeNum + 1; // 中
return result;
该部分精简之后代码为:
return countNodes(root.left) + countNodes(root.right) + 1;
/**
* @Description 递归法(完全二叉树)
* @Param root
* @Return {@link int}
* @Author 君君
* @Date 2024/7/6 5:02
*/
public int countNodes(TreeNode root) {
if (root == null) return 0;
TreeNode left = root.left;
TreeNode right = root.right;
int leftDepth = 0;
int rightDepth = 0;
//求左子树深度
while (left != null) {
left = left.left;
leftDepth++;
}
//求右子树深度
while (right != null) {
right = right.right;
rightDepth++;
}
//此时为满二叉树
if (leftDepth == rightDepth) {
// 注意(2<<1) 相当于2^2,所以leftDepth初始为0
return (2 << leftDepth) - 1;
}
//如果不是满二叉树,那么,返回的就是左右子树节点的和
return countNodes(root.left) + countNodes(root.right) + 1;
}
时间复杂度:O(log n × log n)
空间复杂度:O(log n)