专题六:二叉树

目录

二叉树理论基础

1、二叉树的种类

满二叉树 

完全二叉树 

二叉搜索树 

平衡二叉搜索树 

2、二叉树的存储方式

链式存储 

顺序存储

3、二叉树的遍历方式

深度优先遍历 

广度优先遍历 

实现方式

4、链式存储的二叉树节点定义方式

题型一:二叉树的遍历方式

二叉树的递归遍历 

 例1:144. 二叉树的前序遍历 - 力扣(LeetCode)

例2:94. 二叉树的中序遍历 - 力扣(LeetCode)

例3:145. 二叉树的后序遍历 - 力扣(LeetCode) 

 二叉树的迭代遍历

  例1:144. 二叉树的前序遍历 - 力扣(LeetCode)

 例2:94. 二叉树的中序遍历 - 力扣(LeetCode)

例3:145. 二叉树的后序遍历 - 力扣(LeetCode) 

二叉树的层序遍历

例1:102. 二叉树的层序遍历 - 力扣(LeetCode)

补充 

题型二:二叉树的修改与构造

例1: 226. 翻转二叉树 - 力扣(LeetCode)

解法一:递归法 (前序遍历)

 解法二:迭代法(前序遍历)

解法三:层序遍历

说明

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

思路

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

思考题

补充

例4: 654. 最大二叉树 - 力扣(LeetCode)

总结

例5:617. 合并二叉树 - 力扣(LeetCode)

解法一:递归法 

解法二:迭代法 

题型三:二叉树的属性

例1:101. 对称二叉树 - 力扣(LeetCode)

 解法一:递归

 解法二:迭代法

例2:572. 另一棵树的子树 - 力扣(LeetCode)

例3:104. 二叉树的最大深度 - 力扣(LeetCode)

 解法一:后序遍历

解法二:层序遍历

思路

例4:559. N 叉树的最大深度 - 力扣(LeetCode)

例5:111. 二叉树的最小深度 - 力扣(LeetCode)

 解法一:后序遍历

解法二:层序遍历

例6:222. 完全二叉树的节点个数 - 力扣(LeetCode)

 解法一:层序遍历(普通二叉树方法)

 解法二:层序遍历(普通二叉树方法)

 解法三:公式法(完全二叉树方法)

例7:110. 平衡二叉树 - 力扣(LeetCode)

解法一:递归法(后序遍历) 

 思路

注意

例8:257. 二叉树的所有路径 - 力扣(LeetCode)

 解法一:递归+回溯

补充

解法二:迭代法(前序遍历) 

例9:404. 左叶子之和 - 力扣(LeetCode)

 解法一:递归法(后序遍历)

解法二:迭代法(前序遍历)

注意

例10:513. 找树左下角的值 - 力扣(LeetCode)

例11:112. 路径总和 - 力扣(LeetCode)

例12:113. 路径总和 II - 力扣(LeetCode)

总结

扩展 

题型四:二叉搜索树的属性

例1: 700. 二叉搜索树中的搜索 - 力扣(LeetCode)

 解法一:递归法

 思路

解法二:迭代法 

 思路

 例2:98. 验证二叉搜索树 - 力扣(LeetCode)

 解法一:递归法

解法二:迭代法 

例3:530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)

 解法一:递归法

解法二:迭代法

例4:501. 二叉搜索树中的众数 - 力扣(LeetCode)

解法一:递归法

解法二:迭代法

思路

题型五:二叉树公共祖先问题

例1: 236. 二叉树的最近公共祖先 - 力扣(LeetCode)

思路 

​编辑

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

 解法一:递归法

解法二:迭代法

思路

题型六:二叉搜索树的修改与构造

例1:701. 二叉搜索树中的插入操作 - 力扣(LeetCode) 

解法一:递归法 

 解法二:迭代法

 注意

例2:450. 删除二叉搜索树中的节点 - 力扣(LeetCode) 

 思路

例3: 669. 修剪二叉搜索树 - 力扣(LeetCode)

解法一:递归法 

 解法二:迭代法

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

例5: 538. 把二叉搜索树转换为累加树 - 力扣(LeetCode)

 解法一:递归法

解法二:迭代法

声明:

参考:代码随想录 

to_string函数的用法_c++ to_string_snowcatvia的博客-CSDN博客

C++中vector内存泄露问题_c++vector为什么不会内存泄漏_Capricorn_L的博客-CSDN博客

C++:vector中使用.clear()函数_vector.clear_Upupup6的博客-CSDN博客

个人总结归纳,仅用于复习回顾,不作他用!如发现文章内容有误,恳请批评指出! 

二叉树理论基础

1、二叉树的种类

满二叉树 

满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。

这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。 

完全二叉树 

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

优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。

二叉搜索树 

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

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

平衡二叉搜索树 

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

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

C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。

2、二叉树的存储方式

链式存储 

链式存储方式就用指针,通过指针把分布在各个地址的节点串联一起。

顺序存储

 顺序存储的方式就是用数组,顺序存储的元素在内存是连续分布的。

用数组来存储二叉树如何遍历的呢?

如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。

所以大家要了解,用数组依然可以表示二叉树。

3、二叉树的遍历方式

深度优先遍历 

先往深走,遇到叶子节点再往回走。

  • 前序遍历-根左右(递归法,迭代法)
  • 中序遍历-左根右(递归法,迭代法)
  • 后序遍历-左右根(递归法,迭代法)

这里前中后,其实指的就是根节点的遍历顺序 

广度优先遍历 

一层一层的去遍历。

  • 层次遍历(迭代法)

实现方式

1、使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。

2、栈其实就是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。

3、广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。

4、链式存储的二叉树节点定义方式

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

题型一:二叉树的遍历方式

二叉树的递归遍历 

确定递归算法的三个要素,每次写递归按照三要素来写:

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

 例1:144. 二叉树的前序遍历 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    void traversal(TreeNode* cur, vector<int>& vec) {
        if (cur == nullptr) {
            return;
        }
        vec.push_back(cur->val);//根
        traversal(cur->left, vec);//左
        traversal(cur->right, vec);//右
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

 1、确定递归函数的参数和返回值:因为要打印出前序遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void。

2、确定终止条件:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return。

3、确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值。

例2:94. 二叉树的中序遍历 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    void traversal(TreeNode* cur, vector<int>& vec) {
        if (cur == nullptr) {
            return;
        }
        traversal(cur->left, vec);//左
        vec.push_back(cur->val);//根
        traversal(cur->right, vec);//右
    }
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

例3:145. 二叉树的后序遍历 - 力扣(LeetCode) 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    void traversal(TreeNode* cur, vector<int>& vec) {
        if (cur == nullptr) {
            return;
        }
        traversal(cur->left, vec);//左
        traversal(cur->right, vec);//右
        vec.push_back(cur->val);//根
    }
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

 二叉树的迭代遍历

  例1:144. 二叉树的前序遍历 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if (root == nullptr) {
            return result;
        }
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();//中
            st.pop();
            result.push_back(node->val);
            if (node->right) {
                st.push(node->right);//右(空节点不入栈)
            }
            if (node->left) {
                st.push(node->left);//左(空节点不入栈)
            }
        }
        return result;
    }
};
//迭代法
//时间复杂度o(n)
//空间复杂度O(n)

前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。

为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。

 例2:94. 二叉树的中序遍历 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> result;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while (cur != nullptr || !st.empty()) {
            if (cur != nullptr) {// 指针来访问节点,访问到最底层
                st.push(cur);// 将访问的节点放进栈
                cur = cur->left;
            } else {
                cur = st.top();// 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
                st.pop();
                result.push_back(cur->val);
                cur = cur->right;
            }
        }
        return result;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(n)

为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:

  1. 处理:将元素放进result数组中
  2. 访问:遍历节点

分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。

那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

例3:145. 二叉树的后序遍历 - 力扣(LeetCode) 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if (root == nullptr) {
            return result;
        }
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            result.push_back(node->val);
            if (node->left) {
                st.push(node->left);// 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
            }
            if (node->right) {
                st.push(node->right);// 空节点不入栈
            }
        }
        reverse(result.begin(), result.end());// 将结果反转之后就是左右中的顺序了
        return result;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(n)

再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了。

二叉树的层序遍历

例1:102. 二叉树的层序遍历 - 力扣(LeetCode)

解法一:非递归法 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != nullptr) {
            que.push(root);
        }
        vector<vector<int>> result;
        while (!que.empty()) {
            int length = que.size();
            vector<int> vec;
             // 这里一定要使用固定大小length,不要使用que.size(),因为que.size是不断变化的
            for (int i = 0; i < length; i++) {
                TreeNode* node = que.front();
                que.pop();
                vec.push_back(node->val);
                if (node->left) {
                    que.push(node->left);
                }
                if (node->right) {
                    que.push(node->right);
                }
            }
            result.push_back(vec);
        }
        return result;
    }
};
//队列
//时间复杂度O(n)
//空间复杂度O(n)

解法二:递归法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    void order(TreeNode* cur, vector<vector<int>>& result, int depth) {
        if (cur == nullptr) {
            return;
        }
        if (result.size() == depth) {
            result.push_back(vector<int>());
        }
        result[depth].push_back(cur->val);
        order(cur->left, result, depth + 1);
        order(cur->right, result, depth + 1);
    }
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        int depth = 0;
        order(root, result, depth);
        return result;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

补充 

1、morris遍历

morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为O(1)。

彻底理解被称为二叉树神级遍历的莫里斯(Morris)算法_morris算法_有裂痕的石头的博客-CSDN博客

2、递归与迭代究竟谁优谁劣呢?

从时间复杂度上其实迭代法和递归法差不多(在不考虑函数调用开销和函数调用产生的堆栈开销),但是空间复杂度上,递归开销会大一些,因为递归需要系统堆栈存参数返回值等等。

递归更容易让程序员理解,但收敛不好,容易栈溢出。

这么说吧,递归是方便了程序员,难为了机器(各种保存参数,各种进栈出栈)。

在实际项目开发的过程中我们是要尽量避免递归!因为项目代码参数、调用关系都比较复杂,不容易控制递归深度,甚至会栈溢出。

题型二:二叉树的修改与构造

例1: 226. 翻转二叉树 - 力扣(LeetCode)

 

解法一:递归法 (前序遍历)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    //1.确定递归函数的参数和返回值
    TreeNode* invertTree(TreeNode* root) {
        //2.确定终止条件
        if (root == nullptr) {
            return root;
        }
        //3.确定单层递归的逻辑
        //先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树
        swap(root->left, root->right);
        invertTree(root->left);
        invertTree(root->right);
        return root;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

 解法二:迭代法(前序遍历)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if (root == nullptr) {
            return root;
        }
        stack<TreeNode*> st;
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            swap(node->left, node->right);
            if (node->right) {
                st.push(node->right);
            }
            if (node->left) {
                st.push(node->left);
            }
        }
        return root;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(n)

解法三:层序遍历

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != nullptr) {
            que.push(root);
        }
        while (!que.empty()) {
            int length = que.size();
            for (int i = 0; i < length; i++) {
                TreeNode* node = que.front();
                que.pop();
                swap(node->left, node->right);
                if (node->left) {
                    que.push(node->left);
                }
                if (node->right) {
                    que.push(node->right);
                }
            }
        }
        return root;
    }
};
//层序遍历
//时间复杂度O(n)
//空间复杂度O(1)

说明

注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果

这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了

那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!

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

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    TreeNode* traversal(vector<int>& inorder, vector<int>& postorder) {
        if (postorder.size() == 0) return nullptr;

        //后序遍历数组最后一个元素,就是当前的中间节点
        int rootValue = postorder[postorder.size() - 1];
        TreeNode* root = new TreeNode(rootValue);

        //叶子节点
        if (postorder.size() == 1) return root;

        //找到中序遍历的切割点
        int delimiterIndex;
        for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
            if (inorder[delimiterIndex] == rootValue) break;
        }

        //切割中序数组
        //左闭右开区间:[0, delimiterIndex)
        vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
        //[delimiterIndex + 1, end)
        vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());

        //postorder 舍弃末尾元素
        postorder.resize(postorder.size() - 1);

        //切割后序数组
        //依然左闭右开,注意这里使用了左中序数组大小作为切割点
        //[0, leftInorder.size)
        vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
        //[leftInorder.size(), end)
        vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());

        root->left = traversal(leftInorder,leftPostorder);
        root->right = traversal(rightInorder, rightPostorder);

        return root;
    }
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0) return nullptr;
        return traversal(inorder, postorder);
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

思路

  • 第一步:如果数组大小为零的话,说明是空节点了。

  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。

  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点

  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)

  • 第五步:切割后序数组,切成后序左数组和后序右数组

  • 第六步:递归处理左区间和右区间

注意:

1、切割区间及边界值的确定,坚持循环不变量原则,本题答案中选择的是左闭右开区间

2、一定要先切割中序数组,因为中序数组的最后一个元素是根节点,确定了根节点,就可以根据后序数组分割出左右子树。 

此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。

中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。

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

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    TreeNode* traversal(vector<int>& preorder, vector<int>& inorder) {
        if (preorder.size() == 0) return nullptr;
        
        //前序遍历数组第一个元素,就是当前的中间节点
        int rootValue = preorder[0];
        TreeNode* root = new TreeNode(rootValue);

        //叶子节点
        if (preorder.size() == 1) return root;

        //找到中序遍历的切割点
        int delimiterIndex;
        for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
            if (inorder[delimiterIndex] == rootValue) break;
        }

        //切割中序数组
        //左闭右开区间:[0, delimiterIndex)
        vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
        //[delimiterIndex + 1, end)
        vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());

        //切割前序数组
        //依然使用左闭右开,注意这里使用了左中序数组大小作为切割点
        //[preorder.begin() + 1, leftorder.size)
        vector<int> leftPreorder(preorder.begin() + 1, preorder.begin() + 1 + leftInorder.size());
        //[leftPreorder.size, end)
        vector<int> rightPreorder(preorder.begin() + 1 + leftInorder.size(), preorder.end());

        root->left = traversal(leftPreorder, leftInorder);
        root->right = traversal(rightPreorder, rightInorder);

        return root;
    }
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        if (inorder.size() == 0 || preorder.size() == 0) return nullptr;
        return traversal(preorder, inorder);
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

思考题

前序和中序可以唯一确定一棵二叉树。

后序和中序可以唯一确定一棵二叉树。

前序和后序不能唯一确定一棵二叉树!,因为没有中序遍历无法确定左右部分,也就是无法分割。

补充

 resize()函数

void resize (size_type n);
void resize (size_type n, const value_type& val); 

作用:
改变容器的大小,使得其包含n个元素。常见三种用法。

1、如果n小于当前的容器大小,那么则保留容器的前n个元素,去除(erasing)超过的部分。

2、如果n大于当前的容器大小,则通过在容器结尾插入(inserting)适合数量的元素使得整个容器大小达到n。且如果给出val,插入的新元素全为val,否则,执行默认构造函数。

3、如果n大于当前容器的容量(capacity)时,则会自动重新分配一个存储空间。

注意:如果发生了重新分配,则使用容器的分配器分配存储空间,这可能会在失败时抛出异常。
 

例4: 654. 最大二叉树 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    // 在左闭右开区间[left, right),构造二叉树
    TreeNode* traversal(vector<int>& nums, int left, int right) {
        if (left >= right) return nullptr;
         // 分割点下标:maxValueIndex
        int maxValueIndex = left;
        for (int i = left + 1; i < right; i++) {
            if (nums[i] > nums[maxValueIndex]) maxValueIndex = i;
        }
        TreeNode* root = new TreeNode(nums[maxValueIndex]);
        // 左闭右开:[left, maxValueIndex)
        root->left = traversal(nums, left, maxValueIndex);
        // 左闭右开:[maxValueIndex + 1, right)
        root->right = traversal(nums, maxValueIndex + 1, right);
        return root;
    }
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return traversal(nums, 0, nums.size());
    }
};
//递归法
//时间复杂度O(n^2)
//空间复杂度O(n)

总结

构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。 

注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。

什么时候递归函数前面加if,什么时候不加if?

其实就是不同代码风格的实现,一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。

例5:617. 合并二叉树 - 力扣(LeetCode)

解法一:递归法 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        if (root1 == nullptr) return root2;
        if (root2 == nullptr) return root1;

        // 重新定义新的节点,不修改原有两个树的结构
        TreeNode* root = new TreeNode(0);
        root->val = root1->val + root2->val;
        root->left = mergeTrees(root1->left, root2->left);
        root->right = mergeTrees(root1->right, root2->right);
        return root;
    }
};
//递归法(前序遍历)
//时间复杂度O(min(m,n))
//空间复杂度O(min(m,n))

解法二:迭代法 

#include <iostream>
#include <vector>
#include <string>
#include <stack>
#include <queue>
using namespace std;

struct TreeNode {
	int val;
	TreeNode* left;
	TreeNode* right;
	TreeNode() : val(0), left(nullptr), right(nullptr) {}
	TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
	TreeNode(int x, TreeNode* left, TreeNode* right) : val(x), left(left), right(right) {}
};

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        if (root1 == nullptr) return root2;
        if (root2 == nullptr) return root1;
        queue<TreeNode*> que;
        que.push(root1);
        que.push(root2);

        while (!que.empty()) {
            TreeNode* node1 = que.front(); que.pop();
            TreeNode* node2 = que.front(); que.pop();
            node1->val += node2->val;

            if (node1->left != nullptr && node2->left != nullptr) {
                que.push(node1->left);
                que.push(node2->left);
            }

            if (node1->right != nullptr && node2->right != nullptr) {
                que.push(node1->right);
                que.push(node2->right);
            }

            if (node1->left == nullptr && node2->left != nullptr) {
                node1->left = node2->left;
            }

            if (node1->right == nullptr && node2->right != nullptr) {
                node1->right = node2->right;
            }
        }
        return root1;
    }

public:
    vector<int> result;
    vector<int> preorder(TreeNode* root) {
        if (root == nullptr) return result;
        result.push_back(root->val);
        preorder(root->left);
        preorder(root->right);
        return result;
    }
};


int main()
{
	TreeNode* node14 = new TreeNode(5, nullptr, nullptr);
	TreeNode* node13 = new TreeNode(2, nullptr, nullptr);
	TreeNode* node12 = new TreeNode(3, node14, nullptr);
	TreeNode* node11 = new TreeNode(1, node12, node13);

    TreeNode* node25 = new TreeNode(7);
    TreeNode* node24 = new TreeNode(4);
    TreeNode* node23 = new TreeNode(3, nullptr, node25);
    TreeNode* node22 = new TreeNode(1, nullptr, node24);
    TreeNode* node21 = new TreeNode(2, node22, node23);

	Solution A;
	TreeNode* output = A.mergeTrees(node11, node21);
    vector<int> res =  A.preorder(output);
    for (int num : res) {
        cout << num << '\t';
    }
}

题型三:二叉树的属性

例1:101. 对称二叉树 - 力扣(LeetCode)

 

 解法一:递归

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool compare(TreeNode* left, TreeNode* right) {
        //首先排除空节点的情况
        if (left == nullptr && right != nullptr) return false;
        else if (left != nullptr && right == nullptr) return false;
        else if (left == nullptr && right == nullptr) return true;
        //排除了空节点,再排除数值不相同的情况
        else if (left->val != right->val) return false;

        // 此时就是:左右节点都不为空,且数值相同的情况
        // 此时才做递归,做下一层的判断
        bool outside = compare(left->left, right->right);// 左子树:左、 右子树:右
        bool inside = compare(left->right, right->left);// 左子树:右、 右子树:左
        bool isSame = outside && inside;                // 左子树:中、 右子树:中 (逻辑处理)
        return isSame;
    }
    bool isSymmetric(TreeNode* root) {
        if (root == nullptr) return true;
        return compare(root->left, root->right);
    }
};
//递归
//时间复杂度O(n)
//空间复杂度O(n)

 思路:

递归三部曲

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

因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。

返回值自然是bool类型。

代码如下:

bool compare(TreeNode* left, TreeNode* right)
  1. 确定终止条件

要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。

节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点

  • 左节点为空,右节点不为空,不对称,return false
  • 左不为空,右为空,不对称 return false
  • 左右都为空,对称,返回true

此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:

  • 左右都不为空,比较节点数值,不相同就return false

此时左右节点不为空,且数值也不相同的情况我们也处理了。

代码如下:

if (left == NULL && right != NULL) return false;
else if (left != NULL && right == NULL) return false;
else if (left == NULL && right == NULL) return true;
else if (left->val != right->val) return false; // 注意这里我没有使用else

注意上面最后一种情况,我没有使用else,而是else if, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。

  1. 确定单层递归的逻辑

此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。

  • 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
  • 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
  • 如果左右都对称就返回true ,有一侧不对称就返回false 。

代码如下:

bool outside = compare(left->left, right->right);   // 左子树:左、 右子树:右
bool inside = compare(left->right, right->left);    // 左子树:右、 右子树:左
bool isSame = outside && inside;                    // 左子树:中、 右子树:中(逻辑处理)
return isSame;

 解法二:迭代法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if (root == nullptr) return true;
        queue<TreeNode*> que;
        que.push(root->left);// 将左子树头结点加入队列
        que.push(root->right);// 将右子树头结点加入队列

        while (!que.empty()) {// 接下来就要判断这两个树是否相互翻转
            TreeNode* leftNode = que.front(); que.pop();
            TreeNode* rightNode = que.front(); que.pop();
            if (!leftNode && !rightNode) continue;// 左节点为空、右节点为空,此时说明是对称的
            // 左右一个节点不为空,或者都不为空但数值不相同,返回false
            if (!leftNode || !rightNode || leftNode->val != rightNode->val) return false;
            que.push(leftNode->left);// 加入左节点左孩子
            que.push(rightNode->right);// 加入右节点右孩子
            que.push(leftNode->right); // 加入左节点右孩子
            que.push(rightNode->left);// 加入右节点左孩子
        }
        return true;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(n)

        其实可以发现,这个迭代法,其实是把左右两个子树要比较的元素顺序放进一个容器,然后成对成对的取出来进行比较,那么其实使用栈也是可以的,只要把队列原封不动的改成栈就可以了。

例2:572. 另一棵树的子树 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
//递归
class Solution {
public:
    /*  外层深度优先遍历:前序遍历在root树找到与subRoot树根节点值相同的节点*/
    //确定递归的参数和返回值
    //参数:root树的节点和subRoot树的根节点,返回值:布尔类型
    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
        //确定终止条件
        //如果root树节点为空
        if (!root) return false;
        //确定单层递归逻辑
        //compare(root, subRoot);   //中
        //isSubtree(root->left, subRoot);   //左
        //isSubtree(root->right, subRoot);  //右
        return compare(root, subRoot) || isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
    }
private:
    /*  内层深度优先遍历:遍历当前节点的左右子树*/
    //确定递归的参数和返回值
    //参数:两棵树的节点,返回值:布尔类型
    bool compare(TreeNode* root, TreeNode* subRoot) {
        //确定终止条件
        //如果两棵树的节点都为空,则两树具有相同结构和节点值
        if (!root && !subRoot) return true;
        //两棵树任意一个节点为空,则两树结构不同
        else if (!root || !subRoot) return false;
        //两棵树对应节点的值不等,则两树节点值不同
        else if (root->val != subRoot->val) return false;
        //确定单层逻辑
        //递归两棵树的左右子树,比较结构和节点值
        else return compare(root->left, subRoot->left) && compare(root->right, subRoot->right);
    }
};

例3:104. 二叉树的最大深度 - 力扣(LeetCode)

 解法一:后序遍历

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    //1.确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回这棵树的深度,所以返回值为int类型。
    int getDepth(TreeNode* node) {
        //2.确定终止条件:如果为空节点的话,就返回0,表示高度为0
        if (node == nullptr) return 0;
        //确定单层递归的逻辑:先求它的左子树的深度,再求右子树的深度,最后取左右深度最大的数值 
        //再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度
        int leftdepth = getDepth(node->left);//左
        int rightdepth = getDepth(node->right);//右
        int depth = 1 + max(leftdepth, rightdepth);//中
        return depth;
    }
public:
    int maxDepth(TreeNode* root) {
        return getDepth(root);
    }
};
//后序遍历
//时间复杂度O(n)
//空间复杂度O(logn)

解法二:层序遍历

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        int depth = 0;
        queue<TreeNode*> que;
        que.push(root);
        while (!que.empty()) {
            int length = que.size();
            depth++;
            for (int i = 0; i < length; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return depth;
    }
};
//层序遍历(迭代法)
//时间复杂度O(n)
//空间复杂度O(n)

思路

本题可以使用前序(中左右),也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序求的是高度。

  • 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始)
  • 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数后者节点数(取决于高度从0开始还是从1开始)

而根节点的高度就是二叉树的最大深度,所以本题中我们通过后序求的根节点高度来求的二叉树最大深度。

例4:559. N 叉树的最大深度 - 力扣(LeetCode)

 

 

/*
// Definition for a Node.
class Node {
public:
    int val;
    vector<Node*> children;

    Node() {}

    Node(int _val) {
        val = _val;
    }

    Node(int _val, vector<Node*> _children) {
        val = _val;
        children = _children;
    }
};
*/

class Solution {
public:
    int maxDepth(Node* root) {
       queue<Node*> que; 
       if (root != nullptr) que.push(root);
       int depth = 0;
       while (!que.empty()) {
           int length = que.size();
           depth++;
           for (int i = 0; i < length; i++) {
                Node* node = que.front();
                que.pop();
                for (int j = 0; j < node->children.size(); j++) {
                    if (node->children[j]) que.push(node->children[j]);
                }
           }
       }  
       return depth;
    }
};
//层序遍历
//时间复杂度O(n)
//空间复杂度O(n)

例5:111. 二叉树的最小深度 - 力扣(LeetCode)

 

 解法一:后序遍历

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    int getDepth(TreeNode* node) {
        if (node == nullptr) return 0;
        int leftDepth = getDepth(node->left);//左
        int rightDepth = getDepth(node->right);//右
                                               //中
        // 当一个左子树为空,右不为空,这时并不是最低点
        if (node->left == nullptr && node->right != nullptr) {
            return 1 + rightDepth;
        }
        // 当一个右子树为空,左不为空,这时并不是最低点
        if (node->left != nullptr && node->right == nullptr) {
            return 1 + leftDepth;
        }
        int result = 1 + min(leftDepth, rightDepth);
        return result;
    }
public:
    int minDepth(TreeNode* root) {
        return getDepth(root);
    }
};
//后序遍历
//时间复杂度O(n)
//空间复杂度O(logn)

解法二:层序遍历

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int minDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        int depth = 0;
        queue<TreeNode*> que;
        que.push(root);
        while (!que.empty()) {
            int length = que.size();
            depth++;//记录最小深度
            for (int i = 0; i < length; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
                if (!node->left && !node->right) {// 当左右孩子都为空的时候,说明是最低点的一层了,退出
                    return depth;
                }
            }
        }
        return depth;
    }
};
//层序遍历
//时间复杂度O(n)
//空间复杂度O(n)

例6:222. 完全二叉树的节点个数 - 力扣(LeetCode)

 

 解法一:层序遍历(普通二叉树方法)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    int getNodesNum(TreeNode* cur) {
        if (cur == nullptr) return 0;
        int leftNodesNum = getNodesNum(cur->left);//左
        int rightNodesNum = getNodesNum(cur->right);//右
        int treeNum =  leftNodesNum + rightNodesNum + 1;//中
        return treeNum;
    }
public:
    int countNodes(TreeNode* root) {
        return getNodesNum(root);
    }
};
//后序遍历(普通二叉树方法)
//时间复杂度O(n)
//空间复杂度O(logn)

 解法二:层序遍历(普通二叉树方法)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int countNodes(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != nullptr) que.push(root);
        int result = 0;
        while (!que.empty()) {
            int length = que.size();
            for (int i = 0; i < length; i++) {
                TreeNode* node = que.front();
                que.pop();
                result++;
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right); 
            }
        }
        return result;
    }
};
//层序遍历(普通二叉树方法)
//时间复杂度O(n)
//空间复杂度O(n)

 解法三:公式法(完全二叉树方法)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int countNodes(TreeNode* root) {
        if (root == nullptr) return 0;
        TreeNode* left = root->left;
        TreeNode* right = root->right;
        int leftDepth = 0,rightDepth = 0;// 这里初始为0是有目的的,为了下面求指数方便
        while (left) {// 求左子树深度
            left = left->left;
            leftDepth++;
        }
        while (right) {// 求右子树深度
            right = right->right;
            rightDepth++;
        }
        if (leftDepth == rightDepth) {
            return (2 << leftDepth) - 1;// 注意(2<<1) 相当于2^2,所以leftDepth初始为0
        }
        return countNodes(root->left) + countNodes(root->right) + 1;
    }
};
//公式法(完全二叉树方法)
//时间复杂度O(logn×logn)
//空间复杂度O(logn)

例7:110. 平衡二叉树 - 力扣(LeetCode)

 

 

解法一:递归法(后序遍历) 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    // 返回以该节点为根节点的二叉树的高度,如果不是平衡二叉树了则返回-1
    int getHeight(TreeNode* node) {
        if (node == nullptr) return 0;
        int leftHeight = getHeight(node->left);//左
        if (leftHeight == -1) return -1;
        int rightHeight = getHeight(node->right);//右
        if (rightHeight == -1) return -1;
        int result;
        if (abs(leftHeight - rightHeight) > 1) {//中
            return -1;
        }
        else {
            return 1 + max(leftHeight, rightHeight);//以当前节点为根节点的树的最大高度
        }
        //return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight);
    }
public:
    bool isBalanced(TreeNode* root) {
        return getHeight(root) == -1 ? false : true;
    }
};
//递归法(后序遍历)
//时间复杂度O(n)
//空间复杂度O(logn)

 思路

既然要求比较高度,必然是要后序遍历。

递归三步曲分析:

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

参数:当前传入节点。 返回值:以当前传入节点为根节点的树的高度。

那么如何标记左右子树是否差值大于1呢?

如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。

所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。

        2.明确终止条件

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

        3.明确单层递归的逻辑

如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。

分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。

注:此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。

例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!

因为对于回溯算法已经是非常复杂的递归了,如果再用迭代的话,就是自己给自己找麻烦,效率也并不一定高。

注意

二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。

二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。

但leetcode中强调的深度和高度很明显是按照节点来计算的

关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。

迭代法中究竟什么时候用队列,什么时候用栈?

如果是模拟前中后序遍历就用栈,如果是适合层序遍历就用队列,当然还是其他情况,那么就是 先用队列试试行不行,不行就用栈。

例8:257. 二叉树的所有路径 - 力扣(LeetCode)

 解法一:递归+回溯

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) {
        path.push_back(cur->val);// 中,中为什么写在这里,因为最后一个节点也要加入到path中
        //到叶子节点
        if (cur->left == nullptr && cur->right == nullptr) {
            string sPath;
            for (int i = 0; i < path.size() - 1; i++) {
                sPath += to_string(path[i]);
                sPath += "->";
            }
            sPath += to_string(path[path.size() - 1]);
            result.push_back(sPath);
            return;
        }
        if (cur->left) {//左
            traversal(cur->left, path, result);
            path.pop_back();//回溯
        }
        if (cur->right) {//右
            traversal(cur->right, path, result);
            path.pop_back();//回溯
        }
    }
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> result;
        vector<int> path;
        if (root == nullptr) return result;
        traversal(root, path, result);
        return result;
    }
};
//递归+回溯
//时间复杂度O(n^2)
//空间复杂度O(n^2)

补充

to_string 函数:将数字常量转换为字符串,返回值为转换完毕的字符串

头文件:#include<string>

string s = to_string(i); //将整数i转换为字符串表示形式

string to_string (int val);
string to_string (long val);
string to_string (long long val);
string to_string (unsigned val);
string to_string (unsigned long val);
string to_string (unsigned long long val);
string to_string (float val);
string to_string (double val);
string to_string (long double val);

回溯和递归是一一对应的,有一个递归,就要有一个回溯 

解法二:迭代法(前序遍历) 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        stack<TreeNode*> treeSt;//保存树的遍历节点
        stack<string> pathSt;//保存遍历路径的节点
        vector<string> result;//保存最终路径集合
        if (root == nullptr) return result;
        treeSt.push(root);
        pathSt.push(to_string(root->val));
        while (!treeSt.empty()) {
            TreeNode* node = treeSt.top(); treeSt.pop();//取出节点 中
            string path = pathSt.top(); pathSt.pop();//取出该节点对应的路径
            if (node->left == nullptr && node->right == nullptr) {//遇到叶子节点              
                result.push_back(path);
            }
            if (node->right) {//右
                treeSt.push(node->right);
                pathSt.push(path + "->" + to_string(node->right->val));
            }
            if (node->left) {//左
                treeSt.push(node->left);
                pathSt.push(path + "->" + to_string(node->left->val));
            }
        }
        return result;
    }
};
//迭代法(前序遍历)
//时间复杂度O(n^2)
//空间复杂度O(n^2)

例9:404. 左叶子之和 - 力扣(LeetCode)

 解法一:递归法(后序遍历)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        if (root == nullptr) return 0;
        if (root->left == nullptr && root->right == nullptr) return 0;//其实这个也可以不写,如果不写不影响结果,但就会让递归多进行了一层。
        int leftValue = sumOfLeftLeaves(root->left);//左
        if (root->left && !root->left->left && !root->left->right) {//左子树就是一个左叶子的情况
            leftValue = root->left->val;
        }
        int rightValue = sumOfLeftLeaves(root->right);//右
        int sum = leftValue + rightValue;//中
        return sum;
    }
};
//递归法(后序遍历)
//时间复杂度O(n)
//空间复杂度O(logn)

解法二:迭代法(前序遍历)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int sumOfLeftLeaves(TreeNode* root) {
        stack<TreeNode*> st;
        int result = 0;
        if (root == nullptr) return 0;
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top(); st.pop();
            if (node->left && !node->left->left && !node->left->right) {
                result += node->left->val;
            }
            if (node->right) st.push(node->right);
            if (node->left) st.push(node->left);
        }
         return result;
    }
};
//迭代法(前序遍历)
//时间复杂度O(n)
//空间复杂度O(n)

注意

左叶子的明确定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点

例10:513. 找树左下角的值 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*> que;
        int result = 0;
        if (root != nullptr) que.push(root);
        while (!que.empty()) {
            int length = que.size();
            for (int i = 0; i < length; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (i == 0) result = node->val;// 记录最后一行第一个元素
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return result; 
    }
};
//迭代法(层序遍历)
//时间复杂度O(n)
//空间复杂度O(n)

例11:112. 路径总和 - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    bool traversal(TreeNode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) return true;// 遇到叶子节点,并且计数为0
        if (!cur->left && !cur->right) return false;// 遇到叶子节点直接返回

        if (cur->left) {// 左
            count -= cur->left->val;// 递归,处理节点;
            if (traversal(cur->left, count)) return true;
            count += cur->left->val;// 回溯,撤销处理结果
        }
        if (cur->right) {// 右
            count -= cur->right->val;// 递归,处理节点;
            if (traversal(cur->right, count)) return true;
            count += cur->right->val;// 回溯,撤销处理结果
        }
        return false;
    } 
public:
    bool hasPathSum(TreeNode* root, int targetSum) {
        if (root == nullptr) return false;
        return traversal(root, targetSum - root->val);
    }
};
//递归法(前序遍历)
//时间复杂度O(n)
//空间复杂度O(logn)

例12:113. 路径总和 II - 力扣(LeetCode)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    // 递归函数不需要返回值,因为我们要遍历整个树
    void traversal(TreeNode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) {// 遇到了叶子节点且找到了和为sum的路径
            result.push_back(path);
            return;
        }
        if (!cur->left && !cur->right) return;// 遇到叶子节点而没有找到合适的边,直接返回
        if (cur->left) {// 左 (空节点不遍历)
            path.push_back(cur->left->val);
            count -= cur->left->val;
            traversal(cur->left, count);// 递归
            path.pop_back();// 回溯
            count += cur->left->val;// 回溯
        } 
        if (cur->right) {
            path.push_back(cur->right->val);
            count -= cur->right->val;
            traversal(cur->right, count);// 递归
            path.pop_back();// 回溯
            count += cur->right->val;// 回溯
        }
        return;
    }
public:
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        //我认为这里clear的作用就是初始化吧。。。。
        result.clear();
        path.clear();
        if (root == nullptr) return result;
        path.push_back(root->val);// 把根节点放进路径
        traversal(root, targetSum - root->val);
        return result;
    }
};
//递归法
//时间复杂度o(n^2)
//空间复杂度O(n)

总结

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

  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(如:113.路径总和ii)
  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (如:236.二叉树的最近公共祖先)
  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(如112.路径总和)

扩展 

1、vector.clear()函数并不会把所有元素清零。

vector有两个参数,一个是size,表示当前vector容器内存储的元素个数,一个是capacity,表示当前vector在内存中申请的这片区域所能容纳的元素个数。

通常capacity会比size大,如果往vector中push_back数据,这样就不用重新申请内存和拷贝元素到新内存区域了,便于节省时间。

所以,vector.clear()的真正作用是:把size设置成0,capacity不变

2、vector的内存泄漏问题

当vector、string大量插入数据后,即使删除了大量数据(或者全部都删除,即clear) 并没有改变容器的容量(capacity),所以仍然会占用着内存。 为了避免这种情况,我们应该想办法改变容器的容量使之尽可能小的符合当前 数据所需(shrink to fit)

#include <iostream>
#include <vector>
using namespace std;

vector <string> v;
char ch;

int main()
{
	for (int i = 0; i < 10000000; i++) {
		v.push_back("abcdefghijkfdsgdafbg");
		//此时vector大小为1,容量为1
		cout << "Vector 的大小为" << v.size() << endl;
		cout << "Vector 的容量为" << v.capacity() << endl;
		//vector.clear后大小为0,容量不变仍未1
		v.clear();
		cout << "Vector 的大小为" << v.size() << endl;
		cout << "Vector 的容量为" << v.capacity() << endl;
		//vecter.swap后大小,容量均为0  成功释放数据内存
		vector<string>(v).swap(v);
		cout << "Vector 的大小为" << v.size() << endl;
		cout << "Vector 的容量为" << v.capacity() << endl;
		
		return 0;
	}
}

即先创建一个临时拷贝与原先的vector一致,值得注意的是,此时的拷贝 其容量是尽可能小的符合所需数据的。紧接着将该拷贝与原先的vector v进行交换。好了此时,执行交换后,临时变量会被销毁,内存得到释放。此时的v即为原先 的临时拷贝,而交换后的临时拷贝则为容量非常大的vector(不过已经被销毁)

因此使用swap()才是正确释放内存的方法

题型四:二叉搜索树的属性

例1: 700. 二叉搜索树中的搜索 - 力扣(LeetCode)

 

 解法一:递归法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        if (root == nullptr || root->val == val) return root;
        TreeNode* result = nullptr;
        if (root->val > val) result = searchBST(root->left, val);
        if (root->val < val) result = searchBST(root->right, val);
        return result;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

二叉搜索树是一个有序树:

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

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

 思路

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

递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。

代码如下:

TreeNode* searchBST(TreeNode* root, int val)

      2.确定终止条件

如果root为空,或者找到这个数值了,就返回root节点。

if (root == NULL || root->val == val) return root;

      3.确定单层递归的逻辑 

看看二叉搜索树的单层递归逻辑有何不同。

因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。

如果root->val > val,搜索左子树,如果root->val < val,就搜索右子树,最后如果都没有搜索到,就返回NULL。

代码如下:

TreeNode* result = NULL;
if (root->val > val) result = searchBST(root->left, val);
if (root->val < val) result = searchBST(root->right, val);
return result;

注意:

直接写 searchBST(root->left, val),却忘了 递归函数还有返回值。

递归函数的返回值是什么? 是 左子树如果搜索到了val,要将该节点返回。 如果不用一个变量将其接住,那么返回值不就没了。

所以要 result = searchBST(root->left, val)

解法二:迭代法 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        while (root) {
            if (root->val > val) root = root->left;
            else if (root->val < val) root = root->right;
            else return root;
        }
        return nullptr;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(1)

 思路

由于二叉搜索树的特殊性,即节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。

对于一般二叉树,递归过程中还有回溯的过程,例如走一个左方向的分支走到头了,那么要调头,在走右分支。而对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。

例如要搜索元素为3的节点,我们不需要搜索其他节点,也不需要做回溯,查找的路径已经规划好了。

中间节点如果大于3就向左走,如果小于3就向右走,如图:

 例2:98. 验证二叉搜索树 - 力扣(LeetCode)

 

 解法一:递归法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* pre = nullptr;//用来记录前一个节点
    bool isValidBST(TreeNode* root) {
        if (root == nullptr) return true;
        bool left = isValidBST(root->left);
        if (pre != nullptr && pre->val >= root->val) return false;
        pre = root;//记录前一个节点
        bool right = isValidBST(root->right);
        return left && right;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

中序遍历下,输出的二叉搜索树节点的数值是有序序列。

有了这个特性,验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。

解法二:迭代法 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    bool isValidBST(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* pre = nullptr;// 记录前一个节点
        while (cur != nullptr || !st.empty()) {
            if (cur != nullptr) {
                st.push(cur);
                cur = cur->left;// 左
            }else {
                cur = st.top();// 中
                st.pop();
                if (pre != nullptr && cur->val <= pre->val) return false;
                pre = cur;//保存前一个访问的结点

                cur = cur->right; // 右
            }
        }
        return true;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(n)

例3:530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)

 解法一:递归法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    int result = INT_MAX;
    TreeNode* pre = nullptr;
    void traversal(TreeNode* cur) {
        if (cur == nullptr) return;
        traversal(cur->left);     //左
        if (pre != nullptr) {     //中
            result = min(result, cur->val - pre->val);
        }
        pre = cur;                //记录前一个
        traversal(cur->right);    //右
    }
public:
    int getMinimumDifference(TreeNode* root) {
        traversal(root);
        return result;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

解法二:迭代法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int getMinimumDifference(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* pre = nullptr;
        int result = INT_MAX;
        while (cur != nullptr || !st.empty()) {
            if (cur != nullptr) {      // 指针来访问节点,访问到最底层
                st.push(cur);          // 将访问的节点放进栈
                cur = cur->left;       // 左
            } else {
                cur = st.top();
                st.pop();
                if (pre != nullptr) {  // 中
                    result = min(result, cur->val - pre->val);
                }
                pre = cur;
                cur = cur->right;      // 右
            }
        }
        return result;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(n)

 注意是二叉搜索树,二叉搜索树可是有序的。

遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值。

例4:501. 二叉搜索树中的众数 - 力扣(LeetCode)

 

解法一:递归法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    int maxCount = 0;    // 最大频率
    int count = 0;       // 统计频率
    TreeNode* pre = nullptr;
    vector<int> result;
    void searchBST(TreeNode* cur) {
        if (cur == nullptr) return;
        searchBST(cur->left);         //左
                                      //中
        if (pre == nullptr) {         // 第一个节点
            count = 1;
        } else if (pre->val == cur->val) {    // 与前一个节点数值相同
            count++;
        } else {                              // 与前一个节点数值不同
            count = 1;
        }
        pre = cur;                            // 更新上一个节点

        if (count == maxCount) {              // 如果和最大值相同,放进result中
            result.push_back(cur->val);
        }
        if (count > maxCount) {               // 如果计数大于最大值频率
            maxCount = count;                 // 更新最大频率
            result.clear();                   // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
            result.push_back(cur->val);
        }
        searchBST(cur->right);        //右
        return;
    }
public:
    vector<int> findMode(TreeNode* root) {
        int maxCount = 0;
        int count = 0;
        TreeNode* pre = nullptr;              // 记录前一个节点
        result.clear();
        //上面这四行有啥用啊,不写代码也能跑通啊,而且前面都已经定义过了。。
        searchBST(root);
        return result;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(1)  (假设由递归产生的隐式调用栈的开销不被计算在内)

解法二:迭代法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> findMode(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* pre = nullptr;
        vector<int> result;
        int maxCount = 0;// 最大频率
        int count = 0;// 统计频率
        while (cur != nullptr || !st.empty()) {
            if (cur != nullptr) {      // 指针来访问节点,访问到最底层
                st.push(cur);          // 将访问的节点放进栈
                cur = cur->left;       //左
            } else {
                cur = st.top();
                st.pop();              //中
                if (pre == nullptr) {  // 第一个节点
                    count = 1;
                } else if (pre->val == cur->val) {    // 与前一个节点数值相同
                    count++;
                } else {                              // 与前一个节点数值不同
                    count = 1;
                }
                if (count == maxCount) {                   // 如果和最大值相同,放进result中
                result.push_back(cur->val);
                }
                if (count > maxCount) {                    // 如果计数大于最大值频率
                maxCount = count;                      // 更新最大频率
                result.clear();                        // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
                result.push_back(cur->val);
                }
                pre = cur;
                cur = cur->right;          // 右
            }
        }
        return result;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(1)

思路

1、二叉搜索树的中序遍历是有序的,升序,

弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。

而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。

2、众数:出现频率最高的那个数,一组数中,众数可能不只有一个,因此是个集合。

怎样只遍历一遍二叉搜索树,就能找到所有众数呢?

(1)如果 频率count 等于 maxCount(最大频率),就要把这个元素加入到结果集中

(2)频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集,因为结果集之前的元素都失效了。 

题型五:二叉树公共祖先问题

例1: 236. 二叉树的最近公共祖先 - 力扣(LeetCode)

 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == p || root == q || root == NULL) return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        if (left != NULL && right != NULL) return root;

        if (left == NULL && right != NULL) return right;
        else if (left != NULL && right == NULL) return left;
        else {   //  (left == NULL && right == NULL)
            return NULL;
        }
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

思路 

 自底向上查找,找到公共祖先——回溯

后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。

 情况一:

如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。

判断逻辑是 如果递归遍历遇到q,就将q返回,遇到p 就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q 和p 的最近祖先。

情况二: 

节点本身p(q),它拥有一个子孙节点q(p)

其实情况一 和 情况二 代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。因为遇到 q 或者 p 就返回,这样也包含了 q 或者 p 本身就是 公共祖先的情况。

该题需要遍历整棵树。 

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

 解法一:递归法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

class Solution {
private:
    TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) {
        if (cur == NULL) return cur;
                                                         //中
        if (cur->val > p->val && cur->val > q->val) {    //左
            TreeNode* left = traversal(cur->left, p, q);
            if (left != NULL) {
                return left;
            }
        }
        if (cur->val < p->val && cur->val < q->val) {     //右
            TreeNode* right = traversal(cur->right, p, q);
            if (right != NULL) {
                return right;
            }
        }
        return cur;
    }
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        return traversal(root, p, q);
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

解法二:迭代法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        while (root) {
            if (root->val > p->val && root->val > q->val) {
                root = root->left;
            } else if (root->val < p->val && root->val < q->val) {
                root = root->right;
            } else return root;
        }
        return NULL;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(1)

思路

二叉搜索树的最近公共祖先问题要比普通二叉树的最近公共祖先问题简单。

不用使用回溯,二叉搜索树自带方向性,可以方便的从上向下查找目标区间,遇到目标区间内的节点,直接返回。

二叉搜索树是有序的,在有序树里,如何判断一个节点的左子树里有p,右子树里有q呢?

因为是有序树,所有 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p。

那么只要从上到下去遍历,遇到 cur节点是数值在[p, q]区间中则一定可以说明该节点cur就是q 和 p的公共祖先。 那问题来了,一定是最近公共祖先吗

如图,我们从根节点搜索,第一次遇到 cur节点是数值在[p, q]区间中,即 节点5,此时可以说明 p 和 q 一定分别存在于 节点 5的左子树,和右子树中。

此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为q的祖先, 如果从节点5继续向右遍历则错过成为p的祖先。

所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[p, q]区间中,那么cur就是 p和q的最近公共祖先。

本题是标准的搜索一条边立刻返回的写法 。 

一些关于二叉树的问题: 

   1.平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合?

        是的,是二叉搜索树和平衡二叉树的结合。

     2.平衡二叉树与完全二叉树的区别在于底层节点的位置?

        是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。

     3.堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?

        堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树

题型六:二叉搜索树的修改与构造

例1:701. 二叉搜索树中的插入操作 - 力扣(LeetCode) 

解法一:递归法 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (root == nullptr) {
            TreeNode* node = new TreeNode(val);
            return node;
        }
        if (root->val > val) root->left = insertIntoBST(root->left, val);
        if (root->val < val) root->right = insertIntoBST(root->right, val);
        return root;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

 解法二:迭代法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* insertIntoBST(TreeNode* root, int val) {
        if (root == nullptr) {
            TreeNode* node = new TreeNode(val);
            return node;
        }
        TreeNode* cur = root;
        TreeNode* parent = root;
        while (cur != nullptr) {
            parent = cur;
            if (cur->val > val) cur = cur->left;
            else cur = cur->right;
        }
        TreeNode* node = new TreeNode(val);
        if (parent->val > val) parent->left = node;
        else parent->right = node;
        return root;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(1)

 注意

可以不考虑题目中提示所说的改变树的结构的插入方式。

只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。

例2:450. 删除二叉搜索树中的节点 - 力扣(LeetCode) 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        if (root == nullptr) return root;// 第一种情况:没找到删除的节点,遍历到空节点直接返回了
        if (root->val == key) {
        // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
            if (root->left == nullptr && root->right == nullptr) {
                ///! 内存释放
                delete root;
                return nullptr;
            }
            // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
            else if (root->left == nullptr) {
                auto retnode = root->right;
                ///! 内存释放
                delete root;
                return retnode;
            }
            // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
            else if (root->right == nullptr) {
                auto retnode = root->left;
                delete root;
                return retnode;
            }
            // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
            // 并返回删除节点右孩子为新的根节点。
            else {
                TreeNode* cur = root->right;// 找右子树最左面的节点
                while (cur->left != nullptr) {
                    cur = cur->left;
                }
                cur->left = root->left;// 把要删除的节点(root)左子树放在cur的左孩子的位置
                TreeNode* tmp = root;// 把root节点保存一下,下面来删除
                root = root->right;// 返回旧root的右孩子作为新root
                delete tmp;// 释放节点内存
                return root;
            }
        }
        if (root->val > key) root->left = deleteNode(root->left, key);
        if (root->val < key) root->right = deleteNode(root->right, key);
        return root;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

 思路

有以下五种情况:

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

第五种情况详解:

            

            

(1)删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。

(2)将删除节点(元素7)的左孩子放到删除节点(元素7)的右子树的最左面节点(元素8)的左孩子上,就是把5为根节点的子树移到了8的左孩子的位置。

(3)要删除的节点(元素7)的右孩子(元素9)为新的根节点。.

(4)这样就完成删除元素7的逻辑,最好动手画一个图,尝试删除一个节点试试。

例3: 669. 修剪二叉搜索树 - 力扣(LeetCode)

解法一:递归法 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if (root == nullptr) return nullptr;
        if (root->val < low) {
            TreeNode* right = trimBST(root->right, low, high);// 寻找符合区间[low, high]的节点
            return right;
        }
        if (root->val > high) {
            TreeNode* left = trimBST(root->left, low, high);// 寻找符合区间[low, high]的节点
            return left;
        }
        root->left = trimBST(root->left, low, high);// root->left接入符合条件的左孩子
        root->right = trimBST(root->right, low, high);// root->right接入符合条件的右孩子
        return root;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

 解法二:迭代法

因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。

在剪枝的时候,可以分为三步:

  • 将root移动到[L, R] 范围内,注意是左闭右闭区间
  • 剪枝左子树
  • 剪枝右子树
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* trimBST(TreeNode* root, int low, int high) {
        if (!root) return nullptr;
        // 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭
        while (root != nullptr && (root->val < low || root->val > high)) {
            if (root->val < low) root = root->right;// 小于L往右走
            else root = root->left;// 大于R往左走
        }
        TreeNode* cur = root;
        // 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况
        while (cur != nullptr) {
            while (cur->left && cur->left->val < low) {
                cur->left = cur->left->right;
            }
            cur = cur->left;
        }
        cur = root;
        // 此时root已经在[L, R] 范围内,处理右孩子大于R的情况
        while (cur != nullptr) {
            while (cur->right && cur->right->val > high) {
                cur->right = cur->right->left;
            }
            cur = cur->right;
        }
        return root;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(1)

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

 

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    TreeNode* traversal(vector<int>& nums, int left, int right) {
        if (left > right) return nullptr;
        int mid = left + ((right - left) / 2);
        TreeNode* root = new TreeNode(nums[mid]);
        root->left = traversal(nums, left, mid - 1);
        root->right = traversal(nums, mid + 1, right);
        return root;
    }
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        TreeNode* root = traversal(nums, 0, nums.size() - 1);
        return root;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(logn)

注意:

1、高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

2、取数组中间元素的位置,不难写出int mid = (left + right) / 2;这么写其实有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,在二分法中尤其需要注意!所以可以这么写:int mid = left + ((right - left) / 2);

3、这里int mid = left + ((right - left) / 2);的写法相当于是如果数组长度为偶数,中间位置有两个元素,取靠左边的。

4、注意:在调用traversal的时候传入的left和right为什么是0和nums.size() - 1,因为定义的区间为左闭右闭

例5: 538. 把二叉搜索树转换为累加树 - 力扣(LeetCode)

 

 解法一:递归法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    int pre = 0;// 记录前一个节点的数值
    void traversal(TreeNode* cur) {// 右中左遍历
        if (cur == nullptr) return;
        traversal(cur->right);
        cur->val += pre;
        pre = cur->val;
        traversal(cur->left);
    }
public:
    TreeNode* convertBST(TreeNode* root) {
        pre = 0;
        traversal(root);
        return root;
    }
};
//递归法
//时间复杂度O(n)
//空间复杂度O(n)

解法二:迭代法

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    int pre = 0; // 记录前一个节点的数值
    void traversal(TreeNode* root) {
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while (cur != nullptr || !st.empty()) {
            if (cur != nullptr) {
                st.push(cur);
                cur = cur->right;// 右
            } else {
                cur = st.top();// 中
                st.pop();
                cur->val += pre;
                pre = cur->val;
                cur = cur->left; // 左
            }
        }
    }
public:
    TreeNode* convertBST(TreeNode* root) {
        pre = 0;
        traversal(root);
        return root;
    }
};
//迭代法
//时间复杂度O(n)
//空间复杂度O(n)

从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了

  • 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。

  • 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。

  • 求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值