图的存储方式
图在内存中存储方式有很多种,最经典的包括邻接矩阵、邻接表、逆邻接表和十字链表。
1. 邻接矩阵
图的邻接矩阵是通过两个数组来表示,第一个数组来记录顶点的信息,另一个二维数组来储存边的信息(邻接矩阵).
js
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 10001
//带权无向图为例。n个点 m条边 n<=100 0<=w<=10000
int n,m;
char v[105];
int g[105][105];
int Find(char x)
{
int i=0;
for(i=1;i<=n;i++)
{
if(v[i]==x)
{
break;
}
}
return i;
}
int main()
{
scanf("%d %d",&n,&m);
getchar();
for(int i=1;i<=n;i++)
{
scanf("%c",&v[i]);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
g[i][j]=inf;
//if(i==j)g[i][j]=0;
}
}
char x,y;
int xi,yi,w;
for(int i=1;i<=m;i++)
{
getchar();
scanf("%c %c %d",&x,&y,&w);
xi=Find(x);//找两个端点的编号
yi=Find(y);
g[xi][yi]=g[yi][xi]=w;//一条无向边 当作两条方向相反 权值相同的有向边
}
getchar();
scanf("%c",&x);
int d=0;
xi=Find(x);
for(int j=1;j<=n;j++)
{
if(g[xi][j]<inf)d++;
}
printf("%d\n",d);
return 0;
}
/*
4 5
ABCD
A B 3
A D 6
A C 0
B D 9
D C 4
*/
代码以带权无向图为例,先声明两个数组v105,g105105.在主函数中先读入n个端点,再把邻接矩阵全部初始化.
#define inf 10001inf代表的是这两个顶点之间无限大即没有连接.
接着就是把边的信息全部读入邻接矩阵里面,分别读入m条边,这里读入的端点信息是char类型的所以我们要补一个Find函数找到他在v数组里面的下标.xi,yi即是x,y所对应的下标.因为是无向图所以一条无向边是等同于两条方向相反权值相等的有向边.最后把g数组中xiyi和yixi处的数值更新为这条边的权值即可.
统计一个顶点x的度只需要遍历二维数组中xi这一行或者xi这一列数值小于inf的值的数量即可.
无向图的邻接矩阵是关于主对角线对称的.所以以后拿到一个邻接矩阵,如果是对称的话可能是有向图或者无向图.但如果不对称那么一定是有向图.
邻接矩阵的缺点在于如果顶点很多而边很少的时候(n >> m)会浪费很多的内存.
2. 邻接表
邻接表是用一个结构体数组来储存顶点的信息,这个结构体包括顶点的数据域,还有一个指针域.这个指针域接上一个链表来记录这个顶点的邻接点.
js
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 10001
//带权无向图为例。n个点 m条边 n<100 0<=w<=10000
int n,m;
typedef struct ENode
{
int adj;//邻接点的下标
int w;//边权
struct ENode* next;
}ENode;
struct Graph
{
char data;//顶点数据
ENode* first;//指向该点边链表(邻接点链表)的指针
}g[105];
int Find(char x)
{
int i=0;
for(i=1;i<=n;i++)
{
if(g[i].data==x)
{
break;
}
}
return i;
}
int main()
{
scanf("%d %d",&n,&m);
getchar();
for(int i=1;i<=n;i++)
{
scanf("%c",&g[i].data);
g[i].first=NULL;
}
char x,y;
int xi,yi,w;
for(int i=1;i<=m;i++)
{
getchar();
scanf("%c %c %d",&x,&y,&w);
xi=Find(x);//找两个端点的编号
yi=Find(y);
//xi------->yi yi是xi的邻接点。yi 插入到xi到链表中
ENode* e=(ENode*)malloc(sizeof(ENode));
e->adj=yi;
e->w=w;
e->next=g[xi].first;
g[xi].first=e;
//yi------->xi xi是yi的邻接点。xi 插入到yi到链表中
e=(ENode*)malloc(sizeof(ENode));
e->adj=xi;
e->w=w;
e->next=g[yi].first;
g[yi].first=e;
}
getchar();
scanf("%c",&x);
int d=0;
xi=Find(x);
ENode* p=g[xi].first;
while(p!=NULL)
{
d++;
p=p->next;
}
printf("%d\n",d);
return 0;
}
/*
4 5
ABCD
A B 3
A D 6
A C 0
B D 9
D C 4
*/
代码以带权无向图为例,首先先给出邻接链表的结点的结构体,首先包含这个邻接点在顶点数组的下标,这条边的权值,以及指向下一个邻接点的指针next.再给出这个图的结构体数组,这个结构体包含顶点的信息,以及后面挂置的链表的first指针.
main函数中还是先读入所有的顶点,并且初始化所有的first指针为NULL.再分别读入每一条边的两个端点和权值.因为读入的端点是char类型的,所以仍然需要一个Find函数查找在g数组中的下标xi,yi.因为这是一个单链表,为了方便选用头插法.
xi------->yi yi是xi的邻接点。yi 插入到xi的链表中.先malloc一个新节点e,再更新他的邻接点的数据,权值最后把结点e头插到链表中.yi------->xi xi是yi的邻接点,xi 插入到yi到链表中同理.
统计结点的度只需要在结构体数组中找到对应的顶点,然后顺着他的链表去遍历,看他链表中挂了几个结点即是他的度.
有向图的邻接表会被分为邻接表和逆邻接表,邻接表可以统计顶点的出度,逆邻接表可以统计顶点的入度.对有向图的处理需要另建立一个逆邻接表,我们是否可以把这两个表结合起来呢?结合出来的就是十字链表!
3. 十字链表
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以Vi为尾的弧,也容易找到以Vi为头的弧,因而容易求得顶点的出度和入度。
js
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 10001
//带权有向图为例。n个点 m条边 n<100 0<=w<=10000
//十字链表的结点结构
typedef struct ENode
{
int w;//边权
int taili;//弧尾下标
struct ENode* tnext;//指向弧尾下一条出边的指针,通过tnext构成了弧尾的出边链表
int headi;//弧头下标
struct ENode* hnext;//指向弧头下一条如边的指针,通过hnext构成了弧头的入边链表
}ENode;
struct Graph
{
char data;//顶点的数据域
ENode* firstout;//顶点的出边链表指针
ENode* firstin;//顶点的入边链表指针
}g[105];
int n,m;
int Find(char x)
{
int i=0;
for(i=1;i<=n;i++)
{
if(g[i].data==x)
{
break;
}
}
return i;
}
int main()
{
scanf("%d %d",&n,&m);
getchar();
for(int i=1;i<=n;i++)
{
scanf("%c",&g[i].data);
g[i].firstin=g[i].firstout=NULL;
}
char x,y;
int xi,yi,w;
for(int i=1;i<=m;i++)
{
getchar();
scanf("%c %c %d",&x,&y,&w);//x->y 有向边
xi=Find(x);
yi=Find(y);
ENode* e=(ENode*)malloc(sizeof(ENode));
//if(e==NULL)
e->taili=xi;
e->headi=yi;
e->w=w;
//把xi---》yi 当作xi的出边 插入到xi到出边链表中
e->tnext=g[xi].firstout;
g[xi].firstout=e;
//把xi---》yi 当作yi的入边 插入到yi到入边链表中
e->hnext=g[yi].firstin;
g[yi].firstin=e;
}
getchar();
scanf("%c",&x);
int outd=0,ind=0;
xi=Find(x);
//出度
ENode* p=g[xi].firstout;
while(p!=NULL)
{
outd++;
p=p->tnext;
}
//入度
p=g[xi].firstin;
while(p!=NULL)
{
ind++;
p=p->hnext;
}
printf("出度是%d,入度是%d\n",outd,ind);
return 0;
}
/*
4 5
ABCD
A B 3
A D 6
A C 0
B D 9
D C 4
*/
十字链表的结点结构记录了这条边的边权,弧尾下标taili,弧头下标headi,tnext是指向弧尾下一条出边的指针,hnext指向弧头下一条如边的指针.
图的结构体也要做出一定的改变,记录顶点的数据域,指针域包含了firstout是顶点出边链表的指针,firstin是顶点入边链表的指针.
读入顶点操作与之前无异,读入边仍要读入弧尾x,弧头y及边权w(x-->y的一个有向边),malloc一个新节点e记录,更新弧尾下标taili为xi,弧头下标headi为yi,边权为w.
把xi---》yi 当作xi的出边 插入到xi到出边链表中,头插到xi的firstout链表里.把xi---》yi 当作yi的入边 插入到yi到入边链表中,头插到yi的firstin链表里.
统计一个顶点的出度和入读,就分别遍历他的firstout链表和firstin链表即可.
4. 邻接多重表
邻接表对边的操作显然很不方便,因此,我们可以仿照十字链表的方式,对边表结构进行改装
js
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 10001
//带权无向图为例。n个点 m条边 n<100 0<=w<=10000
//多重邻接表:边链表的结点结构
typedef struct ENode
{
int w;//边权
int x;//端点x的下标
struct ENode* xnext;//指向x所连接的下一条边的指针,通过xnext构成了的度边链表
int y;//端点y的下标
struct ENode* ynext;//指向y下一条边的指针,通过ynext构成了y的边链表
}ENode;
struct Graph
{
char data;//顶点的数据域
ENode* first;//顶点的边链表指针
}g[105];
int n,m;
int Find(char a)
{
int i=0;
for(i=1;i<=n;i++)
{
if(g[i].data==a)
{
break;
}
}
return i;
}
int main()
{
scanf("%d %d",&n,&m);
getchar();
for(int i=1;i<=n;i++)
{
scanf("%c",&g[i].data);
g[i].first=NULL;
}
char a,b;
int ai,bi,w;
for(int i=1;i<=m;i++)
{
getchar();
scanf("%c %c %d",&a,&b,&w);//x---y 无向边
ai=Find(a);//找两个端点的编号
bi=Find(b);
ENode* e=(ENode*)malloc(sizeof(ENode));
//if(e==NULL)
e->x=ai;
e->y=bi;
e->w=w;
//e插入到a的链表中
e->xnext=g[ai].first;
g[ai].first=e;
//e插入到b的链表中
e->ynext=g[bi].first;
g[bi].first=e;
}
getchar();
scanf("%c",&a);
int d=0;
ai=Find(a);
ENode* p=g[ai].first;
while(p!=NULL)
{
d++;
if(p->x==ai)p=p->xnext;
else p=p->ynext;
}
printf("度为%d\n",d);
return 0;
}
/*
4 5
ABCD
A B 3
A D 6
A C 0
B D 9
D C 4
*/
代码以带权无向图为例,边链表结点记录边权w,端点x的下标x,xnext指向x所连接的下一条边的指针,端点y的下标y,ynext指向y下一条边的指针.
读入边之和malloc一个新结点e,更新x端点下标为ai,y端点下标为bi.e插入到a的链表中,e还要插入到b的链表中.
统计顶点的度跟之前有一点不同,在遍历的时候要判断ai是结点的x端点还是y端点,其对应的指针不同.