zoukankan      html  css  js  c++  java
  • 图的遍历

    记录遍历状态

    对于图结构来说,图的遍历和树的遍历有类似之处,树结构的遍历从根结点出发,图结构的遍历从某一结点出发。出发之后,按照某种手法无重复地访问所有的结点,这也是后续解决图的连通性、拓扑排序和关键路径的预备知识。
    由于在图结构中,任意顶点都有可能与其他顶点相互邻接,因此如果没有对已走过的路径进行记录的话,很有可能会由于结点的重复访问而无法遍历所有顶点。因此我们需要一种手法记录访问过的顶点,一种直接而有效的手法是使用一个 visited[n] 数组,先将其每一个元素初始化为 0,当我访问了第 i 个顶点时,就将 visited[i] 的值赋值为 1,表示已经访问过。当我访问某一个顶点时,可以通过 visited 数组来确定我接下来是否要从这个顶点往下走。

    DFS

    深度优先搜索

    深度优先搜索( Depth First Serarch )我们之前是接触过的,在迷宫问题(栈实现)和树结构的递归遍历法中,我们用其思想实现了一些功能,现在我们来详细谈一谈。所谓 DFS,我称之为视角放在路径的手法,思想是通过对某一条路径的顶点的挖掘,从而试探出一条可行的路径。当我使用递归或者通过修饰后的栈结构,可以实现回溯的效果,以获取全部的路径。

    算法流程

    对于一个连通图,DFS 的遍历过程为:

    1. 选择图中的某个顶点出发,并访问该顶点;
    2. 找出刚访问过的顶点的第一个未被访问的邻接点并访问;
    3. 以该顶点为新顶点,步骤 2 直至刚访问过的顶点没有未被访问的邻接点;
    4. 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,并访问该顶点;
    5. 重复上述 2、3、4 步骤直至所有顶点访问完毕。

    模拟遍历

    例如上图的连通图,我们选择顶点 A 出发,访问该顶点:

    选取 A 的邻接点 B,访问该结点:

    选取 B 的邻接点 D,访问该结点:

    选取 D 的邻接点 H,访问该结点:

    选取 H 的邻接点 E,访问该结点:

    由于 E 顶点的所有邻接点都遍历完了,因此需要回溯。回溯需要 4 次回到顶点 A,并访问下一个邻接点 C:

    选取 C 的邻接点 F,访问该结点:

    选取 F 的邻接点 G,访问该结点:

    遍历完毕,顺序为 A->B->D->H->E->C->F->G。其深度优先生成树为:

    代码实现

    由于 DFS 需要涉及到回溯问题,因此我们想到使用递归来实现。

    邻接矩阵 DFS

    int visited[MAXV] = { 0 };
    void DFS(MGraph g, int v)    //v 表示当前所在的顶点
    {
        cout << v << " ";    //输出顶点
        visited[v] = 1;    //标记已访问
        for (int i = 1; i <= g.n; i++)    //使用循环控制回溯
        {
    	if (g.edges[v][i] == 1 && visited[i] == 0)
    	{
    	    DFS(g, i);    //当前顶点与 i 顶点邻接且未被访问,递归搜索
    	}
        }
    }
    

    邻接表 DFS

    int visited[MAXV] = { 0 };
    void DFS(AdjGraph* G, int v)    //从 v 顶点开始深度遍历
    {
        ArcNode* ptr;
    
        cout << v << " ";    //输出顶点
        visited[v] = 1;    //标记已访问
        ptr = G->adjlist[v].firstarc;
        while (ptr)    //沿着邻接表搜索路径
        {
    	if (visited[ptr->adjvex] == 0)
    	{
    	    DFS(G, ptr->adjvex);    //当前顶点与 i 顶点邻接且未被访问,递归搜索
    	}
    	ptr = ptr->nextarc;    //继续访问邻接表
        }
    }
    

    时间复杂度

    当我们遍历一个连通图时,对图中每个顶点至多调用一次 DFS 函数,并且当这个顶点被访问过之后,由于被标志成已被访问,就不再从它出发进行搜索。因此,遍历图的过程实质上是对每个顶点查找其邻接点的过程,其时间复杂度则取决于使用的存储结构。当用邻接矩阵描述图结构时,查找每个顶点的邻接点的时间复杂度为 O(n2),其中 n 为图结构中的顶点数。当以邻接表做图的存储结构时,查找邻接点的时间复杂度为 O(e),e 为图结构的边数。由此当以邻接表做存储结构时,深度优先搜索遍历图的时间复杂度为 O(n + e)

    BFS

    广度优先搜索

    广度优先搜索( Breadth First Serarch )我们之前也有接触过,在迷宫问题(队列实现)和树结构的层序遍历法中,我们用其思想实现了一些功能,现在我们来详细谈一谈。所谓 BFS,我称之为视角放在整个图结构的手法,思想是通过从某个顶点向外扩散,从而囊括所有顶点的手法。当我借助队列结构时,可以实现该算法。

    算法流程

    对于一个连通图,DFS 的遍历过程为:

    1. 选择图中的某个顶点出发,并访问该顶点;
    2. 依次访问 v 的每个未曾访问过的邻接点 i;
    3. 分别从这些邻接点出发,依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问;
    4. 重复步骤 3,直至图中所有己被访问的顶点的邻接点全部访问到。

    模拟遍历

    例如上图的连通图,我们选择顶点 A 出发,访问该顶点:

    访问顶点 A 的所有邻接点:

    访问顶点 B 的所有邻接点:

    访问顶点 C 的所有邻接点:

    访问顶点 D 的所有邻接点:

    接着访问剩下的顶点,由于所有的顶点都被访问过了,因此没有新顶点入队列。遍历顺序为:A->B->C->D->E->F->G,广度优先生成树为:

    代码实现

    由于 BFS 需要涉及到回溯问题,因此我们想到使用递归来实现。
    广度优先搜索遍历图结构其实和树结构的层序遍历是一个玩意,尽可能先对横向进行搜索。设 x 和 y 是两个相继被访问过的顶点,若当前是以 X 为出发点进行搜索,则在访问 x 的所有未曾被访问过的邻接点之后,紧接着是以 y 为出发点进行横向搜索,并对搜索到的 y 的邻接点中尚未被访问的顶点进行访问。也就是说,先访问的顶点其邻接点亦先被访问,因此我们需要一个队列来辅助实现算法。

    邻接矩阵 BFS

    void BFS(MGraph g, int v)
    {
        int front, rear;    //头指针与尾指针
        int point[MAXV];    //构造队列结构(可以是非循环队列)
    
        front = 0;
        point[front] = v;    //顶点 v 入队列
        rear = visited[v] = 1;    //标记顶点已访问
        while (front != rear)    //队列不为空,搜索继续
        {
    	for (int i = 1; i <= g.n; i++)    //遍历表头顶点的邻接点
    	{
    	    if (g.edges[point[front]][i] == 1 && visited[i] == 0)
    	    {
    	        point[rear++] = i;    //顶点 i 入队列
    		visited[i] = 1;    //标记顶点已访问
    	    }
            }
    	cout << point[front++] << " ";    //输出顶点
    }
    

    邻接表 BFS

    void BFS(AdjGraph* G, int v)    //顶点 v 开始广度遍历
    {
        int front, rear;    //头指针与尾指针
        int a_que[MAXV];   //构造队列结构(可以是非循环队列
        ArcNode* ptr;
    
        front = 0;
        a_que[front] = v;    //顶点 v 入队列
        visited[v] = rear = 1;    //标记顶点已访问
        while (front != rear)    //队列不为空,搜索继续
        {
            ptr = G->adjlist[a_que[front]].firstarc;
    	while (ptr)    //遍历表头顶点的邻接点
    	{
    	    if (visited[ptr->adjvex] == 0)
    	    {
    	        a_que[rear++] = ptr->adjvex;    //顶点 i 入队列
    		visited[ptr->adjvex] = 1;    //标记顶点已访问
    	    }
    	    ptr = ptr->nextarc;
    	}
    	cout << point[front++] << " ";    //输出顶点
        }
    }
    

    时间复杂度

    对于 BFS,每个顶点至多进一次队列,因此遍历图的过程实质上是通过边找邻接点的过程。因此广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,两种遍历方法的不同之处仅仅在于对顶点访问的顺序不同。

    实例:六度空间

    情景需求

    输入样例

    10 9
    1 2
    2 3
    3 4
    4 5
    5 6
    6 7
    7 8
    8 9
    9 10
    

    输出样例

    1: 70.00%
    2: 80.00%
    3: 90.00%
    4: 100.00%
    5: 100.00%
    6: 100.00%
    7: 100.00%
    8: 90.00%
    9: 80.00%
    10: 70.00%
    

    情景解析

    这道题表面上看好像不好懂,但在本质上是一个有限的图结构遍历,即我们可能在遍历到某些顶点的时候就需要提前结束了。基于这一点,我们反应过来用 BFS 做比较方便一点,因为 BFS 的流程可以抽象为一层一层地往外探测,这与我们的思路是符合的。
    那么接下来我们读题,根据题意,我们需要找到关系网在 6 层以内的结点。那也就是说,我们需要去记录某个顶点,它对于初始顶点来说是第几层的关系网。为了方便理解,我展示的做法是去修改记录结点信息的数组的结构体为:

    typedef struct
    {
    	int level;    //表示结点相对目标顶点的距离
    	int v;
    }AVertex;
    

    即多开一个成员来记录层次的信息。那么层次的信息怎么操作?首先我们先初始化为 0,接下来每进行一轮 BFS,添加入队列的顶点就从它的上一层继承层数,可以用这行代码实现:

    a_que[rear].level = a_que[front].level + 1;
    

    解决了层次的确定问题,剩下的就是我们喜闻乐见的建图和遍历的事情啦。

    伪代码

    代码实现

    int getCount(AdjGraph* G, int v)
    {
        int visited[MAXV] = { 0 };
        space a_que[MAXV] = { 0 };
        int front, rear;
        int count = 1;
        ArcNode* ptr;
    
        front = rear = 0;
        a_que[rear].v = v;    //目标顶点 v 入队列
        a_que[rear++].level = 0;    //初始化层数为 0
        visited[v] = 1;
        while (front != rear)
        {
    	if (a_que[front].level == 6)
    	{
    	    break;    //表头顶点在第 6 层,结束 BFS
    	}
    	ptr = G->adjlist[a_que[front].v].firstarc;
    	while (ptr)
    	{
    	    if (visited[ptr->adjvex] == 0)
    	    {
    	        a_que[rear].v = ptr->adjvex;
    		a_que[rear++].level = a_que[front].level + 1;    //继承上一个顶点的层数
    		visited[ptr->adjvex] = 1;
    		count++;
    	    }
    	    ptr = ptr->nextarc;
            }
    	front++;
        }
        return count;
    }
    

    扩充资料

    六度空间理论(数学领域的猜想)

    实例:判断 DFS 序列合法性

    左转我另一篇博客——PTA习题解析——判断DFS序列的合法性

    参考资料

    《大话数据结构》—— 程杰 著,清华大学出版社
    《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社

  • 相关阅读:
    86. Partition List
    328. Odd Even Linked List
    19. Remove Nth Node From End of List(移除倒数第N的结点, 快慢指针)
    24. Swap Nodes in Pairs
    2. Add Two Numbers(2个链表相加)
    92. Reverse Linked List II(链表部分反转)
    109. Convert Sorted List to Binary Search Tree
    138. Copy List with Random Pointer
    为Unity的新版ugui的Prefab生成预览图
    ArcEngine生成矩形缓冲区
  • 原文地址:https://www.cnblogs.com/linfangnan/p/12784323.html
Copyright © 2011-2022 走看看