题目:平衡二叉树
- 给定一个二叉树,判断它是否是高度平衡的二叉树。一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
题解
-
这道题中的平衡二叉树的定义是:二叉树的每个节点的左右子树的高度差的绝对值不超过 1,则二叉树是平衡二叉树。根据定义,一棵二叉树是平衡二叉树,当且仅当其所有子树也都是平衡二叉树,因此可以使用递归的方式判断二叉树是不是平衡二叉树,递归的顺序可以是自顶向下或者自底向上。
-
注意:二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0。
-
因为求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)。
-
对于本题应该明白了既然要求比较高度,必然是要后序遍历。递归三步曲分析:
-
明确递归函数的参数和返回值。参数:当前传入节点。 返回值:以当前传入节点为根节点的树的高度。那么如何标记左右子树是否差值大于1呢?如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。
-
明确终止条件。递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0
-
明确单层递归的逻辑。如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。
-
class Solution { public: int getHeight(TreeNode *node){ if(node==nullptr){ return 0; } int leftH=getHeight(node->left); if(leftH==-1) return -1; int rightH=getHeight(node->right); if(rightH==-1) return -1; return abs(leftH-rightH)>1?-1:1+max(leftH,rightH); } bool isBalanced(TreeNode* root) { return getHeight(root)==-1?false:true; } };
-
题目:二叉树的所有路径
- 给你一个二叉树的根节点
root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。叶子节点 是指没有子节点的节点。
题解
-
最直观的方法是使用深度优先搜索。在深度优先搜索遍历二叉树时,我们需要考虑当前的节点以及它的孩子节点。如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到答案即可。
-
要求从根节点到叶子的路径,所以需要前序遍历,这样才方便让父节点指向孩子节点,找到对应的路径。在这道题目中将第一次涉及到回溯,因为我们要把路径记录下来,需要回溯来回退一个路径再进入另一个路径。
-
先使用递归的方式,来做前序遍历。要知道递归和回溯就是一家的,本题也需要回溯。
-
递归函数参数以及返回值,要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值
-
确定递归终止条件。因为本题要找到叶子节点,就开始结束的处理逻辑了(把路径放进result里)。那么什么时候算是找到了叶子节点? 是当 cur不为空,其左右孩子都为空的时候,就找到叶子节点。这里使用vector 结构path来记录路径,所以要把vector 结构的path转为string格式,再把这个string 放进 result里。那么为什么使用了vector 结构来记录路径呢? 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。
-
确定单层递归逻辑。因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中。然后是递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了。
-
-
class Solution { public: void traversal(TreeNode* cur,string path,vector<string>& res){ path+=to_string(cur->val); if(cur->left==nullptr&&cur->right==nullptr){ res.push_back(path); return; } if(cur->left){ traversal(cur->left,path+"->",res); } if(cur->right){ traversal(cur->right,path+"->",res); } } vector<string> binaryTreePaths(TreeNode* root) { vector<string> res; string path; if(root==nullptr){ return res; } traversal(root,path,res); return res; } };
-
注意在函数定义的时候
void traversal(TreeNode* cur, string path, vector<string>& result)
,定义的是string path
,每次都是复制赋值,不用使用引用,否则就无法做到回溯的效果。(这里涉及到C++语法知识) -
那么在如上代码中,貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在
traversal(cur->left, path + "->", result);
中的path + "->"
。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。如果想把回溯加上,就要 在上面代码的基础上,加上回溯,就可以AC了。-
void traversal2(TreeNode *cur,string path,vector<string>& res){ path+=to_string(cur->val); if(cur->left==nullptr&&cur->right==nullptr){ res.push_back(path); return; } if(cur->left){ path+="->"; traversal2(cur->left,path,res); path.pop_back();// 回溯 '>' path.pop_back();// 回溯 '-' } if(cur->right){ path+="->"; traversal2(cur->right,path,res); path.pop_back(); path.pop_back(); } }
-
如果把
path + "->"
作为函数参数就是可以的,因为并没有改变path的数值,执行完递归函数之后,path依然是之前的数值(相当于回溯了).第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里,第一种递归写法虽然代码多一些,但是把每一个逻辑处理都完整的展现出来了。
-
-
在第二版本的代码中,其实仅仅是回溯了
->
部分(调用两次pop_back,一个pop>
一次pop-
),大家应该疑惑那么path += to_string(cur->val);
这一步为什么没有回溯呢? 一条路径能持续加节点 不做回溯吗?其实关键还在于 参数,使用的是string path
,这里并没有加上引用&
,即本层递归中,path + 该节点数值,但该层递归结束,上一层path的数值并不会受到任何影响。-
节点4 的path,在遍历到节点3,path+3,遍历节点3的递归结束之后,返回节点4(回溯的过程),path并不会把3加上。所以这是参数中,不带引用,不做地址拷贝,只做内容拷贝的效果。(这里涉及到C++引用方面的知识)
-
在第一个版本中,函数参数我就使用了引用,即
vector<int>& path
,这是会拷贝地址的,所以 本层递归逻辑如果有path.push_back(cur->val);
就一定要有对应的path.pop_back()
。 -
那有同学可能想,为什么不去定义一个
string& path
这样的函数参数呢,然后也可能在递归函数中展现回溯的过程,但关键在于,path += to_string(cur->val);
每次是加上一个数字,这个数字如果是个位数,那好说,就调用一次path.pop_back()
,但如果是 十位数,百位数,千位数呢? 百位数就要调用三次path.pop_back()
,才能实现对应的回溯操作,这样代码实现就太冗余了。 -
所以,第一个代码版本中,我才使用 vector 类型的path,这样方便给大家演示代码中回溯的操作。 vector类型的path,不管 每次 路径收集的数字是几位数,总之一定是int,所以就一次 pop_back就可以。
-
// 版本一 class Solution { private: void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) { path.push_back(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中 // 这才到了叶子节点 if (cur->left == NULL && cur->right == NULL) { string sPath; 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 (cur->left) { // 左 traversal(cur->left, path, result); path.pop_back(); // 回溯 } if (cur->right) { // 右 traversal(cur->right, path, result); path.pop_back(); // 回溯 } } public: vector<string> binaryTreePaths(TreeNode* root) { vector<string> result; vector<int> path; if (root == NULL) return result; traversal(root, path, result); return result; } };
题目:左叶子之和
- 给定二叉树的根节点
root
,返回所有左叶子之和。
题解
-
因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点。判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。
-
递归的遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和。递归三部曲:
-
确定递归函数的参数和返回值:判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int
-
确定终止条件:如果遍历到空节点,那么左叶子值一定是0;注意,只有当前遍历的节点是父节点,才能判断其子节点是不是左叶子。 所以如果当前遍历的节点是叶子节点,那其左叶子也必定是0,那么终止条件为:
-
if (root == NULL) return 0; if (root->left == NULL && root->right== NULL) return 0; //其实这个也可以不写,如果不写不影响结果,但就会让递归多进行了一层。
-
确定单层递归的逻辑:当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。
-
class Solution { public: int sumOfLeftLeaves(TreeNode* root) { if(root==nullptr){ return 0;} if(root->left==nullptr&&root->right==nullptr){ return 0; } int leftV=sumOfLeftLeaves(root->left); if(root->left&&!root->left->left&&!root->left->right){ leftV=root->left->val; } int rightV=sumOfLeftLeaves(root->right); int sum=leftV+rightV; return sum; } };
-
-
一个节点为「左叶子」节点,当且仅当它是某个节点的左子节点,并且它是一个叶子结点。因此我们可以考虑对整棵树进行遍历,当我们遍历到节点 node 时,如果它的左子节点是一个叶子结点,那么就将它的左子节点的值累加计入答案。
-
class Solution { public: bool isleaf(TreeNode* node){ return !node->left&&!node->right; } int dfs(TreeNode *node){ int ans=0; if(node->left){ ans+=isleaf(node->left)?node->left->val:dfs(node->left); } if(node->right&&!isleaf(node->right)){ ans+=dfs(node->right); } return ans; } int sumOfLeftLeaves(TreeNode* root) { return root?dfs(root):0; } };
-
时间复杂度:O(n),其中 n 是树中的节点个数。空间复杂度:O(n)。空间复杂度与深度优先搜索使用的栈的最大深度相关。在最坏的情况下,树呈现链式结构,深度为 O(n),对应的空间复杂度也为 O(n)。
-
-
广度优先遍历
-
if(!root){ return 0; } queue<TreeNode*> q; q.push(root); int ans=0; while(!q.empty()){ TreeNode* node=q.front(); q.pop(); if(node->left){ if(isleaf(node->left)){ ans+=node->left->val; }else{ q.push(node->left); } } if(node->right){ if(!isleaf(node->right)){ q.push(node->right); } } } return ans;
-
时间复杂度:O(n),其中 n 是树中的节点个数。空间复杂度:O(n)。空间复杂度与广度优先搜索使用的队列需要的容量相关,为 O(n)。
-