提示:DDU,供自己复习使用。欢迎大家前来讨论~
二叉树Part02
继续学习,差了层序遍历的题目,后面补一下
一、理论基础
递归和迭代
是两种常见的算法设计策略,它们在解决问题时有着不同的方法和特点。
以下是递归和迭代的主要区别:
- 定义:
- 递归:递归是一种在问题解决过程中自我调用的方法。一个递归函数会调用自身来解决问题的一个更小的部分,直到达到基本情况(base case)。
- 迭代:迭代是通过重复执行一系列操作来解决问题的方法。它通常使用循环结构(如 for 循环或 while 循环)。
- 内存使用:
- 递归:每次递归调用都会在调用栈上创建一个新的栈帧,这可能导致较高的内存使用,特别是在深度递归时。
- 迭代:迭代通常使用固定的内存空间,因为它不需要额外的栈空间来存储调用信息。
- 性能:
- 递归:递归可能因为重复计算和大量的函数调用而导致性能下降。
- 迭代:迭代通常具有更好的性能,因为它避免了函数调用的开销和递归带来的额外内存使用。
- 控制流程:
- 递归:递归的控制流程是隐式的,通过函数调用自身来实现。
- 迭代:迭代的控制流程是显式的,通过循环和条件语句来控制。
- 适用场景:
- 递归:递归适用于那些可以自然分解为相似子问题的问题,如树和图的遍历、分治算法等。
- 迭代:迭代适用于那些需要逐步处理数据集合的问题,如排序、搜索等。
- 风险:
- 递归:递归的主要风险是可能导致栈溢出,特别是当递归深度很大时。
- 迭代:迭代的主要风险是可能因为错误的循环条件或无限循环而导致性能问题。
二、题目
题目一:226.翻转二叉树
解题思路:
遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。
注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果。
下文以前序遍历为例:
递归法
递归三部曲:
-
确定递归函数的返回值和参数
-
确定终止条件
-
确定单层逻辑
因为是先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。
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;
}
};
假设我们有以下二叉树:
1
/ \
2 3
/ \
4 5
我们要调用 invertTree
来翻转这棵树。
- 初始调用:
invertTree(1)
被调用,1
被压栈。 - 交换左右子节点:
1
的左右子节点2
和3
被交换。 - 第一次递归调用:
invertTree(3)
被调用,3
被压栈。 - 第二次递归调用:
invertTree(2)
被调用,2
被压栈。 - 叶子节点到达:当
4
和5
被访问时,它们没有子节点,递归停止。 - 返回和出栈:从叶子节点开始,
5
返回NULL
,4
返回NULL
,然后2
返回,此时2
的左右子节点4
和5
被交换。接着3
返回,1
返回。 - 最终结果:最终,
1
返回,此时整个树已经被翻转,变成了:
1
/ \
3 2
/ \
5 4
在这个过程中,每次递归调用都会创建一个新的上下文,这个上下文包含了当前节点的指针和它的状态(左右子节点)。当递归调用结束时,这个上下文会被弹出栈,控制权返回给上一个调用者,直到最初的调用者得到最终结果。
迭代法:
深度优先遍历
C++代码迭代法(前序遍历):根据前中后序迭代方式的写法,所以本题可以很轻松的写出如下迭代法的代码:
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;
}
};
广度优先遍历
也就是层序遍历,层数遍历也是可以翻转这棵树的,因为层序遍历也可以把每个节点的左右孩子都翻转一遍,代码如下:
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;
}
};
注意:
直接使用中序遍历的代码是不可以的,避免同一个子树操作两次。
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == NULL) return root;
invertTree(root->left); // 左
swap(root->left, root->right); // 中
invertTree(root->left); // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了
return root;
}
};
但使用迭代方式统一写法的中序是可以的。
代码如下:
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
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();
swap(node->left, node->right); // 节点处理逻辑
}
}
return root;
}
};
针对翻转二叉树,给出了一种递归,三种迭代(两种模拟深度优先遍历,一种层序遍历)的写法,都是之前我们讲过的写法,融汇贯通一下而已。
题目二: 101. 对称二叉树
解题思路
首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!
二叉树的对称性检查实际上是在比较经过翻转的两个子树是否相同。
比较的是两个子树的里侧和外侧的元素是否相等。如图所示:
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
递归三部曲:
- 确定递归函数的返回值和参数
- 确定终止条件
- 确定单层递归逻辑
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);
}
};
优化后的代码:
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);
}
};
题目三:104.二叉树的最大深度
递归法
本题可以使用前序(中左右),也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序求的是高度。
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始)
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数或者节点数(取决于高度从0开始还是从1开始)
根节点的高度就是二叉树的最大深度=》本题中我们通过后序求的根节点高度来求的二叉树最大深度。
递归三部曲:
- 确定递归函数的返回值和参数:参数就是传入树的根节点,返回就返回这棵树的深度,所以返回值为int类型。
int getdepth(TreeNode* node)
- 确定终止条件:如果为空节点的话,就返回0,表示高度为0。
if (node == NULL) return 0;
- 确定单层递归的逻辑:先求它的左子树的深度,再求右子树的深度,最后取左右深度最大的数值 再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度。
int leftdepth = getdepth(node->left); // 左
int rightdepth = getdepth(node->right); // 右
int depth = 1 + max(leftdepth, rightdepth); // 中
return depth;
完整代码:
class Solution {
public:
int getdepth(TreeNode* node) {
if (node == NULL) return 0;
int leftdepth = getdepth(node->left); // 左
int rightdepth = getdepth(node->right); // 右
int depth = 1 + max(leftdepth, rightdepth); // 中
return depth;
}
int maxDepth(TreeNode* root) {
return getdepth(root);
}
};
代码精简之后c++代码如下:
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == null) return 0;
return 1 + max(maxDepth(root->left), maxDepth(root->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();
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);
}
}
return depth;
}
};
题目四:559.N叉树的最大深度
依然可以提供递归法和迭代法,来解决这个问题,思路是和二叉树思路一样的,直接给出代码如下:
解题思路:
通过递归遍历每个节点的所有子节点,找到最深的子树并加上当前节点的层级来确定整棵树的最大深度。
与二叉树的最大高度问题相比,N叉树的最大深度问题在递归逻辑上稍有不同。在二叉树中,每个节点最多有两个子节点,递归函数会分别计算左子树和右子树的深度,然后取两者的最大值加1作为当前节点的深度。而在N叉树中,每个节点可能有多达N个子节点,因此递归函数需要遍历所有这些子节点,计算每个子节点的深度,并找出最深的一个,然后同样加1来考虑当前节点。
核心思想是分而治之,即分别求解子问题,然后将子问题的解合并以得到原问题的解。
递归法代码如下(这么写也是 通过的):
//方式一:
class Solution {
public:
int getdepth(Node* node) {
// 检查节点是否为空
if (node == NULL) return 0;
// 递归获取左右子树的深度
int leftdepth = getdepth(node->children.size() > 0 ? node->children[0] : NULL);
int rightdepth = getdepth(node->children.size() > 1 ? node->children[1] : NULL);
// 取左右子树深度的最大值并加1(当前节点)
int depth = 1 + max(leftdepth, rightdepth);
return depth;
}
int maxDepth(Node* root) {
// 直接调用getdepth函数计算并返回树的深度
return getdepth(root);
}
};
//方法二:
class Solution {
public:
int maxDepth(Node* root) {
if (root == 0) return 0;
int depth = 0;
for (int i = 0; i < root->children.size(); i++) {
depth = max (depth, maxDepth(root->children[i]));
}
return depth + 1;
}
};
==迭代法==代码如下:
层序遍历。
class Solution {
public:
int maxDepth(Node* root) {
queue<Node*> que;
if (root != NULL) que.push(root);
int depth = 0;
while (!que.empty()) {
int size = que.size();
depth++; // 记录深度
for (int i = 0; i < size; i++) {
Node* node = que.front();
que.pop();
for (int j = 0; j < node->children.size(); j++) {
if (node->children[j]) que.push(node->children[j]);
}
}
}
return depth;
}
};
题目五: 111.二叉树的最小深度
直觉上好像和求最大深度差不多,其实还是差不少的。
本题依然是前序遍历和后序遍历都可以,前序求的是深度,后序求的是高度。
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始)
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数后者节点数(取决于高度从0开始还是从1开始)
那么使用后序遍历,其实求的是根节点到叶子节点的最小距离,就是求高度的过程,不过这个最小距离 也同样是最小深度。
本题的坑:
要求是求得,叶节点的最小深度。图中左边的这个最小深度为1,并不是要求的结果。
**最小深度是从根节点到最近叶子节点的最短路径上的节点数量。*注意是*叶子节点。
递归法:
- 确定递归函数的返回值和参数。
- 确定终止条件
- 确定单层循环逻辑
整体代码如下:
class Solution {
public:
int getDepth(TreeNode* root){
if(root == NULL) return 0;
int l = getDepth(root->left); //左
int r = getDepth(root->right);//右
//中
if(root->left != NULL&&root->right==NULL) return 1 + l;
if(root->right != NULL&&root->left==NULL) return 1 + r;
int res = 1 + min(l, r);
return res;
}
int minDepth(TreeNode* root) {
return getDepth(root);
}
};
精简后的代码:(精简之后的代码根本看不出是哪种遍历方式,如果对二叉树的操作还不熟练,尽量不要直接照着精简代码来学。)
class Solution {
public:
int minDepth(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right != NULL) {
return 1 + minDepth(root->right);
}
if (root->left != NULL && root->right == NULL) {
return 1 + minDepth(root->left);
}
return 1 + min(minDepth(root->left), minDepth(root->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;
}
}
}
return depth;
}
};
总结
- 二叉树的相关操作,题目都是有模板和方法的,优先记住递归方法
- 落下的有点多了,最近加油补一下,不然补不上来了。以后尽量提前,不要推。