最近看到数据结构真的是头大,刚好想到之前自己因为不会存图被xxx怒怼,又没有学过数据结构,作为一个acmer来说,怎么能不会这种操作呢。然后现在来总结一下图的存储方式。
图的分类有很多,这里不再赘述。
来看一个一般的无向图:通俗地讲,一张图是由边、顶点集构成,每条边上可能还会有相应的边权(带权的),这里讲带权的。
然后想我们怎样存储它呢,下面介绍几种存储方式。
1、邻接矩阵
图的邻接矩阵存储方式是用两个数组来存图的,一个一维数组存储顶点集,一个二维数组(邻接矩阵)存储的是图中边的信息。
例如:上图就可以用一个一维数组head[4]={ v0,v1,v2,v3}存储顶点信息,一个二维数组edge[4][4]存储边的信息(比如edge[i][j]=2,可以将这个数组看作点i到点j的边权为2),上面这个图中,1表示图中存在点i到点j的边0表示不存在。但是细心的你也许注意到了,上面的图是一个无向图,也就是说i-j的同是j-i也是一样的,也就是说edge[i][j]==edge[j][i];这个关系利用对称矩阵可以证明,也就是说,用主对角线为轴的上三角形和下三角形相对应的元素是相等的。而在刚开始的时候将邻接矩阵初始化为0表示所有点都没有联通,时间复杂度O(n^2).好了,具体我们见代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5;
#define INF 0x3f3f3f3f
int head[maxn];//存储顶点,这里不需要
int edge[maxn][maxn];
int n,m;//n个点,m条边,点的编号为1-n
void init()
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(i==j) edge[i][j]=0;//自己到自己是0
else edge[i][j]=INF;//初始化为无穷大
}
}
}
int main()
{
while(scanf("%d%d",&n,&m)!=EOF)
{
init();//初始化邻接矩阵
for(int i=0;i<m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
//if(u!=v&&edge[u][v]>w)这里主要是处理重边和自环
edge[u][v]=edge[v][u]=w;
}
for(int i=1;i<=n;i++)
{
for(int j=i;j<=n;j++)
{
printf("%d-%d=%d\n",i,j,edge[i][j]);
}
}
}
return 0;
}
2、邻接表
邻接表存图主要是使用链表存储,这里存储顶点信息依然使用动态的一维head[] 数组,而存储各个边信息则需要使用多重链表。具体我们见代码:
上图的邻接表如下:
#include<bits/stdc++.h>
using namespace std;
const int maxn=10005;//点的数量
const int maxm=10005;//边的数量
int head[maxn];//存储顶点
int cnt;
struct node
{
int u;//起点
int v;//终点
int w;//权值
int next;//指向上一条边的编号
}edge[maxn*4];//一般都是要开到边的四倍
void add(int u,int v,int w)
{
edge[cnt].u=u;
edge[cnt].v=v;
edge[cnt].w=w;
edge[cnt].next=head[u];
head[u]=cnt++;//顶点编号
}
/*
两种方式都可以,下面的是用C++构造实现
struct node
{
int v,w,next;
node(){}
node(int v,int w,int next):v(v),w(w),next(next){}
}E[4*maxn];
void add(int u,int v,int w)
{
E[cnt]=node(v,w,head[u]);
head[u]=cnt++;
}
*/
void init()
{
cnt=0;
memset(head,-1,sizeof(head));//表头数组初始化
}
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)!=EOF)
{
init();
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,w);//双向边
}
int u;
scanf("%d",&u);//输入一个起点
for(int i=head[u];i!=-1;i=edge[i].next)//输出所有与起点为u相连的边的终点和权值
{
int v=edge[i].v;
int w=edge[i].w;
printf("%d %d\n",v,w);
}
}
return 0;
}
二者区别:
先介绍一下稠密图和稀疏图:
对于一个含有n个点m条边的无向图,邻接表表示有n个邻接表结点和m个边表结点。边的数量接近于n*(n-1)的称作稠密图,反之为稀疏图。考虑到邻接表中要附加链域,这时候用邻接矩阵比较合适。
对于一个含有n个点m条边的有向图,邻接表表示有n个邻接表结点和2*m个边表结点。边的数目远小于n^2的为稀疏图,反之为稠密图。
还有一种存图方式为vector存图,有兴趣可以了解一下,
下面给出代码:https://paste.ubuntu.com/p/NK5vxQfZBT/
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5;
struct node
{
int u;//起点
int v;//终点
int w;//权值
}E;
vector<node>edge[maxn];//edge[i]表示起点是i,vector里面存储的是边
int main()
{
int n,m;//n个点m条边
while(scanf("%d%d",&n,&m)!=EOF)
{
//点的编号1-n
for(int i=1;i<=m;i++)
{
int u;
scanf("%d%d%d",&u,&E.v,&E.w);
edge[u].push_back(E);
}
for(int i=1;i<=n;i++)//这里vector实际上相当于一个二维的动态数组,第二维大小不确定
{
for(int j=0;j<edge[i].size();j++)
{
node e=edge[i][j];
printf("%d-%d=%d\n",i,e.v,e.w);
}
}
}
return 0;
}
以上为C++语言实现,下面我们看一下Python实现的邻接表。
该图所对应的邻接表如下图所示:
对于Python实现邻接表存图,我们采用两个类实现,一个为顶点类Vertex,另一个为Graph类,具体包含信息如上图所示,其中Vertex类中的每一个顶点采用一个字典来记录它所连接的顶点以及这条边的权重,也就是connectedTo这个字典。
class Vertex(object):
# 初始化顶点
def __init__(self, key):
self.id = key #初始化顶点的键
self.connectedTo = {} #初始化顶点的值
# 添加邻居顶点,参数nbr是邻居顶点的键,默认权重为0
def addNeighbor(self, nbr, weight=0):
self.connectedTo[nbr] = weight
def __str__(self):
return str(self.id) + ' connectedTo: ' + str([x.id for x in self.connectedTo])
# 获取该顶点所有邻居顶点的键
def getConnections(self):
return self.connectedTo.keys()
# 获取顶点的键
def getId(self):
return self.id
# 获取到某邻居顶点的权重
def getWeight(self, nbr):
return self.connectedTo[nbr]
# 自定义图类
class Graph(object):
# 初始化图
def __init__(self):
self.vertList = {} #初始化邻接表
self.numVertices = 0 #初始化顶点数
# 添加顶点
def addVertex(self, key):
newVertex = Vertex(key) #创建顶点
self.vertList[key] = newVertex #将新顶点添加到邻接表中
self.numVertices = self.numVertices + 1 #邻接表中顶点数+1
return newVertex
# 获取顶点
def getVertex(self, n):
if n in self.vertList: #若待查询顶点在邻接表中,则
return self.vertList[n] #返回该顶点
else:
return None
# 使之可用in方法
def __contains__(self, n):
return n in self.vertList
# 添加边,参数f为起始顶点的键,t为目标顶点的键,cost为权重
def addEdge(self, f, t, cost=0):
if f not in self.vertList: #起始顶点不在邻接表中,则
self.addVertex(f) #添加起始顶点
if t not in self.vertList: #目标顶点不在邻接表中,则
self.addVertex(t) #添加目标顶点
self.vertList[f].addNeighbor(self.vertList[t], cost)#在邻接表中添加起始点的目标点及权重
# 获取邻接表中所有顶点的键
def getVertices(self):
return self.vertList.keys()
# 迭代显示邻接表的每个顶点的邻居节点
def __iter__(self):
return iter(self.vertList.values())
g = Graph() #实例化图类
for i in range(6):
g.addVertex(i) #给邻接表添加节点
print(g.vertList) #打印邻接表
g.addEdge(0, 1, 5) #给邻接表添加边及权重
g.addEdge(0, 5, 2)
g.addEdge(1, 2, 4)
g.addEdge(2, 3, 9)
g.addEdge(3, 4, 7)
g.addEdge(3, 5, 3)
g.addEdge(4, 0, 1)
g.addEdge(5, 4, 8)
g.addEdge(5, 2, 1)
for v in g: #循环每个顶点
for w in v.getConnections(): #循环每个顶点的所有邻居节点
print("(%s, %s)" % (v.getId(), w.getId())) #打印顶点和其邻居节点的键
python实现部分均来自:邻接表的Python实现