前言
企图通过9个习题,拿捏树的一些解题门道。通过本文:
① 可以重温树的前序、中序、后序遍历。
② 切实捋清楚递归的逻辑。
第一题、2236. 判断根结点是否等于子结点之和
题目描述
解题报告
啊,可爱水题~
参考代码(C++版本)
/**
* 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 checkTree(TreeNode* root) {
return root->val == (root->left->val + root->right->val);
}
};
第二题、 面试题 04.10. 检查子树
本质层序遍历
题目描述
解题报告
其实第一想法了,是将数据哈希,然后比较树T1中出现的数值是否大于等于子树T2中出现的数值。
先放递归的玩法吧,老老实实,能Ac再去玩花的。参考代码(C++版本)
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool checkSubTree(TreeNode* t1, TreeNode* t2) {
//既是特判,也是待会递归的基线条件
if(t2 == NULL) return true;
if(t1 == NULL) return false;
//然后逐层比较吧
//倘若当前层相同,就递归进去比较各自的左右子树
if(t2->val == t1->val)
{
//既然要递归,回去补充递归的边界条件,看谁为NULL的时候为真为假
return (checkSubTree(t1->left,t2->left) && checkSubTree(t1->right,t2->right));
}
else
//否则比较,是不是t1的子树的值了
return (checkSubTree(t1->left,t2) || checkSubTree(t1->right,t2));
}
};
第三题 面试题 04.06. 后继者
本质中序遍历
题目描述
解题报告
题目中说了,要我们去找当前结点的一下个,也就是中序遍历的后继,那么就安装中序遍历去递归,即找到目标结点之后了,递归进右子树,也就是其后继结点中去找答案。
总体来说,也就是英雄哥阐述的这种了。
参考代码(C++版本)
/**
* 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* ans;
bool flag;
void dfs(TreeNode* root_dfs, TreeNode* p)
{
if(root_dfs == NULL) return;
//按照中序搜
//处理左子树
dfs(root_dfs->left,p);
//处理当前的结点,是在找到p结点,并且是后继的情况下
if(flag && !ans) ans = root_dfs;
if(root_dfs == p) flag = true;//找到p这个结点了,标记它,待会要进行的是搜右子树,也就是找它的后继。
//处理右子树,也就是进去找后继结点
dfs(root_dfs->right,p);
}
TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
//按照题目的意思去搜索吧,找中序遍历的后继。
//中序遍历是左子树、根、右子树
flag = false;//标记是否找到结点p了
ans = NULL;
dfs(root,p);
return ans;
}
};
第四题 1110. 删点成林
本质后序遍历 + bool标记
题目描述
解题报告
题目是对后序遍历的变型。
为什么这么说了,因为要求我们把需要删除的结点的子树统计起来。那么整体的逻辑应该是,处理左右子树,再回到根判断当前的结点是不是需要删除的结点了。倘若是,那么就将左右子树存储到返回结果的集合中。
那么这个大流程就是左子树、右子树、根
。也就是后序遍历了,我们在递归的时候也按照这种大逻辑进行,各个小的板块,也会践行这种逻辑的,最终自顶向下的搜索,自底向上的回溯结果。参考代码(C++版本)
/**
* 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 hash[1010];//统计要删除结点的一个哈希表
vector<TreeNode*> ans;
void dfs(TreeNode* father,bool falg_left, TreeNode* node)
{
//设置递归的边界
if(node == nullptr) return;
//按照后序遍历去扫描
dfs(node,true,node->left);
dfs(node,false,node->right);
//找到需要删除的结点了
if(hash[node->val] == 1)
{
if(father)//倘若它有父结点,就需要父结点断开链接
{
//通过标记判断有没有左右子树
if(falg_left) father->left = nullptr;
else father->right = nullptr;
}
//将需要删除的结点root不使用了,其子树的现在单独成树
if(node->left) ans.push_back(node->left);
if(node->right) ans.push_back(node->right);
}
}
vector<TreeNode*> delNodes(TreeNode* root, vector<int>& to_delete) {
//姑且可以理解为,类似后序遍历。找到要删除的结点(也可以说它是父结点),看其左右子树存在与否
if(root == nullptr) return {};
ans.clear();
memset(hash,0,sizeof(hash));
int n = to_delete.size();
for(int i = 0; i < n;i++)
hash[to_delete[i]] ++;
//搜索并统计符合要求的结果
//三个参数分别是,当前结点的父结点(用于和删除结点断开)、左子树的标记、当前的结点。
dfs(nullptr,false,root);
//判断最初的根节点形成的树是否被动过,倘若没有。加入结果集,形成子树
if(hash[root->val] == 0) ans.push_back(root);
return ans;
}
};
这个标记的思想,和前面的一个dijkstra习题很像,都是因为搜索的落实都是一致,只是有细微的区别,比如现在是搜左子树和右子树,之前的dijkstra是搜索的出发和回来。本题是可以放一个bool 标记,之前的这个题是放置的偏移量,本质都是相同逻辑的再运用。
第五题 P1629 邮递员送信
题目描述
解题报告
我自己是图论废物,题目意思倒是能get到:邮递员先去取,取了再拿回邮局,问最短时间,也就是求最短路径吧。
直接去看了许多小伙伴的题解,有说用堆优化的迪杰斯特拉的,有的用SPFA,也有Floyd卡分的,不会什么学什么吧,不想逃避了,发现问题,解决问题
。
关于最短路
最短路的概念阐释我直接去
薅的引用的执梗的文章,需要的小伙伴可以自行点击链接,跳转到原文嗷~
零基础学算法100天第1天——Dijkstra(图解最短路算法)
Dijkstra算法目前是有两个版本,一种是朴素版本,用于稠密图,一种是堆优化版本,用于稀疏图。英雄哥的建议值着重掌握堆优化版本。
下面放的算法板子,完全来自acwing中闫总的总结,想看原文档的小伙伴也可以自行点击实现跳转:
acwing朴素dijkstra算法
Q:① 什么是稠密图?
A:稠密图是题目给的数据点比较小,比如几十个点,或者几百个点,一般不会上千的,同时有上万条边,比如下图中的数据范围:
对于这张图整体而言,是比较稠密的,使用一个二维数组
来存储(一般称呼为邻接矩阵
)的效果就很好。
此时使用的就是这张算法板子,权当了解吧,辅助记忆下面堆优化版本的板子,
闫总代码中的注释也挺详细的了,我就不赘述啦堆优化版dijkstra
需要使用堆优化版本的dijkstra 的数据范围大致是这种的,点和边差不多一样多,这种时候使用邻接矩阵来存储了,就十分浪费空间,因此考虑使用邻接表来存储(
一般使用数组实现,结合数据结构中链表的思想来模拟
)
也是直接放闫总的板子了,然后关于存储点,不一定需要使用pair
,用结构体也是可以的,哪个舒服又能Ac就用哪个。
浅复习了一下知识点,那咱们看看这个题,大佬们是怎么解决的了。因为邮递员有出去的动作,还有回来的过程,按照题意理解了,确实需要两遍最短路算法。参考代码(C++版本)
#include
#define x first
#define y second
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N = 1010 , M = 2e5 + 10;
int n,m;
LL ans;
int h[N*2],e[M],ne[M],w[M],idx;//用邻接表存储所有的边的信息
int dist[N];//存所有点到1号点的距离
bool vis[N];//存储每个点的最短路径是否已经确定了
//构建邻接表的板子
void add(int a,int b,int c)
{
e[idx] = b,ne[idx] = h[a],w[idx] = c,h[a] = idx++;
}
//传入的参数是偏移量,决定是否是反向图
void dijkstra(int d)
{
memset(vis,0,sizeof vis);//都标记为没有探索过
memset(dist,0x3f,sizeof dist);//将距离数组初始化为正无穷
//初始化一步dist数组,dist[1] = 0;
dist[1] = 0;
priority_queue<PII,std::vector<PII>,greater<>> heap;//大根堆:按照first从大到小排序,背过
heap.push({0,1+d});
//有点像BFS 了,队头不空,取队头,拓展队头
while(heap.size())
{
auto t = heap.top();
heap.pop();
int var = t.y;
if(vis[var-d]) continue;
vis[var-d] = true;
//用当前探索的结果来拓展
for(int i = h[var];i != -1;i = ne[i])
{
int j = e[i];
if(!vis[j-d] && dist[j-d] > dist[var-d] + w[i])//更新最短路
{
dist[j-d] = dist[var-d] + w[i];
heap.push({dist[j-d],j});
}
}
}
}
int main(int argc, char const *argv[])
{
//输入环节
cin >> n >> m;
//初始化邻接表
memset(h,-1,sizeof h);
//按照题目意思构建m条边的图
for(int i = 1;i <= m;i++)
{
int a,b,c;
cin >> a >> b >>c;
//因为每条路是单行独立的,因此需要构建出去和回来的图。
add(a,b,c),add(b+N,a+N,c);
}
//扫一遍出去取的最小路径
dijkstra(0);
for(int i = 2; i <= n;i++) ans += dist[i];
//扫一遍取完回来的
dijkstra(N);
for(int i = 2;i <= n;i++) ans += dist[i];
cout << ans;
return 0;
}
浅跟着我代码敲一遍,可以发现,几乎都是需要背的算法模板,也几乎和闫总给的差不多,只是在思维上,为了把快递员出去取货物和从各个点将货物运回来这儿,需要加一个偏移量来区分,这种就不用写两遍dijkstra了。
第六题 144. 二叉树的前序遍历
题目描述
解题报告
在根节点存在的情况下,存根节点的信息,递归进左子树、右子树。
参考代码(C++版本)
/**
* 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> ans;
vector<int> preorderTraversal(TreeNode* root) {
//在根节点存在的情况下,根左右
if(root)
{
ans.push_back(root->val);
preorderTraversal(root->left);
preorderTraversal(root->right);
}
return ans;
}
};
第七题 94. 二叉树的中序遍历
题目描述
解题报告
换汤不换药,水题嘎嘎乱杀~
参考代码(C++版本)
/**
* 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> ans;
vector<int> inorderTraversal(TreeNode* root) {
if(root)
{
inorderTraversal(root->left);
ans.push_back(root->val);
inorderTraversal(root->right);
}
return ans;
}
};
第八题 145. 二叉树的后序遍历
题目描述
解题报告
参考代码(C++版本)
/**
* 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> ans;
vector<int> postorderTraversal(TreeNode* root) {
if(root)
{
postorderTraversal(root->left);
postorderTraversal(root->right);
ans.push_back(root->val);
}
return ans;
}
};
第九题 104. 二叉树的最大深度
递归
题目描述
解题报告
参考代码(C++版本)
/**
* 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 == nullptr) return 0;
//也是直接递归解决吧
//统计左右子树的最大值+1
return max(maxDepth(root->left),maxDepth(root->right))+1;
}
};
总结
① 递归是先自顶向下的搜索,当到达了可以求解的位置,再带着解慢慢回溯回来。那么我就考虑最大的板块的逻辑就好,其他小的板块是和现在的逻辑一样的。
比如最后一题求二叉树的最大深度,那么整体的逻辑就是求左子树和右子树的最大深度。再补加上根这层的深度1。
那么整个代码会递归进去按照同样的逻辑处理:在左子树和右子树中也做类似的操作,最后回溯回来的,其实就是各个子树的最大深度。
回溯到根root
这儿的时候,就是最初始表示的,左子树的最大深度和右子树的最大深度。
② 搜索不应该被限定在哪儿可以用,万物皆可搜,更何况是对于搜索的源泉——树