Day15 二叉树:110.平衡二叉树 257.二叉树的所有路径 404.左叶子之和

84 阅读6分钟

110.平衡二叉树

难度指数:😀😐

重点讲解如何通过后序遍历判断二叉树是否是平衡二叉树。

当我们发现任何一个节点,它的左右孩子不符合平衡二叉树的条件,那么整棵树就不是平衡二叉树。

递归解法:(后序)

代码思路:

 int getheight(node)
 {
     if (node == NULL) {
         return 0;  //若一个节点下面的节点是空节点,那么高度就是0
     }
     leftheight = getheight(node->left);  //左    统计左子树的高度
     if (leftheight == -1) {  //说明左子树本身不是一棵平衡二叉树
         return -1;
     }
     
     rightheight = getheight(node->right);  //右
     if (rightheight == -1) {
         return -1;
     }
     int result;
     //中
     if (abs(rightheight - leftheight) > 1) {
         result = -1;  //return -1; 也是可以的     表示不是平衡二叉树
     }
     else {
         result = 1 + max(rightheight, leftheight);
     }
     return result;
 }

Q:计算这个高度干啥?

A:计算这个高度,是为了计算高度差是否小于等于1,来判断是不是平衡二叉树。

代码并不复杂,关键在于对高度的理解。

求高度,为什么要用后序遍历?

可以看到在比较高度(计算左右高度差),是由前面的 向左递归,得到左子树的高度;以及 向右递归得到右子树的高度。

只有在的下面才能进行两个高度的比较,然后将比较的结果返回给上一层节点,

所以前序遍历是不可以的!

最后返回给根节点的信息,就告诉我们这棵二叉树是不是平衡二叉树。

AC代码: (核心代码模式)

 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(rightHeight - leftHeight) > 1) {
             result = -1;  //return -1; 也可以(只不过不够清晰)    表示不是平衡二叉树
         }
         else {
             result = 1 + max(rightHeight, leftHeight);
         }
         return result;
     }
 ​
     bool isBalanced(TreeNode* root) {
         return getHeight(root) == -1 ? false : true;
     }
 };

迭代解法:

待补充

AC代码: (核心代码模式)

 ​

257.二叉树的所有路径

难度指数:😀😐

求路径,要使用前序遍历。

只有前序遍历,才能让父节点指向它的孩子节点,这样才能把路径按照要求的顺序输出。

牛角尖发问:中序、后序遍历写法就不行?

你可以试试,不可以!

递归解法:(前序)

说到递归,就不得不提回溯,本题是第一次正式提到回溯,其实回溯和递归是相辅相成的,只要有递归就一定有回溯。

那我前面写过的那么多题目,怎么没感到有回溯?其实无论你用没用回溯,回溯都在那里。回溯的过程就在递归函数的下面。

那这个题为什么会有回溯的过程?

一个容器收集路径,收集到了1->2->5,

那么,如何把5弹出去,把2弹出去,再重新收集到1->3,(把5弹出去,2弹出去,这个过程就是回溯的过程。)

代码思路:

传入一个根节点,传入一个数组( path 用来记录单条路径),传入一个数组(放最后结果,每个数组元素是字符串类型,放每条路径

path定义的是一个vector数组的类型,而且参数列表里使用了引用,引用是地址拷贝。(这么做是为了方便展示回溯的过程)

path 也可以定义成 string 类型,也可以不用引用,代码会简短,但是会把回溯的过程隐藏了。)

path 是数组,需要把数组转成 string 类型,同时每个元素之间需要加 ->

⚠️本题有点特殊,要写在终止条件的上面。

如果把写在终止条件之后,那么最后遍历到叶子节点的时候,叶子节点的值就没有放到path里面。

因为遍历到叶子节点,该路径就结束了,会导致这个没有收集到叶子节点,

所以路径收集的节点要放在终止条件的上面,这样,最后的叶子节点也能放到path。

 void traversal(TreeNode* node, vector<int>& path, vector<string>& result) {
     
     path.push_back(node->val);  //中
     //终止条件
     if (node->left == NULL && node->right == NULL) {  //如果左、右孩子都为空,说明遍历到叶子节点了
         result.push_back(path);
         return;
     }
     //node就不怕是空节点吗?   如果是空节点,那这样就是空指针异常了
     if (node->left) {  //所以,向左遍历的时候会判断如果这个节点不为空,才会向左遍历
         traversal(node->left, path, result);  //递归  (有递归就有回溯)
         path.pop_back();  //回溯
     }
     if (node->right) {
         traversal(node->right, path, result);
         path.pop_back();
     }
 }

代码看起来还是比较冗余的,主要为了体现回溯的过程,便于理解。

AC代码: (核心代码模式)

 class Solution {
 public:
     void traversal(TreeNode* node, vector<int>& path, vector<string>& result) {
         path.push_back(node->val);  //中
         //终止条件
         if (node->left == NULL && node->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 (node->left) {
             traversal(node->left, path, result);  //递归
             path.pop_back();  //回溯
         }
         if (node->right) {
             traversal(node->right, path, result);
             path.pop_back();  //回溯
         }
     }
     vector<string> binaryTreePaths(TreeNode* root) {
         vector<string> result;
         vector<int> path;
         if (root == NULL) {
             return result;
         }
         traversal(root, path, result);
         return result;
     }
 };

思考:path和spath的区别是什么?

迭代解法:

待补充

404.左叶子之和

难度指数:😀😐

其左、右孩子为空,那么该节点一定是叶子节点。

左叶子节点:首先一定要是叶子节点,然后是其父节点的左孩子。

15.01.png

遍历到6时,我可以知道它是不是叶子节点,因为判断6的左右孩子均为空时,它就是叶子节点,

但是无法判断6是不是它的父节点的左孩子。

解决方法:我们选择只遍历到9,判断9的左孩子是否为空;如果左孩子不为空,同时左孩子的左、右孩子均为空,

那么当前遍历的节点9的左孩子就是我们要的左叶子节点。

用后序遍历比较好,因为需要一层一层向上返回,这样父节点可以得到左子树的左叶子之和,右子树的左叶子之和,相加。

一层一层往上,传给根节点。

递归解法:(后序)

代码思路:

直接使用力扣上给我们的主函数就可以

 int traversal(TreeNode* node) {
     //终止条件
     if (node == NULL) {  //如果这个节点是空
         return 0;  //左叶子之和一定是0
     }
     //如果遇到叶子节点
     if (node->left == NULL && node->right == NULL) {  //这步不写也没啥问题
         return 0;
     }
     //左
     int leftNum = traversal(node->left);  //收集左子树中符合左叶子条件的所有数之和
     if (node->left !=NULL && node->left->left == NULL && node->left->right == NULL) {
         leftNum = node->left->val;
     }
     //右
     int rightNum = traversal(node->right);  //收集右子树中符合左叶子条件的所有数之和
     //中
     int sum = leftNum + rightNum;
     return sum;
 }

我们只有遍历到9的时候,才能把这个9收集起来,

所以遇到叶子节点的时候,也是return 0;

代码谈不上精简,有很大的精简空间。

不建议初学者看精简版的代码,

AC代码: (核心代码模式)

 class Solution {
 public:
     int sumOfLeftLeaves(TreeNode* node) {
         if (node == NULL) {
             return 0;
         }
         //如果遇到叶子节点
         if (node->left == NULL && node->right == NULL) {
             return 0;
         }
         //左
         int leftNum = sumOfLeftLeaves(node->left);  //收集左子树中符合左叶子条件的所有数之和
         if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) {
             leftNum = node->left->val;
         }
         //右
         int rightNum = sumOfLeftLeaves(node->right);  //收集左子树中符合左叶子条件的所有数之和
         //中
         int sum = leftNum + rightNum;
 ​
         return sum;
     }
 };

迭代解法:

待补充