二叉树的高度和深度
-
二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。求深度一般用前序遍历。
-
二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。求高度用后序遍历。
Q1:求高度一定要用后序遍历,为什么?
我们可以先遍历左右孩子,返回给父节点后,父节点再根据左右孩子的高度情况 + 1,即加上自身的高度,这样层层返回到根节点就可以知道整个二叉树的高度,也可以知道每个结点的高度。
Q2:求深度一般用前序遍历,为什么?
因为深度的起点统一是根节点,从根节点开始层层往下遍历符合前序遍历的思路。
平衡二叉树
题干
题目:给定一个二叉树,判断它是否是平衡二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。
思路和代码
递归法
我们可以将大问题分解为二叉树的左右子树是否高度差都小于1,即每个结点的左右子树是否都是平衡二叉树。如果我们在遍历某棵子树的时候发现已经不平衡了,那么整棵二叉树肯定也不平衡,此时需要特殊的标记,即返回 -1。
Q:递归求每个结点高度差的过程该是什么样的?
-
递归的参数和返回值:参数即当前要传入的结点。如果左右子树出现不平衡,则返回 -1;如果左右子树平衡,则返回左右子树的高度,方便我们后续计算当前结点的左右子树高度差。这就是我们需要子树返回给父节点的两层信息,后序遍历的关键也在这。
-
递归结束的条件:当传入的结点为空,则说明高度为 0.
-
递归的顺序:遵循后序遍历,当我们传入一个结点,要先判断结点的左右子树是否平衡,再判断当前自身结点的左右子树高度差,所以先调用自身递归左子树,再调用自身递归右子树。只要左右子树都平衡,则当前结点的高度 = 左右子树高度的最大值 + 1(加上自身),我们需要继续向上层返回该结点的高度。
class Solution {
public:
// 若高度差小于1,则返回结点的高度;若高度差大于1,则返回 -1,表示已经不平衡
int height(TreeNode* node){
if (node == nullptr){
return 0;
}
// 后序遍历
int leftHeight = height(node->left);
if (leftHeight == -1){ // 左子树已不平衡
return -1;
}
int rightHeight = height(node->right);
if (rightHeight == -1){ // 右子树已不平衡
return -1;
}
if (abs(leftHeight - rightHeight) > 1){
// 左右子树都平衡,但左右子树高度差大于 1,当前结点不平衡
return -1;
} else{
// 左右子树和当前结点都平衡,返回当前节点的高度
// 当前结点的高度 = 左右子树高度的最大值 + 1(加上自身)
return 1+ max(leftHeight,rightHeight);
}
}
bool isBalanced(TreeNode* root) {
if (root == nullptr) return true;
int heightDifference = height(root); // 高度差
if (heightDifference == -1) { // 不平衡
return false;
} else{ // 平衡
return true;
}
}
};
二叉树的所有路径
题干
题目:给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。叶子节点 是指没有子节点的节点。
思路和代码
递归法:后序遍历
将问题分解为每个结点出发到叶子结点都有哪些路径。同样采用后序遍历,将从左右孩子结点出发的所有路径都返回给父节点,层层向上返回。 每返回到一个父节点,就往路径中添加这个父节点,这样最后返回到根结点时就可以获取全部的路径。
-
递归参数和返回值:参数是传入的结点,返回值是所有的路径数组。
-
递归结束条件:当找到叶子节点时,说明已经到达最底层,此时递归结束要开始向上返回,返回的该就是该结点字符,向上返回后可以和父节点组成一条条路径。
-
递归顺序:根据 “左右中” 的顺序,先收集结点左子树的路径,再收集右子树的路径,最后才将当前结点插入到左右子树的路径中,更新路径。
class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
if (root == nullptr) return {};
vector<string> result; // 记录从当前结点到叶子结点的所有路径
// 叶子结点
if (root->left == nullptr && root->right == nullptr) {
result.push_back(to_string(root->val)); // 返回叶子结点字符值
return result;
}
vector<string> leftPaths = binaryTreePaths(root->left); // 先收集左子树的路径
vector<string> rightPaths = binaryTreePaths(root->right); // 再收集右子树的路径
// 将当前的父节点插入左右子树的路径中,更新路径
for (int i = 0; i < leftPaths.size(); ++i) {
string path = to_string(root->val)+"->"+leftPaths[i];
result.push_back(path);
}
for (int i = 0; i < rightPaths.size(); ++i) {
string path = to_string(root->val)+"->"+rightPaths[i];
result.push_back(path);
}
return result;
}
};
递归法:前序遍历
我们也可以通过一个指针 cur 从根节点开始往下不断遍历直到找到叶子结点,即找到一条路径。但同时我们要知道在往下寻找的过程中都遍历了哪些结点,因为这条路径找完以后我们需要回溯到上一个父节点,从而开启另一条新路径,这就涉及到 “ 回溯 ”。
-
递归参数和返回值:参数是传入的结点、一条路径(vector<int>型)、路径结果集,没有返回值。
-
递归结束条件:当找到叶子结点时,说明找到了一条路径,此时将该条路径插入结果数组中,同时要回溯到上一个父节点。
-
递归顺序:根据 “中左右” 的顺序,先将当前结点插入路径之中,之后继续查找当前结点的左子树有哪些路径、右子树有哪些路径。
Q:如何完成 “回溯” 的过程?
我们传入的参数中包含一条路径,这条路径其实就记录了我们遍历过的历史结点。按照前序遍历的顺序,我们先遍历该节点的左子树,后遍历右子树,当左、右子树的路径找完以后表示当前结点的所有路径都找完了,需要回溯到上一个结点,此时就可以在路径中弹出该结点,以重新开始新路径。
class Solution {
public:
// 找路径
void findPaths(TreeNode* node, vector<int> &path, vector<string> &result){
// 直接在 path 路径中插入当前结点
path.push_back(node->val);
// 当前结点是叶子结点,说明已找到一条路径,将该路径加入 结果集 中
if (node->left == nullptr && node->right == nullptr) {
string s = to_string(path[0]);
for (int i = 1; i < path.size(); ++i) {
s = s + "->" + to_string(path[i]);
}
result.push_back(s);
return;
}
// 当前结点有左子树,还要继续往下遍历直到找到叶子节点
if (node->left != nullptr){
findPaths(node->left, path, result);
path.pop_back(); // 弹出当前结点,也就是回溯的一个过程
}
if (node->right != nullptr){
findPaths(node->right,path,result);
path.pop_back();
}
}
vector<string> binaryTreePaths(TreeNode* root) {
if (root == nullptr) return {};
vector<string> result; // 记录所有路径
vector<int> path;
findPaths(root,path,result);
return result;
}
};
左叶子之和
题干
题目:给定二叉树的根节点 root ,返回所有左叶子之和。
思路和代码
要找到二叉树中的所有左叶子,首先要明确左叶子的定义:若节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点。也就是说,如果要判断是否为左叶子,需要通过他的父节点来判断。
递归法:前序遍历
把问题分解为找到左右子树的左叶子。
-
递归参数:参数为传入的结点 node、左叶子之和 sum。无返回值。在不断递归过程中修改 sum。
-
递归结束条件:遍历完所有的结点则递归自动结束。
-
递归顺序:按照“中左右”的顺序,先判断当前结点是否为左叶子结点,如果不是则继续递归遍历左子树,后递归遍历右子树。
class Solution {
public:
void findLeftLeaves(TreeNode* node, int &sum){
if (node->left){
// 左叶子的判断逻辑
if (node->left->left == nullptr && node->left->right == nullptr){
sum += node->left->val;
} else{
findLeftLeaves(node->left,sum); // 遍历左子树
}
}
if (node->right){
findLeftLeaves(node->right,sum); // 遍历右子树
}
}
int sumOfLeftLeaves(TreeNode* root) {
if (root == nullptr) return 0;
if (root->left == nullptr && root->right == nullptr) return 0;
int sum = 0;
findLeftLeaves(root,sum);
return sum;
}
};
递归法:后序遍历
后序遍历递归的思路是分别收集左右子树的左叶子之和返回给上一层的父节点,再相加即可。
-
递归参数和返回值:参数是传入的结点,返回值是每个结点所在子树的左叶子之和。
-
递归结束条件:当结点为空,则说明没有左叶子,递归结束,返回 0。
-
递归顺序:根据 “左右中” 的顺序,先遍历左子树找到左子树的左叶子之和,再找右子树的左叶子之和,中间结点的左叶子之和就是左右子树的左叶子之和相加。
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
if (root == nullptr) return 0;
if (root->left == nullptr && root->right == nullptr) return 0; // 遇到叶子结点,返回 0
int leftSum = sumOfLeftLeaves(root->left); // 找左子树的左叶子之和
// 为什么这一步是放在这里?
// 左子树如果刚好有一个左叶子,但在此之前碰到叶子结点时返回的是0,并不是左叶子的数值,所以在这里要多一步判断
// 在之前的叶子结点返回 0 也是为避免多计算了右叶子结点
if (root->left){
// 找到左叶子
if (root->left->left == nullptr && root->left->right == nullptr)
leftSum = root->left->val;
}
int rightSum = sumOfLeftLeaves(root->right); // 找右子树的左叶子之和
return leftSum+rightSum;
}
};
迭代法:前序遍历
前序遍历每一个结点,再判断该结点是否有左叶子结点即可。
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
if (root == nullptr) return 0;
stack<TreeNode*> tmpNode;
TreeNode* cur = root;
int sum = 0;
// 前序遍历所有结点
while (cur != nullptr || !tmpNode.empty()){
if (cur == nullptr){
cur = tmpNode.top();
tmpNode.pop();
}
if (cur->left){
// 找到左叶子结点
if (cur->left->left == nullptr && cur->left->right == nullptr){
sum += cur->left->val;
}
}
if (cur->right != nullptr) tmpNode.push(cur->right);
cur = cur->left;
}
return sum;
}
};
完全二叉树的结点个数
题干
题目:给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^h 个节点。
思路和代码
迭代法:前序遍历
前序遍历所有结点,遍历过程统计结点个数即可。时间复杂度O(n),空间复杂度O(n)。
class Solution {
public:
int countNodes(TreeNode* root) {
if (root == nullptr) return 0;
TreeNode* cur = root;
stack<TreeNode*> tmpNode;
int count = 0; // 统计结点个数
while (cur != nullptr || !tmpNode.empty()){
count++;
if (cur == nullptr){
cur = tmpNode.top();
tmpNode.pop();
}
if (cur->right != nullptr) tmpNode.push(cur->right);
cur = cur->left;
}
return count;
}
};
递归法:后序遍历
把问题分解为找左右子树的节点个数,最后再加上自身结点的个数,即加一。时间复杂度O(n),空间复杂度O(logn),空间复杂度主要表现为递归调用栈的深度。
-
递归参数和返回值:参数是传入的结点,返回值是当前结点所在子树的结点总数。
-
递归结束的条件:遇到叶子结点,则返回 1.
-
递归顺序:后序遍历。
class Solution {
public:
int countNodes(TreeNode* root) {
if (root == nullptr) return 0;
// 找到叶子结点
if (root->left == nullptr && root->right == nullptr){
return 1;
}
int leftCount = countNodes(root->left);
int rightCount = countNodes(root->right);
return leftCount+rightCount+1; // 不要忘记加上自己
}
};
递归法:利用完全二叉树的性质
若满二叉树有 h 层(根节点为第 1 层),则满二叉树的结点总数为 2^h-1。也就是说满二叉树只要知道层高就快速计算出结点总数。
但完全二叉树不一定是满二叉树,要怎么办?
我们发现,完全二叉树可以分解为多个满二叉树,我们只需向下不断递归找到符合是满二叉树的结点即可。
如何判断一棵树是否是满二叉树?
分别设置两个指针,一个不断往左遍历,一个不断往右遍历,只要最后二者深度相同,就说明是满二叉树。
-
递归参数和返回值:参数是传入的结点,返回值是当前结点所在子树的结点总数。
-
递归结束的条件:一种情况是当结点为空,说明结点个数为0,递归结束。另一种情况是以当前结点为根结点的子树是满二叉树,则可以直接利用满二叉树的高度计算出结点总数,不需要再递归了。
-
递归顺序:本质是后序遍历,先计算左右子树的结点总数,再层层返回给父节点,就可以知道整个二叉树的结点个数。但不同的是当左右子树是满二叉树时,即可直接计算出子树的结点总数。
class Solution {
public:
int countNodes(TreeNode* root) {
if (root == nullptr) return 0;
TreeNode* left = root; // 遍历左子树
TreeNode* right = root; // 遍历右子树
int leftHeight = 0; // 记录左子树的深度
int rightHeight = 0; // 记录右子树的深度
while (left){
leftHeight++;
left = left->left;
}
while (right){
rightHeight++;
right = right->right;
}
if (leftHeight == rightHeight){ // 当前结点所在子树是满二叉树,递归结束
return pow(2,leftHeight)-1;
}
// 如果不是满二叉树,要继续向下递归左右子树
int leftNum = countNodes(root->left); // 记录左子树的结点总数
int rightNum = countNodes(root->right); // 记录右子树的结点总数
return leftNum+rightNum+1;
}
};