leetcode 110.平衡二叉树
回顾一下二叉树高度和深度的概念:
二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
递归法
求高度,我们用的是后序遍历,求子节点的高度并层层向上返回。而求深度,我们用的是前序遍历,层层向下遍历统计节点的深度。
递归三部曲:
确定递归函数的参数和返回值
参数:当前传入节点。返回值:以当前传入节点为根节点的树的高度。
那么如何标记左右子树是否差值大于1呢?
如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。
所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。
// -1 表示已经不是平衡二叉树了,否则返回值是以该节点为根节点树的高度
int getHeight(TreeNode* node)
确定终止条件
递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0
if(node == NULL) return 0;
确定单层递归的逻辑
判断当前节点的左子树和右子树高度的差值与1的关系,如果差值小于等于1,则返回当前二叉树的高度。如果差值大于1,则返回-1,表示不为平衡二叉树。
int leftHeight = getHeight(node->left);
if(leftHeight == -1) return -1;
int rightHeight = getHeight(node->right);
if(rightHeight == -1) return -1;
int result;
if(abs(leftHeight - rightHeight) > 1) result = -1;
else result = 1 + max(leftHeight, rightHeight);
return result;
整体代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int getHeight(TreeNode* node){
if(node == NULL) return 0;
int leftHeight = getHeight(node->left);
if(leftHeight == -1) return -1;
int rightHeight = getHeight(node->right);
if(rightHeight == -1) return -1;
int result;
if(abs(leftHeight - rightHeight) > 1)
result = -1;
else
result = 1 + max(leftHeight, rightHeight);
return result;
}
bool isBalanced(TreeNode* root) {
return getHeight(root) == -1 ? false : true;
}
};
迭代法
可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。
本题的迭代方式可以先定义一个函数,专门用来求高度。
这个函数通过栈模拟的后序遍历(统一遍历方式)找每一个节点的高度(其实是通过求传入节点为根节点的最大深度来求的高度)。代码如下:
int getDepth(TreeNode* cur){
stack<TreeNode*> st;
if(cur != NULL) st.push(cur);
int depth = 0, result = 0;
while(!st.empty()){
TreeNode* node = st.top();
if(node != NULL){
st.pop();
st.push(node); // 中
st.push(NULL);
depth++;
if(node->right) st.push(node->right); // 右
if(node->left) st.push(node->left); // 左
}
else{
st.pop();
node = st.top();
st.pop();
depth--;
}
result = result > depth ? result : depth;
}
return result;
}
然后再用栈来模拟后序遍历,遍历每一个节点的时候,再去判断左右孩子的高度是否符合,代码如下:
bool isBalanced(TreeNode* root) {
stack<TreeNode*> st;
if(root == NULL) return true;
st.push(root);
while(!st.empty()){
TreeNode* node = st.top();
st.pop(); // 中
if(abs(getDepth(node->left) - getDepth(node->right)) > 1)
return false;
if(node->right) st.push(node->right); // 右
if(node->left) st.push(node->left); // 左
}
return true;
}
总体代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int getDepth(TreeNode* cur){
stack<TreeNode*> st;
if(cur != NULL) st.push(cur);
int depth = 0, result = 0;
while(!st.empty()){
TreeNode* node = st.top();
if(node != NULL){
st.pop();
st.push(node);
st.push(NULL);
depth++;
if(node->right) st.push(node->right);
if(node->left) st.push(node->left);
}
else{
st.pop();
node = st.top();
st.pop();
depth--;
}
result = result > depth ? result : depth;
}
return result;
}
bool isBalanced(TreeNode* root) {
stack<TreeNode*> st;
if(root == NULL) return true;
st.push(root);
while(!st.empty()){
TreeNode* node = st.top();
st.pop();
if(abs(getDepth(node->left) - getDepth(node->right)) > 1)
return false;
if(node->right) st.push(node->right);
if(node->left) st.push(node->left);
}
return true;
}
};
此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。
虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。
例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!
因为对于回溯算法已经是非常复杂的递归了,如果再用迭代的话,就是自己给自己找麻烦,效率也并不一定高。
leetcode 257.二叉树的所有路径
这道题目要求从根节点到叶子的路径,所以需要前序遍历,这样才方便让父节点指向孩子节点,找到对应的路径。在这道题目中将涉及到回溯,因为我们要把路径记录下来,需要回溯来回退一个路径再进入另一个路径。
前序遍历以及回溯的过程如图:

要记住:递归和回溯是一家
递归法
递归三部曲:
确定递归函数的参数和返回值
传入根节点,传入一个用于记录单条路径的path数组和存放结果集的result数组,题目要求返回结果是一个string类型的数组,故result的类型是vector<string>。
void traversal(TreeNode* cur, vector<int>& path, vector<string>& result)
确定终止条件
本题的终止条件是找到了叶子节点就相当于找到了一条路径的终点,即某节点的左右孩子均为空。
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;
}
这里使用vector 结构path来记录路径,所以要把vector 结构的path转为string格式,再把这个string 放进 result里。
那么为什么使用了vector 结构来记录路径呢? 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。
确定单层递归的逻辑
我们使用的是前序遍历(中左右),其中的“中”就是处理过程,本题中就是将节点添加到vector<int> path中,每遍历一个节点,就要把节点添加进来。
path.push_back(cur->val); // 中
// 终止条件处理逻辑
// 左
// 右
这里有一个细节,我们需要把“中”的处理过程写到终止条件判断的前面。因为对于叶子节点来说,如果写到后面的话,在终止条件判断的if中就直接return了,那么叶子节点的值将不会被放到path中。
然后是递归和回溯的过程,上面的终止条件判断中没有判断cur是否为空,可能会出现对空指针进行操作的异常情况,那么在这里递归的时候,如果为空就不进行下一层递归了。
/****
所以递归前要加上判断语句,下面要递归的节点是否为空。递归完,还要做回溯,因为path不能一直加入节点,它还要删节点,然后才能加入新的节点。回溯和递归是一一对应的,有一个递归,就要有一个回溯,递归和回溯要写到同一个花括号里面才可以。
/****
代码如下:
path.push(cur->val);
// ...
if(cur->left){ // 花括号不能去,否则函数执行顺序会不对
traversal(cur->left, path, result);
path.pop_back(); // 回溯
}
if(cur->right){
traversal(cur->right, path, result);
path.pop_back(); // 回溯
}
总体代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
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;
}
};
代码的精简版如下:
class Solution {
private:
void traversal(TreeNode* cur, string path, vector<string>& result) {
path += to_string(cur->val); // 中
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;
}
};
如上代码精简了不少,也隐藏了不少东西。
/****
注意在函数定义的时候void traversal(TreeNode* cur, string path, vector<string>& result) ,定义的是string path,每次都是复制赋值,不用使用引用,否则就无法做到回溯的效果。(这里涉及到C++语法知识)
那么在如上代码中,貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。
/****
如果写成这样:
if (cur->left) {
path += "->";
traversal(cur->left, path, result); // 左
}
if (cur->right) {
path += "->";
traversal(cur->right, path, result); // 右
}
此时就没有回溯了,这个代码就是通过不了的了。
如果想把回溯加上,就要 在上面代码的基础上,加上回溯,就可以AC了。
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(); // 回溯 '-'
}
}
如果把 path + "->"作为函数参数就是可以的,因为并没有改变path的数值,执行完递归函数之后,path依然是之前的数值(相当于回溯了)。
迭代法
非递归的方式,我们可以依然可以使用前序遍历的迭代方式来模拟遍历路径的过程,这里除了模拟递归需要一个栈,同时还需要一个栈来存放对应的遍历路径。使得保存路径的栈跟随模拟递归的栈同步进行操作,要注意在更新路径时要带上之前已经存储的路径。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
stack<TreeNode*> st; // 保持遍历节点的栈 TreeNode
stack<string> pa; // 保持路径的栈 string
vector<string> result;
if(root == NULL) return result;
st.push(root);
pa.push(to_string(root->val));
while(!st.empty()){
TreeNode* node = st.top();
st.pop();
string path = pa.top();
pa.pop();
if(node->right){
st.push(node->right);
pa.push(path + "->" + to_string(node->right->val));
}
if(node->left){
st.push(node->left);
pa.push(path + "->" + to_string(node->left->val));
}
if(node->left == NULL && node->right == NULL)
result.push_back(path);
}
return result;
}
};
leetcode 404.左叶子之和
给出左叶子的明确定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点。
我们从左叶子的定义中可以知道,无法根据当前节点的信息及其孩子信息来判断该节点是不是左叶子,一定要通过其父节点才知道。所以我们不能直接遍历到叶子节点进行判断,而是要在其上一层进行判断,其判断逻辑如下:
if(node->left != NULL && node->left->left == NULL && node->left->right ==NULL)
递归法
递归的遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和,层层向上返回给根节点,最终得到整一棵二叉树的左叶子之和。
递归三部曲
确定递归函数的参数和返回值
判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int
使用题目中给出的函数就可以了。
int sumOfLeftLeaves(TreeNode* root)
确定终止条件
如果遇到空节点,那么左叶子之和一定为0;如果遇到叶子节点,那么其左叶子之和也为0。
if(root == NULL) return 0;
if(root->left == NULL && root->right == NULL) return 0;
// 注意这里的root不是指的根节点,而是遍历到的每一个节点,因为这里使用的是主函数进行遍历。
确定单层递归的逻辑
当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。
int leftValue = sumOfLeftLeaves(root->left); // 左
// 若当前节点的左孩子就是左叶子,此种情况特别考虑,若进入递归中,则会返回0而不是左叶子的值。
if (root->left && !root->left->left && !root->left->right) {
leftValue = root->left->val;
}
int rightValue = sumOfLeftLeaves(root->right); // 右
int sum = leftValue + rightValue; // 中
return sum;
整体代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
if(root == NULL) return 0;
if(root->left == NULL && root->right == NULL) return 0;
int leftValue = sumOfLeftLeaves(root->left); // 左
if(root->left != NULL && root->left->left == NULL && root->left->right == NULL){
leftValue = root->left->val;
}
int rightValue = sumOfLeftLeaves(root->right); // 右
return leftValue + rightValue; // 中
}
};
迭代法
本题迭代法使用前中后序都是可以的,只要把左叶子节点统计出来,就可以了。
可以写出一个前序遍历的迭代法,与上题迭代法类似。判断条件都是一样的,代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
stack<TreeNode*> st;
int result = 0;
if(root == NULL) return 0;
st.push(root);
while(!st.empty()){
TreeNode* node = st.top();
st.pop();
if(node->left != NULL && node->left->left == NULL && node->left->right == NULL){
result += node->left->val; // 中
}
if(node->right) st.push(node->right); // 右
if(node->left) st.push(node->left); // 左
}
return result;
}
};