以下内容总结了二叉树的基本知识,并安排了习题,大部分借鉴了链接6,右边列出了目录,内容较多,合理安排时间。
1、二叉树遍历
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问依次且仅被访问一次。
二叉树的四种遍历方式分别为:前序遍历、中序遍历、后序遍历、层序遍历。
- 前序遍历:先访问根节点,再访问左节点,最后访问右节点。
- 中序遍历:先访问左节点,再访问根节点,最后访问右节点。
- 后序遍历:先访问左节点,再访问右节点,最后访问根节点。
- 层序遍历:将二叉树的每一层分别遍历,直到最后的叶子节点被全部遍历完,这里要用到的辅助数据结构是队列,队列具有先进先出的性质。
示例:
前序遍历:1245367
中序遍历:4251637
后序遍历:4526731
层序遍历:1234567
代码:
(1)此时代码包含的头文件:
#include<iostream>
#include<vector>
#include<string>
#include<queue>
#include<sstream>
using namespace std;
(2)定义二叉树节点:
//定义二叉树节点
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x):val(x),left(NULL),right(NULL) {}
};
//定义一个字符串,存储遍历后的结果
string serial;
(3)创建二叉树
//创建二叉树,这里采用了比较蠢的手动一个个创建 --!
TreeNode* createBinaryTree()
{
TreeNode *cur, *root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
cur = root->left;
cur->left = new TreeNode(4);
cur->right = new TreeNode(5);
cur = root->right;
cur->left = new TreeNode(6);
cur->right = new TreeNode(7);
return root;
}
(4)四种遍历方式:
①层序遍历:
//层序遍历:每一层分别遍历。
void levelOrderTrave(TreeNode *root)
{
if (!root)
return;
queue<TreeNode*> q;
q.push(root);
TreeNode *cur;
while (!q.empty())
{
cur = q.front();
q.pop();
serial += to_string(cur->val) + " "; //空格隔开
if (cur->left) q.push(cur->left);
if (cur->right) q.push(cur->right);
}
}
②递归调用,实现前中后序三种遍历:
//前序遍历:先访问根节点,再访问左节点,最后访问右节点。
void preOrderTrave(TreeNode *root)
{
if (!root)
return;
serial += to_string(root->val)+" "; //空格隔开
preOrderTrave(root->left);
preOrderTrave(root->right);
}
//中序遍历:先访问左节点,再访问根节点,最后访问右节点。
void inOrderTrave(TreeNode *root)
{
if (!root)
return;
inOrderTrave(root->left);
serial += to_string(root->val) + " "; //空格隔开
inOrderTrave(root->right);
}
//后序遍历:先访问左节点,再访问右节点,最后访问根节点。
void postOrderTrave(TreeNode *root)
{
if (!root)
return;
postOrderTrave(root->left);
postOrderTrave(root->right);
serial += to_string(root->val) + " "; //空格隔开
}
③不使用递归调用,使用栈实现前中后序三种遍历(重要):
以下三个代码架构完全一样,只是节点的压入顺序不一样,非常牛逼,参考链接3
前序遍历:
//使用栈实现前序遍历
//输出顺序为:根->左->右,因此入栈为右->左->根
vector<int> preOrderTrave(TreeNode* root)
{
vector<int> res;
stack<TreeNode*> st;
if(root!=NULL)
st.push(root);
TreeNode* cur;
while( !st.empty() )
{
cur=st.top();
st.pop();
if(cur)
{
if(cur->right) st.push(cur->right); //先压入右节点
if(cur->left) st.push(cur->left); //再压入左节点
st.push(cur); //再压入当前节点
st.push(NULL); //NULL标志区分每个递归调用栈
}else{
res.emplace_back(st.top()->val);
st.pop();
}
}
return res;
}
中序遍历:
//使用栈实现中序遍历
//输出顺序为:左->根->右,因此入栈为右->根->左
vector<int> inOrderTrave(TreeNode* root) {
vector<int> res;
stack<TreeNode*> st;
if(root!=NULL)
st.push(root);
TreeNode* cur;
while(!st.empty())
{
cur=st.top();
st.pop();
if(cur)
{
if(cur->right) st.push(cur->right); //先压入右节点
st.push(cur); //再压入当前节点
st.push(NULL);
if(cur->left) st.push(cur->left); //再压入左节点
}else{
res.emplace_back(st.top()->val);
st.pop();
}
}
return res;
}
后序遍历:
//使用栈实现后序遍历
//输出顺序为:左->右->根,因此入栈为根->右->左
vector<int> postOrderTrave(TreeNode* root) {
vector<int> res;
stack<TreeNode*> st;
if(root!=NULL)
st.push(root);
TreeNode* cur;
while( !st.empty())
{
cur=st.top();
st.pop();
if(cur)
{
st.push(cur); //先压入当前节点
st.push(NULL);
if(cur->right) //再压入右节点
st.push(cur->right);
if(cur->left) //再压入左节点
st.push(cur->left);
}else{
res.emplace_back(st.top()->val);
st.pop();
}
}
return res;
}
(5)主函数
//主函数
int main()
{
//构建二叉树
TreeNode *root = createBinaryTree();
//前序遍历
serial = "";
preOrderTrave(root);
//中序遍历
serial = "";
inOrderTrave(root);
//后序遍历
serial = "";
postOrderTrave(root);
//层序遍历
serial = "";
levelOrderTrave(root);
return 0;
}
2、递归解决问题
基本概念:
“自顶向下” 的解决方案
“自顶向下” 意味着在每个递归层级,我们将首先访问节点来计算一些值,并在递归调用函数时将这些值传递到子节点。 所以 “自顶向下” 的解决方案可以被认为是一种前序遍历。 具体来说,递归函数 top_down(root, params) 的原理是这样的:
1. return specific value for null node
2. update the answer if needed // anwer <-- params
3. left_ans = top_down(root.left, left_params) // left_params <-- root.val, params
4. right_ans = top_down(root.right, right_params) // right_params <-- root.val, params
5. return the answer if needed // answer <-- left_ans, right_ans
示例:给定一个二叉树,请寻找它的最大深度。
1. return if root is null
2. if root is a leaf node:
3. answer = max(answer, depth) // update the answer if needed
4. maximum_depth(root.left, depth + 1) // call the function recursively for left child
5. maximum_depth(root.right, depth + 1) // call the function recursively for right child
“自底向上” 的解决方案
“自底向上” 是另一种递归方法。 在每个递归层次上,我们首先对所有子节点递归地调用函数,然后根据返回值和根节点本身的值得到答案。 这个过程可以看作是后序遍历的一种。 通常, “自底向上” 的递归函数 bottom_up(root) 为如下所示:
1. return specific value for null node
2. left_ans = bottom_up(root.left) // call function recursively for left child
3. right_ans = bottom_up(root.right) // call function recursively for right child
4. return answers // answer <-- left_ans, right_ans, root.val
继续讨论前面关于树的最大深度的问题:
1. return 0 if root is null // return 0 for null node
2. left_depth = maximum_depth(root.left)
3. right_depth = maximum_depth(root.right)
4. return max(left_depth, right_depth) + 1 // return depth of the subtree rooted at root
例题:
(1)二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
具体参考链接4。
方法1:自顶向下
//自顶向下
int ans=0;
void topToBot(TreeNode* root, int d)
{
if(!root)
return ;
if(!root->left && !root->right)
ans=max(ans,d);
topToBot(root->left, d+1);
topToBot(root->right, d+1);
}
//主函数
int maxDepth(TreeNode* root) {
topToBot( root, 1);
return ans;
}
方法2:自底向上
//自底向上
int botToTop(TreeNode* root) //因为要找的是最大深度,所以不用考虑是否是叶子节点(左右子节点均为空)
{
if(!root)
return 0;
int left=botToTop(root->left)+1;
int right=botToTop(root->right)+1;
return max(left,right);
}
//主函数
int maxDepth(TreeNode* root)
{
return botToTop(root);
}
(2)111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
返回它的最小深度 2.
思路:
这里面最重要的一点就是判断最终的节点是否是叶子节点,即没有子节点的节点,非常重要。
代码:
/**
* 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:
// 很多人写出的代码都不符合 1,2 这个测试用例,是因为没搞清楚题意
// 题目中说明:叶子节点是指没有子节点的节点,这句话的意思是 1 不是叶子节点
// 题目问的是到叶子节点的最短距离,所以所有返回结果为 1 当然不是这个结果
int min_depth=INT_MAX;
void topToDown(TreeNode* root, int d)
{
if(root==nullptr) //1.空节点返回
return ;
if( root->left ==nullptr && root->right ==nullptr ) //如果是叶子节点,才更新结果
{
min_depth=min(min_depth, d );
return ;
}
topToDown(root->left, d+1);
topToDown(root->right, d+1);
}
//主函数
int minDepth(TreeNode* root)
{
if(root==nullptr) //空节点返回0
return 0;
topToDown(root, 1);
return min_depth;
}
};
(3)对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
进阶:
你可以运用递归和迭代两种方法解决这个问题吗?
代码:
class Solution {
public:
bool isEqual(TreeNode* left, TreeNode* right)
{
if( left==NULL && right==NULL )
return true;
if(left==NULL || right==NULL || left->val!=right->val)
return false;
return isEqual(left->left, right->right) && isEqual(left->right, right->left);
}
bool isSymmetric(TreeNode* root) {
if(!root)
return true;
return isEqual(root->left,root->right) ;
}
};
(4)路径总和
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
代码:
/**
* 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:
bool topToBot(TreeNode* root, int n)
{
if( root==NULL )
return false;
if(root->left==NULL && root->right==NULL && n==root->val )
return true;
return topToBot( root->left, n-root->val) || topToBot( root->right, n-root->val);
}
bool hasPathSum(TreeNode* root, int sum) {
if(root==NULL)
return false;
return topToBot(root, sum);
}
};
3、还原二叉树
(1)从中序与后序遍历序列构造二叉树
根据一棵树的中序遍历与后序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
思路:
- 后续序列的最后一个元素是根节点,我们可以根据根节点在中序序列中的位置,将中序序列分为左右子树。
- 后续序列接着是倒数第二个元素是右节点,因而我们需要调用中序序列中的右子树序列。
- 后续序列接着是倒数第三个元素是左节点,因而我们需要调用中序序列中的左子树序列。
因此,这道题的关键点为中序序列,通过递归调用,可以实现重构。
代码:
/**
* 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:
unordered_map<int, int> ump; //存储中序序列中,节点及其索引
vector<int> m_inorder;
vector<int> m_postorder;
int m_post_ind;
TreeNode* topToBot(int inL, int inR) //递归调用中序序列的左右子树对应的序列
{
if(inL>inR)
return nullptr;
TreeNode* root=new TreeNode(m_postorder[m_post_ind]); //构造根节点
int in_ind=ump[m_postorder[m_post_ind]];
m_post_ind--;
root->right=topToBot(in_ind+1,inR); //先构造右子树,对应中序序列的右子树序列
root->left=topToBot(inL,in_ind-1); //再构造左子树,对应中序序列的左子树序列
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
for(int i=0;i<inorder.size();i++)
ump.insert({inorder[i],i});
m_inorder=inorder;
m_postorder=postorder;
m_post_ind=postorder.size()-1; //根节点在后序序列中的索引
int inL=0, inR=inorder.size()-1;
return topToBot(inL, inR);
}
};
(2)从前序与中序遍历序列构造二叉树
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
思路:
前序序列(根左右)和后序序列(左右根)的元素顺序是相反的,借助上一题的思路,我们从前序序列的头部开始构建根节点,再构造左子树,其次构造右子树。
递归调用还原二叉树。
代码;
/**
* 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:
unordered_map<int, int> ump;
vector<int> m_preorder;
vector<int> m_inorder;
int m_pre_ind;
TreeNode* topToBot(int inL, int inR )
{
if(inL>inR)
return nullptr;
TreeNode* root=new TreeNode(m_preorder[m_pre_ind]); //构造根节点
int in_ind=ump[m_preorder[m_pre_ind]];
m_pre_ind++;
root->left=topToBot(inL, in_ind-1); //先构造左子树,对应中序序列的左子树序列
root->right=topToBot(in_ind+1,inR); //再构造右子树,对应中序序列的右子树序列
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
m_preorder=preorder;
m_inorder=inorder;
m_pre_ind=0; //根节点索引
for(int i=0;i<inorder.size();i++) //记录中序序列节点与索引
ump.insert({inorder[i],i});
int inL=0, inR=inorder.size()-1;
return topToBot(inL,inR); //从前序序列的头到尾进行构造
}
};
(3)二叉树序列化与恢复
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
思路:
详情参考链接2.
代码:
//采用前序遍历进行序列化,每个节点用空格隔开,空节点记录为null
string serialize(TreeNode* root)
{
if (!root) return "null";
return to_string(root->val) + " " + serialize(root->left) + " " + serialize(root->right) + " ";
}
//恢复, stringstream需要包含头文件#include< sstream>
TreeNode* mydeserialize(stringstream &s)
{
string tmp_s;
if (!(s >> tmp_s) || tmp_s == "null") return NULL;
TreeNode* tmp_node = new TreeNode(stoi(tmp_s));
tmp_node->left = mydeserialize(s);
tmp_node->right = mydeserialize(s);
return tmp_node;
}
//主函数
int main()
{
//构建二叉树
TreeNode *root = createBinaryTree();
//序列化
serial = serialize(root);
//恢复二叉树
if (serial[0] == 'n') return NULL;
stringstream s(serial);
TreeNode *new_root = mydeserialize(s);
return 0;
}
(4)二叉树序列化与恢复Ⅱ
根据前序序列vector< string>来构造二叉树,并且再将构造的二叉树前序遍历,看是否与题设vector一致。
代码:
TreeNode* vecToTree(vector<string> vec, int &start)
{
if (vec[start] == "null")
{
start++;
return nullptr;
}
else {
TreeNode* root = new TreeNode(stoi(vec[start]));
start++;
root->left = vecToTree(vec, start);
root->right = vecToTree(vec, start);
return root;
}
}
void treeToVec(TreeNode* root, vector<string> &vec)
{
if (root == nullptr)
{
vec.push_back("null");
return;
}
vec.push_back(to_string(root->val));
treeToVec(root->left, vec);
treeToVec(root->right, vec);
}
int main()
{
//给出序列
vector<string> tree_vec{ "1","2","4","null","null","null","3","null","5","null","null" };
//根据序列构造二叉树
int ind = 0;
TreeNode* root = vecToTree(tree_vec, ind); //函数中第二个参数是引用传递,因此得创建变量ind=0,不能直接输入0,会报错
//将生成的二叉树前序遍历
vector<string> new_vec;
treeToVec(root, new_vec);
//比较两个序列
if (tree_vec.size() != new_vec.size())
{
cout << "different" << endl;
}
else {
for (int i = 0; i < tree_vec.size(); i++)
{
if (tree_vec[i] != new_vec[i])
{
cout << "different" << endl;
break;
}
}
cout << "same" << endl;
}
return 0;
}
结果:
4、总结与习题
(1)填充每个节点的下一个右侧节点指针
给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
示例:
思路:
- 从根节点的左右子节点开始,先将左子节点连接到右子节点。
- 递归调用,分3组:(左子节点的左子节点,左子节点的右子节点),(左子节点的右子节点,右子节点的左子节点),(右子节点的左子节点,右子节点的右子节点)。
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;
Node() : val(0), left(NULL), right(NULL), next(NULL) {}
Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
*/
class Solution {
public:
void top2Bot(Node* left, Node* right)
{
if( left==nullptr || right==nullptr )
return ;
if(left!=nullptr && right!=nullptr)
left->next=right;
top2Bot(left->left,left->right); //(左子节点的左子节点,左子节点的右子节点)
top2Bot(left->right,right->left); //(左子节点的右子节点,右子节点的左子节点)
top2Bot(right->left,right->right); //(右子节点的左子节点,右子节点的右子节点)
}
Node* connect(Node* root) {
if(root==nullptr)
return nullptr;
top2Bot(root->left,root->right);
return root;
}
};
(2)填充每个节点的下一个右侧节点指针 II
给定一个二叉树
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
进阶:
你只能使用常量级额外空间。
使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
示例:
思路:
使用层次遍历,进行每一层的节点相连。
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;
Node() : val(0), left(NULL), right(NULL), next(NULL) {}
Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
*/
class Solution {
public:
Node* connect(Node* root) {
if(root==nullptr)
return nullptr;
Node* cur;
queue<Node*> q;
q.push(root);
while(!q.empty())
{
int n=q.size();
cur=q.front(); //取出每层第一个节点
q.pop();
if(cur->left) q.push(cur->left); //不要忘记顺序压入子节点
if(cur->right) q.push(cur->right);
for(int i=1;i<n;i++) //将前一个节点与后一个节点相连
{
Node* tmp=q.front();
q.pop();
cur->next=tmp;
cur=tmp;
if(cur->left) q.push(cur->left);
if(cur->right) q.push(cur->right);
}
}
return root;
}
};
(3)二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。*
思路:
参考链接5
代码:
/**
* 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==nullptr || root==p || root==q )
return root;
TreeNode* left=lowestCommonAncestor(root->left, p, q);
TreeNode* right=lowestCommonAncestor(root->right, p, q);
if(left && right )
return root;
return left?left:right;
}
};
(4)平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
返回 true 。
示例 2:
给定二叉树 [1,2,2,3,3,null,null,4,4]
返回 false 。
思路:
判断某一节点的左右子树的高度差不超过1,所以要求其左右子树的高度。
- 对于左右子节点,其高度等于它的左右子树最大高度加1,即 max(左子树高度,右子树高度)+1。递归调用计算某一节点的高度。
- 其次判断左右子树的高度差不超过1,再判断左右子树是否满足平衡二叉树。
代码:
/**
* 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:
int getDepth(TreeNode* root)
{
if(root==NULL)
return 0;
return max(getDepth( root->left),getDepth(root->right))+1;
}
bool isBalanced(TreeNode* root) {
if(root==NULL)
return true;
//判断当前节点是否平衡,以及左右子树是否平衡
return abs(getDepth( root->left)-getDepth(root->right))<=1 && isBalanced( root->left) && isBalanced(root->right);
}
};
结果:
参考链接:
[1] Du~佛系码农:二叉树的四种遍历方式
[2] 消失男孩:[LeetCode][C++] 二叉树的序列化与反序列化
[3] PualKing:完全模仿递归,不变一行。秒杀全场,一劳永逸
[4] 力扣:运用递归解决树的问题
[5] 消失男孩:[LeetCode][C++]二叉树的最近公共祖先
[6] 力扣:二叉树