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 的路径有:

  1. 5 -> 3

  2. 5 -> 2 -> 1

  3. -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等等。并且要牢记树和图的特征,比如子树之间的关系,图的入度出度,掌握特征,有时解题可以考虑特征入手,喜欢(ノ゚▽゚)ノ。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值