Leecode 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

递归法求高度

首先本题是需要求高度而非深度,二者的区别主要在于:

  • 深度是自上往下计数
  • 高度是自下往上计数

如果采用深度的方式进行递归,由于递归到某个节点的时候是自上而下进行计数,还未判断过当前节点的之下的节点,自然无法判断其是否为平衡二叉树的根节点。会导致如果漏判子树中出现非平衡二叉树、而左右子树高度都相等的情况,即下面测试用例:

输入:[1,2,2,3,null,null,3,4,null,null,4]



如果使用深度递归无法判断

另外,如果是采用深度递归,在递归函数的传入参数中,还有一个参数用于表示当前深度,即深度递归函数一般为:int getepth(TreeNode* curNode, int depth)。但如果是高度递归,不需要传入参数,一般形式为:int getHeight(TreeNode* curNode)。在到达空节点的时候返回0,同时在后续递归调用中进行+1操作,这样即可实现自下而上的高度递归。

下面给出本题代码:

class Solution {
public:
int getHeight(TreeNode* curNode){ // 记高度-1表示不是平衡二叉树,此时再计算高度已无意义,故记作-1
if(!curNode) return 0; // 如果当前节点为空,此时返回高度为0
int leftHeight = getHeight(curNode->left); // 获取左子节点的高度
int rightHeight = getHeight(curNode->right); // 获取右子节点的高度
if(leftHeight == -1 || rightHeight == -1) return -1; // 如果两个子节点中有一个的高度为-1,说明已经不是平衡二叉树,返回高度-1
if( abs(leftHeight - rightHeight) > 1) return -1; // 如果两个子节点高度差大于1,说明不是平衡二叉树,则返回高度-1
return max(leftHeight, rightHeight) + 1; // 当前节点的高度 = 左右两个节点中更大的高度 + 1
}
bool isBalanced(TreeNode* root) {
return getHeight(root) != -1; // 只要最后根节点的高度不为 -1, 则为真
}
};

上面代码中,需要注意的一点在于,我们将非平衡二叉树的根节点高度记作-1,原因是此时再记录其高度已经没有意义,使用高度-1并在其中增加一行判断(如果子节点的高度为-1则当前节点高度也为-1),这样可以使得一旦出现非平衡二叉树的节点,就可以一直回传到最终根节点中。

Leecode 257. 二叉树的所有路径

题目描述

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

  • 示例 1:



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

输出:["1->2->5","1->3"]

  • 示例 2:

输入:root = [1]

输出:["1"]

回溯递归

题外话:这题真特么难。。真的是简单题吗,给我做得怀疑人生了。。前几天刷下来感觉自己又行了,今天这一道题又给打回现实了

本题需要输出每一条通往叶节点的路径,考虑采用回溯的方法,遍历走到叶节点之后记录当前路径,然后返回进行回溯。思想上还算能够想通,但是如果没有接触过回溯的话想要实现代码还是有一定难度的(说的就是我)。下面结合代码来进行说明:

class Solution {
public:
void pathHelper(TreeNode* curNode, vector<int>& path ,vector<string>& result){ // 传入参数为:当前节点、从根节点走到当前节点的路径,用于存放结果
path.push_back(curNode->val); // 第一步先将当前节点记录到路径中
if(!curNode->left && !curNode->right){ // 如果当前节点是叶节点,将路径转换为string进行存放
string pathString = to_string(path[0]); // 从这一步开始将vector<int> 中的路径转换为string类型
for(int i = 1; i < path.size(); i++){
pathString += ("->" + to_string(path[i]));
}
result.push_back(pathString); // 将上面路径转换成的字符串存入result中
}
if(curNode->left) { // 如果当前节点不是叶节点,则需要往能走的方法继续走
pathHelper(curNode->left, path, result); // 往左子节点走下去,递归地去左子树中寻找并记录路径
path.pop_back(); // 由于使用上面递归之后,会在递归函数中传入curNode->left->val,现在递归结束回退后需要将其删除
}
if(curNode->right) { // 如果有右子节点,需要去记录右子树中的路径
pathHelper(curNode->right, path, result); // 递归右子树寻找路径
path.pop_back(); // 同样也要将右子节点给弹出
}
return;
} vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result; // 建立用于存放结果的vector
if(!root) return result; // 如果根节点为空,则直接返回空的vector
vector<int> path; //
pathHelper(root, path, result);
return result;
}
};

上面代码中的path.pop_back();体现了回溯的过程,但同样可以使用值传递来代替引用传递,这样可以免去这一步pop回溯。同时可以直接使用字符串来存放路径,遍历到叶节点后直接push记录即可,而不必再使用一个存放int的vector容器。故可以将代码简化如下:

class Solution {
public:
void pathHelper(TreeNode* curNode, string path, vector<string>& result){
if(curNode) path += (to_string(curNode->val) + "->");
if(!curNode->left && !curNode->right){
path.resize(path.size()-2); // 除去最后的箭头符号
result.push_back(path);
}
if(curNode->left) pathHelper(curNode->left, path, result); // path使用值传递,因此不需要回退
if(curNode->right) pathHelper(curNode->right, path, result);
} vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result;
if(!root) return result;
string path = ""; // 初始化path
pathHelper(root, path, result);
return result;
}
};

经过本题的历练,希望之后能够掌握回溯的思想。

Leecode 404. 左叶子之和

题目描述

给定二叉树的根节点 root ,返回所有左叶子之和。

  • 示例 1:



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

输出: 24

解释: 在这个二叉树中,有两个左叶子,分别是 915,所以返回 24

  • 示例 2:

输入: root = [1]

输出: 0

递归法

题目要求只对左叶子求和,那么可以考虑使用一个布尔变量,来记录上一步遍历到当前节点的时候是往左还是往右。如果是往左来的则为true,否则为false。那么我们可以得到如下代码:

class Solution {
public:
void sumHelper(TreeNode* curNode, int &curSum, bool isLeft){ // 使用一个布尔变量来记录当前节点是否为其父节点的左子节点
if(!curNode) return; // 如果当前子节点为空,则直接返回
if(!curNode->left && !curNode->right){ // 如果当前节点是叶节点
if(!isLeft) return; // 还需要判断是否是左子节点,如果不是则直接返回
curSum += curNode->val; // 如果是题目要求得左叶节点,则将其中的值加到curSum中
return;
}
sumHelper(curNode->left, curSum, true); // 递归地对左子节点求和,需要注意此时传入的布尔变量,左为true,右为false
sumHelper(curNode->right, curSum, false); // 递归地对右子节点求和
return;
} int sumOfLeftLeaves(TreeNode* root) {
int sum = 0;
sumHelper(root, sum, false); // 根节点不是左子节点,故一开始传入的布尔变量为false
return sum;
}
};

Leecode 222. 完全二叉树的节点个数

题目描述

给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。

完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(从第 0 层开始),则该层包含 1~ 2h 个节点。

  • 示例 1:



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

输出:6

  • 示例 2:

输入:root = []

输出:0

  • 示例 3:

输入:root = [1]

输出:1

解法1,当做普通二叉树递归求解

如果不考虑这是一颗完全二叉树,那么根据:树中节点数 = 左子树节点数 + 右子树节点数 + 1;使用该公式可以直接得到下面递归代码:

class Solution {
public:
int countNodes(TreeNode* root) {
if(root == nullptr) return 0;
return countNodes(root->left) + countNodes(root->right) + 1;
}
};

我们可以分析上面节点的时间复杂度,相当于将每个节点都遍历了一次,故时间复杂度为\(O(n)\)。接下来我们考虑利用题目中给出的当前这棵树是完全二叉树的情况,如何降低时间复杂度。

解法2 完全二叉树的节点个数

对于完全二叉树而言,如果恰好是一颗满二叉树,那么总节点个数为:深度\(d\)的平方-1,即\(2^d - 1\)。但完全二叉树并不都恰好是满二叉树,不过幸好对于每一颗完全二叉树,其子树最后都有一部分是完全二叉树。故我们可以利用这个规律,得到下面代码:

class Solution {
public:
int countNodes(TreeNode* root) {
if(!root) return 0;
TreeNode* leftNode = root->left;
TreeNode* rightNode = root->right;
int leftDep = 0;
int rightDep = 0;
while(leftNode){
leftNode = leftNode->left;
leftDep++;
}
while(rightNode){
rightNode = rightNode->right;
rightDep++;
}
if(leftDep == rightDep) return (2 << leftDep) - 1; // 左移位运算符相当于是求幂次
return countNodes(root->left) + countNodes(root->right) + 1;
}
};

上面代码利用了完全二叉树的特性,尽管看起来代码变得复杂了很多,但实际上时间复杂度比原本更低,变成了\(O(\log n* \log n)\)。在节点数增多的时候,会比原本的时间复杂度\(O(n)\)要低很多。

另外,对于上面代码中用到的左移位运算符<<,相当于是将二进制数整体左移,得到的结果相当于是乘了一个\(2^x\)的2次幂。

今日总结

今天学习了回溯的算法,需要注意回溯时如果使用引用传递则需要手动进行回溯,而如果使用值传递则不必。同时又学习了完全二叉树的特性,并充分利用其特性降低时间复杂度。

同时记录一下当前力扣刷题记录,已经到了第54题:

代码随想录第十五天 | Leecode 110. 平衡二叉树、257. 二叉树的所有路径、404. 左叶子之和、222. 完全二叉树的节点个数的更多相关文章

  1. 代码随想录算法训练营day17 | leetcode ● 110.平衡二叉树 ● 257. 二叉树的所有路径 ● 404.左叶子之和

    LeetCode 110.平衡二叉树 分析1.0 求左子树高度和右子树高度,若高度差>1,则返回false,所以我递归了两遍 class Solution { public boolean is ...

  2. 代码随想录算法训练营day16 | leetcode ● 104.二叉树的最大深度 559.n叉树的最大深度 ● 111.二叉树的最小深度 ● 222.完全二叉树的节点个数

    基础知识 二叉树的多种遍历方式,每种遍历方式各有其特点 LeetCode 104.二叉树的最大深度 分析1.0 往下遍历深度++,往上回溯深度-- class Solution { int deep ...

  3. x264代码剖析(十五):核心算法之宏块编码中的变换编码

    x264代码剖析(十五):核心算法之宏块编码中的变换编码 为了进一步节省图像的传输码率.须要对图像进行压缩,通常採用变换编码及量化来消除图像中的相关性以降低图像编码的动态范围.本文主要介绍变换编码的相 ...

  4. 代码随想录算法训练营day18 | leetcode 513.找树左下角的值 ● 112. 路径总和 113.路径总和ii ● 106.从中序与后序遍历序列构造二叉树

    LeetCode 513.找树左下角的值 分析1.0 二叉树的 最底层 最左边 节点的值,层序遍历获取最后一层首个节点值,记录每一层的首个节点,当没有下一层时,返回这个节点 class Solutio ...

  5. 代码随想录算法训练营day14 | leetcode 层序遍历 226.翻转二叉树 101.对称二叉树 2

    层序遍历 /** * 二叉树的层序遍历 */ class QueueTraverse { /** * 存放一层一层的数据 */ public List<List<Integer>&g ...

  6. 代码随想录算法训练营day07 | leetcode 454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和

    LeetCode 454.四数相加II 分析1.0 这个最直接暴力法,不过过于暴力了,害怕.jpg 失误 读题理解失误:题目要求的是四元组的个数,读完题到我这里成了输出四元组,悲哉 分析2.0 记录有 ...

  7. 【leetcode 简单】 第九十四题 左叶子之和

    计算给定二叉树的所有左叶子之和. 示例: 3 / \ 9 20 / \ 15 7 在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24 # Definition for a binary ...

  8. 十五个常用的jquery代码段【转】

    好的文章顶一个 回到顶部按钮 通过使用 jQuery 中的 animate 和 scrollTop 方法,你无需插件便可创建一个简单地回到顶部动画: 1 // Back to top 2 $('a.t ...

  9. 十五个常用的jquery代码段

    十五个常用的jquery代码段 回到顶部按钮 通过使用 jQuery 中的 animate 和 scrollTop 方法,你无需插件便可创建一个简单地回到顶部动画: 1 // Back to top ...

  10. python3.4学习笔记(二十五) Python 调用mysql redis实例代码

    python3.4学习笔记(二十五) Python 调用mysql redis实例代码 #coding: utf-8 __author__ = 'zdz8207' #python2.7 import ...

随机推荐

  1. Calcite 获取jdbc连接流程

    一.类调用 简介:calcite可以连接各数据源,做查询.可以收口查询权限,查询多引擎需求 二. 获取Connection发送的请求 请求介绍文档:https://calcite.apache.org ...

  2. VS Code C++ 切换配置集

    前言 最近转型做Golang开发了,但有需求做视频传输,想用ffmpeg做测试,只是加点日志,方便测试,就想直接用VS Code做下开发好了,安装C/C++的插件,用MSYS2编译. 问题 C/C++ ...

  3. 基于 Flink+Iceberg 构建企业级实时数据湖

    Apache Flink 是大数据领域非常流行的流批统一的计算引擎,数据湖是顺应云时代发展潮流的新型技术架构.那么当 Apache Flink 遇见数据湖时,会碰撞出什么样的火花呢?本次分享主要包括以 ...

  4. C#(面向对象的托管语言)类库(区别于应用程序)的异常处理思路

    1.不要做出任何应用程序才需要考虑抉择策略,不能想当然的决定一些错误情形.具体的一个体现形式是什么异常都捕获.这不是类库的职责,因为无法掌握所有的调用者的使用情形,这些不确定性是委托.虚方法.接口等特 ...

  5. 记一次.NET内存居高不下排查解决与启示

    前情 我们有个海外的项目,一共70个服务,前前后后花了超过一年时间完成了云服务迁移和架构调整.主要是架构调整了,原来的docker swarm托管服务,新架构改为Kubernetes托管.几台云服务器 ...

  6. JS用 URL 构造函数来解析 URL

    const url = new URL('http://username:password@hostname:9090/path?arg=value#anchor'); console.log(url ...

  7. k8s NotReady cni config uninitialized

    前言 k8s node 节点 join master 后,状态报错:NOT READY 查看 kubelet 日志 journalctl -xeu kubelet 报错如下:Container run ...

  8. go mgo包 简单封装 mongodb 数据库驱动

    mgo是go编写的mongodb的数据库驱动,集成到项目中进行mongodb的操作很流畅,以下是对其的一些简单封装,具体使用可随意改动封装. 安装 go get gopkg.in/mgo.v2 使用 ...

  9. Excel批量获取当前时间差

    使用now函数获取当前时间 Office 2007 Excel使用now函数 首先打开Excel,选中一个要插入日期的单元格 选中后,点击菜单栏上的插入,选择函数 点击后,会出现一个公式生成器,在上面 ...

  10. 本地部署overleaf服务帮助latex论文编写

    是的,overleaf是一个很好的服务,提供了立刻上手就可以编写的latex文章的服务.但是,overleaf会面对latex超时,所以需要付钱的情况,这常出现在编写期刊的论文的情况. 因为时效性,所 ...