目录
- 8.1 二叉树理论基础
- 8.2 前、中、后序的递归遍历
- 8.3 前中后序的迭代遍历
- 8.4 前中后序统一迭代法
- 8.5 二叉树的层序遍历
- 8.6 反转二叉树
- 8.7 对称二叉树
- 8.7.1 递归法
- 8.8 二叉树的最大深度
- 8.9 二叉树的最小深度
- 8.10 平衡二叉树
- 8.11 二叉树的所有路径
- 8.12 路径总和
- 8.13 构造二叉树
- 8.14 合并两棵二叉树
- 8.15 在二叉搜索树中寻找节点
- 验证二叉搜索树
- 8.17 二叉搜索树的最小绝对差
- 8.18 二叉搜索树中的众数
- 8.19 二叉树的最近公共祖先
- 8.20 在二叉搜索树中插入一个节点
- 8.21 在二叉搜索树中删除一个节点
- 8.22 修剪二叉搜索树
- 8.23 构造一棵平衡二叉搜索树
- 8.24 本章小结
8.1 二叉树理论基础
8.1.1 二叉树的种类
1.满二叉树
如果一棵树只有度为0和度为2的节点,并且度为0的节点在同一层,则称这棵树为满二叉树。假设这棵树的深度为k(从1开始计算),则这棵树又(2^k-1)个节点。
2.完全二叉树
除了底层节点可能没有像满二叉树那样被填满,其余每层的节点数都达到了最大值,并且底层的节点都集中在 该层最左边的若干位置。若底层为第h层,则该层包含[1~2^(h-1)]个节点。
3.二叉搜索树
前面介绍的两种二叉树都没有数值,而二叉搜索树是由数值的:
若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值。
若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值。
它的左右子树也分别是二叉排序树。
4.平衡二叉搜索树
平衡二叉搜索树又称为AVI(Adelson-Velsky and Landis)树,它是一棵空树或它的左右子树的高度差的绝对值不超过1,并且左右子树又都是一颗平衡二叉树。
C++中map、set、multimap、multiset的底层实现都是平衡二叉搜索树(红黑树),所以它们的查找操作的时间复杂度为O(logn),而unordered_map、unordered_set的底层实现是哈希表。
在使用自己熟悉的编程语言实现算法时,一定要对常用的容器底层实现了如指掌,否则很难分析自己写的代码性能。
8.1.2 二叉树的存储方式
二叉树既可以链式存储也可以顺序存储。
链式存储使用的是指针,顺序存储使用的是数组。顺序存储的元素在内存中是连续分布的,而链式存储通过指针把散落在各个地址上的节点串联起来。
那么顺序(数组)存储的二叉树是如何遍历的呢?如果父节点的数组下标是i,那么它的左孩子就是 i2+1,右孩子就是i2+2。 但用链式存储的二叉树更利于我们理解,所以一般使用链式存储的方式。
8.1.3 二叉树的遍历方式
二叉树主要有两种遍历方式:
1.深度优先遍历,遇到叶子节点再往回遍历。
2.广度优先遍历,一层一层的遍历。
以上是图论中最基本的两种遍历方式,将这两种方式进一步扩展:
1.深度优先遍历:前序遍历(递归法、迭代法)、中序遍历(递归法、迭代法)、后序遍历(递归法、迭代法),前中后指的是中间节点遍历的位置。
2.广度优先遍历:层序遍历(迭代法)。
我们做二叉树相关题目时,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归比较方便。编程语言都是通过栈来实现递归的,也就是说前中后序遍历可以使用栈来实现非递归方式。而广度优先遍历一般使用队列来实现,这是因为通过先进先出的结构,才能一层一层的遍历二叉树。
8.1.4 二叉树的定义
在本章的二叉树面试题目中,节点均采用下面的定义方式:
struct TreeNode {
int val
TreeNode* left;
TreeNode* right;
TreeNode(int val) : val(val), left(nullptr), right(nullptr) {}
};
我们发现,二叉树的定义和 链表的定义是差不多的,相对于链表,二叉树的节点中多了两个分别指向左孩子右孩子的指针。在现场面试的时候面试官可能要求面试者手写代码,所以一定要锻炼自己可以手写数据结构的定义和简单逻辑的代码。
8.2 前、中、后序的递归遍历
本节介绍三种递归写法,通过简单的题目建立方法论,有了方法论,后面才能应付复杂的递归。
递归三要素,每次写递归算法时都基于下面的三部曲:
1.确定递归函数的返回值和参数:确定哪些参数在递归过程中需要处理就在递归函数中加上这些参数,并且明确每次递归的返回值是什么,进而确定递归函数的返回类型。
2.确定终止条件:写完递归算法,程序运行的时候经常会遇到栈溢出的错误,原因就是没写终止条件或终止条件写得不对。在操作系统中,也是用一个栈来保存每一次递归的信息的,如果递归没有终止,那么操作系统中的内存栈必然会溢出。
3.确定单层递归的逻辑:确定每一层递归需要处理的信息。这里会重复调用函数本身来实现递归的过程。
我们以前序遍历为例子:
1.确定递归函数的返回值和参数。
因为要打印出前序遍历节点的数值,所以参数中需要传入数组(C++中使用vector)来存放数值,除了这一点,就不需要再处理其他数据了,也不需要返回值,所以递归函数的返回值就是void,代码如下:
void traversal(TreeNode* cur, vector<int>& vec)
2.确定终止条件。
在递归的过程中,如何确定递归结束了呢?当当前遍历的节点为空时,说明本层递归就要结束了。如果当前遍历的这个节点为空,则直接返回,代码如下:
if (cur == NULL) return;
3.确定单层循环的逻辑。
前序遍历的顺序是中->左->右,所以单层循环的逻辑就是先取中节点的数值,再处理左子树和右子树,代码如下:
vec.push_back(val->val);//中
traveral(cur->left, vec);//左
traveral(cur->right, vec);//右
完整代码如下:
class Solution{
public:
void traveral(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur);
traveral(cur->left, vec);
trvaeral(cur->right, vec);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traveral(root, result);
return result;
}
};
中序和后序遍历只是处理顺序不一样,代码如下:
void traveral(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traveral(cur->left, vec);
vec.push_back(cur);
trvaeral(cur->right, vec);
}
void traveral(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traveral(cur->left, vec);
trvaeral(cur->right, vec);
vec.push_back(cur);
}
8.3 前中后序的迭代遍历
为什么可以用迭代法来实现二叉树的前中后序遍历呢?因为递归的实现就是:每次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈,然后在结束本层递归的时候,从栈顶弹出上一次递归的各项参数,这就是为什么递归可以返回上一层位置的原因。
8.3.1 前序遍历
力扣题号: 144.二叉树的前序遍历。
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,2,3]
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
进阶:递归算法很简单,你可以通过迭代算法完成吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-tree-preorder-traversal/
思路
前序遍历的顺序是中左右,每次先处理中间节点,先将中间节点入栈,然后将右孩子入栈,最后将左孩子入栈。为什么要先将右孩子入栈,再将左孩子入栈呢?因为只有这样出栈顺序才是中左右,符合前序遍历的顺序。
题解
/**
* 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 == NULL) return result;
st.push(root);//根节点入栈
while (!st.empty()) {//栈不空执行循环(栈中还有元素说明还有节点未遍历)
TreeNode* node = st.top();
st.pop(); //先处理中间节点
result.push_back(node->val);//中间节点存入result
if (node->right) st.push(node->right);//右孩子先进栈
if (node->left) st.push(node->left);//左孩子后进栈,才能保证出栈顺序先左后右(中间节点已经遍历完成加入result)
}
return result;
}
};
8.3.2 后序遍历
力扣题号: 145.二叉树的后序遍历
给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[3,2,1]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
树中节点的数目在范围 [0, 100] 内
-100 <= Node.val <= 100
进阶:递归算法很简单,你可以通过迭代算法完成吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-tree-postorder-traversal
思路
后序遍历的顺序左右中,而前序遍历的顺序是中左右,是不是只要将前序遍历的顺序改成中右左,最后再反转result就能得到左右中了?对前序遍历的代码稍作修改,后序遍历的代码如下:
题解
/**
* 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 == NULL) return result;
st.push(root);//根节点入栈
while (!st.empty()) {//栈不空执行循环(栈中还有元素说明还有节点未遍历)
TreeNode* node = st.top();
st.pop(); //先处理中间节点
result.push_back(node->val);//中间节点存入result
if (node->left) st.push(node->left);//左孩子先进栈
if (node->right) st.push(node->right);//右孩子后进栈,才能保证出栈顺序先右后左(中间节点已经遍历完成加入result)
}
reverse(result.begin(), result.end());//将结果反转,得到左右中的遍历顺序
return result;
}
};
此时我们会发现使用迭代法写出前序和后序遍历的代码好像并不难。是不是改一改前序遍历的代码顺序就又可以实现中序遍历了呢?答案是漏!
8.3.2 中序遍历
力扣题号: 94.二叉树的中序遍历
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-tree-inorder-traversal
思路
当我们使用迭代法处理元素的过程中,涉及以下两个操作。
处理:将元素放入result数组。
访问:遍历节点。
之所以前序遍历和中序遍历的代码不能通用,是因为前序遍历的顺序是中左右,先访问和处理的元素是中间节点,要访问和处理的元素的顺序是一样的,都是中间节点,所以才能写出相对简洁的代码。
而中序遍历的顺序是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问直到树最左面的底部,然后再开始处理节点(把节点放入result数组),可以看出访问顺序和处理顺序是不一致的。
那该怎样处理才能使用迭代法实现中序遍历呢?需要借助指针的遍历来访问节点,使用栈处理节点上的元素。
题解
/**
* 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) {
stack<TreeNode*> st;
vector<int> result;
TreeNode* cur = root;
while(cur != NULL || !st.empty()) {//当cur指针指向的对象不为空或栈不空,执行循环(这两种情况都表面还有元素未遍历或处理)
if (cur != NULL) {
st.push(cur);//将访问的节点加入栈中,等待处理
cur = cur->left;//左,不断向下,向栈中存入待处理的节点
}
else {//触底反弹,弹出的节点就是要处理的节点
cur = st.top();
st.pop();
result.push_back(cur->val);//中
cur = cur->right;//右
}
}
return result;
}
};
至此,我们使用迭代法分别实现了前中后序遍历,可以看出前序和中序完全是两种代码风格,并不像递归写法那样代码稍作调整就可以实现前中后序遍历。
因为前序遍历中访问节点(遍历节点)和处理节点(将元素放入result数组)是可以同步处理的,但中序遍历就无法做到同步。
我们总想使用一种统一的方式(即只需调整代码顺序就可以实现中序和后序)来进行三种遍历,可以做到吗?可以!
8.4 前中后序统一迭代法
解法的方法——标记法:是将要访问的节点放入栈中,将要处理的节点也放入栈中但要做标记,即将要处理的节点放入栈之后,紧接着放入一个空指针作为标记。
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> inorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root !=NULL) st.push(root);
while (!st.empty()) {//栈不空说明还有元素未加处理(未加入结果集合)
TreeNode* node = st.top();//node节点被访问过但还未处理
if (node != NULL) {//将该节点弹出,避免重复操作,下面再将右左中节点添加到栈中
st.pop();
if (node->right) st.push(node->right);//右
st.push(node);//添加中节点
st.push(NULL);//中节点访问过,但还未处理,加入空节点作为标记
if (node->left) st.push(node->left);//左
}
else {//只有遇到空节点的时候,才将下一个节点放入结果集
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);//加入结果集
}
}
return result;
}
};
2.使用迭代法实现前序遍历
/**
* 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) {
stack<TreeNode*> st;
vector<int> result;
if (root !=NULL) st.push(root);
while (!st.empty()) {//栈不空说明还有元素未加处理(未加入结果集合)
TreeNode* node = st.top();//node节点被访问过但还未处理
if (node != NULL) {//将该节点弹出,避免重复操作,下面再将右左中节点添加到栈中
st.pop();
if (node->right) st.push(node->right);//右
if (node->left) st.push(node->left);//左
st.push(node);//添加中节点
st.push(NULL);//中节点访问过,但还未处理,加入空节点作为标记
}
else {//只有遇到空节点的时候,才将下一个节点放入结果集
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);//加入结果集
}
}
return result;
}
};
3.使用迭代法实现后序遍历
/**
* 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) {
stack<TreeNode*> st;
vector<int> result;
if (root !=NULL) st.push(root);
while (!st.empty()) {//栈不空说明还有元素未加处理(未加入结果集合)
TreeNode* node = st.top();//node节点被访问过但还未处理
if (node != NULL) {//将该节点弹出,避免重复操作,下面再将右左中节点添加到栈中
st.pop();
st.push(node);//添加中节点
st.push(NULL);//中节点访问过,但还未处理,加入空节点作为标记
if (node->right) st.push(node->right);//右
if (node->left) st.push(node->left);//左
}
else {//只有遇到空节点的时候,才将下一个节点放入结果集
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);//加入结果集
}
}
return result;
}
};
此时我们写出了统一风格的迭代法代码,但这样的代码并不好理解,而且在面试直接写出来还是有难度的。可以根据自己的喜好,对于前中后序遍历选择一种自己容易理解的递归和迭代法进行记忆。
8.5 二叉树的层序遍历
力扣题号: 102.二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示:
树中节点数目在范围 [0, 2000] 内
-1000 <= Node.val <= 1000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal
思路
层序遍历也需要借助辅助数据结构——队列,队列先进先出,符合一层一层遍历的逻辑。层序遍历的方式就是图论中广度优先遍历,只不过我们是用在二叉树上。
另外,因为题目要求按每层输出到一个vec中,所以在内循环中还需定义一个vector用来存储每层的节点。
题解
/**
* 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 != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {//栈不空循环,说明还有未加入结果集的节点,遍历每层
int size = que.size();
//这里一定要使用固定大小的size,不能使用que.size(),因为que.size是不断变化的
vector<int> vec;//用来存储每层的节点
for (int i = 0; i < size; 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);
}
//此时,vec存放的即是本层的所有节点,应该加入结果集,而que中存放了下一层的所有节点
result.push_back(vec);
}
return result;
}
};
8.6 反转二叉树
力扣题号:226.反转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:
输入:root = [2,1,3]
输出:[2,3,1]
示例 3:
输入:root = []
输出:[]
提示:
树中节点数目范围在 [0, 100] 内
-100 <= Node.val <= 100
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/invert-binary-tree
思路
反转二叉树,只要将每个节点的左右孩子交换一次即可。那么该用哪种遍历方式呢?只把左右孩子反转一次的遍历方式都是可以的,前后序和层序都是可以的,唯独中序不可以,因为中序会把某些节点的左右孩子反转两次。
8.6.1 递归法
1.确定递归函数的返回值和参数
参数是要传入节点的指针,通常此时需要定下来主要参数,如果在递归的逻辑中发现还需其他参数,则可以随时补充。返回值是要返回root节点的指针,所以函数的返回类型为TreeNode*。
TreeNode* invertTree(TreeNode* root)
2.确定终止条件
当前节点为NULL时,就返回NULL。
if (root = NULL) return root;
3.确定单层递归逻辑
这里采用前序遍历,中序和后序都是一样的,只需改变代码顺序。前序遍历先交换左右孩子节点(中),然后再递归左子树(左)和右子树(右)。
swap(root->left,root->right);
invertTree(root->left);
invertTree(root->right);
题解
/**
* 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 == NULL) return root;
swap(root->left,root->right);
invertTree(root->left);
invertTree(root->right);
return root;
}
};
8.6.2 迭代法
使用迭代法模拟后序遍历代码
题解
/**
* 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 == NULL) return root;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();//中
if (node->left) st.push(node->left);//左
if (node->right) st.push(node->right);//右 这两步,先左先右都可,因为我们只是要反转左右孩子,先遍历哪个孩子都可
swap(node->left, node->right);
//处理顺序左右 中
}
return root;
}
};
使用迭代法模拟前序遍历代码
题解
/**
* 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 == NULL) 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;
}
};
层序遍历
题解
/**
* 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 != NULL) que.push(root);
while (!que.empty()) {
int size = que.size();
for (int i = 0; i < size; 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;
}
};
针对二叉树的问题,解题之前一定要想清楚究竟使用前中后,还是层序遍历。二叉树解题的大忌就是自己稀里糊涂就把代码写出来了,但不清楚是如何遍历的二叉树。
8.7 对称二叉树
力扣题号: 101.对称二叉树
给你一个二叉树的根节点 root , 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
提示:
树中节点数目在范围 [1, 1000] 内
-100 <= Node.val <= 100
进阶:你可以运用递归和迭代两种方法解决这个问题吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/symmetric-tree
思路
判断二叉树是否对称,要比较的不是左右节点,而是要比较根节点的左子树和右子树是否是相互反转的。所以我们其实要比较的是两棵树,所以在遍历过程中,我们要同时遍历两棵树。
比较两棵树怎么比较呢?要比较的是两棵树里侧和外侧的节点是否相等。
遍历的顺序是什么样的?本题只能是后序遍历。因为我们要通过递归函数的返回值来判断两颗子树的内侧和外侧节点是否相等。 因为要遍历两棵树,而且要比较内侧和外侧节点是否相等,所以准确的说,左子树的遍历顺序是左右中,右子树的遍历顺序是右左中。
8.7.1 递归法
1.确定递归函数的参数和返回值。
因为我们要比较根节点的两颗子树是否相互反转,进而判断这棵树是不是对称树,所以要比较的是两棵树,参数是左子树节点和右子树节点,返回值是bool类型。(力扣上给出的参数是root,后续我们在他给出的函数中调用我们的compare函数)
bool compare(TreeNode* left, TreeNode* right)
2.确定终止条件。
要比较两个节点的数值是否相等,首先要确定两个节点是否为空,否则后面比较数值时就会操作空指针了。
节点为空的情况:
1.左节点为空,右节点不为空,返回fasle。
2.左节点不为空,右节点为空,返回fasle。
3.左右节点都为空,返回true。
此时已经排除节点为空的情况,剩下的就是都不为空的情况
4.左右节点都不为空,比较数值,不相等就返回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 if 因为把以上情况都排除后,剩下的就是节点数值相等的情况,再进行递归操作
3.确定单层递归的逻辑。
单层递归的逻辑就是处理左右节点都不为空且数值相等的情况:
1.比较二叉树外侧是否对称:传入的是左节点的左孩子和右节点的右孩子。
2.比较二叉树内侧是否对称:传入的是左节点的右孩子和右节点的左孩子。
3.如果内外侧都对称就返回true,有一侧不对称就返回false。
bool outside = compare(left->left, right->right);
bool inside = compare(left->right, right->left);
bool isSame = outside && inside;
return isSame;
整体代码如下:
题解
class Solution {
public:
bool compare(TreeNode* left, TreeNode* right) {
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;
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 == NULL) return true;
return compare(root->left, root->right);
}
};
这里给出的代码并不简洁,但是把每一步判断的逻辑都清晰的展示出来了。精简的代码隐藏了逻辑,条理不清晰,执行速度慢,递归三部曲完全体现不出来。我们在解题时一定要想清楚每一步的逻辑,再追求代码精简。代码精简后如下:
题解
/**
* 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 == 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 return compare(left->left, right->right) && compare(left->right, right->left);
}
bool isSymmetric(TreeNode* root) {
if (root == NULL) return true;
return compare(root->left, root->right);
}
};
8.7.2 迭代法
这道题也可以使用迭代法,但是此处的迭代法可不是前中后序遍历的迭代法,而是针对本题使用队列或栈进行迭代。本题的本质是判断两棵树是否相互反转,已经不是所谓的二叉树遍历的前中后序的关系了。
我们可以使用队列来比较两棵树(根节点的左右子树)是否相互反转(注意这不是利用队列进行层序遍历)。
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:
bool isSymmetric(TreeNode* root) {
if (root == NULL) 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;//左右节点都为空,说明对称,继续循环,比较队列中后续节点
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;//直到队列为空也未找到不对称节点,则说明整颗树是对称的。
}
};
这种迭代法只是利用了队列的容器功能:把左右两颗子树要比较的元素按照一定顺序放进一个容器,然后再成对取出来进行比较。用栈也是可以实现的。
题解
/**
* 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 == NULL) return true;
stack<TreeNode*> st;
st.push(root->left);
st.push(root->right);
while (!st.empty()) {//队列不空则进行循环,说明还有节点未进行比较
TreeNode* leftNode = st.top();
st.pop();
TreeNode* rightNode = st.top();
st.pop();
if (!leftNode && !rightNode) continue;//左右节点都为空,说明对称,继续循环,比较队列中后续节点
if (!leftNode || !rightNode || leftNode->val != rightNode->val) return false;//如果左右节点只有一个空,或两节点的数值不同,说明不对称
st.push(leftNode->left);
st.push(rightNode->right); //外侧
st.push(leftNode->right);
st.push(rightNode->left);//内侧
}
return true;//直到队列为空也未找到不对称节点,则说明整颗树是对称的。
}
};
这种利用栈进行迭代的解法执行速度最快。(为什么?)
8.8 二叉树的最大深度
力扣题号: 104.二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/maximum-depth-of-binary-tree
8.8.1 递归法
因为要通过递归函数的返回值来计算树的深度,所以本题需要使用后序遍历(左右中)。类似的如上节题目需要递归函数的返回值来判断是否对称。
1.确定递归函数的返回值和参数
参数就是传入二叉树的根节点,返回值就是这棵树的深度(int类型)
int getDepth(TreeNode* root)
2.确定终止条件
如果遇到空节点,则返回0,代表这颗子树的深度为0
if (node == NULL) return 0;
3.确定单层递归逻辑
先求左子树的深度,再求右子树的深度,最后取二者最大值加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:
int getDepth(TreeNode* node) {
if (node == NULL) return 0;
int leftDepth = getDepth(node->left);//左
int rightDepth = getDepth(node->right);//右
int Depth = max(leftDepth, rightDepth) + 1;//中
return Depth;
}
int maxDepth(TreeNode* root) {
return getDepth(root);
}
};
精简代码如下:
题解
/**
* 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 == NULL) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
精简后的代码看不出是哪种遍历方式,也看不出递归三部曲的步骤。如果对二叉树的遍历还不是很熟练,还是不要照着精简版学习,而是应该一步步的分析,最后再精简代码。
8.8.2 迭代法
在二叉树中一层一层地遍历二叉树,遍历的层数就是二叉树的深度。可以使用8.5节层序遍历模板来解答:
题解
/**
* 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 == NULL) return 0;
int depth = 0;//累计深度
queue<TreeNode*> que;
que.push(root);
while (!que.empty()) {
int size = que.size();//切记不能用que.size,因为que大小是动态变化的,在此记录的是本层的节点个数
depth++;//遍历一层深度+1
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
}
return depth;
}
};
8.9 二叉树的最小深度
力扣题号: 111.二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6]
输出:5
提示:
树中节点数的范围在 [0, 105] 内
-1000 <= Node.val <= 1000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-depth-of-binary-tree
思路
遍历顺序依旧是后序遍历(因为要根据返回值来比较最小深度),但在处理中间节点的逻辑上,最小深度容易有一个误区:
最小深度是从根节点到最近叶子节点的最短路径上的节点数量,而左右孩子都为空的节点才是叶子节点。
8.9.1 递归法
1.确定递归函数的参数和返回值。
参数为要传入的二叉树的根节点,返回值是int类型的深度。
int getDepth(TreeNode* node)
2.确定终止条件。
终止条件是遇到空节点就返回0,表示当前节点的深度为0.
if (node == NULL) return 0;
3.确定单层递归的逻辑。
这部分和求最大深度不一样,如果这么求解,那么没有左孩子的分支会作为最小深度。而实际上的最小深度是右子树深度+1。例如,如果左子树为空,右子树不为空,则说明最小深度是右子树的深度+1。如果右子树为空,左子树不为空,则最小深度是左子树深度+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:
int getDepth(TreeNode* node) {
if (node == NULL) return 0;
int leftDepth = getDepth(node->left);//左
int rightDepth = getDepth(node->right); //右
//中
//当左子树为空,右子树不为空时,最小深度为右子树深度+1
if (node->left == NULL && node->right != NULL) return 1 + rightDepth;
//当右子树为空,左子树不为空时,最小深度为左子树深度+1
if (node->right == NULL && node->left != NULL) return 1 + leftDepth;
int result = min(leftDepth, rightDepth) + 1;
return result;
}
int minDepth(TreeNode* root) {
return getDepth(root);
}
};
精简版代码如下:
题解
/**
* 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 == NULL) return 0;
if (root->left == NULL && root->right != NULL) return minDepth(root->right) + 1;
if (root->right == NULL && root->left != NULL) return minDepth(root->left) + 1;
return 1 + min(minDepth(root->right), minDepth(root->left));
}
};
8.9.2 迭代法
本题还可以使用层序遍历的迭代法,思路是一样的。同样需要注意的是,只有当左右孩子都为空时,才说明遍历到最低点了。如果其中一个孩子为空,则不是最低点。
题解
/**
* 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 == NULL) return 0;
int depth = 0;
queue<TreeNode*> que;
que.push(root);
while (!que.empty()) {
int size = que.size();
depth++;
for (int i = 0; i < size; 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;//当遇到左右孩子都为空的节点时退出返回当前depth
}
}
return depth;
}
};
8.10 平衡二叉树
力扣题号: 110.平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
示例 3:
输入:root = []
输出:true
提示:
树中的节点数在范围 [0, 5000] 内
-104 <= Node.val <= 104
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/balanced-binary-tree
拓展
这题看似和8.8节求二叉树最大深度很像,其实不然。
高度和深度是两个概念:
二叉树节点的深度:从根节点到该节点的最长简单路径边的条数(或者节点数)
二叉树节点的高度:从该节点到叶子节点的最长简单路径边的条数(或节点数)
如果按照节点数计算根节点的深度为1,按边计算根节点的深度为0,本身一致采用按节点数来计算根节点的深度。
求高度和求深度所有的遍历方式是不一样的,求深度要从上到下去查找,所以需要前序遍历(中左右),而高度只能从下到上去查找,所以需要后序遍历(左右中)。
那么我们就有疑惑了,为什么8.8节求二叉树最大深度也用的后序遍历呢?哦,原来代码的逻辑实际上是求根节点的高度,根节点的高度就是这棵树的最大深度。
在8.8节中,如果真正求二叉树的最大深度,则代码应该如下(前序遍历):
class Solution {
public:
int result;//全局
void getDepth(TreeNode* node, int depth) {
result = depth > result ? depth : result;//中
if (node->left == NULL && node->right == NULL) return ;
if (node->left) {//左
depth++;
getDepth(node->left, depth);
depth--;//回溯,深度-1
}
if (node->right) {//右
depth++;
getDepth(node-right, depth);
depth--;//回溯,深度-1
}
return;
}
int maxDepth(TreeNode* root) {
result = 0;
if (root == 0) return result;
getDepth(root, 1);
return result;
}
};
上述代码采用了前序遍历,这才是真正求深度的逻辑。上述代码展现了回溯的逻辑,简化后的代码如下:
class Solution {
public:
int result;
void getDepth(TreeNode* node, int depth) {
result = depth > result ? depth : result;//中
if (node->left == NULL && node->right == NULL) return ;
if (node->left) {
getDepth(node->left, depth + 1);
}
if (node->right) {
getDepth(node->right, depth + 1);
}
return;
}
int maxDepth(TreeNode* root) {
result = 0;
if (root == 0) return result;
getDepth(root, 1);
return result;
}
};
8.10.1 递归法
如果要求二叉树的高度,则应该采用后序遍历:
1.确定递归函数参数和返回值
参数为传入的节点指针,返回值是以传入节点为根节点的二叉树的高度。如何标记左右子树的高度差值是否大于1呢?如果当前以传入节点为根节点的二叉树已经不是二叉平衡树了,那么返回高度就没意义了,可以返回-1来标记此二叉树不是平衡树。
int getDepth(TreeNode* node)//返回-1表示该树已经 不是平衡二叉树了,否则返回值是以该节点为根节点的树的高度
确定终止条件
递归的过程仍然是遇到空节点就终止,返回0,表示当前节点为根节点的树的高度为0.
if (node == NULL) return 0;
确定单层递归的逻辑
判断是否为平衡二叉树,当然要看左右子树的高度之差。
int leftDepth = getDepth(node->left);
if (leftDepth == -1) return -1;
int rightDepth = getDepth(node->right);
if (leftDepth == -1) return -1;
return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth);
整体代码如下:
题解
/**
* 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 getDepth(TreeNode* node) {
if (node == NULL) return 0;
int leftDepth = getDepth(node->left);
if (leftDepth == -1) return -1;
int rightDepth = getDepth(node->right);
if (rightDepth == -1) return -1;
return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth);
}
bool isBalanced(TreeNode* root) {
return getDepth(root) == -1 ? false : true;
}
};
8.10.2 迭代法
在8.8节我们可以用层序遍历求深度,但不能直接用层序遍历求高度,这就体现了求深度和求高度的不同。
可以先定义一个函数 int getDepth(TreeNode* cur),专门用来求高度。这个函数通过栈模拟后序遍历(前中后统一迭代法)查找每一个节点高度(通过求以传入节点为根节点的二叉树的最大深度来求高度),整体代码如下:
题解
/**
* 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 getDepth(TreeNode* cur) {//求最大深度,即以cur节点为根节点的二叉树的高度
stack<TreeNode*> st;
if (cur != NULL) st.push(cur);
int depth = 0;//记录深度
int result = 0;
while (!st.empty()) {//栈不空说明还有节点未遍历
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
st.push(node);//中
st.push(NULL);//中节点访问过,但还没有处理,加入NULL作为标记
depth++;
if (node->right) st.push(node->right);//右
if (node->left) st.push(node->left);//左
}
else {
st.pop();//取出空节点
node = st.top();//重新取出栈中节点
st.pop();
depth--;
}
result = result > depth ? result : depth;
}
return result;
}
bool isBalanced(TreeNode* root) {//二叉树用栈模拟后序遍历迭代
stack<TreeNode*> st;
if (root == NULL) return true;
st.push(root);
while(!st.empty()) {
TreeNode* node = st.top();//中
st.pop();
if (abs(getDepth(node->left) - getDepth(node->right)) > 1) return false;
if (node->right) st.push(node->right);//右
if (node->left) st.push(node->left);//左
}
return true;
}
};
当然,此题用迭代法效率很低,执行速度很慢,因为没有很好的模拟回溯的过程,有很多重复的计算。虽然理论上所有递归都可以用迭代来实现,但是在某些场景下,难度会比较大。例如,回溯算法就是用递归来实现的,但是很少人有迭代的方式来实现回溯。
8.11 二叉树的所有路径
力扣题号: 257.二叉树的所有路径
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3,null,5]
输出:[“1->2->5”,“1->3”]
示例 2:
输入:root = [1]
输出:[“1”]
提示:
树中节点的数目在范围 [1, 100] 内
-100 <= Node.val <= 100
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-tree-paths
思路
本题是求根节点到叶子节点的所有路径,所以需要使用前序遍历,这样才方便让父节点指向子节点,找到对应的路径。这道题将正式涉及回溯,因为我们要记录路径,需要回溯操作来回退一条路径到另一条路径。
8.11.1 递归法
1.确定递归函数参数和返回值
传入二叉树的根节点的同时还要两个vector分别记录每一条路径path和最终结果集result,这里不需要返回值。
void traveral(TreeNode* cur, vector<int> path, vector<string>& result)
2.确定递归终止条件
之前我们写终止条件都是遇到空节点就终止,但这样写本题的终止条件会很麻烦,因为本题找到叶子节点后就开始收集路径的处理逻辑了(把路径放入result)。所以我们要在前一步(遇到叶子节点)就开始处理终止逻辑。
if (cur->left == NULL && cur->right == NULL) {
//终止处理逻辑
}
有人可能会问,为什么没有判断cur是否为空呢?因为下面的逻辑会控制空节点不进入递归循环。
再来看一下终止处理逻辑:这里使用vector<int> path
记录路径,所以要把vector转为字符串,再把这个字符串放入result数值。之所以选用vector<int>
结构来记录路径,是因为下面处理单层递归逻辑时,使用vector方便做回溯操作。
有的人可能写出了本题的代码,但没有发现回溯的逻辑。其实是有的,只不过隐藏在函数调用时的参数赋值中(别传reference)。
if (cur->left == NULL && cur->right == NULL) {
string sPath;
//将path中记录的路径转为string格式,未处理最后一个
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;
}
3.确定单层递归逻辑
因为是前序遍历(中左右),所以需要先处理中间节点,中间节点就是我们要记录的路径上的节点,先放进path中,即path.push_back(cur->val)
。
然后是递归和回溯的过程,上面说过要在递归的时候,判断cur是否为空,如果cur为空就不进行下一层的递归了,所以递归前要加上判断语句。此时还要做回溯的操作,因为path不能一直加入节点,当到达二叉树一条边的尽头时,还要删除节点,然后才能加入新的节点。(回溯和递归是一一对应的,有递归就要有回溯,要永远在一起)
if (cur->left) {
traversal(cur->left, path, result);
path.pop_back();//回溯
}
if (cur->right) {
traversal(cur->right, path, result);
path.pop_back();//回溯
}
整体代码如下:
题解
/**
* 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);
if (cur->left == NULL && cur->right == NULL) {//遇到叶子节点,开始收集路径
string sPath;
for (int i = 0; i < path.size() - 1; i++) {
sPath += to_string(path[i]);
sPath += "->";
}
sPath += to_string(path[path.size() - 1]);
result.push_back(sPath);
return;
}
if (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<int> path;
vector<string> result;
if (root == NULL) return result;
traversal(root, path, result);
return result;
}
};
上述代码充分体现了回溯的特点,也可以将回溯的逻辑放进递归函数的参数列表里。
题解
/**
* 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, string path, vector<string>& result) {
path += to_string(cur->val);//中
if (cur->left == NULL && cur->right == NULL) {
result.push_back(path);
return;
}
if (cur->left) traversal(cur->left, path + "->", result);
if (cur->right) traversal(cur->right, path + "->",result);
}
public:
vector<string> binaryTreePaths(TreeNode* root) {
string path;
vector<string> result;
if (root == NULL) return result;
traversal(root, path, result);
return result;
}
};
上述代码精简不少,隐藏了不少逻辑。注意在定义遍历函数时,定义的是string path
,每次都赋值一遍数据进行赋值,不能使用引用,否则就无法实现回溯的逻辑。在上面的代码中,貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在traversal(cur->left, path + "->", result)
中的path + "->"
中,每次函数调用完返回时,path依然没有加上->,这就是回溯的效果。加上回溯的逻辑代码如下:
if (cur->left) {
path += "->";
traversal(cur->left, path, result);
path.pop_back();//回溯-
path.pop_back();//回溯>
}
8.11.2 迭代法
模拟递归过程除了需要一个栈,还需要另外一个栈来存放对应的遍历路径,实现代码如下:
题解
/**
* 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 == NULL) 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 == NULL && node->right == NULL) {//遇到叶子节点
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;
}
};
8.12 路径总和
力扣题号: 112. 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。
示例 3:
输入:root = [], targetSum = 0
输出:false
解释:由于树是空的,所以不存在根节点到叶子节点的路径。
提示:
树中节点的数目在范围 [0, 5000] 内
-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/path-sum
思路
这道题我们要遍历根节点到叶子节点的路径,计算总和是不是目标和。
8.12.1 递归法
我们可以用深度优先遍历(前中序都可,因为中间节点也没有需要处理的逻辑)来遍历二叉树。
1.确定递归函数的参数和返回值
参数:传入二叉树的根节点和一个int类型计数器,计数器用来计算二叉树的一条边的节点之和是否正好是目标和target。返回值:什么时候需要返回值什么时候不需要呢?总结以下三点:
1.如果需要搜索整棵树且不用处理递归的返回值,那么就不需要返回值。(8.12.3)
2.如果需要搜索整颗树且需要处理递归的返回值,那么就需要返回值。(8.19)
3.如果需要搜索其中一条符合条件的路径,那么递归函数一定需要返回值,因为遇到符合条件的路径就要及时返回(本题情况)。
本题可以用bool类型表示。
bool traversal(TreeNode* cur, int count);//注意函数的返回类型
2.确定终止条件
计数器如何统计这条路径上的和呢?我们一般都累加节点的数值然后判断是否等于目标和,但那样代码写起来比较麻烦。我们可以将计数器初始化为目标和,然后每次递减遍历路径节点上的数值,如果最后count等于0,同时遍历到了叶子节点,则说明得到了目标和;如果遍历到了叶子节点但count不为0,则说明没找到等于目标和的路径。
if (!cur->left && !cur->right && count == 0) return true;
if (!cur->left && !cur->right) return false;
3.确定单层递归的逻辑
因为终止条件是判断遍历到叶子节点时是否得到目标和,所以递归的过程中就不要让空节点进入递归函数。递归函数是有返回值的,如果返回true,说明找到了合适的路径,应该立刻返回。
if (cur->left) {
if (traversal(cur->left, count - cur->left->val)) return true;
}
if (cur->right) {
if (traversal(cur->left, count - cur->right->val)) return true;
}
return false;
注意看,这里的回溯逻辑包含在递归函数参数中,同上题,把count - cur->left->val
作为参数直接传入递归函数(而不是传引用),函数结束,count数值没有改变。
为了将回溯的过程展现出来,可以改成下面:
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;
整体代码如下:
题解
/**
* 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;
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 == NULL) return false;
return traversal(root, targetSum - root->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:
bool hasPathSum(TreeNode* root, int targetSum) {
if (root == NULL) return false;
if (!root->left && !root->right && targetSum == root->val) return true;
return hasPathSum(root->left, targetSum - root->val) || hasPathSum(root->right, targetSum - root->val);
}
};
8.12.2 迭代法
如果使用栈模拟递归,该如何实现回溯呢?
此时栈内的一个元素不仅要存放该节点指针,还要记录从头节点到该节点的路径数值的总和。如果是C++,则用pair结果存放这个栈内的元素。将栈内的一个元素定义为pair<TreeNode*, int>
用栈模拟的前序遍历的代码如下:
/**
* 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 hasPathSum(TreeNode* root, int targetSum) {//用栈模拟前序遍历
if (root == NULL) return false;
stack<pair<TreeNode*, int>> st;
st.push(pair<TreeNode*, int>(root, root->val));
while(!st.empty()) {
pair<TreeNode*, int> node = st.top();//中
st.pop();
if (!node.first->left && !node.first->right && targetSum == node.second) return true;
if (node.first->right) {//右
st.push(pair<TreeNode*, int>(node.first->right, node.second + node.first->right->val));
}
if (node.first->left) {//左
st.push(pair<TreeNode*, int>(node.first->left, node.second + node.first->left->val));
}
}
return false;
}
};
8.12.3 路径总和Ⅱ
力扣题号:113. 路径总和Ⅱ
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:[]
示例 3:
输入:root = [1,2], targetSum = 0
输出:[]
提示:
树中节点总数在范围 [0, 5000] 内
-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/path-sum-ii
思路
这道题是让找到二叉树所有符合条件的路径,根据上小节总结的三点,如果需要搜索整颗二叉树且不用处理递归函数的返回值,则递归函数就不需要返回值。所以本题递归函数不用返回值。但不是所有需要遍历整颗二叉树的题目都不需要返回值,还在于递归逻辑的判断,这一点会在8.19节结合具体问题进行讲解。
为了将整体的逻辑细节体现处理,给出如下代码:
题解
/**
* 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) {//遇到叶子节点
result.push_back(path);
return;
}
if (!cur->left && !cur->right) return;//遇到叶子节点 但count!=0,直接返回
if (cur->left) {
path.push_back(cur->left->val);
count -= cur->left->val;
traversal(cur->left, count);
count += cur->left->val;
path.pop_back();
}
if (cur->right) {
path.push_back(cur->right->val);
count -= cur->right->val;
traversal(cur->right, count);
count += cur->right->val;
path.pop_back();
}
return;
}
public:
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
result.clear();
path.clear();
if (root == NULL) return result;
path.push_back(root->val);
traversal(root, targetSum - root->val);
return result;
}
};
精简代码如下:
题解
/**
* 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) {//遇到叶子节点
result.push_back(path);
return;
}
if (!cur->left && !cur->right) return;//遇到叶子节点 但count!=0,直接返回
if (cur->left) {
path.push_back(cur->left->val);
traversal(cur->left, count - cur->left->val);
path.pop_back();
}
if (cur->right) {
path.push_back(cur->right->val);
traversal(cur->right, count- cur->right->val);
path.pop_back();
}
return;
}
public:
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
result.clear();
path.clear();
if (root == NULL) return result;
path.push_back(root->val);
traversal(root, targetSum - root->val);
return result;
}
};
8.13 构造二叉树
8.13.1 使用中序与后序遍历序列构造二叉树
力扣题号:106. 使用中序与后序遍历序列构造二叉树
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal
思路
原理:以前序或后序遍历的第一个元素或最后一个元素作为切割点,先切割中序数组,然后根据中序数组,反过来再切割前序或后序数组。一层一层切下去,每次前序或后序数组的最后一个元素就是节点元素。
很明显,这种一层一层切割的方式应该使用递归:
1.如果后序数组长度为零,则说明是空节点。
2.如果后序数组不为空,那么将后序数组的最后一个元素作为节点元素。
3.找到后序数组的最后一个元素在中序数组中的位置并将其作为切割点。
4.切割中序数组,切成中序左数组和中序右数组(一定先切割中序数组)
5.切割后序数组,切成后序左数组和后序右数组。
6.递归处理左区间和右区间。
伪代码如下:
TreeNode* traversal(vector<int>& inorder,vector<int>& postorder) {
if (postorder.size() == 0) return NULL;//第一步
int rootValue = postorder[postorder.size()];//第二步,此节点即是当前的中间节点
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;
}
//第四步
//第五步
root->left = traversal(中序左区间,后序左区间);
root->right = traversal(中序右区间,后序右区间)
return root;
}
此处的难点就是如何切割数组,以及找到边界值。切割的标准是左闭右开、左开右闭还是左闭右闭,这个标准是不变量,要在递归过程中保持不变量不变化。因为在切割过程中会产生四个区间,如果把握不住不变量,那么代码逻辑就会陷入混乱。
另外为什么要先切割中序数组呢?切割点是后序数组的最后一个元素,基于这个元素来切割中序数组。代码如下:
//左闭右开区间:[0,delimiterIndex)
vector<int> leftinorder(inorder.begin(), inorder.begin() + delimiterIndex);
//[delimiterIndex, end)
vector<int> rightinorder(inorder.begin() + delimiterIndex + 1, inorder.end());
切割后序数组:首先后序数组的最后一个元素不考虑,因为这个元素既是切割点,又是二叉树的中间节点的元素。 那么怎么查找后序数组的切割点呢?有一个关键点:中序数组的长度一定和后序数组的长度相同。我们可以将后序数组按左中序数组的长度进行切割,切割成左后序数组和右后序数组。
代码如下:
//舍弃末尾元素,因为这个元素就是中间节点,已经用过了;第五步
postorder.resize(postorder.size() - 1);
//左闭右开,注意这里使用左中序数组的长度作为切割点:[0,leftIndor.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);
完整代码如下:
题解
/**
* 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 NULL;
int rootValue = postorder[postorder.size() - 1];
TreeNode* root = new TreeNode(rootValue);
if (postorder.size() == 1) return root;//postorder序列会不断更改大小;叶子节点
int delimiterIndex;
for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
//中序序列inorder.size() + delimiterIndex 这个节点已经用过
postorder.resize(postorder.size() - 1);//舍弃末尾节点,因为已经用过了
vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
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 NULL;
return traversal(inorder, postorder);
}
};
提交代码我们发现,执行时间和内存消耗都很多,这是因为在代码中每次都定义了新的vector来存储分割的序列既耗时又耗内存。下面我们将使用下标分割数组的代码版本(思路一样,只不过用下标来分割数组)
题解
/**
* 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, int inorderBegin, int inorderEnd, vector<int>& postorder, int postorderBegin, int postorderEnd) {
if (postorderBegin == postorderEnd) return NULL;
int rootValue = postorder[postorderEnd - 1];
TreeNode* root = new TreeNode(rootValue);
if (postorderEnd - postorderBegin == 1) return root;//叶子节点
int delimiterIndex;//中序分割点
for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
//中序左区间
int leftInorderBegin = inorderBegin;
int leftInorderEnd = delimiterIndex;
// 中序右区间
int rightInorderBegin = delimiterIndex + 1;
int rightInorderEnd = inorderEnd;
//后序左区间
int leftPostorderBegin = postorderBegin;
int leftPostorderEnd = postorderBegin + (delimiterIndex - inorderBegin);
//后序右区间
int rightPostorderBegin = postorderBegin + (delimiterIndex - inorderBegin);
int rightPostorderEnd = postorderEnd - 1;
//递归建立二叉树
root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd);
root->right = traversal(inorder,rightInorderBegin,rightInorderEnd, postorder, rightPostorderBegin,rightPostorderEnd);
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return NULL;
return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size());
}
};
8.13.2 使用前序与中序遍历序列构造二叉树
力扣题号: 105. 使用前序与中序遍历序列构造二叉树
本题和上节原理一样,代码如下:
题解
/**
* 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, int inorderBegin, int inorderEnd, vector<int>& preorder, int preorderBegin, int preorderEnd) {
if (preorderBegin == preorderEnd) return NULL;
int rootValue = preorder[preorderBegin];//切记不能用0,因为是根据分割后的前序序列首元素来确定根节点
TreeNode* root = new TreeNode(rootValue);
if (preorderEnd - preorderBegin == 1) return root;//叶子节点
int delimiterIndex;//中序分割点
for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
//中序左区间
int leftInorderBegin = inorderBegin;
int leftInorderEnd = delimiterIndex;
// 中序右区间
int rightInorderBegin = delimiterIndex + 1;
int rightInorderEnd = inorderEnd;
//后序左区间
int leftPreorderBegin = preorderBegin + 1;
int leftPreorderEnd = preorderBegin + 1 + (delimiterIndex - inorderBegin);
//后序右区间
int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin);
int rightPreorderEnd = preorderEnd;
//递归建立二叉树
root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd);
root->right = traversal(inorder,rightInorderBegin,rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd);
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (inorder.size() == 0 || preorder.size() == 0) return NULL;
return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size());
}
};
8.14 合并两棵二叉树
力扣题号: 617. 合并两颗二叉树
给你两棵二叉树: root1 和 root2 。
想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。
返回合并后的二叉树。
注意: 合并过程必须从两个树的根节点开始。
示例 1:
输入:root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
输出:[3,4,5,5,4,null,7]
示例 2:
输入:root1 = [1], root2 = [1,2]
输出:[2,2]
提示:
两棵树中的节点数目在范围 [0, 2000] 内
-104 <= Node.val <= 104
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/merge-two-binary-trees
思路
我们要同时遍历两颗二叉树然后进行合并,递归函数传入两颗树的根节点来进行合并操作。
8.14.1 递归法
针对二叉树使用递归法首先要考虑的是采用哪种遍历方式,本道题使用哪种遍历方式都可。我们以前序遍历为例。
1.确定递归函数的参数和返回值。
因为要合并两棵树,参数至少要传入两棵树的根节点。函数的返回值则是合并后的二叉树的根节点。
TreeNode* traversal(TreeNode* t1, TreeNode* t2)
2.确定函数终止条件。
因为传入两棵树,所以判断两棵树当前遍历的节点t1和t2,如果t1为NULL,则两节点合并之后就是t2,当t2为NULL,则两节点合并之后就是t1(同时为NULL的情况已经包含)。
if (t1 == NULL ) return t2;
if (t2 == NULL ) return t1;
3.确定单层递归的逻辑。
这里我们不新定义节点,直接在t1上合并两棵树,t1就是合并之后的根节点。单层递归中就要将两棵树节点值加到t1树上。所以t1的左子树就是t1左子树和t2左子树合并之后的树,t1的右子树就是t1右子树和t2右子树合并之后的树。
t1->val += t2->val;
t1->left = traversal(t1->left, t2->left):
t1->right = traversal(t1->right, t2->right);
return t1;
完整代码如下:
题解
/**
* 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(TreeNode* t1, TreeNode* t2) {
if (t1 == NULL) return t2;
if (t2 == NULL) return t1;
t1->val += t2->val;
t1->left = traversal(t1->left, t2->left);
t1->right = traversal(t1->right, t2->right);
return t1;
}
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
return traversal(root1, root2);
}
};
我们也可以不改变树的结构,重新定义一棵树,前序遍历代码如下:
题解
/**
* 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 == NULL) return root2;
if (root2 == NULL) 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;
}
};
8.14.2 迭代法
同时处理两棵树?我们在8.10节好像做过?处理二叉树对称的时候就把两棵树同时放入队列或者栈中进行比较。在同时处理两棵树的问题上的迭代解法中,我们一般使用队列模拟层序遍历,同时处理两棵树的节点,这种方式最容易理解,如果我们使用模拟递归的方式则要复杂一点。
本题也使用对立模拟层序遍历,代码如下:
题解
/**
* 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) {
queue<TreeNode*> que;
if (root1 == NULL) return root2;
if (root2 == NULL) return root1;
//将两棵树的根节点同时放入栈中
que.push(root1);
que.push(root2);
while (!que.empty()) {//栈不空说明还有节点未遍历
TreeNode* node1 = que.front();
que.pop();
TreeNode* node2 = que.front();
que.pop();
//此时两个节点一定不为空,val相加
node1->val += node2->val;
if (node1->left != NULL && node2->left != NULL) {//两棵树左节点都不为空才加入队列,之后弹出合并,
que.push(node1->left);
que.push(node2->left);
}
if (node1->left == NULL && node2->left != NULL) {//node1左节点为空,node2左节点不为空,将node2左节点赋给node1
node1->left = node2->left; //不能赋val,因为此时node1->left为NULL
}
if (node1->right != NULL && node2->right != NULL) {//两棵树右节点都不为空才加入队列,之后弹出合并,
que.push(node1->right);
que.push(node2->right);
}
if (node1->right == NULL && node2->right != NULL) {//node1右节点为空,node2右节点不为空,将node2右节点赋给node1
node1->right = node2->right; //不能赋val,因为此时node1->right为NULL
}
//当node2的左或右节点为空时,不用合并(以root1为蓝本)
}
return root1;
}
};
8.15 在二叉搜索树中寻找节点
力扣题号: 700.二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点 root 和一个整数值 val。
你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。
示例 1:
输入:root = [4,2,7,1,3], val = 2
输出:[2,1,3]
Example 2:
输入:root = [4,2,7,1,3], val = 5
输出:[]
提示:
数中节点数在 [1, 5000] 范围内
1 <= Node.val <= 107
root 是二叉搜索树
1 <= val <= 107
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-a-binary-search-tree
思路
上面的小节我们讲的都是普通二叉树,接下来分析二叉搜索树。二叉搜索树特性如下:
1.若它的左子树不空,则左子树上所有节点均小于它的根节点的值。
2.若它的右子树不空,则右子树上所有节点均大于它的根节点的值。
3.它的左右子树也分别为二叉搜索树。
8.15.1 递归法
1.确定递归函数的返回值和参数
递归函数的参数是要搜索的二叉树根节点和要搜索的数值;返回值就是搜索到的数值所在的节点。
TreeNode* searchBST(TreeNode* root, int val)
2.确定递归函数终止条件
如果root为空或找到了val就终止递归返回root节点。
if (root == NULL || root->val == val) return root;
3.确定单层递归逻辑
由于二叉搜索树是有序的,所以我们可以有方向的搜索。
if (root->val > root) return searchBST(root->right, val);
if (root->val < root) return searchBST(root->left ,val);
return NULL;//没搜索到目标节点
细心的读者看到上面的代码可能会感到疑惑,在调用递归函数的时候,什么时候直接返回递归函数的返回值,什么时候不用加这个return呢?
我们在之前8.12节讲过:如果要搜索一条边,那么递归函数就要加返回值,因为找到符合条件的边就要及时返回,本题也 同理。如果不加return,那么就是遍历整颗树了。
本题整体代码如下:
/**
* 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 == NULL || root->val == val) return root;
if (root->val < val) return searchBST(root->right, val);
if (root->val > val) return searchBST(root->left, val);
return NULL;
}
};
8.15.2 迭代法
之前我们提到迭代法,都是通过栈模拟深度优先遍历或通过队列模拟广度优先遍历,但对于二叉搜索树就不一样,由于二叉搜索树的特殊性(有序性),不使用辅助栈或队列就可以写出迭代法。
对于一般的二叉树,递归过程中还有回溯的过程。例如,遍历一个左方向的分支到头了,就需要掉头,再遍历右分支。而对于二叉搜索树不需要回溯,基于节点的有序性就可以确定搜索方向。
迭代法代码如下:
题解
/**
* 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 != NULL) {
if (root->val < val) root = root->right;
else if (root->val > val) root = root->left;
else return root;
}
return NULL;
}
};
验证二叉搜索树
力扣题号: 98.验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
提示:
树中节点数目范围在[1, 104] 内
-231 <= Node.val <= 231 - 1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/validate-binary-search-tree
思路
我们采用中序遍历输出二叉树的节点,如果是有序的,则这个二叉树是二叉搜索树。
8.16.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 {
private:
vector<int> vec;//存放中序遍历二叉树的序列
//遍历整棵树,不需要返回值
void traversal(TreeNode* cur) {
if (cur == NULL) return;
traversal(cur->left);
vec.push_back(cur->val);
traversal(cur->right);
}
public:
bool isValidBST(TreeNode* root) {
vec.clear();
traversal(root);
for (int = 1; i < vec.size(); i++) {
if (vec[i] <= vec[i - 1]) return false;//二叉搜索树不能有相同节点
}
return true;
}
};
有些同学在解答这道题时会陷入两个误区:
1.单纯地比较左节点和中间节点或右节点和中间节点大小:
if (root->val > root->left->val && root->val < root->right->val) return true;
else return false;
我们要比较的是左子树的所有结点是否均小于中间节点,右子树所有节点是否均大于中间节点。
2.示例中的最小节点可能是int类型整数的最小值,使用最小的int类型整数来实现判断逻辑也是不行的。此时可以初始化比较元素为long long类型数组的最小值。那么问题又来了,如果 实例中根节点的val可能是longlong类型数组的最小值,又该 怎么办呢?建议避免初始化最小值,通过以下方式获取最左面节点的数值来实现判断的逻辑。代码如下:
题解
/**
* 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* pre = NULL;//记录前一个节点
public:
bool isValidBST(TreeNode* root) {
if (root == NULL) return true;
bool left = isValidBST(root->left);
if (pre != NULL && pre->val >= root->val) return false;
pre = root;
bool right = isValidBST(root->right);
return left && right;
}
};
8.16.2 迭代法
我们对8.3节给出的迭代法中序遍历稍加改动即可,代码如下:
题解
/**
* 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* pre = NULL;//记录前一个节点
public:
bool isValidBST(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* pre = NULL;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) {
st.push(cur);
cur = cur->left;
}
else {
cur = st.top();
st.pop();
if (pre != NULL && cur->val <= pre->val) return false;
pre = cur;
cur = cur->right;
}
}
return true;
}
};
8.17 二叉搜索树的最小绝对差
力扣题号: 530.二叉搜索树的最小绝对差
给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
差值是一个正数,其数值等于两值之差的绝对值。
示例 1:
输入:root = [4,2,6,1,3]
输出:1
示例 2:
输入:root = [1,0,48,null,null,12,49]
输出:1
提示:
树中节点的数目范围是 [2, 104]
0 <= Node.val <= 105
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst
思路
注意本题要求的是在二叉搜索树上求最小绝对差,二叉搜索树是有序的,所以我们在遇到二叉搜索树上求最值,求差值问题,就把它想成在一个有序数组上求最值、求差值。
8.17.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 {
private:
vector<int> vec;
void traversal(TreeNode* root) {
if (root == NULL) return;
traversal(root->left);
vec.push_back(root->val);
traversal(root->right);
}
public:
int getMinimumDifference(TreeNode* root) {
vec.clear();
traversal(root);
if (vec.size() < 2) return 0;
int result = INT_MAX;//int类型最大值
for (int i = 1; i < vec.size(); i++) {
result = min(result, vec[i] - vec[i - 1]);
}
return result;
}
};
这样的解法额外使用了vector,我们可以直接找到相邻两个节点的差值,用一个pre节点记录cur节点的前一个节点。代码如下:
题解
/**
* 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 = NULL;
void traversal(TreeNode* cur) {
if (cur == NULL) return;
traversal(cur->left);
if (pre != NULL) result = min(result, cur->val - pre->val);
pre = cur;//记录前一个节点的指针
traversal(cur->right);
}
public:
int getMinimumDifference(TreeNode* root) {
traversal(root);
return result;
}
};
8.17.2 迭代法
下面给出一种中序遍历的迭代法,代码如下:
/**
* 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 = NULL;
int result = INT_MAX;
while(cur != NULL || !st.empty()) {
if (cur != NULL) {
st.push(cur);
cur = cur->left;
}
else {
cur = st.top();
st.pop();
if (pre != NULL) {
result = min(result, cur->val - pre->val);
}
pre = cur;//记录前一个节点指针
cur = cur->right;
}
}
return result;
}
};
8.18 二叉搜索树中的众数
力扣题号:501.二叉搜索树中的众数
给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。
如果树中有不止一个众数,可以按 任意顺序 返回。
假定 BST 满足如下定义:
结点左子树中所含节点的值 小于等于 当前节点的值
结点右子树中所含节点的值 大于等于 当前节点的值
左子树和右子树都是二叉搜索树
示例 1:
输入:root = [1,null,2,2]
输出:[2]
示例 2:
输入:root = [0]
输出:[0]
提示:
树中节点的数目在范围 [1, 104] 内
-105 <= Node.val <= 105
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-mode-in-binary-search-tree
8.18.1 递归法
思路
递归法按遍历一次二叉树或遍历一次二叉树和遍历一次数组可分为两种,为考虑最优解,我们这里只讲解遍历一次的解法。
在上一小节中,我们利用了pre指针(指向了前一个节点),这样我们就可以比较cur和pre,当pre的数值等于cur时,count++,当count等于maxCount时,则要把元素加入结果集(众数不止一个)。这是就有疑问了。这个maxCount会是真正的最大频率呢?所以我们要在后面更新maxCount,当count大于maxCount时,更新maxCount,之后再清空结果集,将maxCount对应的数值加入结果集。完整代码如下:
题解
/**
* 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<int> result;
int count;
int maxCount;
TreeNode* pre = NULL;
void traversal(TreeNode* cur) {
if (cur == NULL) return;
traversal(cur->left);//左
//中
if (pre == NULL) count = 1;//第一个节点
else if (pre->val == cur->val) count++; //必须用else if,不能用if,否则pre为空时出现空指针->val
else count = 1;//与上一节点不同
pre = cur;
if (count == maxCount) result.push_back(cur->val);
if (count > maxCount) {
maxCount = count;
result.clear();
result.push_back(cur->val);
}
//右
traversal(cur->right);
return;
}
public:
vector<int> findMode(TreeNode* root) {
traversal(root);
return result;
}
};
遍历一次二叉树再遍历数组的解法代码如下:
题解
/**
* 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<int> vec;
vector<int> result;
int count;
int maxCount;
void traversal(TreeNode* cur) {
if (cur == NULL) return;
traversal(cur->left);
vec.push_back(cur->val);
traversal(cur->right);
}
public:
vector<int> findMode(TreeNode* root) {
count = 1;
maxCount = 1;
traversal(root);
result.push_back(vec[0]);//将第一个节点加入结果集合
if (vec.size() < 2) {//二叉树只有一个节点
return result;
}
for (int i = 1; i < vec.size(); i++) {
if (vec[i] == vec[i - 1]) count++;
else count = 1;
if (count == maxCount) result.push_back(vec[i]);
if (count > maxCount) {
result.clear();
maxCount = count;
result.push_back(vec[i]);
}
}
return result;
}
};
8.18.2 迭代法
只需把中序递归转为迭代即可,代码如下:
题解
/**
* 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* pre = NULL;
TreeNode* cur = root;
int count = 0;
int maxCount = 0;
vector<int> result;
while(cur != NULL || !st.empty()) {
if (cur != NULL) {
st.push(cur);
cur = cur->left;
}
else {
cur = st.top();
st.pop();
if (pre == NULL) count = 1;
else if (pre->val == cur->val) count++;
else count = 1;
if (count == maxCount) result.push_back(cur->val);
if (count > maxCount) {
result.clear();
maxCount = count;
resutl.push_back(cur);
}
pre = cur;
cur = cur->right;
}
}
return result;
}
};
8.18.3 普通二叉树
这道题目可以进一步扩展,如果是普通二叉树该怎么求解。
思路
如果不是二叉搜索树,则一定是遍历整棵树,用map统计元素出现的频率并排序,最后取前面高频的元素的集合。代码如下:
题解
/**
* 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, unordered_map<int, int>& map) {
if (cur == NULL) return;
map[cur->val]++;
traversal(cur->left, map);
traversal(cur->right, map);
}
bool static cmp (const pair<int, int>& a,const pair<int, int>& b){//从大到小排序
return a.second > b.second;
}
public:
vector<int> findMode(TreeNode* root) {
unordered_map<int, int> map;
vector<int> result;
traversal(root, map);
vector<pair<int, int>> vec(map.begin(), map.end());//将map转换成vector才能排序
sort(vec.begin(), vec.end(), cmp);//从大到小排序
result.push_back(vec[0].first);//先将最大频率元素放入结果集
for (int i = 1; i < vec.size(); i++) {//后面还可能会有同 频率的元素
if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
else break;//遇到不等就退出循环,表示不会再有同频率的元素
}
return result;
}
};
8.19 二叉树的最近公共祖先
力扣题号: 236.二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 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 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
提示:
树中节点数目在范围 [2, 105] 内。
-109 <= Node.val <= 109
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree
8.19.1 普通二叉树
思路
二叉树如果可以从底向上遍历查找就好了,这样就很容易找到公共祖先。那么二叉树怎么才可以自低向上查找呢?答案是回溯!而后序遍历就符合回溯的过程,最先处理的一定是叶子节点。
如何判断一个节点是不是节点p和q的公共祖先呢?如果找到一个节点,发现其左子树出现节点p,右子树出现节点q,或者左子树出现节点q,右子树出现节点p,那么该节点就是pq的公共祖先。
使用后序遍历,在回溯的过程中,自底向上遍历节点,一旦发现符合这个条件的节点,那么该节点就是最近公共祖先了。
1.确定递归函数参数和返回值
通过递归函数的返回值来确认是否找到节点q或p,可以用bool类型吗?因为我们还要返回最近的公共祖先,所以返回值应该是TreeNode* ,而不是bool。
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
2.确定终止条件
如果找到了节点p或q,或者遇到空节点,那么就返回这个节点。
if (root == p || root == q || root == NULL) return root;
3.确定单层递归逻辑
直到注意的是,本题的函数有返回值,这是因为回溯的过程中需要通过递归函数的返回值判断某个节点是否是公共祖先节点,但在本题中依然要遍历树的所有节点。
如果递归函数有返回值,那么如何区分是搜索一条边,还是搜索整颗树呢?
搜索一条边的写法:
if (递归函数(root->left)) return;
if (递归函数(root->right)) return;
在递归函数有返回值的情况下:如果搜索一条边,那么在递归函数的返回值不为空的时候,立刻返回。
搜索整棵树的写法:
left = 递归函数(root->left);
right = 递归函数(root->right);
left与right的处理逻辑;
如果搜索整棵树,则用变量left、right保存返回值,接下来的逻辑就是用left、right判断节点是否为公共节点,也就是后序遍历中处理中间节点的逻辑(也是回溯)。
为什么要遍历整棵树呢?直观上看,如果想找到最近公共祖先,那么直接原地返回即可。事实上依然要遍历根节点的右子树(即使已经找到目标节点)。如果想利用left和right进行逻辑处理,那么递归函数就不能立刻返回,而是要等到left与right逻辑处理完之后才能返回。所以此时要遍历整棵树。先用left和right保存左子树和右子树的返回值。
TreeNode* left = lowestCommonAncestor(root->left, p ,q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
如果left和right不为空,则说明此时root就是最近公共节点。
如果left为空right不为空,则返回right,说明目标节点是通过right返回的,反之亦然。
如果left和right都为空,那么返回left和right都是可以的。
整体代码如下:
题解
/**
* 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 == NULL || root == p || root == q) 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和right都为NULL
return NULL;
}
}
};
精简代码如下:
题解
/**
* 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 == NULL || root == p || root == q) 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) return right;
return left;
}
};
有的读者可能未必真正了解本题解法中的回溯过程,已经结果是如何一层一层的传上去的。归纳如下三点:
1.求最近公共祖先,需要自底向上遍历,如果是二叉树,那么只能通过后序遍历(即回溯)实现自底向上的遍历方式。
2.在回溯的过程中,必然要遍历整颗二叉树,即使已经找到结果,依然要把其他节点遍历完,这是因为要使用递归函数的返回值(也就是left和right)做逻辑判断。
3.如果返回值left为空、right不为空,则返回right。
可以说这里的每一步都是有难度的,都需要对二叉树、递归和回溯有一定的理解,本题普通二叉树没有给出迭代法,是因为迭代法不适合模拟回溯的过程 。
8.19.2 二叉搜索树
思路
记住二叉搜索树的特性:有序。在有序树中,如何判断一个节点的左子树中有p、右子树中有q呢?
在从上到下的遍历中,如果cur节点的数值在[p,q]区间,则说明该节点cur就是最近公共祖先。在普通二叉树中我们采用了后序遍历,而在二叉搜索树中(有序,自带方向)只需从上到下遍历二叉树即可。可以采用前序遍历(其实这里没有处理中间节点的逻辑,什么遍历方式都可)。
单层递归逻辑:如果cur->val > p->val && cur->val > q->val,说明公共祖先在左子树,都小于说明在右子树,否则cur就是公共祖先。整体代码如下:
题解
/**
* 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* root, 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) return left;//找到
}
if (cur->val < p->val && cur->val < q->val) {
TreeNode* right = traversal(cur->right, p, q);//公共祖先在左子树
if (right) return right;//找到
}
return cur;
}
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
return traversal(root, p, q);
}
};
精简代码如下:
题解
/**
* 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->val > p->val && root->val > q->val) return lowestCommonAncestor(root->left, p, q);
eles if (root->val < p->val && root->val < q->val) return lowestCommonAncestor(root->right, p, q);
else return root;
}
};
利用二叉搜索树的有序性,迭代方式还是比较简单的,代码如下:
/**
* 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;//加不加都可
}
};
8.20 在二叉搜索树中插入一个节点
力扣题号: 701.二叉 搜索树中的插入操作
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
示例 1:
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:
示例 2:
输入:root = [40,20,60,10,30,50,70], val = 25
输出:[40,20,60,10,30,50,70,null,null,25]
示例 3:
输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
输出:[4,2,7,1,3,5]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/insert-into-a-binary-search-tree
思路
从示例1可以看出,在二叉搜索树中添加节点可以不改变树的节点,只要遍历二叉搜索树,找到空节点插入元素即可。
8.20.1 递归法
1.确定递归函数的返回值和参数
参数是根节点指针和要插入的元素。返回值可以有也可以没有:如果没有返回值,那么实现起来比较麻烦;如果有返回值,则可以利用返回值完成新加入的节点与其父节点的赋值操作。
TreeNode* insertIntoBST(TreeNode* root, int val);
2.确定终止条件
如果当前遍历的节点为NULL,则该节点就是要插入节点的位置,并把插入的节点返回。这里把添加的节点返回给上一层,就完成了父子节点的赋值操作。
if (root == nullptr) {
TreeNode* node = new TreeNode(val);
return node;
}
3.确定单层递归的逻辑
搜索树是有方向的,可以根据插入元素的数值决定递归方向。
if (root->val > val) root->left = insertIntoBST(root->left, val);
if (root->val > val) root->right = insertIntoBST(root->right, val);
return root;
整体代码如下:
题解
/**
* 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 == NULL) {
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;
}
};
递归函数没有返回值,找到插入节点的位置,直接让父节点指向插入节点,结束递归。因为递归函数没有返回值,**则需要记录上一节点(parent)。**当前遍历的指针遇到空节点时,就让parent的左孩子或右孩子指向新插入的节点,然后结束递归。代码如下:
题解
/**
* 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* parent;
void traversal(TreeNode* cur, int val) {
if (cur == NULL) {//应该插入的位置
TreeNode* node = new TreeNode(val);
if (parent->val > val) parent->left = node;
else parent->right = node;
return;
}
parent = cur;
if (cur->val > val) traversal(cur->left, val);
if (cur->val < val) traversal(cur->right, val);
return;
}
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
parent = new TreeNode(0);//parent初始化
if (root == NULL) root = new TreeNode(val);//根节点为空的情况
traversal(root, val);
return root;
}
};
通过这个题解,我们可以看出通过递归函数的返回值来完成父子节点赋值(root->left = insertIntoBST())是非常方便的。
8.20.2 迭代法
在迭代法中,我们仍需要借助pre节点来记录当前遍历节点的父节点,这样才能插入节点。代码如下:
题解
/**
* 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 == NULL) {//根节点为空,直接插入
TreeNode* node = new TreeNode(val);
return node;
}
TreeNode* cur = root;
TreeNode* parent = NULL;
while (cur != NULL) {//未遍历到空节点
parent = cur;//保存父节点
if (cur->val > val) cur = cur->left;
else cur = cur->right;
}
//此时cur==NULL,该插入节点了
if (parent->val > val) parent->left = new TreeNode(val);
else parent->right = new TreeNode(val);
return root;
}
};
8.21 在二叉搜索树中删除一个节点
力扣题号: 450.删除二叉搜索树中的节点。
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。
示例 1:
输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
另一个正确答案是 [5,2,6,null,4,null,7]。
示例 2:
输入: root = [5,3,6,2,4,null,7], key = 0
输出: [5,3,6,2,4,null,7]
解释: 二叉树不包含值为 0 的节点
示例 3:
输入: root = [], key = 0
输出: []
提示:
节点数的范围 [0, 104].
-105 <= Node.val <= 105
节点值唯一
root 是合法的二叉搜索树
-105 <= key <= 105
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/delete-node-in-a-bst
思路
删除二叉搜索树要比增加节点复杂得多,需要考虑很多情况。
8.21.1 递归法
1.确定递归函数的返回值和参数
递归函数的参数是传入的根节点和要删除的值;我们可以通过递归函数的返回值来删除节点。
TreeNode* deleteNode(TreeNode* root, int key)
2.确定递归函数终止条件
当我们没找到要删除的节点也就是遇到空节点时返回。
if (root == nullptr) return root;
3.确定单层递归的逻辑
在二叉搜索树中删除节点可能遇到的情况有以下五种:
1.没找到要删除的节点,遍历到空节点就直接返回。
2.被删除节点的左右孩子都为空(叶子节点),直接删除该节点,返回NULL。
3.被删除节点的左孩子为空,右孩子不为空,删除该节点后,右孩子补位,返回右孩子为根节点。
4.被删除节点的右孩子为空,左孩子不为空,删除该阶段后,左孩子补位,返回左孩子为根节点。
5.被删除节点的左右孩子都不为空,将该节点的左子树的头节点(左孩子)放到删除节点的右子树的最左面节点左孩子上,返回删除节点的右孩子为新的根节点。(将被删节点的直接后继(或直接前驱)替代它,然后从bst中删除) 在示例1中就是将被删节点3的左子树头节点2,放到删除节点的右子树最左面节点4的左孩子上,返回删除节点的右孩子为新的根节点。
整体代码如下:
题解
/**
* 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) {//递归函数的返回值是删除节点后的新root
if (root == nullptr) return root;//情况1
if (root->val == val) {//root为被删节点
if (root->left == nullptr) return root->right;//情况2,3合并
else if (root->right == nullptr) return root->left;//情况4
else {//情况5
TreeNode* cur = root->right;//查找右子树最左面节点
while (cur->left != nullptr) {
cur = cur->left;
}
//此时cur指向root右子树最左面的节点
cur->left = root->left;//把要删除节点的左子树放在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;
}
};
8.21.2 迭代法
使用迭代法来删除节点还是复杂一点,但逻辑是一样的,迭代法代码如下:
题解
/**
* 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* deleteNodeOperation(TreeNode* target) {
if (target == nullptr) return target;//情况1:没找到
if (target->right == nullptr) return target->left;//情况2:叶子节点;情况4:右节点为空
TreeNode* cur = target->right;
while (cur->left) cur = cur->left;//此时cur指向target右子树的最左边节点
//情况3:target->left==nullptr,二叉树不变,返回target->right
//情况5:target左右节点都为空,将target的左孩子放到target右子树的最左边节点的左孩子上,然后target->right
cur->left = target->left;
return target->right;
}
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root;
TreeNode* cur = root;
TreeNode* pre = nullptr;//记录父节点,用来删除cur
while (cur) {
if (cur->val == key) break;//找到
pre = cur;
if (cur->val > key) cur = cur->left;
else cur = cur->right;
}
if (pre == nullptr) {//只有头节点的情况:cur->val==key,就直接break,pre此时为nullpter无左右孩子
return deleteNodeOperation(cur);
}
//pre用于判断删除左孩子还是删除右孩子
if (pre->left && pre->left->val == key) pre->left = deleteNodeOperation(cur);
if (pre->right && pre->right->val == key) pre->right = deleteNodeOperation(cur);
return root;
}
};
8.22 修剪二叉搜索树
力扣题号: 669. 修建二叉搜索树
给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。
所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。
示例 1:
输入:root = [1,0,2], low = 1, high = 2
输出:[1,null,2]
示例 2:
输入:root = [3,0,4,null,2,null,null,1], low = 1, high = 3
输出:[3,2,null,1]
提示:
树中节点数在范围 [1, 104] 内
0 <= Node.val <= 104
树中每个节点的值都是 唯一 的
题目数据保证输入是一棵有效的二叉搜索树
0 <= low <= high <= 104
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/trim-a-binary-search-tree
8.22.1 递归法
1.确定递归函数返回值和参数
本题和上题一样可以有返回值也可以没有,因为要遍历整颗树,所以不需要返回值也可以(从二叉树中删除节点)。但是又返回值更方便,可以通过递归函数的返回值来删除节点。
TreeNode* trimBST(TreeNode* root, int low, int high)
2.确定终止条件。
修建的操作并不是在终止条件下进行的,所以遇到空节点返回即可。
if (root == nullptr) return nullptr;
3.确定单层递归的逻辑
如果root(当前节点)小于左边界low,那么应该递归右子树,并返回右子树符合条件的头节点。(相当于将root删除,将root的右孩子赋给了root父节点的左孩子)
如果root(当前节点)大于右边界high,那么应该递归左子树,并返回左子树符合条件的头节点(相当于将root删除,将root的左孩子赋给了root父节点的右孩子)
接下来将下一层递归处理左子树的结果赋值给root->left,处理右子树的结果赋值给root->right,最后返回root节点。
if (root->val < low) {//相当于把示例2中节点0的右孩子(节点2)返回给了上一层
TreeNode* right = trimBST(root->right, low, high);
return right;
}
if (root->val > high) {
TreeNode* left = trimBST(root->left, low, high);
return left;
}
root->left = trimBST(root->left, low, high);//把下层返回的节点0的右孩子(节点2)赋给了节点3的左孩子
//此时节点3的左孩子就变成了节点2,将 节点0从二叉树中删除了
root->right = trimBST(root->right, low, high);
return root;
整体代码如下:
题解
/**
* 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) {//相当于把示例2中节点0的右孩子(节点2)返回给了上一层
//寻找符合区间的节点,不符合就继续递归
TreeNode* right = trimBST(root->right, low, high);
return right;
}
if (root->val > high) {
//寻找符合区间的节点,不符合就继续递归
TreeNode* left = trimBST(root->left, low, high);
return left;
}
//此时root节点符合区间,要将下一层递归结果赋给左右孩子
root->left = trimBST(root->left, low, high);//把下层返回的节点0的右孩子(节点2)赋给了节点3的左孩子
//此时节点3的左孩子就变成了节点2,将 节点0从二叉树中删除了
root->right = trimBST(root->right, low, high);
return root;
}
};
本题精简版代码反倒容易理解:
题解
/**
* 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) return trimBST(root->right, low, high);//不符合区间就返回给上一层递归root右子树的递归结果,相当于删除root
if (root->val > high) return trimBST(root->left, low, high);//不符合区间就返回给上一层递归root左子树的递归结果,相当于删除root
root->left = trimBST(root->left, low, high);//符合区间就返回root左子树的递归结果给root左子树
root->right = trimBST(root->right, low, high);//符合区间就返回root右子树的递归结果给root右子树
return root;
}
};
8.22.2 迭代法
因为二叉搜索树是有序的,所以不用使用栈来模拟递归的过程。在剪枝的过程中,可以分为三步处理:
1.将root移动到区间内,左闭右闭。
2.剪枝左子树。
3.剪枝右子树。
代码如下:
题解
/**
* 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;
while (root != nullptr && (root->val < low || root->val > high)) {//root不为空且root不在区间内执行循环
if (root->val < low) root = root->right;
else root = root->left;
}
//此时root已经在区间内
TreeNode* cur = root;
//处理左孩子元素小于low的情况
while (cur != nullptr) {
while (cur->left && cur->left->val < low) {
cur->left = cur->left->right;//删除cur->left
}
cur = cur->left;//cur左子树一定也小于low,继续删除
}
cur = root;//更新cur为root,以继续删除root右子树大于high的节点
//处理右孩子元素大于high的情况
while (cur != nullptr) {
while (cur->right && cur->right->val > high) {
cur->right = cur->right->left;//删除cur->right
}
cur = cur->right;
}
return root;
}
};
8.23 构造一棵平衡二叉搜索树
力扣题号: 108.将有序数组转换为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 按 严格递增 顺序排列
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree
思路
之前在8.13节我们讲解了如何根据数组构造一棵二叉树,本质上就是寻找分割点,将分割点作为当前节点,然后递归处理左区间和右区间。
基于有序数组构造二叉搜索树,寻找分割点就很容易了,分割点就是数组中间位置的节点。 如果数组的长度为偶数,中间节点有两个,该取哪个呢?都行,只不过会构成不同平衡二叉树,答案不唯一。
8.23.1 递归法
1确定递归函数的返回值和参数
本题依然使用递归函数的返回值来确定中间节点左右孩子,从而构造二叉树。参数是传入的数组,以及左下标left和右下标right。8.13节我们提过,尽量不要重新定义左右区间数组,而是用下标来操作原数组。
TreeNode* traversal(vector<int>& nums, int left, int right)
2.确定递归终止条件
这里定义的是左闭右闭的区间,所以当左边界left大于右边界right时,该区间所对应的节点是空节点。
if (left > right) return nullptr;
3.确定单层递归的逻辑
首先取中间元素的位置,以中间位置的元素构造节点,接着划分区间,下一层左区间的构造节点赋值给root的左孩子,下一层右区间构造的节点赋值给roo的右孩子,最后返回root节点。整体代码如下:
题解
/**
* 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;
}
};
8.23.2 迭代法
可以通过三个队列来模拟迭代法,第一个队列保存遍历的节点,第二个队列保存左区间下标,第三个队列保存右区间下标。模拟的就是不断分割的过程,代码如下:
题解
/**
* 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* sortedArrayToBST(vector<int>& nums) {
if (nums.size() == 0) return nullptr;
TreeNode* root = new TreeNode(0);//初始化根节点
queue<TreeNode*> nodeQue;
queue<int> leftQue;
queue<int> rightQue;
nodeQue.push(root);
leftQue.push(0);
rightQue.push(nums.size() - 1);
while (!nodeQue.empty()) {//队列不空,说明还有未赋值的节点
TreeNode* curNode = nodeQue.front();
nodeQue.pop();
//curNode->val 根据当前curNode对应的left和right确定mid来构造节点
int left = leftQue.front();
leftQue.pop();
int right = rightQue.front();
rightQue.pop();
int mid = left + ((right - left) / 2);
curNode->val = nums[mid];//构造中间节点
//处理左区间
if (left <= mid - 1) {
curNode->left = new TreeNode(0);
nodeQue.push(curNode->left);
leftQue.push(left);
rightQue.push(mid - 1);
}
if (right >= mid + 1) {
curNode->right = new TreeNode(0);
nodeQue.push(curNode->right);
leftQue.push(mid + 1);
rightQue.push(right);
}
}
return root;
}
};
8.24 本章小结
在二叉树题目中选择哪种遍历顺序是不少读者头疼的事情,下面对遍历顺序进行分类:
1.涉及二叉树构造,例如8.13、8.14、8.20、8.21、8.22、8.23,无论构造普通二叉树还是二叉搜索树,一定选择前序遍历,先构造中间节点,或者通过递归函数的返回值来添加或删除节点。
2.求普通二叉树的属性,例如8.7节、8.8节、8.9节、8.10节,选择的是后序遍历,一般通过递归函数的返回值做计算或逻辑判断。
3.求二叉搜索树的属性,例如8.15节、8.16节、8.17节、8.18 节,一定选择中序遍历,充分利用二叉搜索树的特性。
注意,对应求普通二叉树的属性,也有个别情况需要使用前序遍历,例如单纯求深度就是要前序遍历,8.11节也使用了前序遍历,这是为了让父节点指向子节点。