mirror of
https://github.com/youngyangyang04/leetcode-master.git
synced 2025-07-24 17:12:24 +08:00
更新二叉树专题
This commit is contained in:
205
problems/周总结/20200927二叉树周末总结.md
Normal file
205
problems/周总结/20200927二叉树周末总结.md
Normal file
@ -0,0 +1,205 @@
|
||||
|
||||
# 本周小结!(二叉树)
|
||||
|
||||
**周日我做一个针对本周的打卡留言疑问以及在刷题群里的讨论内容做一下梳理吧。**,这样也有助于大家补一补本周的内容,消化消化。
|
||||
|
||||
**注意这个周末总结和系列总结还是不一样的(二叉树还远没有结束),这个总结是针对留言疑问以及刷题群里讨论内容的归纳。**
|
||||
|
||||
## 周一
|
||||
|
||||
本周我们开始讲解了二叉树,在[关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A)中讲解了二叉树的理论基础。
|
||||
|
||||
有同学会把红黑树和二叉平衡搜索树弄分开了,其实红黑树就是一种二叉平衡搜索树,这两个树不是独立的,所以C++中map、multimap、set、multiset的底层实现机制是二叉平衡搜索树,再具体一点是红黑树。
|
||||
|
||||
对于二叉树节点的定义,C++代码如下:
|
||||
|
||||
```
|
||||
struct TreeNode {
|
||||
int val;
|
||||
TreeNode *left;
|
||||
TreeNode *right;
|
||||
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
|
||||
};
|
||||
```
|
||||
对于这个定义中`TreeNode(int x) : val(x), left(NULL), right(NULL) {}` 有同学不清楚干什么的。
|
||||
|
||||
这是构造函数,这么说吧C语言中的结构体是C++中类的祖先,所以C++结构体也可以有构造函数。
|
||||
|
||||
构造函数也可以不写,但是new一个新的节点的时候就比较麻烦。
|
||||
|
||||
例如有构造函数,定义初始值为9的节点:
|
||||
|
||||
```
|
||||
TreeNode* a = new TreeNode(9);
|
||||
```
|
||||
|
||||
没有构造函数的话就要这么写:
|
||||
|
||||
```
|
||||
TreeNode* a = new TreeNode();
|
||||
a->val = 9;
|
||||
a->left = NULL;
|
||||
a->right = NULL;
|
||||
```
|
||||
|
||||
在介绍前中后序遍历的时候,有递归和迭代(非递归),还有一种牛逼的遍历方式:morris遍历。
|
||||
|
||||
morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为O(1),感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。
|
||||
|
||||
## 周二
|
||||
|
||||
在[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)中讲到了递归三要素,以及前中后序的递归写法。
|
||||
|
||||
文章中我给出了leetcode上三道二叉树的前中后序题目,但是看完[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA),依然可以解决n叉树的前后序遍历,在leetcode上分别是
|
||||
* 589. N叉树的前序遍历
|
||||
* 590. N叉树的后序遍历
|
||||
|
||||
大家可以再去把这两道题目做了。
|
||||
|
||||
## 周三
|
||||
|
||||
在[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)中我们开始用栈来实现递归的写法,也就是所谓的迭代法。
|
||||
|
||||
细心的同学发现文中前后序遍历空节点是入栈的,其实空节点入不入栈都差不多,但感觉空节点不入栈确实清晰一些,符合文中动画的演示。
|
||||
|
||||
前序遍历空节点不入栈的代码:(注意注释部分,和文章中的区别)
|
||||
|
||||
```
|
||||
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);
|
||||
if (node->right) st.push(node->right); // 右(空节点不入栈)
|
||||
if (node->left) st.push(node->left); // 左(空节点不入栈)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
后序遍历空节点不入栈的代码:(注意注释部分,和文章中的区别)
|
||||
|
||||
```
|
||||
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);
|
||||
if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈)
|
||||
if (node->right) st.push(node->right); // 空节点不入栈
|
||||
}
|
||||
reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
在实现迭代法的过程中,有同学问了:递归与迭代究竟谁优谁劣呢?
|
||||
|
||||
从时间复杂度上其实迭代法和递归法差不多(在不考虑函数调用开销和函数调用产生的堆栈开销),但是空间复杂度上,递归开销会大一些,因为递归需要系统堆栈存参数返回值等等。
|
||||
|
||||
递归更容易让程序员理解,但收敛不好,容易栈溢出。
|
||||
|
||||
这么说吧,递归是方便了程序员,难为了机器(各种保存参数,各种进栈出栈)。
|
||||
|
||||
**在实际项目开发的过程中我们是要尽量避免递归!因为项目代码参数、调用关系都比较复杂,不容易控制递归深度,甚至会栈溢出。**
|
||||
|
||||
## 周四
|
||||
|
||||
在[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)中我们使用空节点作为标记,给出了统一的前中后序迭代法。
|
||||
|
||||
此时又多了一种前中后序的迭代写法,那么有同学问了:前中后序迭代法是不是一定要统一来写,这样才算是规范。
|
||||
|
||||
其实没必要,还是自己感觉哪一种更好记就用哪种。
|
||||
|
||||
但是**一定要掌握前中后序一种迭代的写法,并不因为某种场景的题目一定要用迭代,而是现场面试的时候,面试官看你顺畅的写出了递归,一般会进一步考察能不能写出相应的迭代。**
|
||||
|
||||
## 周五
|
||||
|
||||
在[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)中我们介绍了二叉树的另一种遍历方式(图论中广度优先搜索在二叉树上的应用)即:层序遍历。
|
||||
|
||||
看完这篇文章,去leetcode上怒刷五题,文章中 编号107题目的样例图放错了(原谅我匆忙之间总是手抖),但不影响大家理解。
|
||||
|
||||
只有同学发现leetcode上“515. 在每个树行中找最大值”,也是层序遍历的应用,依然可以分分钟解决,所以就是一鼓作气解决六道了,哈哈。
|
||||
|
||||
**层序遍历遍历相对容易一些,只要掌握基本写法(也就是框架模板),剩下的就是在二叉树每一行遍历的时候做做逻辑修改。**
|
||||
|
||||
## 周六
|
||||
|
||||
在[二叉树:你真的会翻转二叉树么?](https://mp.weixin.qq.com/s/6gY1MiXrnm-khAAJiIb5Bg)中我们把翻转二叉树这么一道简单又经典的问题,充分的剖析了一波,相信就算做过这道题目的同学,看完本篇之后依然有所收获!
|
||||
|
||||
|
||||
**文中我指的是递归的中序遍历是不行的,因为使用递归的中序遍历,某些节点的左右孩子会翻转两次。**
|
||||
|
||||
如果非要使用递归中序的方式写,也可以,如下代码就可以避免节点左右孩子翻转两次的情况:
|
||||
|
||||
```
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
```
|
||||
|
||||
为什么这个中序就是可以的呢,因为这是用栈来遍历,而不是靠指针来遍历,避免了递归法中翻转了两次的情况,大家可以画图理解一下,这里有点意思的。
|
||||
|
||||
## 总结
|
||||
|
||||
**本周我们都是讲解了二叉树,从理论基础到遍历方式,从递归到迭代,从深度遍历到广度遍历,最后再用了一个翻转二叉树的题目把我们之前讲过的遍历方式都串了起来。**
|
||||
|
||||
|
258
problems/周总结/20201003二叉树周末总结.md
Normal file
258
problems/周总结/20201003二叉树周末总结.md
Normal file
@ -0,0 +1,258 @@
|
||||
# 本周小结!(二叉树系列二)
|
||||
|
||||
本周赶上了十一国庆,估计大家已经对本周末没什么概念了,但是我们该做总结还是要做总结的。
|
||||
|
||||
本周的主题其实是**简单但并不简单**,本周所选的题目大多是看一下就会的题目,但是大家看完本周的文章估计也发现了,二叉树的简答题目其实里面都藏了很多细节。 这些细节我都给大家展现了出来。
|
||||
|
||||
|
||||
## 周一
|
||||
|
||||
本周刚开始我们讲解了判断二叉树是否对称的写法, [二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)。
|
||||
|
||||
这道题目的本质是要比较两个树(这两个树是根节点的左右子树),遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
|
||||
|
||||
而本题的迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,认识到这一点之后就发现:用队列,用栈,甚至用数组,都是可以的。
|
||||
|
||||
那么做完本题之后,在看如下两个题目。
|
||||
* 100.相同的树
|
||||
* 572.另一个树的子树
|
||||
|
||||
**[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)中的递归法和迭代法只需要稍作修改其中一个树的遍历顺序,便可刷了100.相同的树。**
|
||||
|
||||
100.相同的树的递归代码如下:
|
||||
|
||||
```C++
|
||||
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.相同的树,精简之后代码如下:
|
||||
|
||||
```C++
|
||||
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.相同的树,迭代法代码如下:
|
||||
|
||||
```C++
|
||||
class 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了。
|
||||
|
||||
## 周二
|
||||
|
||||
在[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中,我们讲解了如何求二叉树的最大深度。
|
||||
|
||||
本题可以使用前序,也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序呢求的是高度。
|
||||
|
||||
**而根节点的高度就是二叉树的最大深度**,所以本题中我们通过后序求的根节点高度来求的二叉树最大深度,所以[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中使用的是后序遍历。
|
||||
|
||||
本题当然也可以使用前序,代码如下:(**充分表现出求深度回溯的过程**)
|
||||
```C++
|
||||
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;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**可以看出使用了前序(中左右)的遍历顺序,这才是真正求深度的逻辑!**
|
||||
|
||||
注意以上代码是为了把细节体现出来,简化一下代码如下:
|
||||
|
||||
```C++
|
||||
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;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 周三
|
||||
|
||||
在[二叉树:看看这些树的最小深度](https://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA)中,我们讲解如何求二叉树的最小深度, 这道题目要是稍不留心很容易犯错。
|
||||
|
||||
**注意这里最小深度是从根节点到最近叶子节点的最短路径上的节点数量。注意是叶子节点。**
|
||||
|
||||
什么是叶子节点,左右孩子都为空的节点才是叶子节点!
|
||||
|
||||
**求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。**
|
||||
|
||||
注意到这一点之后 递归法和迭代法 都可以参照[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)写出来。
|
||||
|
||||
## 周四
|
||||
|
||||
我们在[二叉树:我有多少个节点?](https://mp.weixin.qq.com/s/2_eAjzw-D0va9y4RJgSmXw)中,讲解了如何求二叉树的节点数量。
|
||||
|
||||
这一天是十一长假的第一天,又是双节,所以简单一些,只要把之前两篇[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg), [二叉树:看看这些树的最小深度](https://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA)都认真看了的话,这道题目可以分分钟刷掉了。
|
||||
|
||||
估计此时大家对这一类求二叉树节点数量以及求深度应该非常熟练了。
|
||||
|
||||
## 周五
|
||||
|
||||
在[二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww)中讲解了如何判断二叉树是否是平衡二叉树
|
||||
|
||||
今天讲解一道判断平衡二叉树的题目,其实 方法上我们之前讲解深度的时候都讲过了,但是这次我们通过这道题目彻底搞清楚二叉树高度与深度的问题,以及对应的遍历方式。
|
||||
|
||||
二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
|
||||
二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。
|
||||
|
||||
**但leetcode中强调的深度和高度很明显是按照节点来计算的**。
|
||||
|
||||
关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。
|
||||
|
||||
当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。
|
||||
|
||||
虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。
|
||||
|
||||
**例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!**
|
||||
|
||||
讲了这么多二叉树题目的迭代法,有的同学会疑惑,迭代法中究竟什么时候用队列,什么时候用栈?
|
||||
|
||||
**如果是模拟前中后序遍历就用栈,如果是适合层序遍历就用队列,当然还是其他情况,那么就是 先用队列试试行不行,不行就用栈。**
|
||||
|
||||
## 周六
|
||||
|
||||
在[二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)中正式涉及到了回溯,很多同学过了这道题目,可能都不知道自己使用了回溯,其实回溯和递归都是相伴相生的。最后我依然给出了迭代法的版本。
|
||||
|
||||
我在题解中第一个版本的代码会把回溯的过程充分体现出来,如果大家直接看简洁的代码版本,很可能就会忽略的回溯的存在。
|
||||
|
||||
我在文中也强调了这一点。
|
||||
|
||||
有的同学还不理解 ,文中精简之后的递归代码,回溯究竟隐藏在哪里了。
|
||||
|
||||
文中我明确的说了:**回溯就隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。**
|
||||
|
||||
如果还不理解的话,可以把
|
||||
```
|
||||
traversal(cur->left, path + "->", result);
|
||||
```
|
||||
|
||||
改成
|
||||
```
|
||||
string tmp = path + "->";
|
||||
traversal(cur->left, tmp, result);
|
||||
```
|
||||
看看还行不行了,答案是这么写就不行了,因为没有回溯了。
|
||||
|
||||
## 总结
|
||||
|
||||
二叉树的题目,我都是使用了递归三部曲一步一步的把整个过程分析出来,而不是上来就给出简洁的代码。
|
||||
|
||||
一些同学可能上来就能写出代码,大体上也知道是为啥,可以自圆其说,但往细节一扣,就不知道了。
|
||||
|
||||
所以刚接触二叉树的同学,建议按照文章分析的步骤一步一步来,不要上来就照着精简的代码写(那样写完了也很容易忘的,知其然不知其所以然)。
|
||||
|
||||
**简短的代码看不出遍历的顺序,也看不出分析的逻辑,还会把必要的回溯的逻辑隐藏了,所以尽量按照原理分析一步一步来,写出来之后,再去优化代码。**
|
||||
|
||||
大家加个油!!
|
||||
|
||||
|
||||
> **相信很多小伙伴刷题的时候面对力扣上近两千道题目,感觉无从下手,我花费半年时间整理了Github项目:「力扣刷题攻略」[https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master)。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解,常用算法模板总结,以及难点视频讲解,按照list一道一道刷就可以了!star支持一波吧!**
|
||||
|
||||
* 公众号:[代码随想录](https://img-blog.csdnimg.cn/20201210231711160.png)
|
||||
* B站:[代码随想录](https://space.bilibili.com/525438321)
|
||||
* Github:[leetcode-master](https://github.com/youngyangyang04/leetcode-master)
|
||||
* 知乎:[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64)
|
||||
|
||||

|
89
problems/周总结/20201010二叉树周末总结.md
Normal file
89
problems/周总结/20201010二叉树周末总结.md
Normal file
@ -0,0 +1,89 @@
|
||||
|
||||
# 本周小结!(二叉树系列三)
|
||||
|
||||
|
||||
## 周一
|
||||
|
||||
在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)中,通过leetcode [257.二叉树的所有路径这道题目](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA),讲解了递归如何隐藏着回溯,一些代码会把回溯的过程都隐藏了起来了,甚至刷过这道题的同学可能都不知道自己用了回溯。
|
||||
|
||||
文章中第一版代码把每一个细节都展示了输出来了,大家可以清晰的看到回溯的过程。
|
||||
|
||||
然后给出了第二版优化后的代码,分析了其回溯隐藏在了哪里,如果要把这个回溯扣出来的话,在第二版的基础上应该怎么改。
|
||||
|
||||
主要需要理解:**回溯隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。**
|
||||
|
||||
|
||||
## 周二
|
||||
|
||||
在文章[二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) 中提供了另一个判断节点属性的思路,平时我们习惯了使用通过节点的左右孩子判断本节点的属性,但发现使用这个思路无法判断左叶子。
|
||||
|
||||
此时需要相连的三层之间构成的约束条件,也就是要通过节点的父节点以及孩子节点来判断本节点的属性。
|
||||
|
||||
这道题目可以扩展大家对二叉树的解题思路。
|
||||
|
||||
|
||||
## 周三
|
||||
|
||||
在[二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)中的题目如果使用递归的写法还是有点难度的,层次遍历反而很简单。
|
||||
|
||||
题目其实就是要在树的**最后一行**找到**最左边的值**。
|
||||
|
||||
**如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。**
|
||||
|
||||
在这篇文章中,我们使用递归算法实实在在的求了一次深度,然后使用靠左的遍历,保证求得靠左的最大深度,而且又一次使用了回溯。
|
||||
|
||||
如果对二叉树的高度与深度又有点模糊了,在看这里[二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww),回忆一下吧。
|
||||
|
||||
[二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)中把我们之前讲过的内容都过了一遍,此外,还用前序遍历的技巧求得了靠左的最大深度。
|
||||
|
||||
**求二叉树的各种最值,就想应该采用什么样的遍历顺序,确定了遍历循序,其实就和数组求最值一样容易了。**
|
||||
|
||||
|
||||
## 周四
|
||||
|
||||
在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)中通过两道题目,彻底说清楚递归函数的返回值问题。
|
||||
|
||||
一般情况下:**如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。**
|
||||
|
||||
特别是有些时候 递归函数的返回值是bool类型,一些同学会疑惑为啥要加这个,其实就是为了找到一条边立刻返回。
|
||||
|
||||
其实还有一种就是后序遍历需要根据左右递归的返回值推出中间节点的状态,这种需要有返回值,例如[222.完全二叉树](https://mp.weixin.qq.com/s/2_eAjzw-D0va9y4RJgSmXw),[110.平衡二叉树](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww),这几道我们之前也讲过。
|
||||
|
||||
## 周五
|
||||
|
||||
之前都是讲解遍历二叉树,这次该构造二叉树了,在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg)中,我们通过前序和中序,后序和中序,构造了唯一的一颗二叉树。
|
||||
|
||||
**构造二叉树有三个注意的点:**
|
||||
|
||||
* 分割时候,坚持区间不变量原则,左闭右开,或者左闭又闭。
|
||||
* 分割的时候,注意后序 或者 前序已经有一个节点作为中间节点了,不能继续使用了。
|
||||
* 如何使用切割后的后序数组来切合中序数组?利用中序数组大小一定是和后序数组的大小相同这一特点来进行切割。
|
||||
|
||||
这道题目代码实现并不简单,大家啃下来之后,二叉树的构造应该不是问题了。
|
||||
|
||||
**最后我还给出了为什么前序和后序不能唯一构成一棵二叉树,因为没有中序遍历就无法确定左右部分,也就无法分割。**
|
||||
|
||||
## 周六
|
||||
|
||||
知道了如何构造二叉树,那么使用一个套路就可以解决文章[二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)中的问题。
|
||||
|
||||
**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下表索引直接在原数组上操作,这样可以节约时间和空间上的开销。**
|
||||
|
||||
文章中我还给出了递归函数什么时候加if,什么时候不加if,其实就是控制空节点(空指针)是否进入递归,是不同的代码实现方式,都是可以的。
|
||||
|
||||
**一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。**
|
||||
|
||||
## 总结
|
||||
|
||||
本周我们深度讲解了如下知识点:
|
||||
|
||||
1. [递归中如何隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)
|
||||
2. [如何通过三层关系确定左叶子](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA)
|
||||
3. [如何通过二叉树深度来判断左下角的值](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)
|
||||
4. [递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)
|
||||
5. [前序和中序,后序和中序构造唯一二叉树](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg)
|
||||
6. [使用数组构造某一特性的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)
|
||||
|
||||
**如果大家一路跟下来,一定收获满满,如果周末不做这个总结,大家可能都不知道自己收获满满,啊哈!**
|
||||
|
||||
|
118
problems/周总结/20201017二叉树周末总结.md
Normal file
118
problems/周总结/20201017二叉树周末总结.md
Normal file
@ -0,0 +1,118 @@
|
||||
|
||||
|
||||
# 本周小结!(二叉树系列四)
|
||||
|
||||
> 这已经是二叉树的第四周总结了,二叉树是非常重要的数据结构,也是面试中的常客,所以有必要一步一步帮助大家彻底掌握二叉树!
|
||||
|
||||
## 周一
|
||||
|
||||
在[二叉树:合并两个二叉树](https://mp.weixin.qq.com/s/3f5fbjOFaOX_4MXzZ97LsQ)中讲解了如何合并两个二叉树,平时我们都习惯了操作一个二叉树,一起操作两个树可能还有点陌生。
|
||||
|
||||
其实套路是一样,只不过一起操作两个树的指针,我们之前讲过求 [二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)的时候,已经初步涉及到了 一起遍历两颗二叉树了。
|
||||
|
||||
**迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。**
|
||||
|
||||
## 周二
|
||||
|
||||
周二开始讲解一个新的树,二叉搜索树,开始要换一个思路了,如果没有利用好二叉搜索树的特性,就容易把简单题做成了难题了。
|
||||
|
||||
学习[二叉搜索树的特性](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg),还是比较容易的。
|
||||
|
||||
大多是二叉搜索树的题目,其实都离不开中序遍历,因为这样就是有序的。
|
||||
|
||||
至于迭代法,相信大家看到文章中如此简单的迭代法的时候,都会感动的痛哭流涕。
|
||||
|
||||
## 周三
|
||||
|
||||
了解了二搜索树的特性之后, 开始验证[一颗二叉树是不是二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q)。
|
||||
|
||||
首先在此强调一下二叉搜索树的特性:
|
||||
|
||||
* 节点的左子树只包含小于当前节点的数。
|
||||
* 节点的右子树只包含大于当前节点的数。
|
||||
* 所有左子树和右子树自身必须也是二叉搜索树。
|
||||
|
||||
那么我们在验证二叉搜索树的时候,有两个陷阱:
|
||||
|
||||
* 陷阱一
|
||||
|
||||
**不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了**,而是左子树都小于中间节点,右子树都大于中间节点。
|
||||
|
||||
* 陷阱二
|
||||
|
||||
在一个有序序列求最值的时候,不要定义一个全局遍历,然后遍历序列更新全局变量求最值。因为最值可能就是int 或者 longlong的最小值。
|
||||
|
||||
推荐要通过前一个数值(pre)和后一个数值比较(cur),得出最值。
|
||||
|
||||
**在二叉树中通过两个前后指针作比较,会经常用到**。
|
||||
|
||||
本文[二叉树:我是不是一棵二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q)中迭代法中为什么没有周一那篇那么简洁了呢,因为本篇是验证二叉搜索树,前提默认它是一棵普通二叉树,所以还是要回归之前老办法。
|
||||
|
||||
## 周四
|
||||
|
||||
了解了[二叉搜索树](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg),并且知道[如何判断二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q),本篇就很简单了。
|
||||
|
||||
**要知道二叉搜索树和中序遍历是好朋友!**
|
||||
|
||||
在[二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ)中强调了要利用搜索树的特性,把这道题目想象成在一个有序数组上求两个数最小差值,这就是一道送分题了。
|
||||
|
||||
**需要明确:在有序数组求任意两数最小值差等价于相邻两数的最小值差**。
|
||||
|
||||
同样本题也需要用pre节点记录cur节点的前一个节点。(这种写法一定要掌握)
|
||||
|
||||
## 周五
|
||||
|
||||
此时大家应该知道遇到二叉搜索树,就想是有序数组,那么在二叉搜索树中求二叉搜索树众数就很简单了。
|
||||
|
||||
在[二叉树:我的众数是多少?](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg)中我给出了如果是普通二叉树,应该如何求众数的集合,然后进一步讲解了二叉搜索树应该如何求众数集合。
|
||||
|
||||
在求众数集合的时候有一个技巧,因为题目中众数是可以有多个的,所以一般的方法需要遍历两遍才能求出众数的集合。
|
||||
|
||||
**但可以遍历一遍就可以求众数集合,使用了适时清空结果集的方法**,这个方法还是很巧妙的。相信仔细读了文章的同学会惊呼其巧妙!
|
||||
|
||||
**所以大家不要看题目简单了,就不动手做了,我选的题目,一般不会简单到不用动手的程度,哈哈**。
|
||||
|
||||
## 周六
|
||||
|
||||
在[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)中,我们开始讲解如何在二叉树中求公共祖先的问题,本来是打算和二叉搜索树一起讲的,但发现篇幅过长,所以先讲二叉树的公共祖先问题。
|
||||
|
||||
**如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。**
|
||||
|
||||
这道题目的看代码比较简单,而且好像也挺好理解的,但是如果把每一个细节理解到位,还是不容易的。
|
||||
|
||||
主要思考如下几点:
|
||||
|
||||
* 如何从底向上遍历?
|
||||
* 遍历整棵树,还是遍历局部树?
|
||||
* 如何把结果传到根节点的?
|
||||
|
||||
这些问题都需要弄清楚,上来直接看代码的话,是可能想不到这些细节的。
|
||||
|
||||
公共祖先问题,还是有难度的,初学者还是需要慢慢消化!
|
||||
|
||||
## 总结
|
||||
|
||||
本周我们讲了[如何合并两个二叉树](https://mp.weixin.qq.com/s/3f5fbjOFaOX_4MXzZ97LsQ),了解了如何操作两个二叉树。
|
||||
|
||||
然后开始另一种树:二叉搜索树,了解[二叉搜索树的特性](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg),然后[判断一棵二叉树是不是二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q)。
|
||||
|
||||
了解以上知识之后,就开始利用其特性,做一些二叉搜索树上的题目,[求最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ),[求众数集合](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg)。
|
||||
|
||||
接下来,开始求二叉树与二叉搜索树的公共祖先问题,单篇篇幅原因,先单独介绍[普通二叉树如何求最近公共祖先](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)。
|
||||
|
||||
现在已经讲过了几种二叉树了,二叉树,二叉平衡树,完全二叉树,二叉搜索树,后面还会有平衡二叉搜索树。 那么一些同学难免会有混乱了,我针对如下三个问题,帮大家在捋顺一遍:
|
||||
|
||||
1. 平衡二叉搜索数是不是二叉搜索树和平衡二叉树的结合?
|
||||
|
||||
是的,是二叉搜索树和平衡二叉树的结合。
|
||||
|
||||
2. 平衡二叉树与完全二叉树的区别在于底层节点的位置?
|
||||
|
||||
是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。
|
||||
|
||||
3. 堆是完全二叉树和排序的结合,而不是平衡二叉搜索树?
|
||||
|
||||
堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 **但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树**。
|
||||
|
||||
大家如果每天坚持跟下来,会发现又是充实的一周![机智]
|
||||
|
Reference in New Issue
Block a user