Leetcode练习题:树与图
相对于其他的数据结构而言,我个人很喜欢树,也不是擅长使用或者出于研究学术的喜欢,就是单纯的喜欢树的结构给我的感觉。做关于树的题目时也比较乐意。
解决图问题,实现比较简单直接二维数组就行。实现树,相对来说麻烦一点,还需要一些常用的基础函数。
truct TreeNode
{
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(NULL), right(NULL) {}
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
101:对称二叉树
问题描述
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
二叉树[1,2,2,null,3,3,null,5,-2,-2,5]是对称的。
1
/ \
2 2
\ /
3 3
/ \ / \
5 -2 -2 5
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
解题思路
这题一开始一想挺简单的,判断对称,肯定递归实现就可以了,左右子树一比较,可是要实现代码的时候仔细一想又不对了,比较的并不是同一个节点的左右节点,而是镜面的左边节点的左孩字和右边节点的右孩字,左边节点的右孩子和右边节点的左孩子。
最关键的一点就是,最初的开始是root与root的比较对比。
代码实现
bool recursion(TreeNode* L,TreeNode* R)
{
if(!L&&!R)
{
return true;
}
if(!L||!R)
{
return false;
}
return L->val==R->val&&recursion(L->right,R->left)&&recursion(L->left,R->right);
}
bool isSymmetric(TreeNode* root) {
return recursion(root,root);
}
反思与收获
这题虽然简单,但是很巧妙,很少遇见root,root的开始比较,可以记住这个镜面对称的方法。
103:二叉树的锯齿形层次遍历
问题描述
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
—/ \
15 7
返回锯齿形层次遍历如下:
[
[3],
[20,9],
[15,7]
]
程序输出:
3 20 9 15 7
解题思路
这题是树层次遍历的升级版,因此我们比起最初的层次的遍历,需要一个flag来判断这次是否需要翻转,以及len参数来记录这一层的个数
代码实现
class Solution {
private:
bool flag;
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
TreeNode* p=root;
vector<vector<int>> ans;
vector<int> temp;
queue<TreeNode*> q;
q.push(p);
while(!q.empty())
{
//记录这一层的个数,因为要一层层存储才需要这个信息
int len=q.size();
while(len-->0)
{
p=q.front();
q.pop();
if(p)
{
temp.push_back(p->val);
q.push(p->left);
q.push(p->right);
}
}
if(!temp.empty())
{
if(flag)
{reverse(temp.begin(),temp.end());}
ans.push_back(temp);
}
temp.clear();
flag=!flag;
}
return ans;
}
};
反思与收获
大多数题目都是基础操作的变形,要熟练掌握常见数据结构的一些基本操作,才能根据题目变化灵活运用
107:二叉树的层次遍历 II
问题描述
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
----/ \
15 7
返回其自底向上的层次遍历为:
[
[15,7],
[9,20],
[3]
]
程序输出为:
15 7 9 20 3
解题思路
层次遍历的升级版,不能简单的想成 先按正常顺序遍历再倒置,因为同一层的元素顺序还是从左到右的。直接建立二维vector ans记录每一层的数据,之后将其倒置即可实现。
代码实现
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
TreeNode* p=root;
vector<vector<int>> ans;
vector<int> temp;
queue<TreeNode*> q;
q.push(p);
while(!q.empty())
{
//记录这一层的个数,因为要一层层存储才需要这个信息
int len=q.size();
while(len-->0)
{
p=q.front();
q.pop();
if(p)
{
temp.push_back(p->val);
q.push(p->left);
q.push(p->right);
}
}
if(!temp.empty())
{
ans.push_back(temp);
}
temp.clear();
}
//最后倒置
reverse(ans.begin(),ans.end());
return ans;
}
};
反思与收获
跟上题类似。
111:二叉树的最小深度
问题描述
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/
9 20
– /
15 7
返回它的最小深度 2.
实际做的是这题:
二叉树深度的中间值
解题思路
都是一个道理,可以通过搜索,然不断更新最深和最短的数值,从而获得想要的数据
代码实现
该代码是平均深度
class Solution {
public:
int max=INT_MIN;
int min=INT_MAX;
void recursion(TreeNode* p,int &now)
{
if(p)
{
now++;
}else
{
return;
}
if(!p->left&&!p->right)
{
//cout<<now<<endl;
if(now<min)
{
min=now;
}
if(now>max)
{
max=now;
}
}
int temp=now;
recursion(p->left,now);
recursion(p->right,temp);
}
double aveDepth(TreeNode* root) {
int now=0;
recursion(root,now);
return (double)(max+min)/2;
}
};
反思与收获
只需要计算最短最长时,用If else也很方便实现
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
else if (!root->left) return minDepth(root->right) + 1;
else if (!root->right) return minDepth(root->left) + 1;
else return min(minDepth(root->left), minDepth(root->right)) + 1;
}
}
112:路径总和
问题描述
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
解题思路
进行dfs搜索
如果当前节点为空返回
如果已经是叶子节点了,并且加起来的总和为k,则返回真,否则为假
否则 加上该节点数值,继续向左右节点搜索
代码实现
class Solution {
public:
bool recursion(TreeNode* p,int sum,int k)
{
if(!p)
{
return false;
}
if(!p->left&&!p->right)
{
return sum+p->val==k;
}
else
{
return recursion(p->left,sum+p->val,k)||recursion(p->right,sum+p->val,k);
}
}
bool hasPathSum(TreeNode* root, int sum) {
return recursion(root,0,sum);
}
};
反思与收获
树当中常常使用深度优先搜索或者宽度优点搜索
124:二叉树中的最大路径和
问题描述
题目详情
代码提交
运行结果
47 二叉树中的最大路径和
作者: Turbo时间限制: 1S章节: DS:树
晚于: 2020-08-05 12:00:00后提交分数乘系数50%
截止日期: 2020-08-12 12:00:00
问题描述 :
给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
示例 1:
输入: [1,2,3]
1
/ \
2 3
输出: 6
示例 2:
输入: [-10,9,20,null,null,15,7]
-10
/ \
9 20
--- / \
15 7
输出: 42
解题思路
这题的难点在于 这次的路径有点特殊,可以是向上走的,但这样思考的话可能就有点难想了。我们先考虑最最简单的情况,也就是例子1,肯定根节点+左右孩子两个节点。为什么呢?因为左右孩子是正的,如果是负的话就没必要加了。
那对于某个节点而言 它应该返回什么呢
–5
-1 6
– 3 1
比如这样,那对于6而言 它返回的值应该是它的值加上左右孩子中较大的值,因为对其而言除非是例子1这样取的情况(其为根节点),它都只能走一边,左或者右。
代码实现
class Solution {
private:
int ans=INT_MIN;
public:
int dfs(TreeNode* p)
{
if(!p)
{
return 0;
}
int left=max(0,dfs(p->left));
int right=max(0,dfs(p->right));
int lnr=left+right+p->val;
ans=max(ans,lnr);
int single=p->val+max(left,right);
return single;
}
int maxPathSum(TreeNode* root)
{
dfs(root);
return ans;
}
};
反思与收获
这题还是很值得思考的,函数返回的值是什么很重要,它将所求的答案和所需的参数分开,答案存在ans中单独来判断,返回的值则根据递归考虑。
问题描述
给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。
例如,从根到叶子节点路径 1->2->3 代表数字 123。
计算从根到叶子节点生成的所有数字之和。
说明: 叶子节点是指没有子节点的节点。
示例 1:
输入: [1,2,3]
1 / \ 2 3
输出: 25
解释:
从根到叶子节点路径 1->2 代表数字 12.
从根到叶子节点路径 1->3 代表数字 13.
因此,数字总和 = 12 + 13 = 25.
示例 2:
输入: [4,9,0,5,1]
4 / \ 9 0 / \ 5 1
输出: 1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495.
从根到叶子节点路径 4->9->1 代表数字 491.
从根到叶子节点路径 4->0 代表数字 40.
因此,数字总和 = 495 + 491 + 40 = 1026.
解题思路
比较典型的dfs题目,之前可能会考虑使用vector来存储路径,但是直接使用string可能方便一点,还不需要自己转换成数字,stoi就行。
代码实现
class Solution {
private:
int sum;
public:
void dfs(TreeNode* p,string &s)
{
if(!p)
{
return;
}
char c=(char)p->val+'0';
s+=c;
//如果是叶子节点
if(!p->left&&!p->right)
{
//cout<<s<<endl;
sum+=stoi(s);
}else
{
dfs(p->left,s);
dfs(p->right,s);
}
s.erase(s.length()-1,1);
}
int sumNumbers(TreeNode* root) {
string s="";
dfs(root,s);
return sum;
}
};
反思与收获
不要思维定式,有时使用string也会方便一点。使用全局变量也可,使用引用也可。
310:最小高度树
问题描述
对于一个具有树特征的无向图,我们可选择任何一个节点作为根。图因此可以成为树,在所有可能的树中,具有最小高度的树被称为最小高度树。给出这样的一个图,写出一个函数找到所有的最小高度树并返回他们的根节点。
该图包含 n 个节点,标记为 0 到 n - 1。给定数字 n 和一个无向边 edges 列表(每一个边都是一对标签)。
你可以假设没有重复的边会出现在 edges 中。由于所有的边都是无向边, [0, 1]和 [1, 0] 是相同的,因此不会同时出现在 edges 里。
示例 1:
输入: n = 4, edges = [[1, 0], [1, 2], [1, 3]]
0 | 1 / \ 2 3
输出: [1]
示例 2:
输入: n = 6, edges = [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]]
0 1 2 \ | / 3 | 4 | 5
输出: [3, 4]
说明:
根据树的定义,树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
树的高度是指根节点和叶子节点之间最长向下路径上边的数量。
解题思路
相当于怎么找到最中间的那一个点,那我们就从外面一层层往里面走,最外面的肯定是度为1的点,将这一圈度为1的点拨出,并将与其相连的那个点度也-1,如果-1后度为1,则说明该点被暴露在最外面了,于是将其塞入队列。
最后留下的就是答案
代码实现
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
if(n==1)
{
return {0};
}
vector<int> degree(n);
//邻接表
map<int,vector<int>> graph;
vector<int> ans;
int a,b;
for(int i=0;i<edges.size();i++)
{
a=edges[i][0];
b=edges[i][1];
degree[a]++;
degree[b]++;
graph[a].push_back(b);
graph[b].push_back(a);
}
queue<int> q;
//叶子节点进队
for(int i=0;i<n;i++)
{
if(degree[i]==1)
{
q.push(i);
}
}
while(!q.empty())
{
ans.clear();
//这外面一层的个数
int levelnum=q.size();
//将这一层剥离
for(int i=0;i<levelnum;i++)
{
int t=q.front();
q.pop();
degree[t]--;
ans.push_back(t);
//把跟t相连的内一层的都加入
for(auto j:graph[t])
{
//跟t相连的这个度
degree[j]--;
if(degree[j]==1)
{
q.push(j);
}
}
}
}
return ans;
}
};
反思与收获
在解决这些问题的时候可能一下子没有头绪,那我们就要从图的特征出发,度,入度,出度,环,邻接表等等。这题就是考虑了度。
也可以找找特征,这些答案都是度不为1的,度为1都是最外圈,也许就能想到解决的办法了。
437:路径总和 III
问题描述
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
示例:
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8
10 / \ 5 -3 / \ \ 3 2 11 / \ \ 3 -2 1
返回 3。和等于 8 的路径有:
5 -> 3
5 -> 2 -> 1
-3 -> 11
解题思路
这题跟112路径总和类似,不同点在于该路径无需从根节点出发,可以只是一小段。
直接暴力用最笨的方法,在每一个节点处都进行以其为起始的dfs搜索,记录全局变量ans
(本来是想记录每一条路径的数值和,然后根据大小关系,考虑是否将最上面的节点弹出,但因为有负数存在,有点麻烦,直接做方便很多也不慢)
代码实现
class Solution {
private:
int ans;
public:
void recursion(TreeNode* p,int sum,int k)
{
if(!p)
{
return;
}
sum+=p->val;
if(sum==k)
{
ans++;
}
recursion(p->left,sum,k);
recursion(p->right,sum,k);
}
int pathSum(TreeNode* root, int sum) {
if(!root)
{
return ans;
}
recursion(root,0,sum);
pathSum(root->left,sum);
pathSum(root->right,sum);
return ans;
}
};
反思与收获
有时候直接暴力解决或者笨方法也可以解决,每个节点都开始dfs寻找,就可以实现这种从任意节点出发的路径搜索
743:网络延迟时间
问题描述
有 N 个网络节点,标记为 1 到 N。
给定一个列表 times,表示信号经过有向边的传递时间。 times[i] = (u, v, w),其中 u 是源节点,v 是目标节点, w 是一个信号从源节点传递到目标节点的时间。
现在,我们从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1。
示例:
输入:times = [[2,1,1],[2,3,1],[3,4,1]], N = 4, K = 2
输出:2
解题思路
图论里面比较经典的问题,从源头到所有节点的最短路径,也有一个经典的算法Dijkstra,之前都学习到过的,但是代码实现好像挺少的。该方法就是不断更新的过程,解题的时候会画一个表格,逐步更新,代码实现也是一样。
代码实现
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
//dijkstra算法
int networkDelayTime(vector<vector<int>>& times, int N, int K) {
//相加才不会溢出
int maxval=1e9;
//初始化 都无边
vector<vector<int>> graph(N+1,vector<int>(N+1,maxval));
for(int i=1;i<=N;i++)
{
graph[i][i]=0;
}
int a,b,time;
for(int i=0;i<times.size();i++)
{
a=times[i][0];
b=times[i][1];
time=times[i][2];
graph[a][b]=time;
}
vector<bool> visited(N+1,false);
visited[K]=true;
int uk=K;
while(1)
{
for(int i=1;i<=N;i++)
{
//有边相连且没有访问过
if(graph[uk][i]!=maxval&&!visited[i])
{
if(graph[K][i]>graph[K][uk]+graph[uk][i])
{
graph[K][i]=graph[K][uk]+graph[uk][i];
}
}
}
for(int ek=INT_MAX,i=1;i<=N;i++)
{
if(!visited[i]&&ek>graph[K][i])
{
ek=graph[K][i];
uk=i;
}
}
if(visited[uk]) break;
visited[uk]=true;
}
int ans=0;
for(int i=1;i<=N;i++)
{
if(graph[K][i]==maxval)
{
return -1;
}
ans=max(ans,graph[K][i]);
}
return ans;
}
};
反思与收获
学习复习了Dijkstra算法以及具体的代码实现,用1e9表示无穷大并且避免了两者相加的时候超出INT的范围。
997:找到小镇的法官
问题描述
在一个小镇里,按从 1 到 N 标记了 N 个人。传言称,这些人中有一个是小镇上的秘密法官。
如果小镇的法官真的存在,那么:
小镇的法官不相信任何人。
每个人(除了小镇法官外)都信任小镇的法官。
只有一个人同时满足属性 1 和属性 2 。
给定数组 trust,该数组由信任对 trust[i] = [a, b] 组成,表示标记为 a 的人信任标记为 b 的人。
如果小镇存在秘密法官并且可以确定他的身份,请返回该法官的标记。否则,返回 -1。
示例 1:
输入:N = 2, trust = [[1,2]]
输出:2
示例 2:
输入:N = 3, trust = [[1,3],[2,3]]
输出:3
示例 3:
输入:N = 3, trust = [[1,3],[2,3],[3,1]]
输出:-1
示例 4:
输入:N = 3, trust = [[1,2],[2,3]]
输出:-1
示例 5:
输入:N = 4, trust = [[1,3],[1,4],[2,3],[2,4],[4,3]]
输出:3
解题思路
算是有趣的应用题了,很明显建立图的模型,如果信任,作为u->v之间有一条边,法官不相信任何人,因此法官这一行应该是全为0,所有人都相信法官,因此法官这一列除了他自己应该是全为0
代码实现
class Solution {
public:
int findJudge(int N, vector<vector<int>>& trust) {
vector<vector<int>> graph(N,vector<int>(N));
int r,c;
for(int i=0;i<trust.size();i++)
{
r=trust[i][0]-1;
c=trust[i][1]-1;
graph[r][c]=1;
}
for(int j=0;j<N;j++)
{
//这一列除了中心以外得都是1
bool flag=true;
for(int i=0;i<N&&flag;i++)
{
if(i==j)
{
if(graph[i][j])
{
flag=false;
}
}
else if(!graph[i][j])
{
flag=false;
}
}
//这一行得都是0
for(int i=0;i<N;i++)
{
if(graph[j][i])
{
flag=false;
}
}
if(flag)
{
return j+1;
}
}
return -1;
}
};
反思与收获
找到合适的模型解决问题。
————————————————————————————————
图和树很多的题目,都是基础操作的升级版或者是综合版,要熟悉掌握树和图非常常见的基础操作,比如层次遍历,dfs等等。并且要牢记树和图的特征,比如子树之间的关系,图的入度出度,掌握特征,有时解题可以考虑特征入手,喜欢(ノ゚▽゚)ノ。