本周赶上了十一国庆,估计大家已经对本周末没什么概念了,但是我们该做总结还是要做总结的。
本周的主题其实是「简单但并不简单」,本周所选的题目大多是看一下就会的题目,但是大家看完本周的文章估计也发现了,二叉树的简答题目其实里面都藏了很多细节。这些细节我都给大家展现了出来。
本周刚开始我们讲解了判断二叉树是否对称的写法, 二叉树:我对称么?。
这道题目的本质是要比较两个树(这两个树是根节点的左右子树),遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
而本题的迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,认识到这一点之后就发现:用队列,用栈,甚至用数组,都是可以的。
那么做完本题之后,再看如下两个题目。
「二叉树:我对称么?中的递归法和迭代法只需要稍作修改其中一个树的遍历顺序,便可刷了100.相同的树。」
100.相同的树的递归代码如下:
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);
}
};
100.相同的树,精简之后代码如下:
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->left) && compare(left->right, right->right);
}
bool isSameTree(TreeNode* p, TreeNode* q) {
return compare(p, q);
}
};
100.相同的树,迭代法代码如下:
lass Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if (p == NULL && q == NULL) return true;
if (p == NULL || q == NULL) return false;
queue<TreeNode*> que;
que.push(p);
que.push(q);
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->left);
que.push(leftNode->right);
que.push(rightNode->right);
}
return true;
}
};
而572.另一个树的子树,则和 100.相同的树几乎一样的了,大家可以直接AC了。
在二叉树:看看这些树的最大深度中,我们讲解了如何求二叉树的最大深度。
本题可以使用前序,也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序呢求的是高度。
「而根节点的高度就是二叉树的最大深度」,所以本题中我们通过后序求的根节点高度来求的二叉树最大深度,所以二叉树:看看这些树的最大深度中使用的是后序遍历。
本题当然也可以使用前序,代码如下:(「充分表现出求深度回溯的过程」)
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++; // 深度+1
getDepth(node->left, depth);
depth--; // 回溯,深度-1
}
if (node->right) { // 右
depth++; // 深度+1
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;
}
};
在二叉树:看看这些树的最小深度中,我们讲解如何求二叉树的最小深度, 这道题目要是稍不留心很容易犯错。
「注意这里最小深度是从根节点到最近叶子节点的最短路径上的节点数量。注意是叶子节点。」
什么是叶子节点,左右孩子都为空的节点才是叶子节点!
「求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。」
注意到这一点之后 递归法和迭代法 都可以参照二叉树:看看这些树的最大深度写出来。
我们在二叉树:我有多少个节点?中,讲解了如何求二叉树的节点数量。
这一天是十一长假的第一天,又是双节,所以简单一些,只要把之前两篇二叉树:看看这些树的最大深度, 二叉树:看看这些树的最小深度都认真看了的话,这道题目可以分分钟刷掉了。
估计此时大家对这一类求二叉树节点数量以及求深度应该非常熟练了。
在二叉树:我平衡么?中讲解了如何判断二叉树是否是平衡二叉树
今天讲解一道判断平衡二叉树的题目,其实 方法上我们之前讲解深度的时候都讲过了,但是这次我们通过这道题目彻底搞清楚二叉树高度与深度的问题,以及对应的遍历方式。
二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
「但leetcode中强调的深度和高度很明显是按照节点来计算的」。
关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。
当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。
虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。
「例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!」
讲了这么多二叉树题目的迭代法,有的同学会疑惑,迭代法中究竟什么时候用队列,什么时候用栈?
「如果是模拟前中后序遍历就用栈,如果是适合层序遍历就用队列,当然还是其他情况,那么就是 先用队列试试行不行,不行就用栈。」
在二叉树:找我的所有路径?中正式涉及到了回溯,很多同学过了这道题目,可能都不知道自己使用了回溯,其实回溯和递归都是相伴相生的。最后我依然给出了迭代法的版本。
我在题解中第一个版本的代码会把回溯的过程充分体现出来,如果大家直接看简洁的代码版本,很可能就会忽略的回溯的存在。
我在文中也强调了这一点。
有的同学还不理解 ,文中精简之后的递归代码,回溯究竟隐藏在哪里了。
文中我明确的说了:「回溯就隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。」
如果还不理解的话,可以把
traversal(cur->left, path + "->", result);
改成
string tmp = path + "->";
traversal(cur->left, tmp, result);
看看还行不行了,答案是这么写就不行了,因为没有回溯了。
二叉树的题目,我都是使用了递归三部曲一步一步的把整个过程分析出来,而不是上来就给出简洁的代码。
一些同学可能上来就能写出代码,大体上也知道是为啥,可以自圆其说,但往细节一扣,就不知道了。
所以刚接触二叉树的同学,建议按照文章分析的步骤一步一步来,不要上来就照着精简的代码写(那样写完了也很容易忘的,知其然不知其所以然)。
「简短的代码看不出遍历的顺序,也看不出分析的逻辑,还会把必要的回溯的逻辑隐藏了,所以尽量按照原理分析一步一步来,写出来之后,再去优化代码。」
下周依然是二叉树,大家加个油!!
在留言区留下你的思路吧!