一、图的定义
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
前面我们学习了线性表、树,到现在学图的相关知识,针对图的定义,简单地了解一下图的几个特点:
(1)线性表中数据元素叫元素;树中数据元素叫结点;在图中的数据元素,我们则称之为顶点。
(2)线性表中可以没有数据元素,称为空表;树中可以没有结点,叫做空树;但是在图结构中,不允许没有顶点。
(3)在线性表中,相邻的数据元素之间具有线性关系;树结构中,相邻两层的结点具有层次关系;在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以为空。
1、各种图定义
无向边:若顶点vi到vj之间的边没有方向,可用有序偶对(vi,vj)来表示。
有向边(弧):若从顶点vi到v之间j的边有方向,可用有序偶对<vi,vj>表示。
无向完全图:任意两个顶点之间都存在边。n个顶点的无向完全图有n*(n-1)/2条边。
有向完全图:任意两个顶点之间都存在方向互为相反的两条弧。n个顶点的有向完全图有n*(n-1)条边。
注意:无向边用小括号“( )”表示,有向边则是用尖括号“< >”表示
--此图摘取数据结构PPT中
路径:如无向图顶点1到顶点5的路径为v1,v2,v5。
路径长度:路径上的边或弧的数目。
回路或环:路径的起点和终点相同。
网路或网:带权的图。
二、图的存储方式
1、邻接矩阵:表示顶点之间的相邻关系的矩阵。无向图的邻接矩阵一定对称。
#define N ?///最大顶点
typedef struct{
char vex[N];///顶点名
int e[N][N];///邻接矩阵
}Mgraph;
void creat_Mgraph(Mgraph *G,int n){
for(int i=0;i<n;i++) scanf("%c",&G->vex[i])///输入顶带你
for(int i=0;i<n;i++)
for(int j=0j<n;j++)
G->e[i][j]=0;///初始化
for(int k=0;k<n;k++){///带权无向图
scanf("%d%d",&a,&b);
G->e[a][b]=1;
G->e[b][a]=1;
}
}
注意:邻接矩阵比较浪费内存,一般只适用边数比较少时;当数据较大时,往往会超时间或者超内存。
2、邻接表:数组与链表相结合的存储方法。
(1)邻接表的链式实现:它的存储由顶点表和边表这两部分组成。
1)顶点表的各个结点由data和firstedge两个域组成。
2)边表结点由adjvex和next两个域组成。
#define N 100
typedef struct EdgeNode{///边表结点
int adjvex;///邻接点域,存储该顶点对应的下标
int weight;///权值
EdgeNode *next;///链域,指示下一条边或弧,指向下一个邻接点
}EdgeNode;
typedef struct{///顶点结点
char data;///顶点信息
struct EdgeNode *firstedge;///指向第一个邻接点,边表头指针
}AdjList[N];
typedef struct ALGraph{
AdjList adj;
int n,m;///顶点数和边数
}ALGraph;
void GreatALGraph(ALGraph *G){///图的邻接表的结构
EdgeNode *p,*q;
cin>>G->n>>G->m;
getchar();
for(int i=0;i<G->n;i++){
cin>>G->adj[i].data;///字符类型的顶点
G->adj[i].firstedge=NULL;
}
getchar();
for(int k=0;k<G->m;k++){
char u,v;
EdgeNode *p,*q;
cin>>u>>v;
int i,j;
for(int s=0;s<G->n;s++){///找输入顶点的下标
if(u==G->adj[s].data)
i=s;
if(v==G->adj[s].data)
j=s;
}
p=(EdgeNode *)malloc(sizeof(EdgeNode));
p->adjvex=j;
p->next=G->adj[i].firstedge;
G->adj[i].firstedge=p;
q=(EdgeNode *)malloc(sizeof(EdgeNode));///从链表头插入
q->adjvex=i;
q->next=G->adj[j].firstedge;
G->adj[j].firstedge=q;
}
}
(2)邻接表的边集数组(链式前向星)实现,不用链表,但是类似链表的原理,非常容易实现;此方法一般用于数据类型为int的顶点,较为简便。它主要由first和next这两个数组组成。first存储的时第一条边的编号(下标),next存储下一条边的编号(下标)。
前向星是一种特殊的边集数组,边集数组中的每一条边按起点从小到大排序,如果起点相同按终点从大到小排序,并记录下以某个点为起点的所有边在数组中的起始位置和存储长度。
///通俗写法。
struct EdgeNode
{
int to;///这条边到达的点
int w;///边权值
int next;///下一条边的编号
}e[N];
///memset(head,-1,sizeof(head));初始化head
void add(int i,int j,int w)///添加边
{
e[k].w = w;///边权值
e[k].to = j;///下一条边的编号
e[k].next = head[i];
head[i] = k++;
}
void traver(int m)///遍历边集合
{
int i,p;
for(p=1;p<=m;p++)
for(i=head[p];i!=-1;i=e[i].next)///遍历的关键
{
printf("%d %d %d\n",p,e[i].to,e[i].w);
}
}
我们可以理解一下《啊哈算法》中实现此方法(都是一样的道理,比较简易),存储执行过程如下:
1)逐一输入特定无序边。
2)对于有多个邻接点的顶点,先next存储从输入编号低到输入编号高的,最后一条边first存储。比如顶点1有2个邻接点2、3.。
它的遍历顺序是先邻接点2后邻接点3。
///核心伪代码
for(int i<=1;i<=n;i++) first[i]=-1;///n为顶点数,初始化数组first
for(int i=1;i<=m;i++){///边数
scanf("%d%d%d",&u[i],&v[i],&w[i])///输入边和权值
next[i]=first[u[i]];///i是输入时的顺序,先找出下一边,然后才能更新first,先进先考虑的原则
first[u[i]]=i;///因为当前顶点可能有多个邻接点,更新first;否则first就是唯一的
}
///怎么去遍历这些边呢
for(int i=1;i<=n;i++){
k=first[i];
while(k!=-1){///遇到-1,当前顶点的邻接点遍历结束
printf("%d %d %d\n",u[k],v[k],w[k]);
k=next[k];
}
}