您的位置:首页 > 理论基础 > 数据结构算法

数据结构 学习之图

2015-08-15 20:47 761 查看
数据结构之图
图作为一种更加复杂的数据结构,其实在我们的生活中用的也是最广泛的,在本篇博客中,我依然秉承上一篇将二叉树的习惯,采取边学边写的方法来介绍一下图。由于图比较复杂,因此在本篇博客中我重点就是介绍一些图的基本概念,对于编程代码部分,由于目前对其研究有限,因此只能尽可能的介绍一下自己理解的编码。

1.图的一些性质概念

图的表示:图是由点和边组成的集合,我们一般采用G(V,E)这种方式来表示一个图,其中V代表这个图的结点而E代表其边,对于这个图中的的一个边,我们可以用(a,b)或者<a,b>这两种形式来表示,前者表示无方向,后者代表有方向,表示从a指向b。

完全图:每两个顶点之间都有连线,有向完全图的边有n(n-1)条,无向完全图有n(n-1)/2条。

连通图和连通分量:连通图是指图中任意两个点之间可以到达(不一定要直接到达),对于有向图,由于有方向,所以要求任意两点可以相互到达,因此称为强连通图。连通分量顾名思义就是把连通图分解成子连通图。

网络:就是我们的边带有权值的图,所谓的权值就是指我们图中的边包含一些信息,比方说图中a点到b点的路劲长10,这个10就是权值。

度:对于无向图,就是指这个与这个节点相连的边数,对于有向图,我们将其分别称为入度和出度,入度表示该节点出发到远方的边数,出度为该节点作为终点的边数。

最小生成树:指将这个图每个节点按树的方式连接,其中得到的权重之和最小的就成为最小生成树。一个图可能有多个最小生成树。

拓扑排序(AOV网络):一种排序方法,可按照下面的步骤来排:。

(1)从图中选出一个入度为0的点,输出。

(2)从图中删除该点及其相关联的弧,调整被删弧的弧头的点入度。

(3)重复执行(1)(2)直到所有节点被输出。

关键路径(AOE网络):表示从起点到终点的最长路径

2.图的遍历方法

图的遍历方法一般有两种,分别为深度优先遍历和广度优先遍历,有关这两种遍历的方法,用文字描述感觉有点表达不清楚,下面我结合一个具体图来说明



深度优先遍历: 首先访问顶点V1,然后访问V4或者V2,如果访问的是V4,那就接着去访问V4的邻接点V3,然后访问V3的邻接点V2或者V5,如果当初访问的是V2,那么我们接着访问V3,然后访问V5,之后访问V4。深度遍历就是这样,总是访问当前结点未被访问的邻接点。如果当前结点的邻接点都被访问了,那就访问当前结点的上一个结点的未被访问的邻接点,依次类推直到整个图结点都被访问了一遍。上面这个图的深度优先遍历的结果可以为:

V1->V4->V3->V2->V5
V1->V4->V3->V5->V2
V1->V2->V3->V4->V5
V1->V2->V3->V5->V4
V1->V2->V5->V3->V4
广度优先遍历:广度优先遍历首先访问出发点V1,然后访问其邻接点V2,V4,然后在访问V2的邻接点V3或者V5,然后访问V4的邻接点V3(如果V2之后访问的V3,那这会就不访问了)。总之广度优先遍历,就好比是一层一层的遍历,如果将出发点定位第一层,那么与其直接相连的为第二层,与其相隔了一个结点连接的为第三层,依次类推。上图的广度优先遍历的结果为:

V1->V2->V4->V3->V5
V1->V2->V4->V5->V3
V1->V4->V2->V3->V5
3.图的存储结构

常见图的存储结构主要有两种:邻接矩阵和邻接表,下面我们就来讲解一下图的这两种存储结构。

(1)邻接矩阵:我们采用两个数组分别保存图中结点的数据元素信息和数据元素之间关系的信息,其中数据元素之间关系的信息我们采用一个二维数组,他反映的是图中结点的相邻关系,如果结点i和结点j相连接,则矩阵中i行j列就设为1,否则为0,对于一个包含N个结点的图,我们必须分配至少N*N的数据空间来表示它的邻接矩阵。对于无向图,其邻接矩阵第i行j列和i列j行必然一样,但是对于有向图则未必,同样以上图为例,这里我们只看其邻接矩阵是什么样的,其是一个无向图,有五个结点,因此其邻接矩阵必是一个5*5的矩阵,按照上面的说明,我们很容易得出其邻接矩阵为:



这个矩阵每一行就表示一个结点与该图中所有结点的关系,1就表示这两个结点有连接,0就表示这两个结点没连接,举个例子,第一行表示V1这个结点与图中所有结点的关系,那么第一行第一列表示V1与V1的关系,这里把自己与自己的关系定为没有连接,因此取0,第一行第二列就是表示V1与V2是否连接,由上图可知,这里V1是与V2连接的,因此我们这里就填入1,依次类推,就可以得到我们的邻接矩阵了。对于有向图的情况,也是于此类似,只不过有方向了,比方说如果有向图中V1指向V2,V2没有回指向V1,那么在邻接矩阵中就该是第一行第二列为1,而第二行第一列就不能写1了。另外如果是网图的话,其每个边都有权值,那么我们这里就不能写1,而是写上对应的权值,如果没有连接,不写0,一般标注为无穷大。

(2)邻接表:邻接表的存储方式就是为图中的每个节点都建立一个链表,下图是我截图网上的一个表示邻接表的图,通过这个图我们应该能很好理解图的邻接表的存储方式。



这是个有向图,有三个节点,所有应该有三个链表,也就是我们上面的第一个表格图,然后V0连接了V1, V2,所以其链表表后面跟了V1,V2节点的信息存储单元;V1只连了V2,因此V1链表后面连接了还包括V2结点信息的存储快;V2没有任何连接,所以其链表后面什么都不跟,为空。需要说明的是,这个链表中我们发现有一些数字,这个表示这两个结点连接边的权重,我们通过图就很容易发现了。

4.创建一个图

由于图主要有两种存储方式,因此创建一个图大体上也可按照这个存储方式分为两种,下面就来介绍其代码实现。

(1)邻接矩阵创建图

上面我们介绍过,图的邻接矩阵存储方式包含两个数组,一个数组用来存储图中结点信息,一个二维数组用来存储图中结点之间的连接信息,我们依旧以上面那个无向图为例,但是为了编程方便,我们将其中的V1->V2->V3->V4->V5的名称改为A->B->C->D->E,看看创建这个无向图的如何用代码实现的,程序代码如下:

/********************
定义一个图的存储结构
*********************/
typedef
structGRAPH_MATRIX
{
charnode[100];
intedge[100][100];

intnode_num;
intedge_num;
}Graph;

/*************************************************************************************/
/********************************
采用邻接矩阵的存储方式创建一个图
*********************************/
char get_ch()
{
char ch;

ch = getchar();
while(!((ch >='a'&&ch<='z')||(ch>='A'&&ch<='Z')))
{
ch = getchar();
}

return ch;
}

int get_pos(chare,Graph* graph)
{
int i;
for(i =0 ; i < graph->node_num ; ++i)
{
if(e ==graph->node[i])
break;

}

if(i>= graph->node_num)
return -1;
returni;
}

void create_graph_matrix(Graph *&graph)
{
chare1,e2;
introw,col;

graph = (Graph*)malloc(sizeof(Graph));
if(graph == NULL)
{
printf("create the graphfailure\n");
return;
}
memset(graph,0,sizeof(Graph));

printf("please input the graph nodenum\n");
scanf("%d",&graph->node_num);
printf("please input the graph edgenum\n");
scanf("%d",&graph->edge_num);
if(graph->node_num<0||graph->edge_num<0)
{
printf("illegal input\n");
free(graph);
return;
}
if(graph->edge_num >(graph->node_num*(graph->node_num-1)/2)) 对于一个包含n个结点的图,其边数最大值不超过n(n-1)/2
{
printf("inputthe error data\n");
free(graph);
return;
}

printf("please input %d graphnodes\n",graph->node_num);
for(inti = 0 ; i < graph->node_num ; ++i)
{
graph->node[i] =get_ch();;
//存放结点信息
}

printf("please input %d graphedges\n",graph->edge_num);
for(inti = 0 ; i < graph->node_num ; ++i)
{
e1 = get_ch();
e2 = get_ch();
row =get_pos(e1,graph);
col = get_pos(e2,graph);
if(e1 == -1 ||e2 == -1)
{
printf("withoutthis edge,please input all again\n");
i = 0;
}

graph->edge[row][col] =1;
//这里是无向图,所以行列对称。如果是网图,则这个位置我们可以取其权值
graph->edge[col][row] =1;
}
}
这就是采用领接矩阵存储方式创建一个图的基本程序,这段程序我也是参考了网上一些高手的代码写出来的,在vs2010上面运行过,没有问题,加上下面的打印函数,我们就可以打印出相应的信息了:

打印函数:

void print(Graph* graph)
{
printf("printthe node\n");
for(int i = 0 ; i < graph->node_num ; i++)
{
printf("%c " , graph->node[i]);
}

printf("\nprintthe edge\n");
for(int i = 0 ; i < graph->node_num ; ++i)
{
for(int j = 0 ; j < graph->node_num ; j++)
{
if(graph->edge[i][j]!= 0)
printf("%c%c " ,graph->node[i],graph->node[j]);
}
}

printf("\n");

for(int i = 0 ; i < graph->node_num ; ++i)
{
printf(" ");
for(int j = 0 ; j < graph->node_num ; ++j)
{
printf("%d " ,graph->edge[i][j]);
}
printf("\n");
}
}
程序运行截图如下:



由于这里是无向图,所以邻接矩阵是对称的,至于有向图,代码基本都一样,只需去掉最后那个graph->edge[i][j]=1中的一个就可以了。

(2)邻接表创建图

如果一个图有很多结点,但是边很少的话,采用邻接矩阵方法来存储会造成很大的空间浪费,这时我们用邻接表来存储图的信息会节省存储空间。笔者参看了网上诸多相关的博客文章,可能是由于比较简单,诸多文章都没有对有关邻接表的编程原理介绍的很清晰,并且程序中的注释也不是很清楚,因此我这里希望能根据自己的理解来详细介绍下邻接表的编程原理。我还是结合程序来说吧,程序代码如下:

邻接表创建图程序:

typedef
struct TABLENODE
{
intpos;
structTABLENODE *next;

}Tablenode;
/***********/
typedef
struct HEADNODE
{
charvdata;
Tablenode *firstnode;
}Headnode;
/***********/
typedef
struct FIGRE
{
Headnode headnode[30];
intnode_num;
intedge_num;
}Figre;

char nodeinfo[30];

void create_Figre_table(Figre* figre)
{
intnodenum;
intedgenum;
inthead,tail;
Tablenode *newnode;

printf("pleaseinput figre's node_num and edge_num\n");

scanf("%d%d",&nodenum,&edgenum);
if(nodenum<0||edgenum<0)
{
printf("illegal input\n");
return;
}
if(edgenum>= (nodenum*(nodenum-1)/2))
{
printf("illegal input\n");
return;
}

figre->node_num = nodenum;
figre->edge_num = edgenum;
printf("%d%d\n", nodenum,edgenum);
printf("pleaseinput the node name\n");
for(int i = 0 ; i < 5 ; ++i)

{
figre->headnode[i].vdata = get_ch_table();
nodeinfo[i] = figre->headnode[i].vdata;
figre->headnode[i].firstnode = NULL;
}

printf("pleaseinput the headnode and tailhead of the edge\n");
for(int i = 0 ; i < edgenum ; ++i)
ª
{
scanf("%d%d",&head,&tail);
newnode = (Tablenode*)malloc(sizeof(Tablenode));
newnode->pos = tail;
newnode->next =figre->headnode[head].firstnode;
figre->headnode[head].firstnode = newnode;

newnode = (Tablenode*)malloc(sizeof(Tablenode));
newnode->pos = head;
newnode->next =figre->headnode[tail].firstnode;
figre->headnode[tail].firstnode = newnode;

}
}
在程序开始部分我们发现有三个结构体,第一个结构体是表示单链表的一个结点,因为我们知道,邻接表的存储方式就是为每一个结点创建一个单链表,这个结构就是这种链表的结点形式,其中成员变量pos表示该节点在原图中的序号,比方说我们上图中的V2,它在原图中的序号是3,所以如果存储这个结点时,其pos就应该赋值3,next表示的就是我们下一个结点的地址了,这个跟单链表很相似,应该很好理解。第二个结构表示头结点的信息,在邻接表的表示中,我们还必须为每个头结点单独定义一个结构来表示其信息,其第一个参数表示的是该结点的一些信息,例如名字,比方说如果以我们的V3作为头结点,V3就是这个结点的名字,因此我们应该把这个vdata赋值为V3。第三个结构体就是用来表示我们整个图了,其成员分别包含了图中每个结点的信息,以及图的结点数,边数等。

介绍完结构,我们再来看看下面的创建程序,前面比较直白,就是让输入一些创建图的信息,包括结点数,边数以及结点名字,主要的重点在最后那个for循环里面,在这个for循环中我们首先输入两个数字,分别表示我们的边的结点在图中的位置,然后为单链表结点分配堆空间,之后就是对单链表的成员赋值,后面又是分配堆空间,为什么会有两个分配?我们知道,AB相连,作为无向图的话,BA也是相连的,因此在A作为头结点的单链表中有B,在B作为头结点的单链表中肯定也有A,所以这里就分配了两个空间。下面我们就打印出邻接表,首先需要一个打印程序,如下:

打印程序:

void print_figre_table(Figre *figre)
{
Tablenode *p;
inttemp = 0;
for(int i = 0 ; i < figre->node_num ; ++i)
{
p = figre->headnode[i].firstnode;
printf("%c", figre->headnode[i].vdata);
while(p!=NULL)
{
temp =p->pos;
printf("->%c", nodeinfo[temp]);

p =p->next;
}
printf("\n");
}
}
程序运行结果如下:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: