二叉树 - 图1

1、二叉树理论基础

1.1、二叉树的种类


我们在解题过程中,二叉树有两种主要的形式:满二叉树和完全二叉树。

1.1.1、满二叉树

满二叉树:如果一棵二叉树只有度为0的节点和度为2的节点,并且度为0的节点在同一层上,则这棵二叉树为满二叉树。
如图所示:
image.png
这棵二叉树为满二叉树,也可以说是深度为k,有 2^k - 1 个节点的二叉树。

1.1.2、完全二叉树

什么是完全二叉树?

完全二叉树的定义如下:在完全二叉树中,除了最底层的节点可能没有填满,其余每层节点数都达到最大值,并且最下面一层的节点都集中在最左边的若干位置。若最底层为第 k 层,则该层包含 1~2^(k - 1) 个节点

大家要看完全二叉树的定义,不要对完全二叉树的判断迷迷瞪瞪。
举个典型的例子:
image.png
之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。

1.1.3、二叉搜索树

前面介绍的树都是没有数值的,而二叉搜索数是有数值的,二叉搜索树是一棵有序树

  • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 它的左、右子树也分别为二叉排序树

下面这两棵树都是搜索树
image.png

1.1.4、平衡二叉搜索树

顾名思义,平衡二叉搜索树是在二叉搜索树的基础上平衡了左右子树高度。

平衡二叉搜索树:又被称为 AVL(Adelson-Velsky and Landis)数,且具有以下性质:它是一棵空树或他的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

如图:
image.png
最后一棵不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。

C++中map、set、multimap、multiset 的低层实现都是平衡二叉搜索树,所以map、set的增删操作时间复杂度是 logn,注意,unordered_map 和 unordered_set 的低层实现是哈希表,增删操作时间复杂度是 O(1)。

如果对数组和链表的增删查操作的时间复杂度还有疑问的,可以看一看这篇文章
链表和数组的插入删除时间复杂度都是o(n),为什么教材网络上说链表效率高?

所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器低层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!

1.2、二叉树的存储方式


二叉树可以是链式存储,也可以是顺序存储。

那么链式存储方式就用指针, 顺序存储的方式就是用数组。
顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起。

链式存储如图:
image.png
链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢?
其实就是用数组来存储二叉树,顺序存储的方式如图:

image.png

用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 i,那么它的左孩子就是 i 2 + 1,右孩子就是 i 2 + 2。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
所以大家要了解,用数组依然可以表示二叉树。

1.3、二叉树的遍历方式


关于二叉树的遍历方式,要知道二叉树遍历的基本方式都有哪些。
一些同学用做了很多二叉树的题目了,可能知道前中后序遍历,可能知道层序遍历,但是却没有框架。
我这里把二叉树的几种遍历方式列出来,大家就可以一一串起来了。

二叉树主要有两种遍历方式:

  1. 深度优先遍历:先往深走,遇到叶子节点再往回走。
  2. 广度优先遍历:一层一层的去遍历。

这两种遍历是图论中最基本的两种遍历方式,后面在介绍图论的时候 还会介绍到。
那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:

  • 深度优先遍历
    • 前序遍历(递归法,迭代法)
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
  • 广度优先遍历
    • 层次遍历(迭代法)

在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。
这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。
看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式

  • 前序遍历:中左右
  • 中序遍历:左中右
  • 后序遍历:左右中

大家可以对着如下图,看看自己理解的前后中序有没有问题。
image.png
最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。
之前我们讲栈与队列的时候,就说过栈其实就是递归的一种是实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
这里其实我们又了解了栈与队列的一个应用场景了。
具体的实现我们后面都会讲的,这里大家先要清楚这些理论基础。

1.4、二叉树的定义


刚刚我们说过了二叉树有两种存储方式顺序存储,和链式存储,顺序存储就是用数组来存,这个定义没啥可说的,我们来看看链式存储的二叉树节点的定义方式。

  1. struck TreeNode{
  2. int val;
  3. TreeNode* left;
  4. TreeNode* right;
  5. TreeNode(int x) : val(x), left(NULL), right(NULL){}
  6. };

大家会发现二叉树的定义 和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子.
这里要提醒大家要注意二叉树节点定义的书写方式。
在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。
因为我们在刷leetcode的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义的时候,有时候会一脸懵逼!


2、二叉树的递归遍历

144. 二叉树的前序遍历 - 力扣(LeetCode)
94. 二叉树的中序遍历 - 力扣(LeetCode)
145. 二叉树的后序遍历 - 力扣(LeetCode)

这里就不贴图了,题目比较简单。如果对基础的二叉树深度搜索的前中后序还有疑问的,可以看看一刷的笔记。

照例还是写递归三部曲:

  1. 确定递归函数的参数和返回值
  2. 确定终止条件
  3. 确定单层递归的逻辑

    1. /**
    2. * Definition for a binary tree node.
    3. * struct TreeNode {
    4. * int val;
    5. * TreeNode *left;
    6. * TreeNode *right;
    7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
    8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
    10. * };
    11. */
    12. class Solution {
    13. public:
    14. vector<int> preorderTraversal(TreeNode* root) {
    15. vector<int> result;
    16. preTraversal(root, result);
    17. return result;
    18. }
    19. // 前序递归
    20. void preTraversal(TreeNode* root, vector<int>& result){
    21. if(root == nullptr){
    22. return;
    23. }
    24. result.push_back(root->val); // 中
    25. preTraversal(root->left, result); // 左
    26. preTraversal(root->right, result); // 右
    27. }
    28. };
  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<int> inorderTraversal(TreeNode* root) {
  15. vector<int> result;
  16. midTraversal(root, result);
  17. return result;
  18. }
  19. // 中序递归
  20. void midTraversal(TreeNode* root, vector<int>& result){
  21. if(root == nullptr){
  22. return;
  23. }
  24. midTraversal(root->left, result); // 左
  25. result.push_back(root->val); // 中
  26. midTraversal(root->right, result); // 右
  27. }
  28. };
  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution{
  13. public:
  14. vector<int> postorderTraversal(TreeNode* root){
  15. vector<int> result;
  16. postTraversal(root, result);
  17. return result;
  18. }
  19. // 后序递归
  20. void postTraversal(TreeNode* root, vector<int>& result){
  21. if(root == nullptr){
  22. return;
  23. }
  24. postTraversal(root->left, result); // 左
  25. postTraversal(root->right, result); // 右
  26. result.push_back(root->val); // 中
  27. }
  28. };

3、二叉树的迭代遍历

144. 二叉树的前序遍历 - 力扣(LeetCode)
94. 二叉树的中序遍历 - 力扣(LeetCode)
145. 二叉树的后序遍历 - 力扣(LeetCode)

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. // 迭代法实现前序遍历,且是统一风格的代码
  15. vector<int> preorderTraversal(TreeNode* root) {
  16. stack<TreeNode*> st;
  17. vector<int> result;
  18. if(root == nullptr) return result;
  19. st.push(root); // 根节点先入栈
  20. while(!st.empty()){
  21. TreeNode* cur = st.top();
  22. if(cur != NULL){ // 如果节点非空,表明当前节点不是我们设置标志的节点
  23. st.pop(); // 如果当前节点非空,我们需要继续判断当前节点是否有左右孩子
  24. if(cur->right) st.push(cur->right); // 右
  25. if(cur->left) st.push(cur->left); // 左
  26. st.push(cur); // 中
  27. st.push(NULL); // 给当前节点设置标志
  28. }
  29. else{
  30. st.pop(); // 弹出空指针(即我们设置的标志)
  31. result.push_back(st.top()->val); // 这是我们想要操作的节点
  32. st.pop();
  33. }
  34. }
  35. return result;
  36. }
  37. };
  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. // 迭代法:统一风格的代码
  15. vector<int> inorderTraversal(TreeNode* root) {
  16. stack<TreeNode*> st;
  17. vector<int> result;
  18. if(root == NULL) return result;
  19. st.push(root);
  20. while(!st.empty()){
  21. TreeNode* cur = st.top();
  22. if(cur != NULL){
  23. st.pop();
  24. if(cur->right) st.push(cur->right);
  25. st.push(cur);
  26. st.push(NULL);
  27. if(cur->left) st.push(cur->left);
  28. }
  29. else{
  30. st.pop();
  31. result.push_back(st.top()->val);
  32. st.pop();
  33. }
  34. }
  35. return result;
  36. }
  37. };
  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution{
  13. public:
  14. vector<int> postorderTraversal(TreeNode* root){
  15. stack<TreeNode*> st;
  16. vector<int> result;
  17. if(root == NULL) return result;
  18. st.push(root); // 根节点先入栈
  19. while(!st.empty()){
  20. TreeNode* node = st.top();
  21. if(node != NULL){ // 如果节点非空,表明当前节点不是我们设置标志的节点
  22. st.pop();
  23. st.push(node); // 中
  24. st.push(NULL); // 给中节点设置标志
  25. if(node->right) st.push(node->right); // 右
  26. if(node->left) st.push(node->left); // 左
  27. }
  28. else{
  29. st.pop(); // 弹出空指针(即我们设置的标志)
  30. result.push_back(st.top()->val); // 这是我们想要操作的节点
  31. st.pop();
  32. }
  33. }
  34. return result;
  35. }
  36. };

4、二叉树的统一迭代法

3、二叉树的迭代遍历 我使用的就是统一的迭代法来解题的,虽然说不使用统一的解法也能把前中后序给求解出来,但是代码不同意,理解的角度也不太一样,为了方便记忆,以后就都使用统一的迭代法来解这种遍历的题目了。

5、二叉树的层序遍历

学会二叉树的层序遍历,可以一口气打完以下十题:

  • 102.二叉树的层序遍历
  • 107.二叉树的层次遍历II
  • 199.二叉树的右视图
  • 637.二叉树的层平均值
  • 429.N叉树的层序遍历
  • 515.在每个树行中找最大值
  • 116.填充每个节点的下一个右侧节点指针
  • 117.填充每个节点的下一个右侧节点指针II
  • 104.二叉树的最大深度
  • 111.二叉树的最小深度

5.1、二叉树的层序遍历

102. 二叉树的层序遍历 - 力扣(LeetCode)
image.png
image.png


层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
使用队列实现二叉树广度优先遍历,动画如下:
二叉树 - 图11
这样就实现了层序从左到右遍历二叉树。
代码如下:这份代码也可以作为二叉树层序遍历的模板,打十个就靠它了

C++代码:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<vector<int>> levelOrder(TreeNode* root) {
  15. vector<vector<int>> result;
  16. queue<TreeNode*> que;
  17. if(root == NULL) return result;
  18. que.push(root);
  19. while(!que.empty()){
  20. vector<int> temp; // 用一个临时的一维数组来存二叉树中一层的节点
  21. int size = que.size(); // 队列的大小,即二叉树当前这一层的节点数
  22. while(size--){
  23. TreeNode* cur = que.front();
  24. temp.push_back(cur->val);
  25. que.pop();
  26. if(cur->left) que.push(cur->left); // 左节点非空就入队
  27. if(cur->right) que.push(cur->right); // 右节点非空就入队
  28. }
  29. result.push_back(temp);
  30. }
  31. return result;
  32. }
  33. };

此时我们就掌握了二叉树的层序遍历了,那么如下九道力扣上的题目,只需要修改模板的两三行代码(不能再多了),便可打倒!

5.2、二叉树的层序遍历 II

107. 二叉树的层序遍历 II - 力扣(LeetCode)
image.png
image.png


这道题目同样也是层序遍历,只不过输出的顺序是从二叉树的叶子节点所在的层到根节点所在的层,逐层从左向右遍历输出。

代码里有详细的解析

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. // 先按顺序自上向下的层序遍历,然后在最后面要输出的时候反转一下数组即可
  15. vector<vector<int>> levelOrderBottom(TreeNode* root) {
  16. vector<vector<int>> result;
  17. queue<TreeNode*> que;
  18. if(root == nullptr){
  19. return result;
  20. }
  21. que.push(root);
  22. while(!que.empty()){
  23. vector<int> temp; // 用一个临时数组来存储一层的节点元素值
  24. int size = que.size(); // 二叉树每一层的节点数
  25. while(size--){ // 遍历完一层才推出循环
  26. TreeNode* cur = que.front();
  27. temp.push_back(cur->val);
  28. que.pop();
  29. if(cur->left) que.push(cur->left);
  30. if(cur->right) que.push(cur->right);
  31. }
  32. result.push_back(temp);
  33. }
  34. reverse(result.begin(), result.end()); // 在这里反转一下数组即可
  35. return result;
  36. }
  37. };

5.3、二叉树的右视图

199. 二叉树的右视图 - 力扣(LeetCode)
image.png
image.png


这道题还是层序遍历,只是要记录的是当前这一层左右边的节点而已,要实现也很简单,判断当前这一层的size等于等于最后一次循环是的值,就可以求解出来了

C++代码

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<int> rightSideView(TreeNode* root) {
  15. vector<int> result;
  16. queue<TreeNode*> que;
  17. if(root == nullptr){
  18. return result;
  19. }
  20. que.push(root);
  21. while(!que.empty()){
  22. int size = que.size();
  23. while(size){
  24. TreeNode* cur = que.front();
  25. que.pop();
  26. if(size == 1){ // 如果是当前这一层中最右边的节点,那么需要把它加入到数组中
  27. result.push_back(cur->val);
  28. }
  29. if(cur->left) que.push(cur->left);
  30. if(cur->right) que.push(cur->right);
  31. size--; // 注意,因为while循环内我有size==1的判断条件,因此这里的size--不能放在while()里面
  32. }
  33. }
  34. return result;
  35. }
  36. };

5.4、二叉树的层平均值

637. 二叉树的层平均值 - 力扣(LeetCode)
image.png
image.png


  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<double> averageOfLevels(TreeNode* root) {
  15. vector<double> result;
  16. queue<TreeNode*> que;
  17. if(root == nullptr){
  18. return result;
  19. }
  20. que.push(root);
  21. while(!que.empty()){
  22. int size = que.size();
  23. int count = size; // count是一个中间变量,也就是当前层的节点数
  24. double sum = 0; // 当前这一层的和
  25. while(size--){
  26. TreeNode* cur = que.front();
  27. que.pop();
  28. sum += cur->val;
  29. if(cur->left) que.push(cur->left);
  30. if(cur->right) que.push(cur->right);
  31. }
  32. result.push_back(sum / count);
  33. }
  34. return result;
  35. }
  36. };

5.5、N 叉树的层序遍历

429. N 叉树的层序遍历
image.png
image.png


N叉树的层序遍历与二叉树的层序遍历简直就是一模一样的,一层之中的节点数由2个变成N个而已,没什么好讲的

C++代码

  1. /*
  2. // Definition for a Node.
  3. class Node {
  4. public:
  5. int val;
  6. vector<Node*> children;
  7. Node() {}
  8. Node(int _val) {
  9. val = _val;
  10. }
  11. Node(int _val, vector<Node*> _children) {
  12. val = _val;
  13. children = _children;
  14. }
  15. };
  16. */
  17. class Solution {
  18. public:
  19. vector<vector<int>> levelOrder(Node* root) {
  20. vector<vector<int>> result;
  21. queue<Node*> que;
  22. if(root == nullptr){
  23. return result;
  24. }
  25. que.push(root);
  26. while(!que.empty()){
  27. vector<int> temp; // temp用来存储N叉树当前这一层的节点
  28. int size = que.size();
  29. while(size--){
  30. Node* cur = que.front();
  31. que.pop();
  32. temp.push_back(cur->val);
  33. // 遍历N个节点
  34. for(int i = 0; i < cur->children.size(); i++){
  35. que.push(cur->children[i]);
  36. }
  37. }
  38. result.push_back(temp);
  39. }
  40. return result;
  41. }
  42. };

5.6、在每个树行中找最大值

515. 在每个树行中找最大值
image.png


  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<int> largestValues(TreeNode* root) {
  15. vector<int> result;
  16. queue<TreeNode*> que;
  17. if(root == nullptr){
  18. return result;
  19. }
  20. que.push(root);
  21. while(!que.empty()){
  22. int size = que.size();
  23. int max = INT_MIN;
  24. while(size--){
  25. TreeNode* cur = que.front();
  26. que.pop();
  27. max = std::max(max, cur->val);
  28. if(cur->left) que.push(cur->left);
  29. if(cur->right) que.push(cur->right);
  30. }
  31. result.push_back(max);
  32. }
  33. return result;
  34. }
  35. };

5.7、填充每个节点的下一个右侧节点指针

116. 填充每个节点的下一个右侧节点指针 - 力扣(LeetCode)
image.png
image.png


  1. /*
  2. // Definition for a Node.
  3. class Node {
  4. public:
  5. int val;
  6. Node* left;
  7. Node* right;
  8. Node* next;
  9. Node() : val(0), left(NULL), right(NULL), next(NULL) {}
  10. Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
  11. Node(int _val, Node* _left, Node* _right, Node* _next)
  12. : val(_val), left(_left), right(_right), next(_next) {}
  13. };
  14. */
  15. class Solution {
  16. public:
  17. Node* connect(Node* root) {
  18. if(root == NULL){
  19. return NULL;
  20. }
  21. queue<Node*> que;
  22. que.push(root);
  23. while(!que.empty()){
  24. int size = que.size();
  25. Node* pre = new Node(0); // cur的前一个节点
  26. while(size){
  27. Node* cur = que.front();
  28. que.pop();
  29. pre->next = cur; // 前一个节点的next指向当前节点
  30. if(size == 1){ // 如果是当前这一层的最右边节点,那么让它的next指向NULL
  31. cur->next = NULL;
  32. }
  33. if(cur->left) que.push(cur->left);
  34. if(cur->right) que.push(cur->right);
  35. pre = cur; // 更新pre节点
  36. size--;
  37. }
  38. }
  39. return root;
  40. }
  41. };

其实也可以不用申请 pre 节点,但是这样代码比较巧妙,有点不好想到

  1. /*
  2. // Definition for a Node.
  3. class Node {
  4. public:
  5. int val;
  6. Node* left;
  7. Node* right;
  8. Node* next;
  9. Node() : val(0), left(NULL), right(NULL), next(NULL) {}
  10. Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
  11. Node(int _val, Node* _left, Node* _right, Node* _next)
  12. : val(_val), left(_left), right(_right), next(_next) {}
  13. };
  14. */
  15. class Solution {
  16. public:
  17. Node* connect(Node* root) {
  18. if(root == NULL){
  19. return NULL;
  20. }
  21. queue<Node*> que;
  22. que.push(root);
  23. while(!que.empty()){
  24. int size = que.size();
  25. while(size){
  26. Node* cur = que.front();
  27. que.pop();
  28. if(cur->left) que.push(cur->left);
  29. if(cur->right) que.push(cur->right);
  30. cur->next = que.front(); // 更新pre节点
  31. if(size == 1){ // 如果是当前这一层的最右边节点,那么让它的next指向NULL
  32. cur->next = NULL;
  33. }
  34. size--;
  35. }
  36. }
  37. return root;
  38. }
  39. };

5.8、填充每个节点的下一个右侧节点指针 II

117. 填充每个节点的下一个右侧节点指针 II - 力扣(LeetCode)
image.png
image.png


这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道

  1. /*
  2. // Definition for a Node.
  3. class Node {
  4. public:
  5. int val;
  6. Node* left;
  7. Node* right;
  8. Node* next;
  9. Node() : val(0), left(NULL), right(NULL), next(NULL) {}
  10. Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
  11. Node(int _val, Node* _left, Node* _right, Node* _next)
  12. : val(_val), left(_left), right(_right), next(_next) {}
  13. };
  14. */
  15. class Solution {
  16. public:
  17. Node* connect(Node* root) {
  18. if(root == NULL){
  19. return NULL;
  20. }
  21. queue<Node*> que;
  22. que.push(root);
  23. while(!que.empty()){
  24. int size = que.size();
  25. while(size){
  26. Node* cur = que.front();
  27. que.pop();
  28. if(cur->left) que.push(cur->left);
  29. if(cur->right) que.push(cur->right);
  30. cur->next = que.front();
  31. if(size == 1){
  32. cur->next = NULL;
  33. }
  34. size--;
  35. }
  36. }
  37. return root;
  38. }
  39. };

5.9、二叉树的最大深度

104. 二叉树的最大深度 - 力扣(LeetCode)
image.png


简单一点的解法,层序遍历就行了,但是效率低。也可以用递归来做,在不是极端的情况下,递归的效率略高,但也没有好到哪里去

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int maxDepth(TreeNode* root) {
  15. if(root == nullptr){
  16. return 0;
  17. }
  18. queue<TreeNode*> que;
  19. int depth = 0;
  20. que.push(root);
  21. while(!que.empty()){
  22. int size = que.size();
  23. while(size--){
  24. TreeNode* cur = que.front();
  25. que.pop();
  26. if(cur->left) que.push(cur->left);
  27. if(cur->right) que.push(cur->right);
  28. }
  29. depth++;
  30. }
  31. return depth;
  32. }
  33. };

5.10、二叉树的最小深度

111. 二叉树的最小深度 - 力扣(LeetCode)
image.png
image.png


最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。

题目交代的很清楚,当一个节点是叶子节点的时候,它所在的路径就是最短路径

C++代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int minDepth(TreeNode* root) {
  15. if(root == nullptr){
  16. return 0;
  17. }
  18. queue<TreeNode*> que;
  19. int depth = 0;
  20. que.push(root);
  21. while(!que.empty()){
  22. int size = que.size();
  23. while(size--){
  24. TreeNode* cur = que.front();
  25. que.pop();
  26. if(!cur->left && !cur->right){
  27. return ++depth; // 如果是最短路径,直接return
  28. }
  29. if(cur->left) que.push(cur->left);
  30. if(cur->right) que.push(cur->right);
  31. }
  32. depth++;
  33. }
  34. return depth;
  35. }
  36. };

总结:
二叉树的层序遍历,就是图论中的广度优先搜索在二叉树中的应用,需要借助队列来实现(此时又发现队列的一个应用了)。
来吧,一口气打十个:

  • 102.二叉树的层序遍历
  • 107.二叉树的层次遍历II
  • 199.二叉树的右视图
  • 637.二叉树的层平均值
  • 429.N叉树的前序遍历
  • 515.在每个树行中找最大值
  • 116.填充每个节点的下一个右侧节点指针
  • 117.填充每个节点的下一个右侧节点指针II
  • 104.二叉树的最大深度
  • 111.二叉树的最小深度

致敬叶师傅!

6、翻转二叉树

226. 翻转二叉树 - 力扣(LeetCode)
image.png
image.png


6.1、递归法

要翻转整棵二叉树,思路就是翻转当前节点的左子树和右子树,然后递归这样的操作。

遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。
注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果
这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了
那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!

image.png

C++代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* invertTree(TreeNode* root) {
  15. if(root == nullptr){
  16. return root;
  17. }
  18. if(root->left) invertTree(root->left); // 左
  19. if(root->right) invertTree(root->right); // 右
  20. // 中(这里代码冗余,但体现出逻辑性)
  21. if(root->left && root->right){
  22. swap(root->left, root->right);
  23. }
  24. else if(root->left && !root->right){
  25. swap(root->left, root->right);
  26. }
  27. else if(!root->left && root->right){
  28. swap(root->left, root->right);
  29. }
  30. return root;
  31. }
  32. };

递归法简化:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* invertTree(TreeNode* root) {
  15. if(root == nullptr){
  16. return root;
  17. }
  18. if(root->left) invertTree(root->left); // 左
  19. if(root->right) invertTree(root->right); // 右
  20. // 中
  21. swap(root->left, root->right);
  22. return root;
  23. }
  24. };

6.2、迭代法

前序和后序都可以,都是一样的思路,稍微改一下代码就行了。我这里使用后序

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* invertTree(TreeNode* root) {
  15. if(root == nullptr){
  16. return root;
  17. }
  18. stack<TreeNode*> st;
  19. st.push(root);
  20. while(!st.empty()){
  21. TreeNode* cur = st.top(); // 中
  22. st.pop();
  23. swap(cur->left, cur->right);
  24. if(cur->right) st.push(cur->right); // 右
  25. if(cur->left) st.push(cur->left); // 左
  26. }
  27. return root;
  28. }
  29. };

广度优先遍历

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* invertTree(TreeNode* root) {
  15. queue<TreeNode*> que;
  16. if(root != nullptr){
  17. que.push(root);
  18. }
  19. while(!que.empty()){
  20. int size = que.size();
  21. for(int i = 0; i < size; i++){
  22. TreeNode* node = que.front();
  23. que.pop();
  24. swap(node->left, node->right); // 交换节点
  25. if(node->left) que.push(node->left);
  26. if(node->right) que.push(node->right);
  27. }
  28. }
  29. return root;
  30. }
  31. };

7、周末二叉树总结

代码随想录 - 二叉树 - 7、周末二叉树总结

8、对称二叉树

101. 对称二叉树 - 力扣(LeetCode)
image.png
image.png


8.1、递归法**

递归三部曲

  1. 确定递归函数的参数和返回值
  2. 确定终止条件
  3. 确定单层递归的逻辑

    1. /**
    2. * Definition for a binary tree node.
    3. * struct TreeNode {
    4. * int val;
    5. * TreeNode *left;
    6. * TreeNode *right;
    7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
    8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
    10. * };
    11. */
    12. class Solution {
    13. public:
    14. bool isSymmetric(TreeNode* root) {
    15. return compare(root->left, root->right);
    16. }
    17. bool compare(TreeNode* left, TreeNode* right){
    18. // 首先排除空节点的情况
    19. if(left != nullptr && right == nullptr) return false;
    20. else if(left == nullptr && right != nullptr) return false;
    21. else if(left == nullptr && right == nullptr) return true; // 这一步很关键,后面会细讲
    22. // 排除了空节点,再排除数值不相同的情况
    23. else if(left->val != right->val) return false;
    24. // 此时就是,左右节点都存在,且数值相等的情况,这个时候才有必要做递归操作
    25. bool inside = compare(left->right, right->left); // 内测节点比较
    26. bool outsize = compare(left->left, right->right); // 外侧节点比较
    27. bool insame = inside && outsize;
    28. return insame;
    29. }
    30. };

为什么下面的代码是错误的?

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. bool isSymmetric(TreeNode* root) {
  15. return compare(root->left, root->right);
  16. }
  17. bool compare(TreeNode* left, TreeNode* right){
  18. // 首先排除空节点的情况
  19. if(left != nullptr && right == nullptr) return false;
  20. else if(left == nullptr && right != nullptr) return false;
  21. else if(left == nullptr && right == nullptr) return true;
  22. else if(left != nullptr && right != nullptr){
  23. if(left->val == right->val) return true;
  24. else return false;
  25. }
  26. // 此时就是,左右节点都存在,且数值相等的情况,这个时候才有必要做递归操作
  27. bool inside = compare(left->right, right->left);
  28. bool outsize = compare(left->left, right->right);
  29. bool insame = inside && outsize;
  30. return insame;
  31. }
  32. };

举个例子,如果测试用例是下面这样
image.png
结果输出会是 true 。为什么?
因为第一次进入方法compare,left->val == right->val,这个时候还没进入递归就return true 了,当然不对了!
所以,我们要的是,如果两个节点进行比较,并且满足 left->val == right->val,不让它们退出,还要继续进行比较,直到 它们的孩子节点都为 nullptr,我们才认为 左子树 和 右子树 是对称的。
所以这就是为什么说else if(left == nullptr && right == nullptr) return true; 这一步是很关键的。

吃透这个思路,是递归法解出这道题目的关键。

8.2、迭代法

这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。

这里我们可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(注意这不是层序遍历),虽然也是一层一层访问节点,但是和层序遍历还是有很大区别的。

使用队列
二叉树 - 图34
如下的条件判断和递归的逻辑是一样的。
代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. bool isSymmetric(TreeNode* root) {
  15. queue<TreeNode*> que;
  16. que.push(root->left); // 将左子树头结点加入队列
  17. que.push(root->right); // 将右子树头结点加入队列
  18. while(!que.empty()){ // 接下来判断左子树和右子树是否对称
  19. TreeNode* leftNode = que.front();
  20. que.pop();
  21. TreeNode* rightNode = que.front();
  22. que.pop();
  23. // 左节点为空、右节点为空,此时说明是对称的,这一步要先判断,不然后面可能会对空指针进行操作
  24. if(!leftNode && !rightNode) continue;
  25. // 左右一个节点不为空,或者都不为空但数值不相同,返回false
  26. else if(leftNode && !rightNode) return false;
  27. else if(!leftNode && rightNode) return false;
  28. else if(leftNode->val != rightNode->val) return false;
  29. que.push(leftNode->right); // 内测节点入队
  30. que.push(rightNode->left);
  31. que.push(leftNode->left); // 外侧节点入队
  32. que.push(rightNode->right);
  33. }
  34. return true;
  35. }
  36. };

细心的话,其实可以发现,这个迭代法,其实是把左右两个子树要比较的元素顺序放进一个容器,然后成对成对的取出来进行比较,那么其实使用栈也是可以的。只是这里不再是前中后序的遍历方式。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. bool isSymmetric(TreeNode* root) {
  15. stack<TreeNode*> st;
  16. st.push(root->left); // 将左子树头结点加入队列
  17. st.push(root->right); // 将右子树头结点加入队列
  18. while(!st.empty()){ // 接下来判断左子树和右子树是否对称
  19. TreeNode* leftNode = st.top();
  20. st.pop();
  21. TreeNode* rightNode = st.top();
  22. st.pop();
  23. // 左节点为空、右节点为空,此时说明是对称的,这一步要先判断,不然后面可能会对空指针进行操作
  24. if(!leftNode && !rightNode) continue;
  25. // 左右一个节点不为空,或者都不为空但数值不相同,返回false
  26. else if(leftNode && !rightNode) return false;
  27. else if(!leftNode && rightNode) return false;
  28. else if(leftNode->val != rightNode->val) return false;
  29. st.push(leftNode->right); // 内测节点入队
  30. st.push(rightNode->left);
  31. st.push(leftNode->left); // 外侧节点入队
  32. st.push(rightNode->right);
  33. }
  34. return true;
  35. }
  36. };

逻辑和使用队列是一模一样的,只是比较的顺序不一样了。队列是内测节点先比较,而栈在不改变代码逻辑的情况下,是外侧节点先比较。

总结
在迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,知道这一本质之后就发现,用队列,用栈,甚至用数组,都是可以的。

9、二叉树的最大深度

104. 二叉树的最大深度 - 力扣(LeetCode)
image.png


9.1、递归法

本题可以使用前序(中左右),也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序求的是高度。
而根节点的高度就是二叉树的最大深度,所以本题中我们通过后序求的根节点高度来求的二叉树最大深度。
这一点其实是很多同学没有想清楚的,很多题解同样没有讲清楚。
我先用后序遍历(左右中)来计算树的高度。

后序遍历C++代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int getDepth(TreeNode* root){
  15. if(root == nullptr) return 0;
  16. int leftDepth = getDepth(root->left); // 左
  17. int rightDepth = getDepth(root->right); // 右
  18. int depth = 1 + max(leftDepth, rightDepth); // 中
  19. return depth;
  20. }
  21. int maxDepth(TreeNode* root) {
  22. return getDepth(root);
  23. }
  24. };

后序遍历其实就是从树的底部往上走,每往上走一层,就记录一个深度,但其实本质是求这棵树的高度。

也可以使用前序遍历来做,(充分表现出求深度回溯的过程)

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int result = 0;
  15. void getDepth(TreeNode* root, int depth){
  16. result = result > depth ? result : depth; // 中
  17. if(root->left == nullptr && root->right == nullptr) return ;
  18. if(root->left){ // 左
  19. getDepth(root->left, depth + 1);
  20. }
  21. if(root->right){ // 右
  22. getDepth(root->right, depth + 1);
  23. }
  24. return;
  25. }
  26. int maxDepth(TreeNode* root) {
  27. if(root == nullptr) return result;
  28. getDepth(root, 1);
  29. return result;
  30. }
  31. };

可以看出使用了前序(中左右)的遍历顺序,这才是真正求深度的逻辑!

9.2、迭代法

迭代法的话使用层序遍历是最合适的,迭代法算是一道模板题了

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int maxDepth(TreeNode* root) {
  15. if(root == nullptr) return 0;
  16. int depth = 0;
  17. queue<TreeNode*> que;
  18. que.push(root);
  19. while(!que.empty()){
  20. int size = que.size();
  21. while(size--){
  22. TreeNode* cur = que.front();
  23. que.pop();
  24. if(cur->left) que.push(cur->left);
  25. if(cur->right) que.push(cur->right);
  26. }
  27. depth++;
  28. }
  29. return depth;
  30. }
  31. };

10、N叉树的最大深度

10.1、递归法

和求解9、二叉树的最大深度的思路是一样的。

  1. /*
  2. // Definition for a Node.
  3. class Node {
  4. public:
  5. int val;
  6. vector<Node*> children;
  7. Node() {}
  8. Node(int _val) {
  9. val = _val;
  10. }
  11. Node(int _val, vector<Node*> _children) {
  12. val = _val;
  13. children = _children;
  14. }
  15. };
  16. */
  17. class Solution {
  18. public:
  19. int result = 0;
  20. void getDepth(Node* root, int depth){
  21. result = result > depth ? result : depth; // 中 ---这里是一定要在程序进来就处理的,不然会遗漏
  22. if(root->children.size() == 0){
  23. return;
  24. }
  25. for(int i = 0; i < root->children.size(); i++){ // 孩子节点
  26. getDepth(root->children[i], depth + 1);
  27. }
  28. return;
  29. }
  30. int maxDepth(Node* root) {
  31. if(root == NULL) return 0;
  32. getDepth(root, 1);
  33. return result;
  34. }
  35. };

代码是最好的注释,后序遍历直接看代码吧

  1. /*
  2. // Definition for a Node.
  3. class Node {
  4. public:
  5. int val;
  6. vector<Node*> children;
  7. Node() {}
  8. Node(int _val) {
  9. val = _val;
  10. }
  11. Node(int _val, vector<Node*> _children) {
  12. val = _val;
  13. children = _children;
  14. }
  15. };
  16. */
  17. class Solution {
  18. public:
  19. int maxDepth(Node* root) {
  20. if(root == NULL) return 0;
  21. int depth = 1;
  22. for(int i = 0; i < root->children.size(); i++){
  23. depth = max(depth, 1 + maxDepth(root->children[i]));
  24. }
  25. return depth;
  26. }
  27. };

10.2、迭代法

迭代法在之前5.5、N叉树的层序遍历的时候已经写过类似的了,不赘述

  1. /*
  2. // Definition for a Node.
  3. class Node {
  4. public:
  5. int val;
  6. vector<Node*> children;
  7. Node() {}
  8. Node(int _val) {
  9. val = _val;
  10. }
  11. Node(int _val, vector<Node*> _children) {
  12. val = _val;
  13. children = _children;
  14. }
  15. };
  16. */
  17. class Solution {
  18. public:
  19. int maxDepth(Node* root) {
  20. if(root == NULL) return 0;
  21. int depth = 0;
  22. queue<Node*> que;
  23. que.push(root);
  24. while(!que.empty()){
  25. int size = que.size();
  26. while(size--){
  27. Node* node = que.front();
  28. que.pop();
  29. for(int i = 0; i < node->children.size(); i++){
  30. que.push(node->children[i]);
  31. }
  32. }
  33. depth++;
  34. }
  35. return depth;
  36. }
  37. };

11、二叉树的最小深度

111. 二叉树的最小深度 - 力扣(LeetCode)
image.png
image.png


11.1、递归法**

前序遍历

这道题目,随想录只给出了后序遍历的做法(递归法用后序遍历确实是最理想的),于是我尝试用前序遍历来做,但是求不出来,前序遍历,每次都会遍历左子树直到底部,所以它就无法比较左右子树的深度,自然就无法求出二叉树的最小深度。

错误的代码

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int result = 0;
  15. bool getdepth(TreeNode* root, int depth){
  16. result = result > depth ? result : depth;
  17. // 即使用标志位true和false来判断,也无法解决该题
  18. if(root->left == nullptr && root->right == nullptr){
  19. return false;
  20. }
  21. if(root->left){
  22. if(getdepth(root->left, depth + 1)){
  23. getdepth(root->left, depth + 1);
  24. }
  25. else{
  26. return false;
  27. }
  28. }
  29. if(root->right){
  30. if(getdepth(root->right, depth + 1)){
  31. getdepth(root->right, depth + 1);
  32. }
  33. else{
  34. return false;
  35. }
  36. }
  37. return true;
  38. }
  39. int minDepth(TreeNode* root) {
  40. if(root == nullptr) return 0;
  41. getdepth(root, 1);
  42. return result;
  43. }
  44. };

后序遍历

直觉上好像和求最大深度差不多,其实还是差不少的。
遍历顺序上依然是后序遍历(因为要比较递归返回之后的结果),但在处理中间节点的逻辑上,最大深度很容易理解,最小深度可有一个误区,如图:
image.png
这就重新审题了,题目中说的是:最小深度是从根节点到最近叶子节点的最短路径上的节点数量。,注意是叶子节点
什么是叶子节点,左右孩子都为空的节点才是叶子节点!

如果这么求的话,没有左孩子的分支会算为最短深度。
所以,如果左子树为空,右子树不为空,说明最小深度是 1 + 右子树的深度。
反之,右子树为空,左子树不为空,最小深度是 1 + 左子树的深度。 最后如果左右子树都不为空,返回左右子树深度最小值 + 1 。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int minDepth(TreeNode* root) {
  15. if(root == nullptr) return 0;
  16. int left = minDepth(root->left); // 左
  17. int right = minDepth(root->right); // 右
  18. // 中
  19. // 如果左子树为空,右子树不为空,说明最小深度是 1 + 右子树的深度。
  20. if(root->left == nullptr && root->right != nullptr){
  21. return 1 + right;
  22. }
  23. // 如果左子树不为空,右子树为空,说明最小深度是 1 + 左子树的深度。
  24. if(root->left != nullptr && root->right == nullptr){
  25. return 1 + left;
  26. }
  27. int result = 1 + min(left, right);
  28. return result;
  29. }
  30. };

遍历的顺序为后序(左右中),可以看出:求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。

代码精简之后

  1. class Solution {
  2. public:
  3. int minDepth(TreeNode* root) {
  4. if (root == NULL) return 0;
  5. if (root->left == NULL && root->right != NULL) {
  6. return 1 + minDepth(root->right);
  7. }
  8. if (root->left != NULL && root->right == NULL) {
  9. return 1 + minDepth(root->left);
  10. }
  11. return 1 + min(minDepth(root->left), minDepth(root->right));
  12. }
  13. };

11.2、迭代法

迭代法还是使用 层序遍历

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int minDepth(TreeNode* root) {
  15. if(root == nullptr) return 0;
  16. queue<TreeNode*> que;
  17. que.push(root);
  18. int result = 0;
  19. while(!que.empty()){
  20. int size = que.size();
  21. while(size--){
  22. TreeNode* cur = que.front();
  23. que.pop();
  24. if(!cur->left && !cur->right){
  25. return ++result;
  26. }
  27. if(cur->left) que.push(cur->left);
  28. if(cur->right) que.push(cur->right);
  29. }
  30. result++;
  31. }
  32. return result;
  33. }
  34. };

12、完全二叉树的节点个数

222. 完全二叉树的节点个数 - 力扣(LeetCode)
image.png
image.png


12.1、递归法

12.1.1、普通二叉树解法

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. // 时间复杂度:$O(\log n × \log n)$
  13. // 空间复杂度:$O(\log n)$
  14. class Solution {
  15. public:
  16. int nodes = 0;
  17. int countNodes(TreeNode* root) {
  18. if(root == nullptr) return 0;
  19. countNodes(root->left);
  20. countNodes(root->right);
  21. return 1 + nodes++; // 1 是当前节点,需要加上, 或者这样写:++nodes
  22. }
  23. };

12.1.2、完全二叉树解法

完全二叉树只有两种情况,

  • 情况一:就是满二叉树
  • 情况二:最后一层叶子节点没有满

对于情况一,可以直接用 2^树深度 - 1 来计算,注意,这里根节点的深度是1
对于情况二:分别递归其左孩子,右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况一来计算。

完全二叉树如图所示:
image.png
可以看出,如果一棵树不是满二叉树,就递归其左右孩子,知道遇到满二叉树为止,用公式计算这棵子树(满二叉树)的节点数量。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int countNodes(TreeNode* root) {
  15. if(root == nullptr) return 0;
  16. TreeNode* left = root->left;
  17. TreeNode* right = root->right;
  18. int leftHeight = 0, rightHeight = 0; // 这里初始化为0是有意的,为了下面求指数方便
  19. while(left){ // 求左子树的深度
  20. left = left->left;
  21. leftHeight++;
  22. }
  23. while(right){ // 求右子树的深度
  24. right = right->right;
  25. rightHeight++;
  26. }
  27. // 如果当前节点的左右子树的深度相同,说明是一棵满二叉树
  28. if(leftHeight == rightHeight){
  29. return(2 << leftHeight) - 1; // 注意(2<<1) 相当于2^2,所以leftHeight初始为0
  30. }
  31. // 如果当前节点所在的树不是满二叉树
  32. return countNodes(root->left) + countNodes(root->right) + 1; // 1表示当前二叉树的根节点,需要加上
  33. }
  34. };

12.2、迭代法

迭代法还是使用层序遍历是最简单的,也比较合适

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int countNodes(TreeNode* root) {
  15. if(root == nullptr){
  16. return 0;
  17. }
  18. queue<TreeNode*> que;
  19. que.push(root);
  20. int count = 0; // 用来记录二叉树的节点个数
  21. while(!que.empty()){
  22. int size = que.size();
  23. while(size--){
  24. TreeNode* cur = que.front();
  25. que.pop();
  26. if(cur->left) que.push(cur->left);
  27. if(cur->right) que.push(cur->right);
  28. count++;
  29. }
  30. }
  31. return count;
  32. }
  33. };

13、平衡二叉树

110. 平衡二叉树 - 力扣(LeetCode)
image.png
image.png


13.1、递归法

递归三步曲分析:

  1. 明确递归函数的参数和返回值

参数:当前传入节点。
返回值:以当前传入节点为根节点的树的高度。
那么如何标记左右子树是否差值大于1呢?
如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。
所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。

  1. 明确终止条件

递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0

  1. 单层递归逻辑

如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。
分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则则返回-1,表示已经不是二叉平衡树了。

C++代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. bool isBalanced(TreeNode* root) {
  15. return getHeight(root) == -1 ? false : true;
  16. }
  17. // -1表示已经不是平衡二叉树了,否则返回值是以该节点为根节点树的高度
  18. int getHeight(TreeNode* root){
  19. if(root == nullptr) return 0;
  20. int leftHeight = getHeight(root->left);
  21. if(leftHeight == -1) return -1;
  22. int rightHeight = getHeight(root->right);
  23. if(rightHeight == -1) return -1;
  24. if(abs(leftHeight - rightHeight) > 1){ // 如果不是平衡二叉树,返回-1
  25. return -1;
  26. }
  27. else{
  28. return 1 + max(leftHeight, rightHeight);
  29. }
  30. }
  31. };

13.2、迭代法

104.二叉树的最大深度(opens new window)中我们可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。
本题的迭代方式可以先定义一个函数,专门用来求高度。
这个函数通过栈模拟的后序遍历找每一个节点的高度(其实是通过求传入节点为根节点的最大深度来求的高度)
代码如下:

  1. // cur节点的最大深度,就是cur的高度
  2. int getDepth(TreeNode* cur) {
  3. stack<TreeNode*> st;
  4. if (cur != NULL) st.push(cur);
  5. int depth = 0; // 记录深度
  6. int result = 0;
  7. while (!st.empty()) {
  8. TreeNode* node = st.top();
  9. if (node != NULL) {
  10. st.pop();
  11. st.push(node); // 中
  12. st.push(NULL);
  13. depth++;
  14. if (node->right) st.push(node->right); // 右
  15. if (node->left) st.push(node->left); // 左
  16. } else {
  17. st.pop();
  18. node = st.top();
  19. st.pop();
  20. depth--;
  21. }
  22. result = result > depth ? result : depth;
  23. }
  24. return result;
  25. }

然后再用栈来模拟前序遍历,遍历每一个节点的时候,再去判断左右孩子的高度是否符合,代码如下:

  1. bool isBalanced(TreeNode* root) {
  2. stack<TreeNode*> st;
  3. if (root == NULL) return true;
  4. st.push(root);
  5. while (!st.empty()) {
  6. TreeNode* node = st.top(); // 中
  7. st.pop();
  8. if (abs(getDepth(node->left) - getDepth(node->right)) > 1) { // 判断左右孩子高度是否符合
  9. return false;
  10. }
  11. if (node->right) st.push(node->right); // 右(空节点不入栈)
  12. if (node->left) st.push(node->left); // 左(空节点不入栈)
  13. }
  14. return true;
  15. }

C++完整代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. bool isBalanced(TreeNode* root) {
  15. stack<TreeNode*> st;
  16. if (root == NULL) return true;
  17. st.push(root);
  18. while (!st.empty()) {
  19. TreeNode* node = st.top(); // 中
  20. st.pop();
  21. if (abs(getDepth(node->left) - getDepth(node->right)) > 1) {
  22. return false;
  23. }
  24. if (node->right) st.push(node->right); // 右(空节点不入栈)
  25. if (node->left) st.push(node->left); // 左(空节点不入栈)
  26. }
  27. return true;
  28. }
  29. // cur节点的最大深度,就是cur的高度
  30. int getDepth(TreeNode* root){
  31. if(root == nullptr) return 0;
  32. stack<TreeNode*> st;
  33. st.push(root);
  34. int depth = 0; // 记录深度
  35. int result = 0;
  36. while(!st.empty()){
  37. TreeNode* node = st.top();
  38. if(node){
  39. st.pop();
  40. st.push(node);
  41. st.push(nullptr);
  42. depth++;
  43. if(node->left) st.push(node->left);
  44. if(node->right) st.push(node->right);
  45. }
  46. else{
  47. st.pop();
  48. node = st.top();
  49. st.pop();
  50. depth--;
  51. }
  52. result = result > depth ? result : depth;
  53. }
  54. return result;
  55. }
  56. };

当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。
虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。
例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!
因为对于回溯算法已经是非常复杂的递归了,如果在用迭代的话,就是自己给自己找麻烦,效率也并不一定高。

通过本题可以了解求二叉树深度 和 二叉树高度的差异,求深度适合用前序遍历,而求高度适合用后序遍历。
本题迭代法其实有点复杂,大家可以有一个思路,也不一定说非要写出来。
但是递归方式是一定要掌握的!

14、二叉树的所有路径

257. 二叉树的所有路径 - 力扣(LeetCode)
image.png
image.png


递归法:回溯

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<string> result;
  15. void backTracking(TreeNode* node, string path){
  16. path += to_string(node->val); // 中
  17. if(node->left == nullptr && node->right == nullptr){
  18. result.push_back(path);
  19. return;
  20. }
  21. if(node->left) backTracking(node->left, path + "->"); // 左
  22. if(node->right) backTracking(node->right, path + "->"); // 右
  23. return;
  24. }
  25. vector<string> binaryTreePaths(TreeNode* root) {
  26. result.clear(); // 也可以不写
  27. string path;
  28. if(root == nullptr) return result;
  29. backTracking(root, path);
  30. return result;
  31. }
  32. };

迭代法
至于非递归的方式,我们可以依然可以使用前序遍历的迭代方式来模拟遍历路径的过程。
这里除了模拟递归需要一个栈,同时还需要一个栈来存放对应的遍历路径。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<string> binaryTreePaths(TreeNode* root) {
  15. vector<string> result; // 保存最终路径集合
  16. stack<string> pathSt; // 保存遍历路径的节点
  17. stack<TreeNode*> treeSt; // 保存遍历树的节点
  18. if(root == nullptr) return result;
  19. treeSt.push(root);
  20. pathSt.push(to_string(root->val));
  21. while(!treeSt.empty()){
  22. TreeNode* cur = treeSt.top(); // 获取当前节点, 中
  23. treeSt.pop();
  24. string path = pathSt.top(); // 获取当前路径
  25. pathSt.pop();
  26. if(cur->left == nullptr && cur->right == nullptr){
  27. result.push_back(path);
  28. }
  29. if(cur->right){ // 右
  30. treeSt.push(cur->right);
  31. pathSt.push(path + "->" + to_string(cur->right->val));
  32. }
  33. if(cur->left){ // 左
  34. treeSt.push(cur->left);
  35. pathSt.push(path + "->" + to_string(cur->left->val));
  36. }
  37. }
  38. return result;
  39. }
  40. };

15、二叉树周末总结

二叉树周末总结

16、二叉树中递归带着回溯

由于一刷的时候我学的比较仔细,二刷基本扫一遍心里就有数了,所以这里也感觉不需要做笔记。如果有什么不懂的,可以去翻笔记,或者看卡哥的代码随想录二叉树中递归带着回溯

17、左叶子之和

404. 左叶子之和 - 力扣(LeetCode)
image.png
image.png


递归法

因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:如果左节点不为空,且左节点没有左右孩子,那么这个节点的左节点就是左叶子

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int sum = 0;
  15. int sumOfLeftLeaves(TreeNode* root) {
  16. if(root == nullptr) return 0;
  17. // 如果当前节点是父节点的左孩子,且当前节点是叶子节点
  18. if(root->left && (!root->left->left && !root->left->right)){
  19. sum += root->left->val;
  20. }
  21. sumOfLeftLeaves(root->left);
  22. sumOfLeftLeaves(root->right);
  23. return sum;
  24. }
  25. };

卡哥的代码

  1. class Solution {
  2. public:
  3. int sumOfLeftLeaves(TreeNode* root) {
  4. if (root == NULL) return 0;
  5. int leftValue = sumOfLeftLeaves(root->left); // 左
  6. int rightValue = sumOfLeftLeaves(root->right); // 右
  7. // 中
  8. int midValue = 0;
  9. if (root->left && !root->left->left && !root->left->right) { // 中
  10. midValue = root->left->val;
  11. }
  12. int sum = midValue + leftValue + rightValue;
  13. return sum;
  14. }
  15. };

精简之后(我还是绝的我的代码更好)

  1. class Solution {
  2. public:
  3. int sumOfLeftLeaves(TreeNode* root) {
  4. if (root == NULL) return 0;
  5. int midValue = 0;
  6. if (root->left != NULL && root->left->left == NULL && root->left->right == NULL) {
  7. midValue = root->left->val;
  8. }
  9. return midValue + sumOfLeftLeaves(root->left) + sumOfLeftLeaves(root->right);
  10. }
  11. };

迭代法:

层序遍历也能做,所以不要被卡哥的思维框住了

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int sumOfLeftLeaves(TreeNode* root) {
  15. queue<TreeNode*> que;
  16. que.push(root);
  17. int sum = 0;
  18. while(!que.empty()){
  19. int size = que.size();
  20. while(size--){
  21. TreeNode* cur = que.front();
  22. que.pop();
  23. if(cur->left && (!cur->left->left && !cur->left->right)){
  24. sum += cur->left->val;
  25. }
  26. if(cur->left) que.push(cur->left);
  27. if(cur->right) que.push(cur->right);
  28. }
  29. }
  30. return sum;
  31. }
  32. };

前中后序都能做,这里图方便用前序写了

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int sumOfLeftLeaves(TreeNode* root) {
  15. stack<TreeNode*> st;
  16. st.push(root);
  17. int sum = 0;
  18. while(!st.empty()){
  19. TreeNode* cur = st.top(); // 中
  20. st.pop();
  21. if(cur->left && !cur->left->left && !cur->left->right){
  22. sum += cur->left->val;
  23. }
  24. if(cur->right) st.push(cur->right); // 左
  25. if(cur->left) st.push(cur->left); // 右
  26. }
  27. return sum;
  28. }
  29. };

18、找树左下角的值

513. 找树左下角的值 - 力扣(LeetCode)
image.png
image.png


递归法

咋眼一看,这道题目用递归的话就就一直向左遍历,最后一个就是答案呗?
没有这么简单,一直向左遍历到最后一个,它未必是最后一行啊。

我们来分析一下题目:在树的最后一行找到最左边的值
首先要是最后一行,然后是最左边的值。
如果使用递归法,如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。
如果对二叉树深度和高度还有点疑惑的话,请看:110.平衡二叉树(opens new window)
所以要找深度最大的叶子节点。

那么如果找最左边的呢?可以使用前序遍历,这样才先优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值。

递归三部曲:

  1. 确定递归函数的参数和返回值

参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。
本题还需要类里的两个全局变量,maxLen用来记录最大深度,maxleftValue记录最大深度最左节点的数值。

  1. int maxLen = INT_MIN; // 全局变量 记录最大深度
  2. int maxleftValue; // 全局变量 最大深度最左节点的数值
  3. void traversal(TreeNode* root, int leftLen)

有的同学可能疑惑,为啥不能递归函数的返回值返回最长深度呢?
其实很多同学都对递归函数什么时候要有返回值,什么时候不能有返回值很迷茫。
如果需要遍历整棵树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!
初学者可能对这个结论不太理解,别急,后面我会安排一道题目专门讲递归函数的返回值问题。这里大家暂时先了解一下。
本题我们是要遍历整个树找到最深的叶子节点,需要遍历整棵树,所以递归函数没有返回值。

  1. 确定终止条件

当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度。

  1. if (root->left == NULL && root->right == NULL) {
  2. if (leftLen > maxLen) {
  3. maxLen = leftLen; // 更新最大深度
  4. maxleftValue = root->val; // 最大深度最左面的数值
  5. }
  6. return;
  7. }
  1. 确定单层递归的逻辑

在找最大深度的时候,递归的过程中依然要使用回溯,代码如下

  1. if (root->left) { // 左
  2. leftLen++; // 深度加一
  3. traversal(root->left, leftLen);
  4. leftLen--; // 回溯,深度减一
  5. }
  6. if (root->right) { // 右
  7. leftLen++; // 深度加一
  8. traversal(root->right, leftLen);
  9. leftLen--; // 回溯,深度减一
  10. }
  11. return;

完整C++代码

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int maxLen = INT_MIN; // 全局变量 记录最大深度
  15. int maxLeftValue = 0; // 全局变量 最大深度最左节点的数值
  16. void traversal(TreeNode* root, int leftLen){
  17. if(root->left == NULL && root->right == NULL){
  18. if(leftLen > maxLen){
  19. maxLen = leftLen;
  20. maxLeftValue = root->val; // 中
  21. }
  22. }
  23. if(root->left){ // 左
  24. traversal(root->left, leftLen + 1); // 回溯
  25. }
  26. if(root->right){ // 右
  27. traversal(root->right, leftLen + 1); // 回溯
  28. }
  29. }
  30. int findBottomLeftValue(TreeNode* root) {
  31. traversal(root, 1); // 根节点深度视为1
  32. return maxLeftValue;
  33. }
  34. };

迭代法:

本题使用层序遍历再合适不过了,比递归要好理解的多!
只需要记录最后一行第一个节点的数值就可以了。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int findBottomLeftValue(TreeNode* root) {
  15. int result = 0;
  16. queue<TreeNode*> que;
  17. que.push(root);
  18. while(!que.empty()){
  19. int size = que.size();
  20. for(int i = 0; i < size; i++){
  21. TreeNode* cur = que.front();
  22. que.pop();
  23. if(i == 0) result = cur->val;
  24. if(cur->left) que.push(cur->left);
  25. if(cur->right) que.push(cur->right);
  26. }
  27. }
  28. return result;
  29. }
  30. };

19、路径总和

112. 路径总和 - 力扣(LeetCode)
image.png
image.png


递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先(opens new window)中介绍)
  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

C++完整代码

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution{
  13. public:
  14. bool traversal(TreeNode* root, int count){
  15. if(root->left == NULL && root->right == NULL && count == 0) return true; // 遇到叶子节点,并且计数为0
  16. if(root->left == NULL && root->right == NULL) return false; // 遇到叶子节点直接返回
  17. if(root->left){ // 左
  18. // 递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。
  19. if(traversal(root->left, count - root->left->val)){
  20. return true;
  21. }
  22. }
  23. if(root->right){ // 右
  24. // 递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。
  25. if(traversal(root->right, count - root->right->val)){
  26. return true;
  27. }
  28. }
  29. return false;
  30. }
  31. bool hasPathSum(TreeNode* root, int targetSum){
  32. if(root == NULL) return false;
  33. return traversal(root, targetSum - root->val);
  34. }
  35. };

迭代法

如果使用栈模拟递归的话,那么如果做回溯呢?
此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。
c++就我们用pair结构来存放这个栈里的元素。
定义为:pair pair<节点指针,路径数值>
这个为栈里的一个元素。
如下代码是使用栈模拟的前序遍历,如下:(详细注释)

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution{
  13. public:
  14. bool hasPathSum(TreeNode* root, int targetSum){
  15. if(root == NULL) return false;
  16. stack<pair<TreeNode*, int>> st;
  17. st.push(pair<TreeNode*, int>(root, root->val));
  18. while(!st.empty()){
  19. pair<TreeNode*, int> cur = st.top();
  20. st.pop();
  21. // 如果该节点是叶子节点了,并且该节点的路径总和等于targetSum,返回true
  22. if(cur.first->left == NULL && cur.first->right == NULL && cur.second == targetSum) return true;
  23. // 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来
  24. if(cur.first->right){ // 右
  25. st.push(pair<TreeNode*, int>(cur.first->right, cur.second + cur.first->right->val));
  26. }
  27. // 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来
  28. if(cur.first->left){ // 左
  29. st.push(pair<TreeNode*, int>(cur.first->left, cur.second + cur.first->left->val));
  30. }
  31. }
  32. return false;
  33. }
  34. };

20、路径总和 II

113. 路径总和 II - 力扣(LeetCode)
image.png
image.png


13.路径总和ii要遍历整个树,找到所有路径,所以递归函数不要返回值!

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<vector<int>> result;
  15. vector<int> path;
  16. void traversal(TreeNode* node, int targetSum){
  17. if(!node->left && !node->right && targetSum == 0){
  18. result.push_back(path);
  19. }
  20. if(!node->left && !node->right) return; // 遇到叶子节点,没有合适的边直接返回
  21. if(node->left){
  22. path.push_back(node->left->val);
  23. traversal(node->left, targetSum - node->left->val);
  24. path.pop_back();
  25. }
  26. if(node->right){
  27. path.push_back(node->right->val);
  28. traversal(node->right, targetSum - node->right->val);
  29. path.pop_back();
  30. }
  31. }
  32. vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
  33. if(root == nullptr) return result;
  34. path.push_back(root->val); // 头节点需要单独处理
  35. traversal(root, targetSum - root->val); // 头节点需要单独处理
  36. return result;
  37. }
  38. };

上面是一刷之后的代码,相对简洁,逻辑也清晰。如果忘了的花,去看卡哥的详细代码二叉树 - 18、路径总和 II

21、从中序与后序遍历序列构造二叉树

106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
image.png
image.png


思路
首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。

为什么可以这样呢?

  1. 因为后序数组最后一个元素肯定就是根节点元素,我们只需要在中序数组中找到与这个根节点相同的元素,这个元素就是中序数组的根节点,
  2. 由中序遍历的特性可知,数组的存放形式是这样的:[左子树,根节点,右子树]且此时左子树的长度肯定等于后序遍历中左子树的长度,即 [ 8, 9, 6] —> [ 8, 6, 9],右子树也是如此。
  3. 递归步骤1和2。

如果让我们肉眼看两个序列,画一棵二叉树的话,应该分分钟都可以画出来。
流程如图:
image.png
那么代码应该怎么写呢?
说到一层一层切割,就应该想到了递归。
来看一下一共分几步:

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间

C++完整代码:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* traversal(vector<int>& inorder, vector<int>& postorder){
  15. // 第1步:如果数组大小为0的话,说明是空节点了
  16. if(inorder.size() == 0 || postorder.size() == 0) return NULL;
  17. // 第2步:如果不为空,那么取后序数组最后一个元素作为节点元素
  18. int rootVal = postorder[postorder.size() - 1];
  19. TreeNode* root = new TreeNode(rootVal);
  20. // 叶子节点
  21. if(postorder.size() == 1) return root;
  22. // 第3步:找到后序数组中最后一个元素在中序数组中的位置,作为切割点
  23. int delimiterIndex = -1;
  24. for(delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++){
  25. if(inorder[delimiterIndex] == rootVal){
  26. break;
  27. }
  28. }
  29. // 第4步:切割中序数组,切成中序左数组和中序右数组
  30. // 左闭右开:[0, delimiterIndex)
  31. vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
  32. // 左闭右开:[delimiterIndex + 1, end)
  33. vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
  34. // 第5步:切割后序数组,切成后序左数组和后序右数组
  35. postorder.resize(postorder.size() - 1);
  36. // 左闭右开
  37. vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
  38. // 左闭右开
  39. vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());
  40. // 第6步:递归处理左区间和右区间
  41. root->left = traversal(leftInorder, leftPostorder);
  42. root->right = traversal(rightInorder, rightPostorder);
  43. return root;
  44. }
  45. TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
  46. if (inorder.size() == 0 || postorder.size() == 0) return nullptr;
  47. return traversal(inorder, postorder);
  48. }
  49. };

此时应该发现了,如上的代码性能并不好,应为每层递归定定义了新的vector(就是数组),既耗时又耗空间,但上面的代码是最好理解的,为了方便读者理解,所以用如上的代码来讲解。
下面给出用下标索引写出的代码版本:(思路是一样的,只不过不用重复定义vector了,每次用下标索引来分割)

C++优化版本

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. // 中序区间:[inorderBegin, inorderEnd), 后序区间: [postorderBegin, postorderEnd)
  15. TreeNode* traversal(vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& postorder, int postorderBegin, int postorderEnd){
  16. // 第1步:如果数组大小为0的话,说明是空节点了
  17. if(postorderBegin == postorderEnd) return NULL;
  18. // 第2步:如果不为空,那么取后序数组最后一个元素作为节点元素
  19. int rootVal = postorder[postorderEnd - 1];
  20. TreeNode* root = new TreeNode(rootVal);
  21. // 叶子节点
  22. if(postorderEnd - postorderBegin == 1) return root;
  23. // 第3步:找到后序数组中最后一个元素在中序数组中的位置,作为切割点
  24. int delimiterIndex = 0;
  25. for(delimiterIndex = 0; delimiterIndex < inorderEnd; delimiterIndex++){
  26. if(inorder[delimiterIndex] == rootVal){
  27. break;
  28. }
  29. }
  30. // 第4步:切割中序数组,切成中序左数组和中序右数组
  31. // 左中序区间: 左闭右开 [leftInorderBegin, leftInorderEnd)
  32. int leftInorderBegin = inorderBegin;
  33. int leftInorderEnd = delimiterIndex;
  34. // 右中序区间: 左闭右开 [rightInorderBegin, rightInorderEnd)
  35. int rightInorderBegin = delimiterIndex + 1;
  36. int rightInorderEnd = inorderEnd;
  37. // 第5步:切割后序数组,切成后序左数组和后序右数组
  38. // 后序左数组: 左闭右开 [leftPostorderBegin, leftPostorderEnd)
  39. int leftPostorderBegin = postorderBegin;
  40. int leftPostorderEnd = postorderBegin + leftInorderEnd - leftInorderBegin;
  41. // 后序右数组: 左闭右开 [rightPostorderBegin, rightPostorderEnd)
  42. int rightPostorderBegin = leftPostorderEnd;
  43. int rightPostorderEnd = postorderEnd - 1;
  44. // 第6步:递归处理左区间和右区间
  45. root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd);
  46. root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd);
  47. return root;
  48. }
  49. TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
  50. if (inorder.size() == 0 || postorder.size() == 0) return nullptr;
  51. return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size());
  52. }
  53. };

22、从前序与中序遍历序列构造二叉树

105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
image.png
image.png


106.从中序与后序遍历序列构造二叉树思路是雷同的,只是有些细节需要改动,具体见代码

C++完整代码

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* traversal(vector<int>& preorder, int PreBegin, int PreEnd, vector<int>& inorder, int inBegin, int inEnd){
  15. // 第1步:如果数组大小为0,说明是空节点了
  16. if(PreEnd - PreBegin == 0) return nullptr;
  17. // 第2步:如果不为空,那么取前序数组的第一个元素作为节点元素
  18. int rootVal = preorder[PreBegin];
  19. TreeNode* root = new TreeNode(rootVal);
  20. // 如果是叶子节点
  21. if(PreEnd - PreBegin == 1) return root;
  22. // 第3步:找到前序数组第一个元素在中序数组中的位置
  23. int delimiterIndex = 0;
  24. for(delimiterIndex = 0; delimiterIndex < inEnd; delimiterIndex++){
  25. if(inorder[delimiterIndex] == rootVal) break;
  26. }
  27. // 第4步:分割中序数组
  28. // 左中序数组
  29. int leftInBegin = inBegin;
  30. int leftInEnd = delimiterIndex;
  31. // 右中序数组
  32. int rightInBegin = leftInEnd + 1;
  33. int rightInEnd = inEnd;
  34. // 第5步:切割前序数组
  35. // 左前序数组:左闭右开->[leftPreBegin, leftPreEnd)
  36. int leftPreBegin = PreBegin + 1;
  37. int leftPreEnd = PreBegin + 1 + leftInEnd - leftInBegin;
  38. // 右前序数组:左闭右开->[rightPreBegin, rightPreEnd)
  39. int rightPreBegin = leftPreEnd;
  40. int rightPreEnd = PreEnd;
  41. // 第6步:递归操作
  42. root->left = traversal(preorder, leftPreBegin, leftPreEnd, inorder, leftInBegin, leftInEnd);
  43. root->right = traversal(preorder, rightPreBegin, rightPreEnd, inorder, rightInBegin, rightInEnd);
  44. return root;
  45. }
  46. TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
  47. if(preorder.size() == 0 || inorder.size() == 0) return nullptr;
  48. return traversal(preorder, 0, preorder.size(), inorder, 0, inorder.size());
  49. }
  50. };

23、最大二叉树

654. 最大二叉树 - 力扣(LeetCode)
image.png
image.png
image.png


这道题和 从中序与后序遍历序列构造二叉树 如出一辙,甚至比它更加简单,因为这道题不用 递归分割两个数组,只需要考虑分割一个数组的情况就行了。啥都不说,直接上代码。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* traversal(vector<int>& nums){
  15. // 第1步:如果数组大小为0,说明是空节点了
  16. if(nums.size() == 0) return nullptr;
  17. // 第2步:获得nums中的最大值
  18. int delimiterIndex = 0; // 记录数组nums中最大值的索引
  19. int rootVal = INT_MIN;
  20. for(int i = 0; i < nums.size(); i++){
  21. if(nums[i] > rootVal){
  22. rootVal = nums[i];
  23. delimiterIndex = i;
  24. }
  25. }
  26. // 第3步:创建一个根节点
  27. TreeNode* root = new TreeNode(rootVal);
  28. if(nums.end() - nums.begin() == 1) return root; // 如果是叶子节点,直接返回
  29. // 第4步:分割左右两个数组, 左闭右开区间
  30. vector<int> leftNums(nums.begin(), nums.begin() + delimiterIndex);
  31. vector<int> rightNums(nums.begin() + delimiterIndex + 1, nums.end());
  32. // 第5步:递归操作
  33. root->left = traversal(leftNums);
  34. root->right = traversal(rightNums);
  35. return root;
  36. }
  37. TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
  38. return traversal(nums);
  39. }
  40. };

跟中后序数组构造二叉树一样,这道题也可以对代码进行优化,优化的部分也是针对在递归操作中反复创建的vector对象。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* traversal(vector<int>& nums, int begin, int end){
  15. // 第1步:如果数组大小为0,说明是空节点了
  16. if(end - begin <= 0) return nullptr;
  17. // 第2步:获得nums中的最大值
  18. int delimiterIndex = begin; // 记录数组nums中最大值的索引
  19. for(int i = begin + 1; i < end; i++){
  20. if(nums[i] > nums[delimiterIndex]){
  21. delimiterIndex = i;
  22. }
  23. }
  24. // 第3步:创建一个根节点
  25. TreeNode* root = new TreeNode(nums[delimiterIndex]);
  26. if(end - begin == 1) return root; // 如果是叶子节点,直接返回
  27. // 第4步:分割左右两个数组, 左闭右开区间
  28. int leftbegin = begin;
  29. int leftend = delimiterIndex;
  30. int rightbegin = delimiterIndex + 1;
  31. int rightend = end;
  32. // 第5步:递归操作
  33. root->left = traversal(nums, leftbegin, leftend);
  34. root->right = traversal(nums, rightbegin, rightend);
  35. return root;
  36. }
  37. TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
  38. return traversal(nums, 0, nums.size());
  39. }
  40. };

注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。
一些同学也会疑惑,什么时候递归函数前面加if,什么时候不加if,这个问题我在最后也给出了解释。
其实就是不同代码风格的实现,一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。

24、二叉树周末总结

代码随想录 - 二叉树 - 21、二叉树周末总结

25、合并二叉树

617. 合并二叉树 - 力扣(LeetCode)
image.png
image.png


相信这道题目很多同学疑惑的点是如何同时遍历两个二叉树呢?
其实和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。

递归

二叉树使用递归,就要想使用前中后哪种遍历方式?
本题使用哪种遍历都是可以的!
我们下面以前序遍历为例。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* combine(TreeNode* root1, TreeNode* root2){
  15. // 递归出口
  16. if(root1 == nullptr) return root2; // 如果root1为空,合并之后就应该是root2
  17. if(root2 == nullptr) return root1; // 如果root2为空,合并之后就应该是root1
  18. // 单层递归逻辑
  19. if(root1 != nullptr && root2 != nullptr){ // 中
  20. root1->val = root1->val + root2->val;
  21. }
  22. root1->left = combine(root1->left, root2->left); // 左
  23. root1->right = combine(root1->right, root2->right); // 右
  24. return root1;
  25. }
  26. TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
  27. if(root1 == nullptr && root2 == nullptr) return nullptr;
  28. return combine(root1, root2);
  29. }
  30. };

中序后序也可以,改一下代码顺序而已。但是前序遍历是最好理解的,我建议大家用前序遍历来做就OK。
如上的方法修改了t1的结构,当然也可以不修改t1和t2的结构,重新定一个树。

不修改输入树的结构,前序遍历,代码如下:

  1. class Solution {
  2. public:
  3. TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
  4. if (t1 == NULL) return t2;
  5. if (t2 == NULL) return t1;
  6. // 重新定义新的节点,不修改原有两个树的结构
  7. TreeNode* root = new TreeNode(0);
  8. root->val = t1->val + t2->val;
  9. root->left = mergeTrees(t1->left, t2->left);
  10. root->right = mergeTrees(t1->right, t2->right);
  11. return root;
  12. }
  13. };

迭代法

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
  15. if(root1 == nullptr) return root2;
  16. if(root2 == nullptr) return root1;
  17. queue<TreeNode*> que;
  18. que.push(root1);
  19. que.push(root2);
  20. while(!que.empty()){
  21. TreeNode* node1 = que.front(); que.pop();
  22. TreeNode* node2 = que.front(); que.pop();
  23. // 后面的程序对非空节点不能入队,所以这里node1和node2肯定不为空
  24. node1->val = node1->val + node2->val;
  25. // 如果node1->left和node2->left都不为空,那么一起入队
  26. if(node1->left && node2->left){
  27. que.push(node1->left);
  28. que.push(node2->left);
  29. }
  30. // 如果node1->right和node2->right都不为空,那么一起入队
  31. if(node1->right && node2->right){
  32. que.push(node1->right);
  33. que.push(node2->right);
  34. }
  35. // 如果node1->left为空,node2->left不为空,将node2->right赋给node1->left
  36. if(!node1->left && node2->left){
  37. node1->left = node2->left;
  38. }
  39. // 如果node1->right为空,node2->right不为空,将node2->right赋给node1->right
  40. if(!node1->right && node2->right){
  41. node1->right = node2->right;
  42. }
  43. }
  44. return root1;
  45. }
  46. };

合并二叉树,也是二叉树操作的经典题目,如果没有接触过的话,其实并不简单,因为我们习惯了操作一个二叉树,一起操作两个二叉树,还会有点懵懵的。
这不是我们第一次操作两棵二叉树了,在二叉树:我对称么?(opens new window)中也一起操作了两棵二叉树。
迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。

26、二叉树搜索树中的搜索

700. 二叉搜索树中的搜索 - 力扣(LeetCode)
image.png
image.png


递归法
二叉搜索树是一个有序树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉搜索树

这就决定了,二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。

  1. 确定递归函数的参数以及返回值
  2. 确定终止条件 如果root为空,或者找到这个数值了,就返回root节点。
  3. 确定单层递归逻辑 如果找到了如何条件的节点,return 即可
    1. /**
    2. * Definition for a binary tree node.
    3. * struct TreeNode {
    4. * int val;
    5. * TreeNode *left;
    6. * TreeNode *right;
    7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
    8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
    10. * };
    11. */
    12. class Solution {
    13. public:
    14. TreeNode* searchBST(TreeNode* root, int val) {
    15. if(root == nullptr || root->val == val) return root;
    16. if(root->val < val) return searchBST(root->right, val);
    17. else if(root->val > val) return searchBST(root->left, val);
    18. return nullptr;
    19. }
    20. };
    使用二叉搜索树解出来之后,尝试了以下用普通二叉树的解法来解,发现在保证代码简洁的情况下似乎不能解出来(当然,如果硬解肯定是可以的,但是如果变复杂了就觉得没有必要)。
    但事实上,我是傻逼,自己功力不够而已,二叉树递归法代码也相当简洁
    1. /**
    2. * Definition for a binary tree node.
    3. * struct TreeNode {
    4. * int val;
    5. * TreeNode *left;
    6. * TreeNode *right;
    7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
    8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
    10. * };
    11. */
    12. class Solution {
    13. public:
    14. TreeNode* searchBST(TreeNode* root, int val) {
    15. if(root == NULL || root->val == val) return root;
    16. TreeNode* left = searchBST(root->left, val);
    17. if(left != NULL){
    18. return left;
    19. }
    20. return searchBST(root->right, val);
    21. }
    22. };

迭代法

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* searchBST(TreeNode* root, int val) {
  15. while(root != NULL){
  16. if(root->val > val) root = root->left;
  17. else if(root->val < val) root = root->right;
  18. else if(root->val == val) return root;
  19. }
  20. return NULL;
  21. }
  22. };

代码简单的让人痛哭流涕。

27、验证二叉搜索树

98. 验证二叉搜索树 - 力扣(LeetCode)
image.png
image.png


要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。
有了这个特性,验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。

27.1、递归法

可以递归中序遍历将二叉搜索树转变成一个数组,然后只要比较一下,这个数组是否是有序的,注意二叉搜索树中不能有重复元素

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<int> vec;
  15. void traversal(TreeNode* root){
  16. if(root == nullptr) return;
  17. traversal(root->left);
  18. vec.push_back(root->val); // 将二叉搜索树转换为有序数组
  19. traversal(root->right);
  20. }
  21. bool isValidBST(TreeNode* root) {
  22. vec.clear();
  23. traversal(root);
  24. for(int i = 1; i < vec.size(); i++){
  25. if(vec[i] <= vec[i - 1]) return false;
  26. }
  27. return true;
  28. }
  29. };

以上代码中,我们把二叉树转变为数组来判断,是最直观的,但其实不用转变成数组,可以在递归遍历的过程中直接判断是否有序。

这道题目比较容易陷入两个陷阱:

  • 陷阱1

不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了
写出了类似这样的代码:

  1. if (root->val > root->left->val && root->val < root->right->val) {
  2. return true;
  3. } else {
  4. return false;
  5. }

我二刷的时候居然还是写出这样的代码!真是绝望!

我们要比较的是 左子树所有节点小于中间节点,右子树所有节点大于中间节点。所以以上代码的判断逻辑是错误的。
例如: [10,5,15,null,null,6,20] 这个case:
image.png

  • 陷阱2

样例中最小节点 可能是int的最小值,如果这样使用最小的int来比较也是不行的。
此时可以初始化比较元素为longlong的最小值。
问题可以进一步演进:如果样例中根节点的val 可能是longlong的最小值 又要怎么办呢?文中会解答。

递归三部曲:

  • 确定递归函数,返回值以及参数

要定义一个longlong的全局变量,用来比较遍历的节点是否有序,因为后台测试数据中有int最小值,所以定义为longlong的类型,初始化为longlong最小值。

注意递归函数要有bool类型的返回值, 我们在二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?(opens new window)中讲了,只有寻找某一条边(或者一个节点)的时候,递归函数会有bool类型的返回值。

其实本题是同样的道理,我们在寻找一个不符合条件的节点,如果没有找到这个节点就遍历了整个树,如果找到不符合的节点了,立刻返回。

  1. long long maxVal = LONG_MIN; // 因为后台测试数据中有int最小值
  2. bool isValidBST(TreeNode* root)
  • 确定终止条件

如果是空节点 是不是二叉搜索树呢?
是的,二叉搜索树也可以为空!

  1. if (root == NULL) return true;
  • 确定单层递归的逻辑

中序遍历,一直更新maxVal,一旦发现maxVal >= root->val,就返回false,注意元素相同时候也要返回false。

  1. bool left = isValidBST(root->left); // 左
  2. // 中序遍历,验证遍历的元素是不是从小到大
  3. if (maxVal < root->val) maxVal = root->val; // 中
  4. else return false;
  5. bool right = isValidBST(root->right); // 右
  6. return left && right;

整体代码如下:

  1. class Solution {
  2. public:
  3. long long maxVal = LONG_MIN; // 因为后台测试数据中有int最小值
  4. bool isValidBST(TreeNode* root) {
  5. if (root == NULL) return true;
  6. bool left = isValidBST(root->left);
  7. // 中序遍历,验证遍历的元素是不是从小到大
  8. if (maxVal < root->val) maxVal = root->val;
  9. else return false;
  10. bool right = isValidBST(root->right);
  11. return left && right;
  12. }
  13. };

以上代码是因为后台数据有int最小值测试用例,所以都把maxVal改成了longlong最小值。
如果测试数据中有 longlong的最小值,怎么办?
不可能在初始化一个更小的值了吧。 建议避免 初始化最小值,如下方法取到最左面节点的数值来比较。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* pre = nullptr; // 用来记录前一个节点
  15. bool isValidBST(TreeNode* root) {
  16. if(root == nullptr) return true; // 空树也是一棵二叉搜索树
  17. bool left = isValidBST(root->left); // 左
  18. // 中序遍历,验证遍历的元素是不是从小到大
  19. if(pre && pre->val >= root->val) return false; // 中
  20. pre = root;
  21. bool right = isValidBST(root->right); // 右
  22. return left && right;
  23. }
  24. };

27.2、迭代法

可以用迭代法模拟二叉树中序遍历,对前中后序迭代法生疏的同学可以看这两篇二叉树:听说递归能做的,栈也能做!(opens new window)二叉树:前中后序迭代方式统一写法(opens new window)
迭代法中序遍历稍加改动就可以了,代码如下:

  1. class Solution {
  2. public:
  3. bool isValidBST(TreeNode* root) {
  4. stack<TreeNode*> st;
  5. TreeNode* cur = root;
  6. TreeNode* pre = NULL; // 记录前一个节点
  7. while (cur != NULL || !st.empty()) {
  8. if (cur != NULL) {
  9. st.push(cur);
  10. cur = cur->left; // 左
  11. } else {
  12. cur = st.top(); // 中
  13. st.pop();
  14. if (pre != NULL && cur->val <= pre->val)
  15. return false;
  16. pre = cur; //保存前一个访问的结点
  17. cur = cur->right; // 右
  18. }
  19. }
  20. return true;
  21. }
  22. };

卡哥用的是非统一风格的中序遍历代码,下面我给出统一风格代码的中序遍历解法

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. bool isValidBST(TreeNode* root) {
  15. stack<TreeNode*> st;
  16. st.push(root);
  17. TreeNode* pre = nullptr;
  18. while(!st.empty()){
  19. TreeNode* cur = st.top();
  20. if(cur != nullptr){
  21. st.pop();
  22. if(cur->right) st.push(cur->right); // 右
  23. st.push(cur); // 中
  24. st.push(nullptr);
  25. if(cur->left) st.push(cur->left); // 左
  26. }
  27. else{
  28. st.pop(); // 弹出标记的节点 nullptr
  29. if(pre != nullptr && pre->val >= st.top()->val){
  30. return false;
  31. }
  32. pre = st.top(); // 记录前一个节点
  33. st.pop(); // 弹出当前节点
  34. }
  35. }
  36. return true;
  37. }
  38. };

28、二叉搜索树的最小值绝对差

530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)
image.png
image.png


这道题的思路和验证二叉搜索树相似,同样不能只是比较父节点和它的左右孩子节点的差,因为有可能父节点与孙子节点的差值更小,在验证二叉搜索树中也有相同的解法,不懂往上翻,话不多说,直接上代码。

二叉搜索树还得是中序遍历

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. vector<int> vec;
  15. void traversal(TreeNode* root){
  16. if(root == nullptr) return;
  17. traversal(root->left);
  18. vec.push_back(root->val);
  19. traversal(root->right);
  20. }
  21. int getMinimumDifference(TreeNode* root) {
  22. vec.clear();
  23. traversal(root);
  24. int result = INT_MAX;
  25. for(int i = 1; i < vec.size(); i++){
  26. if(vec[i] - vec[i - 1] < result){
  27. result = vec[i] - vec[i - 1];
  28. }
  29. }
  30. return result;
  31. }
  32. };

使用pre指针

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int result = INT_MAX;
  15. TreeNode* pre = nullptr;
  16. void traversal(TreeNode* root){
  17. if(root == nullptr) return;
  18. traversal(root->left);
  19. if(pre != nullptr){
  20. result = min(result, abs(pre->val - root->val));
  21. }
  22. pre = root; // 跟新pre指针,这一步比较难想到
  23. traversal(root->right);
  24. return;
  25. }
  26. int getMinimumDifference(TreeNode* root) {
  27. traversal(root);
  28. return result;
  29. }
  30. };

迭代法
迭代法的思路也是和验证二叉搜索树一样的

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. int getMinimumDifference(TreeNode* root) {
  15. stack<TreeNode*> st;
  16. st.push(root);
  17. int result = INT_MAX;
  18. TreeNode* pre = nullptr;
  19. while(!st.empty()){
  20. TreeNode* cur = st.top();
  21. if(cur != nullptr){
  22. st.pop();
  23. if(cur->right) st.push(cur->right);
  24. st.push(cur);
  25. st.push(nullptr); // 设置标志位
  26. if(cur->left) st.push(cur->left);
  27. }
  28. else{
  29. st.pop(); // 弹出nullptr
  30. if(pre != nullptr){
  31. result = min(result, abs(pre->val - st.top()->val));
  32. }
  33. pre = st.top(); // 更新pre指针
  34. st.pop();
  35. }
  36. }
  37. return result;
  38. }
  39. };

29、二叉搜索树中的众数

501. 二叉搜索树中的众数 - 力扣(LeetCode)
image.png
image.png


29.1、递归法


如果不是二叉搜索树

如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合。
具体步骤如下:

  1. 这个树都遍历了,用map统计频率

至于用前中后序那种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病!
这里采用前序遍历,代码如下:

  1. // map<int, int> key:元素,value:出现频率
  2. void searchBST(TreeNode* cur, unordered_map<int, int>& map) { // 前序遍历
  3. if (cur == NULL) return ;
  4. map[cur->val]++; // 统计元素频率
  5. searchBST(cur->left, map);
  6. searchBST(cur->right, map);
  7. return ;
  8. }
  1. 把统计的出来的出现频率(即map中的value)排个序

有的同学可能可以想直接对map中的value排序,还真做不到,C++中如果使用std::map或者std::multimap可以对key排序,但不能对value排序。
所以要把map转化数组即vector,再进行排序,当然vector里面放的也是pair类型的数据,第一个int为元素,第二个int为出现频率。
代码如下:

  1. bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
  2. return a.second > b.second; // 按照频率从大到小排序
  3. }
  4. vector<pair<int, int>> vec(map.begin(), map.end());
  5. sort(vec.begin(), vec.end(), cmp); // 给频率排个序
  1. 取前面高频的元素

此时数组vector中已经是存放着按照频率排好序的pair,那么把前面高频的元素取出来就可以了。
代码如下

  1. result.push_back(vec[0].first);
  2. for (int i = 1; i < vec.size(); i++) {
  3. // 取最高的放到result数组中
  4. if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
  5. else break;
  6. }
  7. return result;

整体C++代码如下:

  1. class Solution {
  2. public:
  3. // 前序遍历
  4. void searchBST(TreeNode* root, unordered_map<int, int>& map){
  5. if(root == nullptr) return;
  6. map[root->val]++; // 中
  7. searchBST(root->left, map); // 左
  8. searchBST(root->right, map); // 中
  9. }
  10. // 自定义sort比较规则, 按照频率大小从大到小排序
  11. bool static cmp(const pair<int, int>& a, const pair<int, int>& b){
  12. return a.second > b.second;
  13. }
  14. vector<int> findMode(TreeNode* root) {
  15. unordered_map<int, int> map;
  16. vector<int> result;
  17. if(root == nullptr) return result;
  18. searchBST(root, map);
  19. vector<pair<int, int>> vec(map.begin(), map.end());
  20. // 对map进行排序
  21. sort(vec.begin(), vec.end(), cmp);
  22. for(int i = 0; i < vec.size(); i++){
  23. if(vec[i].second == vec[0].second){
  24. result.push_back(vec[i].first);
  25. }else break;
  26. }
  27. return result;
  28. }
  29. };

所以如果本题没有说是二叉搜索树的话,那么就按照上面的思路写!

是二叉搜索树

既然是搜索树,它中序遍历就是有序的

中序遍历代码如下:

  1. void searchBST(TreeNode* cur) {
  2. if (cur == NULL) return ;
  3. searchBST(cur->left); // 左
  4. (处理节点) // 中
  5. searchBST(cur->right); // 右
  6. return ;
  7. }

遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。
关键是在有序数组上的话,好搞,在树上怎么搞呢?
这就考察对树的操作了。
二叉树:搜索树的最小绝对差(opens new window)中我们就使用了pre指针和cur指针的技巧,这次又用上了。
弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。
而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。
代码如下:

  1. if (pre == NULL) { // 第一个节点
  2. count = 1; // 频率为1
  3. } else if (pre->val == cur->val) { // 与前一个节点数值相同
  4. count++;
  5. } else { // 与前一个节点数值不同
  6. count = 1;
  7. }
  8. pre = cur; // 更新上一个节点

此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办?
应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合。(因为众数有多个)
这种方式遍历了两遍数组。
那么我们遍历两遍二叉搜索树,把众数集合算出来也是可以的。
但这里其实只需要遍历一次就可以找到所有的众数。
那么如何只遍历一遍呢?
如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),代码如下:

  1. if (count == maxCount) { // 如果和最大值相同,放进result中
  2. result.push_back(cur->val);
  3. }

是不是感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大频率呢。
所以下面要做如下操作:
频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。

  1. if (count > maxCount) { // 如果计数大于最大值
  2. maxCount = count; // 更新最大频率
  3. result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
  4. result.push_back(cur->val);
  5. }

关键代码都讲完了,完整代码如下:(只需要遍历一遍二叉搜索树,就求出了众数的集合

  1. lass Solution {
  2. private:
  3. int maxCount; // 最大频率
  4. int count; // 统计频率
  5. TreeNode* pre;
  6. vector<int> result;
  7. void searchBST(TreeNode* cur) {
  8. if (cur == NULL) return ;
  9. searchBST(cur->left); // 左
  10. // 中
  11. if (pre == NULL) { // 第一个节点
  12. count = 1;
  13. } else if (pre->val == cur->val) { // 与前一个节点数值相同
  14. count++;
  15. } else { // 与前一个节点数值不同
  16. count = 1;
  17. }
  18. pre = cur; // 更新上一个节点
  19. if (count == maxCount) { // 如果和最大值相同,放进result中
  20. result.push_back(cur->val);
  21. }
  22. if (count > maxCount) { // 如果计数大于最大值频率
  23. maxCount = count; // 更新最大频率
  24. result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
  25. result.push_back(cur->val);
  26. }
  27. searchBST(cur->right); // 右
  28. return ;
  29. }
  30. public:
  31. vector<int> findMode(TreeNode* root) {
  32. count = 0;
  33. maxCount = 0;
  34. TreeNode* pre = NULL; // 记录前一个节点
  35. result.clear();
  36. searchBST(root);
  37. return result;
  38. }
  39. };

29.2、迭代法

只要把中序遍历转成迭代,中间节点的处理逻辑完全一样。
二叉树前中后序转迭代,传送门:

下面我给出其中的一种中序遍历的迭代法,其中间处理逻辑一点都没有变(我从递归法直接粘过来的代码,连注释都没改,哈哈)

这里我使用统一风格的代码的中序遍历

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. private:
  14. int maxCount;
  15. int count;
  16. vector<int> result;
  17. TreeNode* pre;
  18. public:
  19. void searchBST(TreeNode* cur){
  20. if(cur == nullptr) return;
  21. searchBST(cur->left); // 左
  22. // 中
  23. if(pre == nullptr){ // 第一个节点
  24. count = 1;
  25. }
  26. if(pre && pre->val == cur->val){ // 与前一个节点数值相同
  27. count++;
  28. }
  29. else{ // 与前一个节点数值不同
  30. count = 1;
  31. }
  32. pre = cur;
  33. if(count == maxCount){ // 如果和最大频率相同,那么记录它
  34. result.push_back(cur->val);
  35. }
  36. if(count > maxCount){ // 如果计数大于最大频率
  37. maxCount = count; // 更新最大频率
  38. result.clear(); // 需要对之前记录的结果清除,很关键的一步
  39. result.push_back(cur->val); // 记录新的出现频率最高的元素
  40. }
  41. searchBST(cur->right); // 右
  42. }
  43. vector<int> findMode(TreeNode* root) {
  44. maxCount = 0;
  45. count = 0;
  46. result.clear();
  47. pre = nullptr;
  48. searchBST(root);
  49. return result;
  50. }
  51. };

30、二叉树的最近公共祖先⭐

236. 二叉树的最近公共祖先 - 力扣(LeetCode)
image.png
image.png


遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。
那么二叉树如何可以自底向上查找呢?

回溯啊,二叉树回溯的过程就是从低到上。
后序遍历就是天然的回溯过程,最先处理的一定是叶子节点。

接下来就看如何判断一个节点是节点q和节点p的公共公共祖先呢。
首先最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。
但是很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q(p)。

使用后序遍历,回溯的过程,就是从低向上遍历节点,一旦发现满足第一种情况的节点,就是最近公共节点了。
但是如果p或者q本身就是最近公共祖先呢?其实只需要找到一个节点是p或者q的时候,直接返回当前节点,无需继续递归子树。如果接下来的遍历中找到了后继节点满足第一种情况则修改返回值为后继节点,否则,继续返回已找到的节点即可。为什么满足第一种情况的节点一定是p或q的后继节点呢?大家可以仔细思考一下。

递归三部曲:

  • 确定递归函数返回值以及参数

需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。
但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。

  1. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
  • 确定终止条件

如果找到了 节点p或者q,或者遇到空节点,就返回。

  1. if (root == q || root == p || root == NULL) return root;
  • 确定单层递归逻辑

值得注意的是 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。
我们在二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?(opens new window)中说了 递归函数有返回值就是要遍历某一条边,但有返回值也要看如何处理返回值!
如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢?
搜索一条边的写法:

  1. if (递归函数(root->left)) return ;
  2. if (递归函数(root->right)) return ;

搜索整个树写法:

  1. left = 递归函数(root->left);
  2. right = 递归函数(root->right);
  3. leftright的逻辑处理;

看出区别了没?

在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)
那么为什么要遍历整棵树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。
如图:
image.png
就像图中一样直接返回7,多美滋滋。
但事实上还要遍历根节点右子树(即使此时已经找到了目标节点了),也就是图中的节点4、15、20。
因为在如下代码的后序遍历中,如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回。

  1. left = 递归函数(root->left);
  2. right = 递归函数(root->right);
  3. leftright的逻辑处理;

所以此时大家要知道我们要遍历整棵树。知道这一点,对本题就有一定深度的理解了。
那么先用left和right接住左子树和右子树的返回值,代码如下:

  1. TreeNode* left = lowestCommonAncestor(root->left, p, q);
  2. TreeNode* right = lowestCommonAncestor(root->right, p, q);

如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解
如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然
这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢?
如图:
image.png
图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去!
这里点也很重要,可能刷过这道题目的同学,都不清楚结果究竟是如何从底层一层一层传到头结点的。
那么如果left和right都为空,则返回left或者right都是可以的,也就是返回空。
代码如下:

  1. if (left == NULL && right != NULL) return right;
  2. else if (left != NULL && right == NULL) return left;
  3. else { // (left == NULL && right == NULL)
  4. return NULL;
  5. }

那么寻找最小公共祖先,完整流程图如下:
image.png
从图中,大家可以看到,我们是如何回溯遍历整棵二叉树,将结果返回给头结点的!
整体代码如下:

  1. class Solution {
  2. public:
  3. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
  4. if (root == q || root == p || root == NULL) return root;
  5. TreeNode* left = lowestCommonAncestor(root->left, p, q);
  6. TreeNode* right = lowestCommonAncestor(root->right, p, q);
  7. if (left != NULL && right != NULL) return root;
  8. if (left == NULL && right != NULL) return right;
  9. else if (left != NULL && right == NULL) return left;
  10. else { // (left == NULL && right == NULL)
  11. return NULL;
  12. }
  13. }
  14. };

稍加精简,代码如下:

  1. class Solution {
  2. public:
  3. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
  4. if (root == q || root == p || root == NULL) return root;
  5. TreeNode* left = lowestCommonAncestor(root->left, p, q);
  6. TreeNode* right = lowestCommonAncestor(root->right, p, q);
  7. if (left != NULL && right != NULL) return root;
  8. if (left == NULL) return right;
  9. return left;
  10. }
  11. };

总结
这道题目刷过的同学未必真正了解这里面回溯的过程,以及结果是如何一层一层传上去的。
那么我给大家归纳如下三点

  1. 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从低向上的遍历方式。
  2. 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。
  3. 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。

可以说这里每一步,都是有难度的,都需要对二叉树,递归和回溯有一定的理解。
本题没有给出迭代法,因为迭代法不适合模拟回溯的过程。理解递归的解法就够了。

31、二叉树周末总结

代码随想录 - 二叉树 - 28、二叉树周末总结

32、二叉搜索树的最近公共祖先⭐

235. 二叉搜索树的最近公共祖先 - 力扣(LeetCode)
image.png
image.png


递归法

做过二叉树:公共祖先问题(opens new window)题目的同学应该知道,利用回溯从底向上搜索,遇到一个节点的左子树里有p,右子树里有q,那么当前节点就是最近公共祖先。
那么本题是二叉搜索树,二叉搜索树是有序的,那得好好利用一下这个特点。
在有序树里,如果判断一个节点的左子树里有p,右子树里有q呢?
其实只要从上到下遍历的时候,cur节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先了。
理解这一点,本题就很好解了。
二叉树:公共祖先问题(opens new window)不同,普通二叉树求最近公共祖先需要使用回溯,从底向上来查找,二叉搜索树就不用了,因为搜索树有序(相当于自带方向),那么只要从上向下遍历就可以了。
那么我们可以采用前序遍历(其实这里没有中节点的处理逻辑,遍历顺序无所谓了)。


递归三部曲如下:

  • 确定递归函数返回值以及参数

参数就是当前节点,以及两个结点 p、q。
返回值是要返回最近公共祖先,所以是TreeNode * 。

  1. TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q)
  • 确定终止条件

其实都不需要这个终止条件,因为题目中说了p、q 为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。

  1. if (cur == NULL) return cur;
  • 确定单层递归的逻辑

在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭又闭)
那么如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。
需要注意的是此时不知道p和q谁大,所以两个都要判断

  1. if (cur->val > p->val && cur->val > q->val) {
  2. TreeNode* left = traversal(cur->left, p, q);
  3. if (left != NULL) {
  4. return left;
  5. }
  6. }

细心的同学会发现,在这里调用递归函数的地方,把递归函数的返回值left,直接return
二叉树:公共祖先问题(opens new window)中,如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树。
搜索一条边的写法:

  1. if (递归函数(root->left)) return ;
  2. if (递归函数(root->right)) return ;

搜索整个树写法:

  1. left = 递归函数(root->left);
  2. right = 递归函数(root->right);
  3. leftright的逻辑处理;

本题就是标准的搜索一条边的写法,遇到递归函数的返回值,如果不为空,立刻返回。
如果 cur->val 小于 p->val,同时 cur->val 小于 q->val,那么就应该向右遍历(目标区间在右子树)。

  1. if (cur->val < p->val && cur->val < q->val) {
  2. TreeNode* right = traversal(cur->right, p, q);
  3. if (right != NULL) {
  4. return right;
  5. }
  6. }

剩下的情况,就是cur节点在区间(p->val <= cur->val && cur->val <= q->val)或者 (q->val <= cur->val && cur->val <= p->val)中,那么cur就是最近公共祖先了,直接返回cur。
代码如下:

  1. return cur;

那么整体递归代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode(int x) : val(x), left(NULL), right(NULL) {}
  8. * };
  9. */
  10. class Solution {
  11. public:
  12. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
  13. if(root == NULL) return root;
  14. if(root->val > p->val && root->val > q->val){ // 左
  15. TreeNode* left = lowestCommonAncestor(root->left, p, q);
  16. if(left != NULL) return left;
  17. }
  18. if(root->val < p->val && root->val < q->val){ // 右
  19. TreeNode* right = lowestCommonAncestor(root->right, p, q);
  20. if(right != NULL) return right;
  21. }
  22. return root;
  23. }
  24. };

对代码进行简化

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode(int x) : val(x), left(NULL), right(NULL) {}
  8. * };
  9. */
  10. class Solution {
  11. public:
  12. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
  13. if(root == NULL) return root;
  14. if(root->val > p->val && root->val > q->val){
  15. if(left != NULL) return lowestCommonAncestor(root->left, p, q);
  16. }
  17. if(root->val < p->val && root->val < q->val){
  18. if(right != NULL) return lowestCommonAncestor(root->right, p, q);
  19. }
  20. return root;
  21. }
  22. };

迭代法
对于二叉搜索树的迭代法,大家应该在二叉树:二叉搜索树登场!(opens new window)就了解了。
利用其有序性,迭代的方式还是比较简单的,解题思路在递归中已经分析了。
迭代代码如下:

  1. class Solution {
  2. public:
  3. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
  4. stack<TreeNode*> st;
  5. st.push(root);
  6. while(!st.empty()){
  7. TreeNode* cur = st.top(); // 中
  8. st.pop();
  9. if(cur->val > p->val && cur->val > q->val){ // 左
  10. st.push(cur->left);
  11. }
  12. else if(cur->val < p->val && cur->val < q->val){// 右
  13. st.push(cur->right);
  14. }
  15. else return cur;
  16. }
  17. return NULL;
  18. }
  19. };

这是我写的,果然还是功力不到位,其实可以利用辅助空间的。

看看卡哥写的

  1. class Solution {
  2. public:
  3. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
  4. while(root){
  5. if(root->val > p->val && root->val > q->val){ // 左
  6. root = root->left;
  7. }
  8. else if(root->val < p->val && root->val < q->val){// 右
  9. root = root->right;
  10. }
  11. else return root;
  12. }
  13. return NULL;
  14. }
  15. };

什么他妈的叫极简!

总结
对于二叉搜索树的最近祖先问题,其实要比普通二叉树公共祖先问题(opens new window)简单的多。
不用使用回溯,二叉搜索树自带方向性,可以方便的从上向下查找目标区间,遇到目标区间内的节点,直接返回。
最后给出了对应的迭代法,二叉搜索树的迭代法甚至比递归更容易理解,也是因为其有序性(自带方向性),按照目标区间找就行了。

33、二叉搜索树中的插入操作

701. 二叉搜索树中的插入操作 - 力扣(LeetCode)
image.png
image.png
image.png


递归法
其实这道题目其实是一道简单题目,但是题目中的提示:有多种有效的插入方式,还可以重构二叉搜索树,一下子吓退了不少人,瞬间感觉题目复杂了很多。
其实可以不考虑题目中提示所说的改变树的结构的插入方式。
如下演示视频中可以看出:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。
二叉树 - 图83
例如插入元素10 ,需要找到末尾节点插入便可,一样的道理来插入元素15,插入元素0,插入元素6,需要调整二叉树的结构么? 并不需要。
只要遍历二叉搜索树,找到空节点 插入元素就可以了,那么这道题其实就简单了。
接下来就是遍历二叉搜索树的过程了。

  • 确定递归函数的参数以及返回值

参数就是根节点指针,以及要插入元素,这里递归函数要不要有返回值呢?
可以有,也可以没有,但递归函数如果没有返回值的话,实现是比较麻烦的,下面也会给出其具体实现代码。
有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作。(下面会进一步解释)

  • 确定终止条件

终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。

  • 确定单层递归的逻辑

此时要明确,需要遍历整棵树么?
别忘了这是搜索树,遍历整棵搜索树简直是对搜索树的侮辱,哈哈。
搜索树是有方向了,可以根据插入元素的数值,决定递归方向。

完整C++代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* insertIntoBST(TreeNode* root, int val) {
  15. if(root == nullptr){ // 遍历的节点为null的时候,就是要插入节点的位置
  16. TreeNode* node = new TreeNode(val);
  17. return node;
  18. };
  19. if(root->val > val) root->left = insertIntoBST(root->left, val);
  20. if(root->val < val) root->right = insertIntoBST(root->right, val);
  21. return root;
  22. }
  23. };

到这里,大家应该能感受到,如何通过递归函数返回值完成了新加入节点的父子关系赋值操作了,下一层将加入节点返回,本层用root->left或者root->right将其接住

刚刚说了递归函数不用返回值也可以,找到插入的节点位置,直接让其父节点指向插入节点,结束递归,也是可以的。
那么递归函数定义如下:

  1. TreeNode* parent; // 记录遍历节点的父节点
  2. void traversal(TreeNode* cur, int val)

没有返回值,需要记录上一个节点(parent),遇到空节点了,就让parent左孩子或者右孩子指向新插入的节点。然后结束递归。

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* parent; // 要插入节点的位置的父节点
  15. void traversal(TreeNode* root, int val){
  16. if(root == nullptr){
  17. TreeNode* node = new TreeNode(val);
  18. if(parent && val > parent->val) parent->right = node;
  19. else if(parent && val < parent->val) parent->left = node;
  20. return;
  21. }
  22. parent = root;
  23. if(root->val > val) traversal(root->left, val);
  24. if(root->val < val) traversal(root->right, val);
  25. return;
  26. }
  27. TreeNode* insertIntoBST(TreeNode* root, int val) {
  28. if(root == nullptr){
  29. root = new TreeNode(val);
  30. }
  31. traversal(root, val);
  32. return root;
  33. }
  34. };

可以看出还是麻烦一些的。
我之所以举这个例子,是想说明通过递归函数的返回值完成父子节点的赋值是可以带来便利的。
网上千变一律的代码,可能会误导大家认为通过递归函数返回节点 这样的写法是天经地义,其实这里是有优化的!

迭代法
再来看看迭代法,对二叉搜索树迭代写法不熟悉,可以看这篇:二叉树:二叉搜索树登场!(opens new window)
在迭代法遍历的过程中,需要记录一下当前遍历的节点的父节点,这样才能做插入节点的操作。
二叉树:搜索树的最小绝对差(opens new window)二叉树:我的众数是多少?(opens new window)中,都是用了记录pre和cur两个指针的技巧,本题也是一样的。
代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* insertIntoBST(TreeNode* root, int val) {
  15. if(root == nullptr) return new TreeNode(val);
  16. TreeNode* temp = root;
  17. TreeNode* parent; // 记录前一个节点
  18. while(temp){
  19. parent = temp;
  20. if(temp->val > val){
  21. temp = temp->left;
  22. if(temp == nullptr) parent->left = new TreeNode(val);
  23. }
  24. else if(temp->val < val){
  25. temp = temp->right;
  26. if(temp == nullptr) parent->right = new TreeNode(val);
  27. }
  28. }
  29. return root;
  30. }
  31. };

总结
首先在二叉搜索树中的插入操作,大家不用恐惧其重构搜索树,其实根本不用重构。
然后在递归中,我们重点讲了如果通过递归函数的返回值完成新加入节点和其父节点的赋值操作,并强调了搜索树的有序性。
最后依然给出了迭代的方法,迭代的方法就需要记录当前遍历节点的父节点了,这个和没有返回值的递归函数实现的代码逻辑是一样的。

34、删除二叉搜索树中的节点

450. 删除二叉搜索树中的节点 - 力扣(LeetCode)
image.png
image.png
image.png


递归法

递归三部曲:

  • 确定递归函数参数以及返回值

说道递归函数的返回值,在二叉树:搜索树中的插入操作(opens new window)中通过递归返回值来加入新节点, 这里也可以通过递归返回值删除节点。

  • 确定终止条件

遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了

  • 确定单层递归的逻辑

这里就把二叉搜索树中删除节点遇到的情况都搞清楚。
有以下五种情况:

  • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  • 找到删除的节点
    • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。

第五种情况有点难以理解,看下面动画:
二叉树 - 图87

  1. class Solution {
  2. public:
  3. TreeNode* deleteNode(TreeNode* root, int key) {
  4. if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  5. if (root->val == key) {
  6. // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
  7. if (root->left == nullptr && root->right == nullptr) {
  8. ///! 内存释放
  9. delete root;
  10. return nullptr;
  11. }
  12. // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
  13. else if (root->left == nullptr) {
  14. auto retNode = root->right;
  15. ///! 内存释放
  16. delete root;
  17. return retNode;
  18. }
  19. // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
  20. else if (root->right == nullptr) {
  21. auto retNode = root->left;
  22. ///! 内存释放
  23. delete root;
  24. return retNode;
  25. }
  26. // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
  27. // 并返回删除节点右孩子为新的根节点。
  28. else {
  29. TreeNode* cur = root->right; // 找右子树最左面的节点
  30. while(cur->left != nullptr) {
  31. cur = cur->left;
  32. }
  33. cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置
  34. TreeNode* tmp = root; // 把root节点保存一下,下面来删除
  35. root = root->right; // 返回旧root的右孩子作为新root
  36. delete tmp; // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧)
  37. return root;
  38. }
  39. }
  40. if (root->val > key) root->left = deleteNode(root->left, key);
  41. if (root->val < key) root->right = deleteNode(root->right, key);
  42. return root;
  43. }
  44. };

普通二叉树的删除方式
这里我在介绍一种通用的删除,普通二叉树的删除方式(没有使用搜索树的特性,遍历整棵树),用交换值的操作来删除目标节点。
代码中目标节点(要删除的节点)被操作了两次:

  • 第一次是和目标节点的右子树最左面节点交换。
  • 第二次直接被NULL覆盖了。

思路有点绕,感兴趣的同学可以画图自己理解一下。
代码如下:(关键部分已经注释)

  1. class Solution {
  2. public:
  3. TreeNode* deleteNode(TreeNode* root, int key) {
  4. if (root == nullptr) return root;
  5. if (root->val == key) {
  6. if (root->right == nullptr) { // 这里第二次操作目标值:最终删除的作用
  7. return root->left;
  8. }
  9. TreeNode *cur = root->right;
  10. while (cur->left) {
  11. cur = cur->left;
  12. }
  13. swap(root->val, cur->val); // 这里第一次操作目标值:交换目标值其右子树最左面节点。
  14. }
  15. root->left = deleteNode(root->left, key);
  16. root->right = deleteNode(root->right, key);
  17. return root;
  18. }
  19. };

有点难,还是搞不明白!

迭代法
删除节点的迭代法还是复杂一些的,但其本质我在递归法里都介绍了,最关键就是删除节点的操作(动画模拟的过程)
代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. // 将目标节点(删除节点)的左子树放到 目标节点的右子树的最左面节点的左孩子位置上
  15. // 并返回目标节点右孩子为新的根节点
  16. // 是动画里模拟的过程
  17. TreeNode* deleteOneNode(TreeNode* target){
  18. if(target == nullptr) return target;
  19. if(target->right == nullptr) return target->left;
  20. if(target->left == nullptr) return target->right; // 其实后序的逻辑已经包含了这里的逻辑判断了,这里省略也是可以的
  21. TreeNode* cur = target->right;
  22. while(cur->left){
  23. cur = cur->left;
  24. }
  25. cur->left = target->left;
  26. return target->right;
  27. }
  28. TreeNode* deleteNode(TreeNode* root, int key) {
  29. if(root == nullptr) return root;
  30. TreeNode* cur = root;
  31. TreeNode* pre = nullptr; // 记录cur的父节点,用来删除cur
  32. while(cur){
  33. if(cur->val == key) break;
  34. pre = cur;
  35. if(cur->val > key) cur = cur->left;
  36. else cur = cur->right;
  37. }
  38. if(pre == nullptr){ // 如果搜索树只有头结点
  39. return deleteOneNode(cur);
  40. }
  41. // pre 要知道是删左孩子还是右孩子
  42. if(pre->left && pre->left->val == key){
  43. pre->left = deleteOneNode(cur);
  44. }
  45. if(pre->right && pre->right->val == key){
  46. pre->right = deleteOneNode(cur);
  47. }
  48. return root;
  49. }
  50. };

35、修剪二叉搜索树

669. 修剪二叉搜索树 - 力扣(LeetCode)
image.png
image.png


递归法
直接想法就是:递归处理,然后遇到 root->val < low || root->val > high 的时候直接return NULL,一波修改,赶紧利落。
不难写出如下代码:

  1. class Solution {
  2. public:
  3. TreeNode* trimBST(TreeNode* root, int low, int high) {
  4. if (root == nullptr || root->val < low || root->val > high) return nullptr;
  5. root->left = trimBST(root->left, low, high);
  6. root->right = trimBST(root->right, low, high);
  7. return root;
  8. }
  9. };

然而[1, 3]区间在二叉搜索树的中可不是单纯的节点3和左孩子节点0就决定的,还要考虑节点0的右子树

从图中可以看出需要重构二叉树,想想是不是本题就有点复杂了。
其实不用重构那么复杂。
在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把节点0从二叉树中移除),如图:
image.png

这道题可以有返回值,也可以没有返回值,但是有返回值处理起来会比较方便。

完整C++代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* trimBST(TreeNode* root, int low, int high) {
  15. if(root == nullptr) return root;
  16. if(root->val < low){ // 当前root->val < low, 应该递归右子树
  17. TreeNode* right = trimBST(root->right, low, high);
  18. return right;
  19. }
  20. if(root->val > high){ // 当前root->val > high, 应该递归左子树
  21. TreeNode* left = trimBST(root->left, low, high);
  22. return left;
  23. }
  24. root->left = trimBST(root->left, low, high);
  25. root->right = trimBST(root->right, low, high);
  26. return root;
  27. }
  28. };

迭代法
因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。(以为做题的经验,需要改变树的结构的,一般都不用使用栈来模拟递归)
在剪枝的时候,可以分为三步:

  • 将root移动到[L, R] 范围内,注意是左闭右闭区间
  • 剪枝左子树
  • 剪枝右子树

代码如下:

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* trimBST(TreeNode* root, int low, int high) {
  15. if(root == nullptr) return root;
  16. // 处理头节点,让root移动到[low, high]范围内
  17. while(root != nullptr && (root->val < low || root->val > high)){
  18. if(root->val < low) root = root->right; // 小于往右走
  19. else root = root->left; // 大于往左走
  20. }
  21. TreeNode* cur = root;
  22. // 此时root已经在[low, high] 范围内,处理左孩子元素小于low的情况
  23. while(cur != nullptr){
  24. while(cur->left && cur->left->val < low){
  25. cur->left = cur->left->right;
  26. }
  27. cur = cur->left; // 遍历左节点
  28. }
  29. cur = root;
  30. // 此时root已经在[low, high] 范围内,处理右孩子元素大于high的情况
  31. while(cur != nullptr){
  32. while(cur->right && cur->right->val > high){
  33. cur->right = cur->right->left;
  34. }
  35. cur = cur->right; // 遍历右节点
  36. }
  37. return root;
  38. }
  39. };

总结
修剪二叉搜索树其实并不难,但在递归法中大家可看出我费了很大的功夫来讲解如何删除节点的,这个思路其实是比较绕的。
最终的代码倒是很简洁。
如果不对递归有深刻的理解,这道题目还是有难度的!
本题我依然给出递归法和迭代法,初学者掌握递归就可以了,如果想进一步学习,就把迭代法也写一写。

36、将有序数组转换为二叉搜索树

108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)
image.png
image.png
image.png


题目中说要转换为一棵高度平衡二叉搜索树。这和转换为一棵普通二叉搜索树有什么差别呢?
其实这里不用强调平衡二叉搜索树,数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取,所以想构成不平衡的二叉树是自找麻烦

二叉树:构造二叉树登场!(opens new window)二叉树:构造一棵最大的二叉树(opens new window)中其实已经讲过了,如果根据数组构造一棵二叉树。
本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间

本题其实要比二叉树:构造二叉树登场!(opens new window)二叉树:构造一棵最大的二叉树(opens new window)简单一些,因为有序数组构造二叉搜索树,寻找分割点就比较容易了。
分割点就是数组中间位置的节点。

那么为问题来了,如果数组长度为偶数,中间节点有两个,取哪一个?
取哪一个都可以,只不过构成了不同的平衡二叉搜索树。

递归法
利用了容器,开销较大

  1. class Solution {
  2. public:
  3. TreeNode* sortedArrayToBST(vector<int>& nums) {
  4. // 如果数组为空,返回nullptr
  5. if(nums.size() == 0) return nullptr;
  6. int delimiterindex = nums.size() / 2; // 数组最中间的元素,就是要分割的节点
  7. TreeNode* root = new TreeNode(nums[delimiterindex]);
  8. // 左闭右开
  9. vector<int> leftNums(nums.begin(), nums.begin() + delimiterindex);
  10. vector<int> rightNums(nums.begin() + delimiterindex + 1, nums.end());
  11. root->left = sortedArrayToBST(leftNums);
  12. root->right = sortedArrayToBST(rightNums);
  13. return root;
  14. }
  15. };

可以对代码进行优化

  1. class Solution {
  2. public:
  3. TreeNode* traversal(vector<int>& nums, int begin, int end){
  4. // 如果数组为空,返回nullptr
  5. if(begin > end) return nullptr;
  6. int mid = begin + (end - begin) / 2; // 数组最中间的元素,就是要分割的节点
  7. TreeNode* root = new TreeNode(nums[mid]);
  8. // 左闭右开
  9. int leftBegin = begin;
  10. int leftEnd = mid - 1;
  11. int rightBegin = mid + 1;
  12. int rightEnd = end;
  13. // 递归处理
  14. root->left = traversal(nums, leftBegin, leftEnd);
  15. root->right = traversal(nums, rightBegin, rightEnd);
  16. return root;
  17. }
  18. TreeNode* sortedArrayToBST(vector<int>& nums) {
  19. return traversal(nums, 0, nums.size() - 1);
  20. }
  21. };

代码可以进一步简化,直接这样写:

  1. // 递归处理
  2. root->left = traversal(nums, begin, mid - 1);
  3. root->right = traversal(nums, mid + 1, end);

但是我觉得没必要,上面那样子才能充分体现出解题的逻辑。

迭代法
迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下标,一个队列放右区间下标。
模拟的就是不断分割的过程,C++代码如下:(我已经详细注释)

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* sortedArrayToBST(vector<int>& nums) {
  15. if(nums.size() == 0) return nullptr;
  16. TreeNode* root = new TreeNode(0); // 初始化根节点
  17. queue<TreeNode*> nodeQue; // 放遍历的节点
  18. queue<int> leftQue; // 保存左区间下标
  19. queue<int> rightQue; // 保存右区间下标
  20. nodeQue.push(root); // 根节点入队列
  21. leftQue.push(0); // 0为左区间下标初始化位置
  22. rightQue.push(nums.size() - 1); // nums.size() - 1为右区间下标初始位置
  23. while(!nodeQue.empty()){
  24. TreeNode* cur = nodeQue.front();
  25. nodeQue.pop();
  26. int left = leftQue.front(); leftQue.pop();
  27. int right = rightQue.front(); rightQue.pop();
  28. int mid = left + ((right - left) >> 1);
  29. cur->val = nums[mid]; // 将mid对应的元素给中间节点
  30. if(left <= mid - 1){ // 处理左节点
  31. cur->left = new TreeNode(0);
  32. nodeQue.push(cur->left);
  33. leftQue.push(left);
  34. rightQue.push(mid - 1);
  35. }
  36. if(right >= mid + 1){ // 处理右节点
  37. cur->right = new TreeNode(0);
  38. nodeQue.push(cur->right);
  39. leftQue.push(mid + 1);
  40. rightQue.push(right);
  41. }
  42. }
  43. return root;
  44. }
  45. };

总结
二叉树:构造二叉树登场!(opens new window)二叉树:构造一棵最大的二叉树(opens new window)之后,我们顺理成章的应该构造一下二叉搜索树了,一不小心还是一棵平衡二叉搜索树
其实思路也是一样的,不断中间分割,然后递归处理左区间,右区间,也可以说是分治。
此时相信大家应该对通过递归函数的返回值来增删二叉树很熟悉了,这也是常规操作。
在定义区间的过程中我们又一次强调了循环不变量的重要性。
最后依然给出迭代的方法,其实就是模拟取中间元素,然后不断分割去构造二叉树的过程。

37、把二叉树转换为累加树

538. 把二叉搜索树转换为累加树
image.png
image.png


累加树的要求:当前节点的值 = 原来的值 + 二叉搜索树中所有节点值大于它的值
所以,很明显了,遍历的顺序应该是:右—>中—>左,而中序遍历的顺序是 左中右,因此把这种解法叫做反中序遍历法

递归法

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* pre = nullptr; // 记录最近访问过的节点
  15. void traversal(TreeNode* root){
  16. if(root == nullptr) return;
  17. traversal(root->right); // 右
  18. if(pre) root->val += pre->val; // 中
  19. pre = root; // 更新pre
  20. traversal(root->left); // 左
  21. }
  22. TreeNode* convertBST(TreeNode* root) {
  23. traversal(root);
  24. return root;
  25. }
  26. };

迭代法

  1. /**
  2. * Definition for a binary tree node.
  3. * struct TreeNode {
  4. * int val;
  5. * TreeNode *left;
  6. * TreeNode *right;
  7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
  8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
  9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
  10. * };
  11. */
  12. class Solution {
  13. public:
  14. TreeNode* convertBST(TreeNode* root) {
  15. if(root == nullptr) return root;
  16. stack<TreeNode*> st;
  17. st.push(root);
  18. TreeNode* pre = nullptr; // 记录最近一次访问过的节点
  19. while(!st.empty()){
  20. TreeNode* cur = st.top();
  21. if(cur){
  22. st.pop();
  23. if(cur->left) st.push(cur->left); // 左
  24. st.push(cur); // 中
  25. st.push(nullptr); // 插入标记
  26. if(cur->right) st.push(cur->right); // 右
  27. }
  28. else{
  29. st.pop();
  30. cur = st.top();
  31. if(pre != nullptr) cur->val += pre->val;
  32. pre = cur;
  33. st.pop();
  34. }
  35. }
  36. return root;
  37. }
  38. };

38、二叉树总结篇