C#与数据结构--图的遍历
http://www.cnblogs.com/abatei/archive/2008/06/06/1215114.html
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.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】图的邻接表存储结构测试
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。
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】深度优先搜索遍历测试
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。
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】广度优先搜索遍历测试
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()方法稍作修改,便可以遍历非连通图。
{
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#与数据结构--图的遍历的更多相关文章
- 【PHP数据结构】图的遍历:深度优先与广度优先
在上一篇文章中,我们学习完了图的相关的存储结构,也就是 邻接矩阵 和 邻接表 .它们分别就代表了最典型的 顺序存储 和 链式存储 两种类型.既然数据结构有了,那么我们接下来当然就是学习对这些数据结构的 ...
- 数据结构-图-Java实现:有向图 图存储(邻接矩阵),最小生成树,广度深度遍历,图的连通性,最短路径1
import java.util.ArrayList; import java.util.List; // 模块E public class AdjMatrixGraph<E> { pro ...
- 数据结构(三十二)图的遍历(DFS、BFS)
图的遍历和树的遍历类似.图的遍历是指从图中的某个顶点出发,对图中的所有顶点访问且仅访问一次的过程.通常有两种遍历次序方案:深度优先遍历和广度优先遍历. 一.深度优先遍历 深度优先遍历(Depth_Fi ...
- C数据结构(文件操作,随机数,排序,栈和队列,图和遍历,最小生成树,最短路径)程序例子
文件操作 文件打开方式 意义 ”r” 只读打开一个文本文件,只允许读数据 ”w” 只写打开或建立一个文本文件,只允许写数据 ”a” 追加打开一个文本 ...
- MySql无限分类数据结构--预排序遍历树算法
MySql无限分类数据结构--预排序遍历树算法 无限分类是我们开发中非常常见的应用,像论坛的的版块,CMS的类别,应用的地方特别多. 我们最常见最简单的方法就是在MySql里ID ,parentID, ...
- 数据结构--图 的JAVA实现(上)
1,摘要: 本系列文章主要学习如何使用JAVA语言以邻接表的方式实现了数据结构---图(Graph),这是第一篇文章,学习如何用JAVA来表示图的顶点.从数据的表示方法来说,有二种表示图的方式:一种是 ...
- 数据结构--图 的JAVA实现(下)
在上一篇文章中记录了如何实现图的邻接表.本文借助上一篇文章实现的邻接表来表示一个有向无环图. 1,概述 图的实现与邻接表的实现最大的不同就是,图的实现需要定义一个数据结构来存储所有的顶点以及能够对图进 ...
- [数据结构]图的DFS和BFS的两种实现方式
深度优先搜索 深度优先搜索,我们以无向图为例. 图的深度优先搜索(Depth First Search),和树的先序遍历比较类似. 它的思想:假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发, ...
- JavaScript数据结构——图的实现
在计算机科学中,图是一种网络结构的抽象模型,它是一组由边连接的顶点组成.一个图G = (V, E)由以下元素组成: V:一组顶点 E:一组边,连接V中的顶点 下图表示了一个图的结构: 在介绍如何用Ja ...
随机推荐
- redis的五种基本数据类型
redis基本数据类型 redis一共分为5中基本数据类型:String,Hash,List,Set,ZSet 第一种String String类型是包含很多种类型的特殊类型,并且是二进制安全的.比如 ...
- memcached 实现读锁
memcached锁,网上大多就介绍乐观锁(cas)[1.2.4以上版本,telnet连接上memcache使用status可以查看版本号].核心就是每次写入数据的时候使用 cas($cas_toke ...
- [LeetCode] Evaluate Reverse Polish Notation stack 栈
Evaluate the value of an arithmetic expression in Reverse Polish Notation. Valid operators are +, -, ...
- 转圈游戏(NOIP2013)
原题传送门 好吧,这道题很水,, 首先我们一看,这就是一道快速幂的题目,k那么大... 然后第X个人的答案就是(x+m*10^k)%n啦!! 好吧,这道题没有什么注意事项 太水了 #include&l ...
- int与Integer区别+Integer类详解
//Integer范围-128~127 //Integer与Integer比较 Integer a_127 = 127; Integer b_127 = 127; Integer c_new_127 ...
- 7.添加OpenStack计算服务
添加计算服务 安装和配置控制器节点 创建数据库 mysql -uroot -ptoyo123 CREATE DATABASE nova; GRANT ALL PRIVILEGES ON nova.* ...
- Switch能否用string做参数
在Java5以前,switch(expr)中,exper只能是byte,short,char,int类型(或其包装类)的常量表达式. 从Java5开始,java中引入了枚举类型,即enum类型. 从J ...
- MVC5 ModelState
ModelState.IsValid 总是false的原因 在做添加功能的时候,发现这个IsValid总是false,这个是它自己的验证机制. 因为是添加,就是说主键是自增的,添加的时候不需要指定这个 ...
- NYOJ16 矩形嵌套 【DAG上的DP/LIS】
矩形嵌套 时间限制:3000 ms | 内存限制:65535 KB 难度:4 描述 有n个矩形,每个矩形可以用a,b来描述,表示长和宽.矩形X(a,b)可以嵌套在矩形Y(c,d)中当且仅当a<c ...
- quailty's Contest #1 道路修建 EXT(启发式合并)
题目链接 道路修建 EXT 考虑并查集的启发式合并,合并的时候小的子树的根成为大的子树的根的儿子. 可以证明这样整棵树的深度不会超过$logn$. 两个根合并的时候,产生的新的边的边权为当前的时间. ...