图的表示 与 图的遍历(DFS 和 BFS)

链表、树与图的关系: 

        链表是特殊化的树,树是特殊化的图。 

一、图的存储

       图的存储有三种方式: 邻接矩阵、出边数组 和 邻接表。

       

上图中三种表示方式的介绍如下:

1.邻接矩阵: 

        矩阵中的值为1, 代表 存在 行坐标 到 列坐标 的边。

        如果有权重值的话,可以将1设置成相应的权重值。

        这种表示方法的缺点是边数相对顶点较少的图,存在对存储空间的浪费。

2.出边数组:

        数组[1,2]  [1,3] 分别代表这存在  结点1 到 结点2 的边结点1 到 结点3 的边。一个图就使用多个数组表示完成了。

        如果有权重值的话,可以使用一个Node类来表示,Node中有两个属性,一个是表示结点,另一个值表示权重值。(比如:Node{ int nextnode, int weight} ),使用Node对象的数组来表示带权重的图。

        出边数组是最方便使用的表示方式,也是最常用的。

3.邻接表:

        使用数组和链表相结合的存储方式,构造一个数据类型,存放结点的 值 和 下一个结点的地址,这种表示方法也是常用的图的表示方法。数据类型如下:

struct Node
{
    int tou;
    Node* next;
};

Node* head[MAX_N];

         head数组存放,所有的邻接表的头结点地址。对于每个结点的连接点,不断地在头结点后面进行连接。

二、图的遍历

深度优先遍历      划分连通块

广度优先遍历       拓扑排序

图的深度优先遍历如图:

 使用一个bool类型的visited数组,对结点是否访问做标记,防止重复访问。

 示意图是一种可能的遍历方式,在走到第5步的时候,会从结点B 回溯到 结点E ,再回溯到结点G。因为有一个visited数组来标记是否访问,访问过的就不再重复访问。

 图的广度优先遍历如图:

 广度优先遍历就是层序遍历,需要额外使用一个 队列 进行遍历的操作。

三、代码示例:

以下两个题目,分别是无向图找环 和 有向图找环  的类型。

1. DFS 深度优先遍历(无向图可以看做 两个双向边的有向图)

Leetcode 684题

在本问题中, 树指的是一个连通且无环的无向图。

输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。

返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。

示例1:

输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
  1
 / \
2 - 3

示例2:

输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
    |   |
    4 - 3

代码如下:

class Solution {
public:
    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        //出现过的最大的点就是n
        for(vector<int>& e:edges)
        {
            int u=e[0],v = e[1];
            n = max(n,u);
            n = max(n,v);
        }
        //模板:出边数组初始化
        edge = vector<vector<int>>(n+1,vector<int>());
        visit = vector<bool>(n+1,false);
        hasCycle = false;
        //加边
        for(vector<int>& e:edges)
        {
            int u = e[0],v = e[1];
            //无向图看作双向边的有向图
            addEdge(u,v);
            addEdge(v,u);
            
            dfs(u,0);
            
            if(hasCycle)
            	return e;
        }
        return {};
    }

private:
    //图的深度优先遍历
    //visit数组,避免重复访问
    //fa是第一次走到x的点,比如示例1中的1 --> 2
    void dfs(int x,int fa)
    {
        //第一步:标记已访问
        visit[x] = true;
        //第二步:遍历所有出边
        for(int y:edge[x])
        {
            if(y==fa) continue; //返回父亲,不是环;避免s->a,a->s的情况
            if(!visit[y]) dfs(y,x);
            else hasCycle = true;
        }
        visit[x]=false;
    }
    //先完成x,再完成y,就形成了x->y的有向边
    void addEdge(int x, int y )
    {
        edge[x].push_back(y);
    }

    int n;
    vector<vector<int>> edge;
    vector<bool> visit;
    bool hasCycle;
};

2.BFS广度优先遍历

Leetcode 207题

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

代码如下:

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        n = numCourses;
        
        //出边数组初始化
        edge = vector<vector<int>>(n,vector<int>());
        inDeg = vector<int>(n,0);

        for(vector<int>& pre : prerequisites)
        {
            int ai = pre[0];
            int bi = pre[1];
            //加边模板
            //因为学bi前要先学ai,就形成了ai到bi的边
            addEdge(bi,ai);
        }
        //拓扑排序
        //学过的课程数等于n,则说明能完成所有课程的学习
        return topsort() == n;
    }
private:
    
    //假如x的连接的边是y,则出边数组的[x,y]
    void addEdge(int x, int y)
    {
        edge[x].push_back(y);
        //x-->y,则y的入度是1
        inDeg[y]++;
    }

    //有向图找环  模板 (拓扑排序)
    //返回学的课程数
    //拓扑排序
    int topsort()
    {
        int learned = 0;
        //拓扑排序基于BFS,需要队列
        queue<int> q;
        //从所有零入度点出发
        for(int i=0 ; i<n ;i++)
            if(inDeg[i] == 0) q.push(i);
        
        //执行BFS
        while(!q.empty())
        {
            int x = q.front();//取队头,这门课学过了
            q.pop();
            learned++;

            //考察x的所有出边(去掉约束关系)
            for(int y : edge[x])
            {
                inDeg[y]--; //去掉约束关系
                if(inDeg[y] == 0)
                {
                    q.push(y);
                }
            }
        }
        //cout<<"learned:"<<learned<<endl;
        return learned;
    }

    int n;
    vector<vector<int>> edge;
    vector<int> inDeg; //入度
};

四、DFS与BFS对比

DFS更适合搜索树形状态空间:

递归本身就会产生树的结构

可以用一个全局变量维护状态中较为复杂的信息(例如:子集方案,排列方案)

不需要队列,节省空间

BFS适合求“最小代价”,“最小步数”的题目

BFS是按层次序搜索,第K步搜完才会搜K+1步,在任意时刻队列中至多只有两层

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值