C#与数据结构--图的遍历

C#与数据结构--图的遍历

8.2 图的存储结构


图 的存储结构除了要存储图中各个顶点的本身的信息外,同时还要存储顶点与顶点之间的所有关系(边的信息),因此,图的结构比较复杂,很难以数据元素在存储区
中的物理位置来表示元素之间的关系,但也正是由于其任意的特性,故物理表示方法很多。常用的图的存储结构有邻接矩阵、邻接表、十字链表和邻接多重表。

8.2.1  邻接矩阵表示法

对于一个具有n个顶点的图,可以使用n*n的矩阵(二维数组)来表示它们间的邻接关系。图8.10和图8.11中,矩阵A(i,j)=1表示图中存在一条边(Vi,Vj),而A(i,j)=0表示图中不存在边(Vi,Vj)。实际编程时,当图为不带权图时,可以在二维数组中存放bool值,A(i,j)=true表示存在边(Vi,Vj),A(i,j)=false表示不存在边(Vi,Vj);当图带权值时,则可以直接在二维数组中存放权值,A(i,j)=null表示不存在边(Vi,Vj)。

图8.10所示的是无向图的邻接矩阵表示法,可以观察到,矩阵延对角线对称,即A(i,j)=
A(j,i)。无向图邻接矩阵的第i行或第i列非零元素的个数其实就是第i个顶点的度。这表示无向图邻接矩阵存在一定的数据冗余。

图8.11所示的是有向图邻接矩阵表示法,矩阵并不延对角线对称,A(i,j)=1表示顶点Vi邻接到顶点Vj;A(j,i)=1则表示顶点Vi邻接自顶点Vj。两者并不象无向图邻接矩阵那样表示相同的意思。有向图邻接矩阵的第i行非零元素的个数其实就是第i个顶点的出度,而第i列非零元素的个数是第i个顶点的入度,即第i个顶点的度是第i行和第i列非零元素个数之和。

由于存在n个顶点的图需要n2个数组元素进行存储,当图为稀疏图时,使用邻接矩阵存储方法将出现大量零元素,照成极大地空间浪费,这时应该使用邻接表表示法存储图中的数据。

8.2.2 邻接表表示法

图 的邻接矩阵存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。邻接表由表头结点和表结点两部分组成,其中图中每个顶点均对应
一个存储在数组中的表头结点。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。如图8.12所示,表结点存放的是邻接顶点在数组中的索引。对于无向图来说,使用邻接表进行存储也会出现数据冗余,表头结点A所指链表中存在一个指向C的表结点的同时,表头结点C所指链表也会存在一个指向A的表结点。

有向图的邻接表有出边表和入边表(又称逆邻接表)之分。出边表的表结点存放的是从表头结点出发的有向边所指的尾顶点;入边表的表结点存放的则是指向表头结点的某个头顶点。如图8.13所示,图(b)和(c)分别为有向图(a)的出边表和入边表。

以上所讨论的邻接表所表示的都是不带权的图,如果要表示带权图,可以在表结点中增加一个存放权的字段,其效果如图8.14所示。

【注意】:观察图8.14可
以发现,当删除存储表头结点的数组中的某一元素,有可能使部分表头结点索引号的改变,从而导致大面积修改表结点的情况发生。可以在表结点中直接存放指向表
头结点的指针以解决这个问题(在链表中存放类实例即是存放指针,但必须要保证表头结点是类而不是结构体)。在实际创建邻接表时,甚至可以使用链表代替数组
存放表头结点或使用顺序表存代替链表存放表结点。对所学的数据结构知识应当根据实际情况及所使用语言的特点灵活应用,切不可生搬硬套。

【例8-1  AdjacencyList.cs】图的邻接表存储结构

using System;
using System.Collections.Generic;
public class AdjacencyList<T>
{
    List<Vertex<T>> items; //图的顶点集合
    public AdjacencyList() : this(10) { } //构造方法
    public AdjacencyList(int capacity) //指定容量的构造方法
    {
        items = new List<Vertex<T>>(capacity);
    }
    public void AddVertex(T item) //添加一个顶点
    {   //不允许插入重复值
        if (Contains(item))
        {
            throw new ArgumentException("插入了重复顶点!");
        }
        items.Add(new Vertex<T>(item));
    }
    public void AddEdge(T from, T to) //添加无向边
    {
        Vertex<T> fromVer = Find(from); //找到起始顶点
        if (fromVer == null)
        {
            throw new ArgumentException("头顶点并不存在!");
        }
        Vertex<T> toVer = Find(to); //找到结束顶点
        if (toVer == null)
        {
            throw new ArgumentException("尾顶点并不存在!");
        }
        //无向边的两个顶点都需记录边信息
        AddDirectedEdge(fromVer, toVer);
        AddDirectedEdge(toVer, fromVer);
    }
    public bool Contains(T item) //查找图中是否包含某项
    {
        foreach (Vertex<T> v in items)
        {
            if (v.data.Equals(item))
            {
                return true;
            }
        }
        return false;
    }
    private Vertex<T> Find(T item) //查找指定项并返回
    {
        foreach (Vertex<T> v in items)
        {
            if (v.data.Equals(item))
            {
                return v;
            }
        }
        return null;
    }
    //添加有向边
    private void AddDirectedEdge(Vertex<T> fromVer, Vertex<T> toVer)
    {
        if (fromVer.firstEdge == null) //无邻接点时
        {
            fromVer.firstEdge = new Node(toVer);
        }
        else
        {
            Node tmp, node = fromVer.firstEdge;
            do
            {   //检查是否添加了重复边
                if (node.adjvex.data.Equals(toVer.data))
                {
                    throw new ArgumentException("添加了重复的边!");
                }
                tmp = node;
                node = node.next;
            } while (node != null);
            tmp.next = new Node(toVer); //添加到链表未尾
        }
    }
    public override string ToString() //仅用于测试
    {   //打印每个节点和它的邻接点
        string s = string.Empty;
        foreach (Vertex<T> v in items)
        {
            s += v.data.ToString() + ":";
            if (v.firstEdge != null)
            {
                Node tmp = v.firstEdge;
                while (tmp != null)
                {
                    s += tmp.adjvex.data.ToString();
                    tmp = tmp.next;
                }
            }
            s += "\r\n";
        }
        return s;
    }
    //嵌套类,表示链表中的表结点
    public class Node
    {
        public Vertex<T> adjvex; //邻接点域
        public Node next; //下一个邻接点指针域
        public Node(Vertex<T> value)
        {
            adjvex = value;
        }
    }
    //嵌套类,表示存放于数组中的表头结点
    public class Vertex<TValue>
    {
        public TValue data; //数据
        public Node firstEdge; //邻接点链表头指针
        public Boolean visited; //访问标志,遍历时使用
        public Vertex(TValue value) //构造方法
        {
            data = value;
        }
    }
}

AdjacencyList<T>类使用泛型实现了图的邻接表存储结构。它包含两个内部类,Vertex<Tvalue>类(109~118行代码)用于表示一个表头结点,Node类(99~107)则用于表示表结点,其中存放着邻接点信息,用来表示表头结点的某条边。多个Node用next指针相连形成一个单链表,表头指针为Vertex类的firstEdge成员,表头结点所代表的顶点的所有边的信息均包含在链表内,其结构如图8.12所示。所不同之处在于:

l        
Vertex类中包含了一个visited成员,它的作用是在图遍历时标识当前节点是否被访问过,这一点在稍后会讲到。

l        
邻接点指针域adjvex直接指向某个表头结点,而不是表头结点在数组中的索引。

AdjacencyList<T>类中使用了一个泛型List代替数组来保存表头结点信息(第5行代码),从而不再考虑数组存储空间不够的情况发生,简化了操作。

由于一条无向边的信息需要在边的两个顶点分别存储信息,即添加两个有向边,所以58~78行代码的私有方法AddDirectedEdge()方法用于添加一个有向边。新的邻接点信息即可以添加到链表的头部也可以添加到尾部,添加到链表头部可以简化操作,但考虑到要检查是否添加了重复边,需要遍历整个链表,所以最终把邻接点信息添加到链表尾部。

【例8-1  Demo8-1.cs】图的邻接表存储结构测试

using System;
class Demo8_1
{
    static void Main(string[] args)
    {
        AdjacencyList<char> a = new AdjacencyList<char>();
        //添加顶点
        a.AddVertex(‘A‘);
        a.AddVertex(‘B‘);
        a.AddVertex(‘C‘);
        a.AddVertex(‘D‘);
        //添加边
        a.AddEdge(‘A‘, ‘B‘);
        a.AddEdge(‘A‘, ‘C‘);
        a.AddEdge(‘A‘, ‘D‘);
        a.AddEdge(‘B‘, ‘D‘);
        Console.WriteLine(a.ToString());
    }
}
运行结果:

A:BCD

B:AD

C:A

D:AB

本例存储的表如图8.12所示,结果中,冒号前面的是表头结点,冒号后面的是链表中的表结点。

8.3 图的遍历

和树的遍历类似,在此,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历(TraversingGraph)。如果只访问图的顶点而不关注边的信息,那么图的遍历十分简单,使用一个foreach语句遍历存放顶点信息的数组即可。但如果为了实现特定算法,就需要根据边的信息按照一定顺序进行遍历。图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。

图的遍历要比树的遍历复杂得多,由于图的任一顶点都可能和其余顶点相邻接,故在访问了某顶点之后,可能顺着某条边又访问到了已访问过的顶点,因此,在图的遍历过程中,必须记下每个访问过的顶点,以免同一个顶点被访问多次。为此给顶点附设访问标志visited,其初值为false,一旦某个顶点被访问,则其visited标志置为true。

图的遍历方法有两种:一种是深度优先搜索遍历(Depth-First Search 简称DFS);另一种是广度优先搜索遍历(Breadth_First
Search 简称BFS)。

8.3.1  深度优先搜索遍历

图的深度优先搜索遍历类似于二叉树的深度优先搜索遍历。其基本思想如下:假定以图中某个顶点Vi为出发点,首先访问出发点,然后选择一个Vi的未访问过的邻接点Vj,以Vj为新的出发点继续进行深度优先搜索,直至图中所有顶点都被访问过。显然,这是一个递归的搜索过程。

现以图8.15为例说明深度优先搜索过程。假定V1是出发点,首先访问V1。因V1有两个邻接点V2、V3均末被访问过,可以选择V2作为新的出发点,访问V2之后,再找V2的末访问过的邻接点。同V2邻接的有V1、V4和V5,其中V1已被访问过,而V4、V5尚未被访问过,可以选择V4作为新的出发点。重复上述搜索过程,继续依次访问V8、V5
。访问V5之后,由于与V5相邻的顶点均已被访问过,搜索退回到V8,访问V8的另一个邻接点V6。接下来依次访问V3和V7,最后得到的的顶点的访问序列为:V1
→ V2 → V4 → V8 → V5 → V6
→ V3 → V7

下面根据上一节创建的邻接表存储结构添加深度优先搜索遍历代码。

【例8-2  DFSTraverse.cs】深度优先搜索遍历

打开【例8-1 
AdjacencyList.cs】,在AdjacencyList<T>类中添加以下代码后,将文件另存为DFSTraverse.cs。

35      public void DFSTraverse() //深度优先遍历
36      {
37          InitVisited(); //将visited标志全部置为false
38          DFS(items[0]); //从第一个顶点开始遍历
39      }
40      private void DFS(Vertex<T> v) //使用递归进行深度优先遍历
41      {
42          v.visited = true; //将访问标志设为true
43          Console.Write(v.data + " "); //访问
44          Node node = v.firstEdge;
45          while (node != null) //访问此顶点的所有邻接点
46          {   //如果邻接点未被访问,则递归访问它的边
47              if (!node.adjvex.visited)
48              {
49                  DFS(node.adjvex); //递归
50              }
51              node = node.next; //访问下一个邻接点
52          }
53      }

98      private void InitVisited() //初始化visited标志
99      {
100         foreach (Vertex<T> v in items)
101         {
102             v.visited = false; //全部置为false
103         }
104     }

【例8-2  Demo8-2.cs】深度优先搜索遍历测试

using System;
class Demo8_2
{
    static void Main(string[] args)
    {
        AdjacencyList<string> a = new AdjacencyList<string>();
        a.AddVertex("V1");
        a.AddVertex("V2");
        a.AddVertex("V3");
        a.AddVertex("V4");
        a.AddVertex("V5");
        a.AddVertex("V6");
        a.AddVertex("V7");
        a.AddVertex("V8");
        a.AddEdge("V1", "V2");
        a.AddEdge("V1", "V3");
        a.AddEdge("V2", "V4");
        a.AddEdge("V2", "V5");
        a.AddEdge("V3", "V6");
        a.AddEdge("V3", "V7");
        a.AddEdge("V4", "V8");
        a.AddEdge("V5", "V8");
        a.AddEdge("V6", "V8");
        a.AddEdge("V7", "V8");
        a.DFSTraverse();
    }
}

运行结果:

V1 V2 V4 V8 V5 V6 V3 V7

本例参照图8-15进行设计,运行过程请参照对图8-15所作的分析。

8.3.2  广度优先搜索遍历

图的广度优先搜索遍历算法是一个分层遍历的过程,和二叉树的广度优先搜索遍历类同。它从图的某一顶点Vi出发,访问此顶点后,依次访问Vi的各个未曾访问过的邻接点,然后分别从这些邻接点出发,直至图中所有已有已被访问的顶点的邻接点都被访问到。对于图8.15所示的无向连通图,若顶点Vi为初始访问的顶点,则广度优先搜索遍历顶点访问顺序是:V1
→ V2 → V3 → V4 → V5 → V6
→ V7 → V8。遍历过程如图8.16的所示。

和二叉树的广度优先搜索遍历类似,图的广度优先搜索遍历也需要借助队列来完成,例8.3演示了这个过程。

【例8-3  BFSTraverse.cs】广度优先搜索遍历

打开【例8-2 
DFSTraverse.cs】,在AdjacencyList<T>类中添加以下代码后,将文件另存为BFSTraverse.cs。

54      public void BFSTraverse() //广度优先遍历
55      {
56          InitVisited(); //将visited标志全部置为false
57          BFS(items[0]); //从第一个顶点开始遍历
58      }
59      private void BFS(Vertex<T> v) //使用队列进行广度优先遍历
60      {   //创建一个队列
61          Queue<Vertex<T>> queue = new Queue<Vertex<T>>();
62          Console.Write(v.data + " "); //访问
63          v.visited = true; //设置访问标志
64          queue.Enqueue(v); //进队
65          while (queue.Count > 0) //只要队不为空就循环
66          {
67              Vertex<T> w = queue.Dequeue();
68              Node node = w.firstEdge;
69              while (node != null) //访问此顶点的所有邻接点
70              {   //如果邻接点未被访问,则递归访问它的边
71                  if (!node.adjvex.visited)
72                  {
73                      Console.Write(node.adjvex.data + " "); //访问
74                      node.adjvex.visited = true; //设置访问标志
75                      queue.Enqueue(node.adjvex); //进队
76                  }
77                  node = node.next; //访问下一个邻接点
78              }
79          }
80      }

【例8-3  Demo8-3.cs】广度优先搜索遍历测试

using System;
class Demo8_3
{
    static void Main(string[] args)
    {
        AdjacencyList<string> a = new AdjacencyList<string>();
        a.AddVertex("V1");
        a.AddVertex("V2");
        a.AddVertex("V3");
        a.AddVertex("V4");
        a.AddVertex("V5");
        a.AddVertex("V6");
        a.AddVertex("V7");
        a.AddVertex("V8");
        a.AddEdge("V1", "V2");
        a.AddEdge("V1", "V3");
        a.AddEdge("V2", "V4");
        a.AddEdge("V2", "V5");
        a.AddEdge("V3", "V6");
        a.AddEdge("V3", "V7");
        a.AddEdge("V4", "V8");
        a.AddEdge("V5", "V8");
        a.AddEdge("V6", "V8");
        a.AddEdge("V7", "V8");
        a.BFSTraverse(); //广度优先搜索遍历
    }
}

运行结果:

V1 V2 V3 V4 V5 V6 V7 V8

运行结果请参照图8.16进行分析。

8.3.3  非连通图的遍历

以上讨论的图的两种遍历方法都是相对于无向连通图的,它们都是从一个顶点出发就能访问到图中的所有顶点。若无向图是非连通图,则只能访问到初始点所在连通分量中的所有顶点,其他连通分量中的顶点是不可能访问到的(如图8.17所示)。为此需要从其他每个连通分量中选择初始点,分别进行遍历,才能够访问到图中的所有顶点,否则不能访问到所有顶点。为此同样需要再选初始点,继续进行遍历,直到图中的所有顶点都被访问过为止。

上例的代码只需对DFSTraverse()方法和BFSTraverse()方法稍作修改,便可以遍历非连通图。

public void DFSTraverse() //深度优先遍历
    {
        InitVisited(); //将visited标志全部置为false
        foreach (Vertex<T> v in items)
        {
            if (!v.visited) //如果未被访问
            {
                DFS(v); //深度优先遍历
            }
        }
    }
    public void BFSTraverse() //广度优先遍历
    {
        InitVisited(); //将visited标志全部置为false
        foreach (Vertex<T> v in items)
        {
            if (!v.visited) //如果未被访问
            {
                BFS(v); //广度优先遍历
            }
        }
    }

C#与数据结构--图的遍历,布布扣,bubuko.com

时间: 2024-10-10 17:51:08

C#与数据结构--图的遍历的相关文章

浅析数据结构-图的遍历

上一篇了解图的基本概念,包括图的分类.术语以及存储结构.本篇就是应用图的存储结构,将图进行数据抽象化,应用遍历方法,对数据进行遍历.由于图复杂的数据结构,一定保证图中所有顶点被遍历.如果只访问图的顶点而不关注边的信息,那么图的遍历十分简单,使用一个foreach语句遍历存放顶点信息的数组即可.但是,如果为了实现特定算法,就必须要根据边的信息按照一定的顺序进行遍历.图的遍历算法是求解图的连通性问题.拓扑排序和求解关键路径等算法的基础. 一.图的遍历   图的数据结构相对树复杂,图的任一顶点都可能和

数据结构-图的遍历

图的遍历是指从一个顶点出发,访问且仅一次访问图中其余所有顶点,不是所有边的处理.是求图的连通性,拓扑排序,路径求解等问题的基础. 非常基本的图的遍历方法有深度优先搜索法和广度(宽度)优先搜索法. 深度优先搜索,Depth First Search,DFS 深度优先搜索法是树的先根遍历的推广,它的基本思想是:从图G的某个顶点v0出发,访问v0,然后选择一个与v0相邻且没被访问过的顶点vi访问,再从vi出发选择一个与vi相邻且未被访问的顶点vj进行访问,依次继续.如果当前被访问过的顶点的所有邻接顶点

数据结构-图的遍历之Bellman-Ford算法和SPFA算法

一.Bellman-Ford算法 用于解决单源最短路径的问题,但也能够处理有负权边的情况.这是与Djikstra算法不同的地方. 关于复杂度,要比Djikstra的复杂度更高一点.O(VE),而Djikstra复杂度是O(V^2),V是点的数量,E是边的数量 原理,就是会出现负环的情况,会使得最短路径越来越小,进而产生错误:如果出现负环,源点无法到达,那么也是不会影响求解的. 设置d数组,用于存储最短路径的距离,如果存在可以到达的负环,那么返回false:如果不存在,那么数组d中存储的就是最短距

42. 蛤蟆的数据结构笔记之四十二图的遍历之广度优先

42. 蛤蟆的数据结构笔记之四十二图的遍历之广度优先 本篇名言:"生活真象这杯浓酒 ,不经三番五次的提炼呵 , 就不会这样一来可口 ! -- 郭小川" 继续看下广度优先的遍历,上篇我们看了深度遍历是每次一个节点的链表是走到底的. 欢迎转载,转载请标明出处:http://write.blog.csdn.net/postedit/47029275 1.  原理 首先,从图的某个顶点v0出发,访问了v0之后,依次访问与v0相邻的未被访问的顶点,然后分别从这些顶点出发,广度优先遍历,直至所有的

41 蛤蟆的数据结构笔记之四十一图的遍历之深度优先

41  蛤蟆的数据结构笔记之四十一图的遍历之深度优先 本篇名言:"对于我来说 , 生命的意义在于设身处地替人着想 , 忧他人之忧 , 乐他人之乐. -- 爱因斯坦" 上篇我们实现了图的邻接多重表表示图,以及深度遍历和广度遍历的代码,这次我们先来看下图的深度遍历. 欢迎转载,转载请标明出处: 1.  原理 图遍历又称图的遍历,属于数据结构中的内容.指的是从图中的任一顶点出发,对图中的所有顶点访问一次且只访问一次.图的遍历操作和树的遍历操作功能相似.图的遍历是图的一种基本操作,图的许多其它

数据结构快速回顾——图的遍历

图的遍历指的是从图中的任一顶点出发,对图中的所有顶点访问一次且只访问一次.图的遍历操作和树的遍历操作功能相似.图的遍历是图的一种基本操作,图的许多其它操作都是建立在遍历操作的基础之上. 图的遍历方法目前有深度优先搜索法和广度(宽度)优先搜索法两种算法. 深度优先搜索法DFS 深度优先搜索法的基本思想是:从图G的某个顶点v0出发,访问v0,然后选择一个与v0相邻且没被访问过的顶点vi访问,再从vi出发选择一个与vi相邻且未被访问的顶点vj进行访问,依次继续.如果当前被访问过的顶点的所有邻接顶点都已

图的遍历 - 数据结构

图的遍历 - 数据结构 概述 图的遍历是指从图中的任一顶点出发,对图中的所有顶点访问一次且只访问一次.图的遍历操作和树的遍历操作功能相似.图的遍历是图的一种基本操作,图的其它算法如求解图的连通性问题,拓扑排序,求关键路径等都是建立在遍历算法的基础之上. 由于图结构本身的复杂性,所以图的遍历操作也较复杂,主要表现在以下四个方面:① 在图结构中,没有一个“自然”的首结点,图中任意一个顶点都可作为第一个被访问的结点.② 在非连通图中,从一个顶点出发,只能够访问它所在的连通分量上的所有顶点,因此,还需考

数据结构例程——图的遍历

本文是[数据结构基础系列(7):图]中第6课时[图的遍历]的例程. 1.深度优先遍历--DFS(linklist.h是图存储结构的"算法库"中的头文件,详情请单击链接-) #include <stdio.h> #include <malloc.h> #include "graph.h" int visited[MAXV]; void DFS(ALGraph *G, int v) { ArcNode *p; int w; visited[v]=

【数据结构】图的遍历

What is 遍历 访问图中的每一个元素一次,仅仅一次.访问,可以是输出打印,改写啊,这样的,根据ADT使用者的回调函数而定. 图的遍历常用的有2种:深度优先搜索,广度优先搜索. 深度优先搜索(Deepth First Search . DFS) 深度优先搜索和树的先序遍历道理是一样的. 需要考虑以下几点: 1.为了避免重复访问,我们需要用一个  bool类型的访问标记数组(visited flag  array),来标记顶点是否已经被访问. 2.要考虑到 非连通图中的 “孤岛”,他们是孤立的