假定我们的图的节点是给定的数字 1 、 2 、 … … 、 n 1、2、……、n 1、2、……、n,节点与节点之间的数字为边权,当然边权可以不止有一个。
下面就来解决图的存储问题。
我们约定顶点的数目为 n n n,边的数目为 m m m。
如下图, n = 5 , m = 6 n=5,m=6 n=5,m=6。
g r a p h [ i ] [ j ] { 0 , i = j w ( i , j ) , ( i , j ) ∈ E + ∞ , ( i , j ) ∉ E graph[i][j] \begin{cases} 0 \ \ , i=j\\ w(i,j) \ \ ,(i,j)\in E \\ +\infty \ \ ,(i,j)\notin E \end{cases} graph[i][j]⎩⎪⎨⎪⎧0 ,i=jw(i,j) ,(i,j)∈E+∞ ,(i,j)∈/E
如上图:
[ 0 2 i n f i n f i n f i n f 0 5 i n f 3 i n f i n f 0 i n f 4 i n f i n f i n f 0 i n f 3 i n f i n f 1 0 ] \begin{bmatrix} 0 & 2 & inf & inf & inf \\ inf & 0 & 5 & inf & 3 \\ inf & inf & 0 & inf & 4 \\ inf & inf & inf &0 & inf \\ 3 & inf & inf & 1 & 0 \\ \end{bmatrix} ⎣⎢⎢⎢⎢⎡0infinfinf320infinfinfinf50infinfinfinfinf01inf34inf0⎦⎥⎥⎥⎥⎤
const int SIZE = 10000;
const int INF = 0x3f3f3f3f;
int n,m;
int graph[SIZE][SIZE];
memset(graph,INF,sizeof(graph));
cin>>n>>m;
// for(int i=1;i<=n;i++){
// graph[i][i] = 0;
// }
for(int i=0;i<m;i++){
int from,to,weight;
cin>>from>>to>>weight;
graph[from][to] = weight;
}
//依次访问n个节点的所有出边
for(int x=1;x<=n;x++){
for(int y=1;y<=n;y++){
if(graph[x][y]!=INF){
//……………………
}
}
}
空间复杂度: O ( n 2 ) O(n^2) O(n2)
优点:实现简单
缺点:耗空间、遍历耗时长。
邻接链表的存储思路:
对于每一个节点,存储以它为起点的所有的边,也就是所有的出边。
如上图:
1 : 2 1 :2 1:2
2 : 3 、 5 2 :3 、5 2:3、5
3 : 4 3 :4 3:4
4 : 4 : 4:
5 : 1 、 4 5 :1、4 5:1、4
很明显需要的存储空间变小了。
空间复杂度: O ( n + m ) O(n+m) O(n+m)
适合一般的图的存储,尤其是稀疏图的存储。
下面给出两种实现方式,一种是动态分配内存,指针实现;
一种是用数组加下标来模拟指针,静态实现。
底层的数据结构为数组加链表,也是哈希表的内部结构。
//图的存储
struct Edge{
int to,val;
Edge* next = nullptr;
Edge(int to,int val):to(to),val(val){}
};
vector<Edge*> graph;
//建图
int n,m;
scanf("%d%d",&n,&m);
graph.resize(n+1);
for(int i=0;i<m;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
Edge* head = new Edge(y,z);
head->next = graph[x];
graph[x] = head;
}
//遍历
for(int x=1;x<=n;x++){
cout<<x<<" : ";
for(Edge* p=graph[x];p;p=p->next){
cout<<p->to<<" ";
}
cout<<endl;
}
这里运用了一个小技巧,就是每次新加一条边都是加到头部,而后将新的头部和原来的连接起来,这样就避免了每次都要通过遍历到链表的尾部。这个小技巧在下面也会使用到。
解释一下各个变量的含义,以及注意点。tot
变量模拟指针,自然增长;head[]
是头表,每次访问一个节点从它开始;Next[]
模拟指针的移动,记录的是一条边存储的下一条边的编号。weight[]
记录边权。
注意,to[]、weight[]、Next[]
的脚标都是边的编号,其中next[]、head[]
存储的也是边的编号。
如果边的编号为0,表示指空。(如果确实有0,那可以初始化为-1,用-1之中)。
const int N = 100010, M = 1000010;
int n,m,tot,head[N],to[M],weight[M],Next[M];
void add(int x,int y,int z){
to[++tot] = y;
weight[tot] = z;
Next[tot] = head[x];
head[x] = tot; //从头部插入
}
//访问节点x的所有出边
void visit(int x){
for(int i = head[x];i;i=Next[i]){
int y = to[i];
int z = weight[i];
}
}
此实现方法比较复杂,我并不常用。
又不想动态分配内存,又不想使用前向星,那么vector
就是最佳选择了。
其中edges
存储的真实的边的信息。
而vector
存储是每个节点的出边的标号。
当然也可以暴力一点直接存储边,用vector
。
struct Edge{
int from,to,weight;
Edge(int from,int to,int weight):from(from),to(to),weight(weight){}
};
const int N = 100010;
vector<int> graph[N];
vector<Edge> edges;
void addEdge(int x,int y,int z){
edges.push_back(Edge(x,y,z));
graph[x].push_back(edges.size()-1);
}
void visit(int x){
for(int idx:graph[x]){
int y = edges[idx].to;
int z = edges[idx].weight;
}
}
vector graph[]
来记录一下每个点的出点,也就是每个点的出边对应的点。在力扣上做图论题的时候,经常遇到需要将字符串作为节点的情况。
当然我们可以将字符串映射为数字,然后就像上面那样去处理就可以了。
但是这样做并不清晰,而且有时候还有点麻烦。
换一种思路,其实之前用节点的值从数字换成字符串之后,无非就是不能直接把数字作为数组的脚标了,其余的没啥区别,这个时候我们可以借助STL的map
的记录非数字的映射关系。
比如字符串之间邻接矩阵表示为,map
,
当然也可以使用unordered_map
。
同样vector
就对应着map
。
类似的就不赘述了