文章目录
110.平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
输入:root = [3,9,20,null,null,15,7]
输出:true
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
高度和深度的概念
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。深度是到根节点的距离。
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。高度是到叶子节点的距离。
但leetcode中强调的深度和高度是按照节点来计算的,如图:
关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,暂时以leetcode为准。根节点的深度初值会根据不同题目变化,比如之前做过的222和111里面根节点深度初值就是不同的。
思路
判断是不是平衡二叉树,需要计算左右子树的高度,并判断高度差是不是<=1.
求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)。这道题目和104.二叉树最大深度比较像,104是通过求根节点最大高度来获得最大深度,本题是直接计算高度。
当我们发现任何一个节点的左右孩子不符合高度差小于等于1,就说明不是平衡二叉树。因此,当我们发现任意一个节点的左右子树高度差不满足条件,直接返回-1,说明这棵树已经不符合平衡条件。
height判定部分
- 先得到左右子树的高度差,才能进行比较,因此本题必须使用后序遍历,不能前序。
- 分别求出左右子树的高度,如果高度差>1直接返回-1,如果高度差<=1,需要返回树的高度,再继续进行计算,而不是直接返回1!
- 注意递归return传递的问题
int getHeight(TreeNode* root){
//终止条件
if(root==NULL){
return 0;
}
//单层递归:左右中
int leftHeight = getHeight(root->left);
int rightHeight = getHeight(root->right);
int Num = abs(leftHeight-rightHeight);
//这里一定要判断左右子树本身,是不是已经高度被赋值为-1了!
if(Num>1||leftHeight==-1||rightHeight==-1){
return -1;
}
else{
//如果满足条件,应该计算当前节点高度,而不是直接返回1!
//return 1;
Num = 1+max(leftHeight,rightHeight);
return Num;
}
}
为什么num<=1平衡还要返回树的高度
为了判断一个二叉树是否平衡,我们需要检查每一个节点的左右子树的高度差是否不超过1。这就意味着我们需要在每一个节点处都进行判断,并且如果在某个节点处发现二叉树已经不平衡,我们就应该立即停止遍历并返回结果,而不是只在根节点处进行判断。因此,每一次的递归都需要返回树的最新高度,再继续递归判断高度差是不是<=1,如果不满足则直接返回-1。
完整版
- 注意一定要判断左右子树的内部节点的高度差,也就是检查左右子树本身是不是-1
- 如果左子树或者右子树的高度是-1
class Solution {
public:
int getHeight(TreeNode* root){
//终止条件
if(root==NULL){
return 0;
}
//单层递归:左右中
int leftHeight = getHeight(root->left);
//必须接收return回来的-1,或者在if(num>1)的判断中接收
if(leftHeight==-1){
return -1;//接收-1并传递
}
int rightHeight = getHeight(root->right);
if(rightHeight==-1){
return -1;
}
int Num = abs(leftHeight-rightHeight);
if(Num>1){
return -1;
}
else{
//如果满足条件,应该计算当前节点高度,而不是直接返回1!
//return 1;
int result = 1+max(leftHeight,rightHeight);
return result;
}
}
bool isBalanced(TreeNode* root) {
int result = getHeight(root);
if(result==-1)return false;
else
return true;
}
};
不能只判断当前节点左右子树的高度差,还要看当前节点的左右子树本身,是不是高度已经是-1了。
递归return传递的问题
本题的写法中,左右子树内部的节点,如果不平衡会return-1,但是,只会在内部节点递归的那一层return -1,如果想要传递回递归的上一层,必须要在递归的上一层接收这个return -1。也就是说,if条件需要写成:
if(Num>1||leftHeight==-1||rightHeight==-1){
//if不能只写if(num>1)
return -1;
}
这里的if不能只写if(num>1)
,第一遍写的时候就犯了这个错误,导致用例不完全通过。
在递归的过程中,我们通常需要在当前层级(或者说,当前的递归调用)检查递归子调用的返回值,以便根据这些返回值进行适当的操作。在这个问题中,如果我们在检查子树的高度时发现子树是不平衡的(即,getHeight返回了-1),那么我们应该立即停止其他所有的检查并返回-1,表示整个树是不平衡的。
这种将递归子调用的返回值“传递回”递归的上一层是递归逻辑的一个重要部分,它可以帮助我们编写出能够有效处理各种复杂情况的代码。在这个问题中,这种方法使得我们可以在检测到树的任何一部分不平衡时立即停止检查并返回-1,从而避免了对其他不必要部分的检查,提高了代码的效率。
257.二叉树的所有路径
给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
输入:root = [1,2,3,null,5]
输出:[“1->2->5”,“1->3”]
遍历顺序选择
因为这道题需要让父节点指向孩子节点,因此需要使用前序遍历方式,中左右,中就是父亲节点。
为什么会有回溯
这道题的解法中必须要有回溯的部分,例如示例中的二叉树,收集路径1->2->5,收集完毕之后需要把5弹出,再把2弹出,再回到根节点1节点继续收集根节点到叶子节点的路径,也就是重新加入3。回退到根节点的过程其实就是回溯。
伪代码
- 本题的处理逻辑,”中“需要写在最前面,因为终止条件是到叶子节点就return了,如果在终止条件之后写”中“的逻辑,也就是
path.push_back()
,那么叶子节点就会被落下,因为遍历到叶子节点的时候就直接返回了。 - 如果path定义成string,引用的时候不用引用直接值传递,就可以省略掉
path.pop_back()
这一步,就是104.二叉树最大深度 的前序遍历写法
void travelsal(TreeNode* root,vector<int>& path,vector<string>& result){ //引用传入
//传入一个vector<int>记录路径,vector<string>记录结果
//path也可以定义成string,也可以不用引用,但是会隐藏回溯的过程
//处理逻辑:前序遍历中左右,中就是处理过程
//处理过程"中":添加路径,该过程必须放在终止条件之前,才能收集到完整路径
path.push_back(root->val);
//终止条件:收集到叶子节点就结束
//收集到叶子节点就是获取结果的时候
if(root->left==NULL&&root->right==NULL){
result.push_back(path); //这里需要一段逻辑把path转成string,并且加上"->"
return;
}
//左:向左递归
if(root->left){
travelsal(root->left,path,result);
//向左递归之后,需要再处理路径,也就是pop
path.pop_back();//回溯
}
//右
if(root->right){
travelsal(root->right,path,result);
path.pop_back();//回溯
}
//如果path定义成string,引用的时候不用引用直接值传递,就可以省略掉path.pop_back()这一步,这就是104.的前序遍历写法
}
完整版
使用vector数组来存放路径,vector来存放结果。
终止条件处理:
if(root->left==NULL&&root->right==NULL){
//遇到叶子节点
string sPath;
//把vector<int> path里面的路径转化成string,这个string需要满足输出格式
for(int i=0;i<path.size();i++){
spath+=to_string(path[i]);
spath+="->";
}
//把遇到的叶子节点加入path中
spath+=to_string(root->val);
result.push_back(spath);
return result;
}
to_string的用法
std::to_string
是C++标准库的一部分,用于将其参数转换为字符串。
例如,to_string(123)
将返回字符串"123"
。
这是一个常见的用法,to_string
函数在许多情况下都非常有用,特别是当你需要将非字符串数据(如整数或浮点数)包含到字符串中时。
加入单层递归逻辑:
//终止条件
if(root->left==NULL&&root->right==NULL){
//遇到叶子节点
string sPath;
//把vector<int> path里面的路径转化成string,这个string需要满足输出格式
for(int i=0;i<path.size();i++){
spath+=to_string(path[i]);
spath+="->";
}
//把遇到的叶子节点加入path中
spath+=to_string(root->val);
result.push_back(spath);
return result;
}
//中
path.push_back(root->val);
//左
if(root->left){
travelsal(root->left,path,result);
//因为这里是引用传递,需要做path的回溯!
path.pop_back();//删除path里在新的一层递归中加入的新节点
}
//右
if(root->right){
travelsal(root->right,path,result);
path.pop_back();
}
vector元素增删
clear()
清除所有元素insert()
支持在某个迭代器位置插入元素、可以插入多个。复杂度与pos
距离末尾长度成线性而非常数的erase()
删除某个迭代器或者区间的元素,返回最后被删除的迭代器。复杂度与insert
一致。push_back()
在末尾插入一个元素,均摊复杂度为 常数,最坏为线性复杂度。pop_back()
删除末尾元素,常数复杂度。swap()
与另一个容器进行交换,此操作是 常数复杂度 而非线性的。
完整的路径记录函数和调用函数
class Solution {
public:
void travelsal(TreeNode* root, vector<int>& path, vector<string>& result) {
//终止条件
if (root->left == NULL && root->right == NULL) {
//遇到叶子节点
string sPath = "";
//把vector<int> path里面的路径转化成string,这个string需要满足输出格式
for (int i = 0; i < path.size(); i++) {
sPath += to_string(path[i]);
sPath += "->";
}
//把遇到的叶子节点加入path中
sPath += to_string(root->val);
result.push_back(sPath);
return;
}
//中
path.push_back(root->val);
//左
if (root->left) {
travelsal(root->left, path, result);
//因为这里是引用传递,需要做path的回溯!
path.pop_back();//删除path里在新的一层递归中加入的新节点
}
//右
if (root->right) {
travelsal(root->right, path, result);
path.pop_back();
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector<int> path;
vector<string>result;
if (root == NULL) {
return result;
}
travelsal(root, path, result);
return result;
}
};
做法存在的问题
上面这种做法存在的问题是如果像示例中的1->3,会只输出3,因为叶子节点直接输出了没有加上根节点
因此,前序遍历的”中“必须放在最前面,以确保根节点能够放入路径里,修改如下:
修改结果
class Solution {
public:
void travelsal(TreeNode* root, vector<int>& path, vector<string>& result) {
//收集节点放在最前面,也就是"中"
path.push_back(root->val);
//终止条件
if (root->left == NULL && root->right == NULL) {
//遇到叶子节点
string sPath = "";
//把vector<int> path里面的路径转化成string,这个string需要满足输出格式
for (int i = 0; i < path.size()-1; i++) {
sPath += to_string(path[i]);
sPath += "->";
}
//叶子节点要单独放入结果集,因为没有"->"
sPath+=to_string(path[path.size()-1]);
result.push_back(sPath);
return;
}
//左
if (root->left) {
travelsal(root->left, path, result);
//因为这里是引用传递,需要做path的回溯!
path.pop_back();//删除path里在新的一层递归中加入的新节点
}
//右
if (root->right) {
travelsal(root->right, path, result);
path.pop_back();
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector<int> path;
vector<string>result;
if (root == NULL) {
return result;
}
travelsal(root, path, result);
return result;
}
};
完整版:值传递的写法
- 这种写法中,traversal函数的参数不是引用传递,而是值传递,这样的话值传递进行下一层递归,并不会影响到本层的path数值,path只需要把
+="->"
的两个字符回溯即可。 - 也有隐藏的回溯,通过值传递实现
class Solution {
private:
void traversal(TreeNode* cur, string path, vector<string>& result) {
path += to_string(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中
if (cur->left == NULL && cur->right == NULL) {
result.push_back(path);
return;
}
if (cur->left) {
path += "->";
traversal(cur->left, path, result); // 左
path.pop_back(); // 回溯 '>'
path.pop_back(); // 回溯 '-'
}
if (cur->right) {
path += "->";
traversal(cur->right, path, result); // 右
path.pop_back(); // 回溯'>'
path.pop_back(); // 回溯 '-'
}
}
public:
//此时的调用函数就不需要递归了
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result;
string path;
if (root == NULL) return result;
traversal(root, path, result);
return result;
}
};
最终版:值传递(省略回溯)的写法
- 如果要省略回溯,将path+"->"作为参数传入递归即可
class Solution {
private:
void traversal(TreeNode* cur, string path, vector<string>& result) {
path += to_string(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中
if (cur->left == NULL && cur->right == NULL) {
result.push_back(path);
return;
}
if (cur->left) {
traversal(cur->left, path+"->", result); // 左
}
if (cur->right) {
traversal(cur->right, path+"->", result); // 右
}
}
public:
//此时的调用函数就不需要递归了
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result;
string path;
if (root == NULL) return result;
traversal(root, path, result);
return result;
}
};
为什么每一层递归都要回溯->的位置
这是因为,在遍历每一个节点后,都添加了 “->” 到路径中。但是,当我们完成了左子树或右子树的遍历后,下一个要处理的节点并不是当前节点的子节点,而是它的兄弟节点或者祖先节点的兄弟节点。在这种情况下,我们需要从路径中移除当前节点和 “->”,以便正确地添加下一个要处理的节点。因此,我们需要回溯 “->” 两个字符的位置。
在给定的代码中,path.pop_back()
被调用两次,一次删除 “>” 字符,一次删除 “-” 字符。这就是我们需要回溯两个字符位置的原因。
简单来说,回溯是为了确保路径字符串正确地表示从根节点到当前处理的节点的路径。如果我们不回溯,就会得到错误的路径。
回溯举例
以下二叉树
1
/ \
2 3
/ \
4 5
我们希望得到所有从根节点到叶节点的路径,那么正确的结果应该是:[“1->2->4”, “1->2->5”, “1->3”]。
按照代码的运行逻辑来分析:
- 从根节点开始,
path = "1"
,然后遇到了一个非叶节点,所以添加"->"
到path
,path = "1->"
。 - 然后递归遍历左子节点2,
path = "1->2"
,它还是一个非叶节点,继续添加"->"
到path
,path = "1->2->"
。 - 再递归遍历左子节点4,
path = "1->2->4"
,它是一个叶节点,所以我们将path
添加到结果中并return。 - 递归一层层返回,回到节点2,我们已经完成了左子树的遍历,但是右子树还没有开始。因此,我们需要从
path
中移除->4
。因为是值传递,值传递的话,那一层的递归依旧保持之前的数值,也就是此时那一层的递归依旧是path = "1->2->"
,但是path+
的部分需要进行回溯,path
变回1->2
。 - 开始递归遍历右子节点5,
path = "1->2->5"
,它是一个叶节点,所以我们将path
添加到结果中。 - 现在,我们完成了节点2的所有子树的遍历,我们需要回溯到节点1,也就是说,我们需要从
path
中移除->2->5
,path
变回1->
,然后开始递归遍历右子节点3,path = "1->3"
,它是一个叶节点,所以我们将path
添加到结果中。
递归的进一步理解
下面的这一部分递归代码
if (cur->left) {
path += "->";
traversal(cur->left, path, result); // 左
path.pop_back(); // 回溯 '>'
path.pop_back(); // 回溯 '-'
}
- 如果cur->left一直存在,也就是说这个节点一直存在左子树,那么这一部分的if语句就会一直进行递归,直到某一层的递归满足终止条件,才会返回,再继续执行下一个if(cur->right)。
- 也就是说这个if的用法类似于while,会首先深度优先遍历左子树,然后再遍历右子树,确保找到从根节点到左右叶子节点的所有路径。
进一步解释,这个递归调用的效果类似于 while
循环,因为它会持续地深入到左子树,直到遇到一个没有左子节点的节点。这种效果是通过递归实现的,而不是通过 while
循环。每一次递归调用都会创建一个新的函数调用上下文,包括一个新的 path
变量,这个新的 path
是旧 path
的一个副本,并添加了新的节点值。
然而,和 while
循环不同的是,每一次递归调用返回后,都会回到调用它的那一层递归,并且会恢复那一层的 path
值。这种方法使用了值传递,值传递的话,那一层的递归依旧保持之前的数值。但是,由于path进行了+="->"的操作,需要进行回溯:将 path
恢复到遍历前的状态,也就是执行两遍path.pop_back()
。
这道题为什么需要把查找所有路径的traversal函数单独写
- 代码的可读性和可维护性:将递归遍历的函数独立出来,可以使主函数更简洁,代码结构更清晰。主函数的目的是返回结果,而辅助函数的目的是进行遍历,各自的职责清晰明确。
- 便于传递额外的参数:在递归过程中,我们通常需要传递一些额外的参数,例如在这个问题中,我们需要传递"cur"(当前节点),“path”(当前的路径)和"result"(最终结果)。如果直接在主函数中进行递归操作,这些参数的传递会变得很麻烦。这道题单独把递归列一个函数,主要是为了方便传入path参数,在每一层内都对path进行更新。