图是一种数据结构,既然是数据结构,那么就需要存储。所以这一篇文章我们来讨论图的存储结构。
二维数组邻接矩阵(适用于稠密图)
定义二维数组:
i
n
t
G
[
N
]
[
N
]
(
N
表示结点的数量
)
int G[N][N](N表示结点的数量)
intG[N][N](N表示结点的数量)
G
[
i
]
[
j
]
G[i][j]
G[i][j]表示一条从结点
i
i
i 到结点
j
j
j 的有向边的权值,定义如下:
{
1
或权值
当结点
i
到结点
j
两点之间有边或弧
0
或
∞
当结点
i
到结点
j
两点之间没有边和弧
\begin{cases} 1或权值 & 当结点i到结点j两点之间有边或弧 \\ 0或∞ & 当结点i到结点j两点之间没有边和弧 \end{cases}
{1或权值0或∞当结点i到结点j两点之间有边或弧当结点i到结点j两点之间没有边和弧
一般来说,当两个节点没有边相连时,若该程序是用于求最小值的,则设为
∞
∞
∞;若用于求最大值,则设为
0
0
0(没有负权值边)或者
−
∞
-∞
−∞(有负权值边)。因为这样反其道而行之,通过这条边则一定是不划算的,以此便免去了判断的功夫。(在后续计算的时候需要假设图是一个完全图,方便计算,后面会提到)
无向无权图的邻接矩阵表示方法
如下图,这是一个无向无权图(边没有方向和权值):
上图对应的邻接矩阵表示如下:
G
(
a
)
=
[
0
1
1
0
1
0
1
1
1
1
0
1
0
1
1
0
]
G(a)= \begin{bmatrix} 0 & 1 & 1 & 0 \\ 1 & 0 & 1 & 1 \\ 1 & 1 & 0 & 1 \\ 0 & 1 & 1 & 0 \end{bmatrix}
G(a)=
0110101111010110
文字表示方法如下:
G
(
a
)
=
[
1
−
1
不通
1
−
2
通
1
−
3
通
1
−
4
不通
2
−
1
通
2
−
2
不通
2
−
3
通
2
−
4
通
3
−
1
通
3
−
2
通
3
−
3
不通
3
−
4
通
4
−
1
不通
4
−
2
通
4
−
3
通
4
−
4
不通
]
G(a)= \begin{bmatrix} 1-1不通 & 1-2通 & 1-3通 & 1-4不通 \\ 2-1通 & 2-2不通 & 2-3通 & 2-4通 \\ 3-1通 & 3-2通 & 3-3不通 & 3-4通 \\ 4-1不通 & 4-2通 & 4-3通 & 4-4不通 \end{bmatrix}
G(a)=
1−1不通2−1通3−1通4−1不通1−2通2−2不通3−2通4−2通1−3通2−3通3−3不通4−3通1−4不通2−4通3−4通4−4不通
首先,该图中没有出现“自环”的情况(即有一条边的起点终点都为同一个结点),所以所有表示“自环”的都设为0。
这是一个无向图,所以只要点A和点B之间存在一条边,就既能从点A通往点B,也能从点B通往点A。图中,结点1和结点4并没有相连的边,即既不能从结点1直接通往结点4,也不能从结点4直接通往结点1。
由于这是一个无权图,所以这个邻接矩阵仅仅用来表示两点之间是否相通。
有向无权图的邻接矩阵表示方法
如下图,这是一个有向无权图(边有方向,但是没有权值):
上图对应的邻接矩阵表示如下:
G
(
a
)
=
[
0
1
1
0
0
1
0
1
0
]
G(a)= \begin{bmatrix} 0 & 1 & 1 \\ 0 & 0 & 1 \\ 0 & 1 & 0 \end{bmatrix}
G(a)=
000101110
文字表示方法如下:
G
(
a
)
=
[
1
−
1
不通
1
−
2
通
1
−
3
通
2
−
1
不通
2
−
2
不通
2
−
3
通
3
−
1
不通
3
−
2
通
3
−
3
不通
]
G(a)= \begin{bmatrix} 1-1不通 & 1-2通 & 1-3通 \\ 2-1不通 & 2-2不通 & 2-3通 \\ 3-1不通 & 3-2通 & 3-3不通 \end{bmatrix}
G(a)=
1−1不通2−1不通3−1不通1−2通2−2不通3−2通1−3通2−3通3−3不通
首先,该图中也没有出现“自环”的情况(即有一条边的起点终点都为同一个结点),所以所有表示“自环”同样地的都设为0。
这是一个有向图,所以只有点A和点B之间存在一条边,并且方向是
A
−
B
A-B
A−B才能在矩阵中表示为1。如图中,结点1可以到结点2、结点3,但是结点2、结点3并不能通往结点1。
这是一个无权图,所以这个邻接矩阵仅仅用来表示点A、点B是否有可以从点A到点B或者从点B到点A的边。
有向有权有自环图的邻接矩阵表示方法
如下图,这是一个有向有权有自环图(边有方向、权值和自环):
上图对应的邻接矩阵表示如下:
G
(
a
)
=
[
∞
5
11
∞
∞
6
∞
25
33
]
G(a)= \begin{bmatrix} ∞ & 5 & 11 \\ ∞ & ∞ & 6 \\ ∞ & 25 & 33 \end{bmatrix}
G(a)=
∞∞∞5∞2511633
文字表示方法如下:
G
(
a
)
=
[
1
−
1
不通
1
−
2
通,权值
5
1
−
3
通,权值
11
2
−
1
不通
2
−
2
不通
2
−
3
通,权值
6
3
−
1
不通
3
−
2
通,权值
25
3
−
3
通,权值
33
]
G(a)= \begin{bmatrix} 1-1不通 & 1-2通,权值5 & 1-3通,权值11 \\ 2-1不通 & 2-2不通 & 2-3通,权值6 \\ 3-1不通 & 3-2通,权值25 & 3-3通,权值33 \end{bmatrix}
G(a)=
1−1不通2−1不通3−1不通1−2通,权值52−2不通3−2通,权值251−3通,权值112−3通,权值63−3通,权值33
我们假设该图是用于求解最小值问题的,所以为了方便求解,我们应当把不能通过的边设为
∞
∞
∞。
该图中出现了“自环”的情况(即有一条边的起点终点都为同一个结点),所以表示结点3自环的一项(3-3)应当标记为它的权值33。
这是一个有向图,所以只有点A和点B之间存在一条边,并且方向是
A
−
B
A-B
A−B才能在矩阵中表示为1。如图中,结点1可以到结点2、结点3,但是结点2、结点3并不能通往结点1。
这是一个有权图,所以这个邻接矩阵用来表示点A与点B两个方向的边的权值(若无,用
∞
∞
∞,上文提及过原因)
建立二维数组邻接表参考程序
该代码适用于C语言和C++
#include <stdio.h> //包含函数scanf和printf
#include <string.h> //包含函数memset
#define N (100) //该程序最多只能输入100个点
int g[N + 1][N + 1],n,k,from,to,w ;
int main(){
scanf("%d",&n) ;
memset(g,0,sizeof(g)) ;//全部清零
//memset(g,0x7f,sizeof(g)) ;//全部设为一个很大的数
//memset(g,0xaf,sizeof(g)) ;//全部设为一个很小的数
scanf("%d",&k) ;
while(k --){
scanf("%d%d%d",&from,&to,&w) ;//输入边连接的顶点的序号及边的权值
g[from][to] = w ; //若无输入权值,设为1即可
g[to][from] = w ; //由于无向图的对称性,需要镜像操作
//如果是有向图,则不需要这句代码
}
/*
接下来的操作......
接下来的操作......
接下来的操作......
*/
return 0 ;
}
邻接表(适用于稀疏图)
图转化为邻接表
邻接表包含了n个单向链表,第i个单向链表存储的是以结点i为起点的边。每次增添新的边时,都是从链表头部插入的。如下:
无向无权图的邻接表表示方式
它的邻接表如下:
G
(
a
)
=
[
3
2
4
3
1
4
2
1
3
2
]
G(a)= \begin{bmatrix} 3 & 2 \\ 4 & 3 & 1 \\ 4 & 2 & 1 \\ 3 & 2 \end{bmatrix}
G(a)=
3443232211
它的文字表示方式是:
G
(
a
)
=
[
有从结点
1
到结点
2
、
3
的边
有从结点
2
到结点
1
、
3
、
4
的边
有从结点
3
到结点
1
、
2
、
4
的边
有从结点
4
到结点
2
、
3
的边
]
G(a)= \begin{bmatrix} 有从结点1到结点2、3的边 \\ 有从结点2到结点1、3、4的边 \\ 有从结点3到结点1、2、4的边 \\ 有从结点4到结点2、3的边 \end{bmatrix}
G(a)=
有从结点1到结点2、3的边有从结点2到结点1、3、4的边有从结点3到结点1、2、4的边有从结点4到结点2、3的边
有向无权图的邻接表表示方式
它的邻接表如下:
G
(
a
)
=
[
3
2
3
2
]
G(a)= \begin{bmatrix} 3 & 2 \\ 3 \\ 2 \end{bmatrix}
G(a)=
3322
它的文字表示方式是:
G
(
a
)
=
[
有从结点
1
到结点
2
、
3
的边
有从结点
2
到结点
3
的边
有从结点
3
到结点
2
的边
]
G(a)= \begin{bmatrix} 有从结点1到结点2、3的边 \\ 有从结点2到结点3的边 \\ 有从结点3到结点2的边 \end{bmatrix}
G(a)=
有从结点1到结点2、3的边有从结点2到结点3的边有从结点3到结点2的边
有向有权有自环图的邻接表表示方式
它的邻接表如下:
G
(
a
)
=
[
3
2
3
3
2
]
G(a)= \begin{bmatrix} 3 & 2 \\ 3 \\ 3 & 2 \end{bmatrix}
G(a)=
33322
它的文字表示方式是:
G
(
a
)
=
[
有从结点
1
到结点
2
、
3
的边,权值分别为
5
、
11
有从结点
2
到结点
3
的边,权值分别为
6
有从结点
3
到结点
2
、
3
的边,权值分别为
25
、
33
]
G(a)= \begin{bmatrix} 有从结点1到结点2、3的边,权值分别为5、11 \\ 有从结点2到结点3的边,权值分别为6 \\ 有从结点3到结点2、3的边,权值分别为25、33 \end{bmatrix}
G(a)=
有从结点1到结点2、3的边,权值分别为5、11有从结点2到结点3的边,权值分别为6有从结点3到结点2、3的边,权值分别为25、33
借助STL库实现邻接表
类型定义
struct Point ; //声明顶点结构体
typedef struct Edge{ //定义边结构体
int v,dis ; //表示这条边的终点结点编号和权值
} Edge ; //用typedef语句,使下次使用时不用加struct
typedef struct Point{ //定义顶点结构体
forward_list<Edge> e ; //用于存储起点为自身的边
} Point ; //用typedef语句,使下次使用时不用加struct
结构体 E d g e Edge Edge 和 P o i n t Point Point 分别是边和点的结构体。一个顶点可能会有多条以自身为起点的边,且数量不确定,所以我们就可以使用STL中的单向链表进行存储这些边(因为在这过程中我们只需要边的遍历,而不需要边的回溯,所以使用单向链表更加合适)。而每条边有两个要存储的信息,终点顶点编号,长度。因为边是存储在起点顶点的链表里的,所以就不用存储起点顶点的编号了。
初始化
顶点的初始化
由于使用了STL库,所以初始化非常简单。我们使用STL库中的vector存储顶点,所以只需要用resize函数预留
n
+
1
n+1
n+1 个顶点的内存即可。这样可使用的范围是0~n,但是我们只需要使用1~n即可。
代码如下:
a.resize(n + 1) ;
//预留足够的内存
边的初始化
使用STL库中的forward_list可以构建一个单向链表。我们只需要在起点顶点的链表使用push_front函数增添边节点即可。
for(int i = 1;i <= n;++ i){
scanf("%d%d%d",&u,&v,&d) ;
//输入点数、边数、权值
a[u].e.push_front({v,d}) ;
//增加这条边
}
完整代码
#include <stdio.h> //包含printf和scanf
#include <forward_list> //包含forward_list
#include <vector> //包含vector
using std::forward_list ; //声明要使用STL库中的forward_list
using std::vector ; //声明要使用STL库中的vector
struct Point ; //声明顶点结构体
typedef struct Edge{ //定义边结构体
int v,dis ; //表示这条边的终点结点编号和权值
} Edge ; //用typedef语句,使下次使用时不用加struct
typedef struct Point{ //定义顶点结构体
forward_list<Edge> e ; //用于存储起点为自身的边
} Point ; //用typedef语句,使下次使用时不用加struct
vector<Point> a ; //用于存储顶点
int n,m,u,v,d ; //点数、边数、起点、终点、权值
int main(){
scanf("%d%d",&n,&m) ;
//输入点数、边数
a.resize(n + 1) ;
//预留足够的内存
for(int i = 1;i <= n;++ i){
scanf("%d%d%d",&u,&v,&d) ;
//输入点数、边数、权值
a[u].e.push_front({v,d}) ;
//增加这条边
}
/*
执行接下来的代码......
执行接下来的代码......
执行接下来的代码......
*/
return 0 ;
}
数组模拟链表实现邻接表
注:因为是数组模拟链表,所以这种方法基于上一种方法的原理。最好先学习第一种方法再来学习此方法。
类型定义
struct Edge{ //定义边结构体
int next,to,dis ; //下一个边结点,这条边的终点结点编号,
//这条边的权值
} ;
这个结构体用于存储边。
变量定义
vector<Edge> edge ; //定义边动态数组edge,节省内存
int head[N + 5],n,m,u,v,d ;
//存储顶点边链表头节点的数组,顶点数,边数,起点顶点,终点顶点,权值
使用动态数组来存储边,边按照编号来使用。head数组用于存储结点所对应的边的链表的头节点,用于插入结点和遍历结点。
n,m,u,v,d分别是顶点数,边数,起点顶点,终点顶点,权值 。
初始化
初始化顶点
这种方法不需要初始化顶点。
初始化边
首先,为了方便使用,我们先往边动态数组中增加一个无效数据,使得有效数据的下标从零开始。
随后输入m条边的起点顶点,终点顶点,权值 。在动态数组中增添一个新的边结点。注意,这个边结点插入在链表的头结点处,所以我们需要把它的
n
e
x
t
next
next 设为原来的头节点的编号,这样就可以完成跳转到原来头节点的操作。最后,把
h
e
a
d
[
u
]
head[u]
head[u] 设为当前结点的编号,表示这个顶点的边链表头节点变更为了现在的这个结点。
完整代码
#include <stdio.h> //包含函数scanf和printf
#include <vector> //包含vector
using std::vector ; //声明要使用STL库中的vector
#define N (100) //最多只能有100个点
#define M (N * (N - 1)) //计算最多会有几条边
struct Edge{ //定义边结构体
int next,to,dis ; //下一个边结点,这条边的终点结点编号,
//这条边的权值
} ;
vector<Edge> edge ; //定义边动态数组edge,节省内存
int head[N + 5],n,m,u,v,d ;
//存储顶点边链表头节点的数组,顶点数,边数,起点顶点,终点顶点,权值
void add_edge(int from,int to,int dis){
//参数:起点顶点,终点顶点,权值
edge.push_back({head[from],to,dis}) ;
//增加新结点,next指针指向原来的头节点
head[from] = edge.size() - 1 ;
//新结点成为新的头节点
return ;
}
int main(){
scanf("%d%d",&n,&m) ;
//输入顶点数、边数
edge.push_back({-1,-1,-1}) ;
//提前加入一个无用的边,仅仅用于让边的编号能从一开始
for(int i = 1;i <= m;++ i){
//输入边
scanf("%d%d%d",&u,&v,&d) ;
//输入起点顶点,终点顶点,权值
add_edge(u,v,d) ;
//增添边
}
/*
执行接下来的代码......
执行接下来的代码......
执行接下来的代码......
*/
return 0 ;
}
自己实现单向链表,用于实现邻接表
注:该部分内容涉及指针、动态内存分配、链表。
类型定义
邻接表中的“表”就是链表,所以我们使用链表来实现,顶点和边分别使用下面的两个结构体:
struct Point ; //声明顶点结构体
typedef struct Edge{ //定义边结构体
struct Point *v ; //这条边的终点节点
struct Edge *next ; //这条边的下一条边
int dis ; //这条边的权值
} Edge ; //使用typedef语句,使下次使用时不用加struct
typedef struct Point{ //定义顶点结构体
Edge *e ; //边链表的的头节点
} Point ; //使用typedef语句,使下次使用时不用加struct
结构体 E d g e Edge Edge 和 P o i n t Point Point 分别是边和点的结构体。一个顶点可能会有多条以自身为起点的边,且数量不确定,所以我们就可以使用链表进行存储这些边。在结构体 P o i n t Point Point 中有一个成员 e e e ,它指向当前顶点的边的单向链表的头结点。结构体 E d g e Edge Edge 用于构建一个单向链表, n e x t next next 用于指向下一个边结点, v v v 指向这条边的终点顶点, d i s dis dis 存储这条边的权值。那么我们就可以通过这两个结构体构建邻接表。
初始化
首先,我们需要先初始化好所有的点,这样才可以进行边的初始化。
由于点的编号是由1~n的,所以为了使用的方便,我们需要分配一个大小为n+1的顶点数组。并且还需要把这些顶点的边链表初始化好,也就是把链表的头、尾指针初始化为0,避免“野指针”的情况出现(因为动态分配内存时,变量的初始值是不确定的,所以为了区分该指针是否分配了内存,需要设为0表示未分配内存)。代码如下:
scanf("%d%d",&n,&m) ;
//输入点数与边数
head = new Point[n + 1] ;
//为head指针分配(n+1)个Point结构体的内存
//可以以head[i]的形式访问(i=0~n)
for(int i = 1;i <= n;++ i)
//由于只需要1~n,所以直接初始化1~n即可
head[i].s = head[i].e = 0 ;
//初始化s,e为零,避免野指针。
紧接着,我们可以开始实现边的增添了。为了使用方便,我们可以定义一个函数addEdge。
addEdge函数需要三个参数,分别是起点顶点、终点顶点和边的权值。我们需要在起点顶点的边链表的末尾增添这条边。由于我们使用的是单链表,所以步骤如下:
首先,假设这个函数的参数为
P
o
i
n
t
∗
变量
f
r
o
m
,
P
o
i
n
t
∗
变量
t
o
,
i
n
t
变量
d
i
s
Point*变量from,Point*变量to,int变量dis
Point∗变量from,Point∗变量to,int变量dis,表示的意义如上。
(1)分配一个新的边结构体。这个边结构体有三个成员变量,
P
o
i
n
t
∗
Point*
Point∗ 类型成员
v
v
v 表示这条边的终点结点,所以把它指向
t
o
to
to 。
E
d
g
e
Edge
Edge 类型指针
n
e
x
t
next
next 指针指向该边链表结点的下一个结点,由于我们只保存下了链表的头结点,所以只能够在链表头部插入节点。那么这个新结点就会是新的头结点,所以它的
n
e
x
t
next
next 指针需要指向原来的头节点。代码如下:
Edge *ed = new Edge ;
//分配一个边结构体
(ed -> v) = to ;
//表示这条边的终点是to顶点
(ed -> dis) = dis ;
//表示这条边的权值是dis
(ed -> next) = (from -> e) ;
//因为只存储了链表的头结点,只能在链表头部插入结点,
//则将新结点的后继指针指向原来的头结点
(2)处理结点之间连接的问题。这个问题同样非常好解决,由于这个新结点成为了头结点,所以我们直接把头结点指针
e
e
e 指向这个新结点即可。
代码如下:
(from -> e) = ed ;
//新结点成为头节点
释放内存
这篇文章仅仅讨论图的存储结构,所以跳过算法部分,直接进入最后一部分——内存的释放。
释放内存分为两步:
(1)释放每个顶点的边链表的内存。对于顶点的遍历,由于顶点使用数组存储,所以我们只需要通过下标遍历即可。我们需要遍历顶点的边链表,从而释放所有边结点的内存。对于链表的遍历并释放内存,我们需要使用两个指针
n
o
w
now
now 和
n
e
x
t
next
next,类型都是
E
d
g
e
∗
Edge*
Edge∗,前者用于存储当前要释放的结点,后者用于存储当前结点的下一个结点。这是因为在一个结点释放掉之后,我们就不能再使用它了,所以需要提前把下一个结点保存下来。在当前结点释放完之后,把
n
o
w
now
now 指针指向
n
e
x
t
next
next,就可以进行下一个结点的访问,这样才能正常地遍历节点。最后,当
n
o
w
now
now 指针为
0
0
0 的时候,就表示遍历、释放完毕了,可以进行下一个结点的遍历了。
(2)释放顶点的内存。我们可以直接使用
d
e
l
e
t
e
[
]
delete[]
delete[] 语句将所有顶点所占用的内存释放掉。
代码如下:
//作用:释放顶点数组和边链表使用的内存
void del(Point* head,int from,int to){
//参数:顶点数组,
//使用到的第一个的顶点和最后一个顶点的编号
if(!head)return ;
//如果顶点数组为空,直接返回
while(from <= to){
if(!head[from].e){ ++ from ; continue ; }
//如果边链表为空,不进行操作
Edge *now = head[from].e,*next ;
//存储当前结点和当前结点的下一个节点
while(now){
//重复直到到达尾结点
next = now -> next ;
//先保存下一个结点
delete now ;
//释放当前结点
now = next ;
//准备释放下一个结点
}
head[from].e = 0 ;
//指针清零
++ from ;
//遍历下一个结点
}
delete[] head ;
//释放顶点数组内存
return ;
}
完整代码
#include <stdio.h> //包含函数scanf和printf
struct Point ; //声明顶点结构体
typedef struct Edge{ //定义边结构体
struct Point *v ; //这条边的终点节点
struct Edge *next ; //这条边的下一条边
int dis ; //这条边的权值
} Edge ; //使用typedef语句,使下次使用时不用加struct
typedef struct Point{ //定义顶点结构体
Edge *s,*e ; //边链表的的头节点和尾结点
} Point ; //使用typedef语句,使下次使用时不用加struct
Point *head ; //顶点指针,用于动态分配顶点,数量可以不确定
void addEdge(Point* from,Point* to,int dis) ;
//声明函数addEdge,用于增加一条由from顶点到to顶点,权值为dis的边
void del(Point* head,int from,int to) ;
//声明函数del,用于释放顶点数组和边链表使用的内存
int n,m,u,v,d ;
//点数,边数,边的起点、终点、权值。
int main(){
scanf("%d%d",&n,&m) ;
//输入点数与边数
head = new Point[n + 1] ;
//为head指针分配(n+1)个Point结构体的内存
//可以以head[i]的形式访问(i=0~n)
for(int i = 1;i <= n;++ i)
//由于只需要1~n,所以直接初始化1~n即可
head[i].s = head[i].e = 0 ;
//初始化s,e为零,避免野指针。
for(int i = 1;i <= m;++ i){
//初始化m条边
scanf("%d%d%d",&u,&v,&d) ;
//输入边的起点、终点的编号及边的权值
addEdge(head + u,head + v,d) ;
//给点u增添一条到点v、长度为d的边
}
/*
执行接下来的代码......
执行接下来的代码......
执行接下来的代码......
*/
del(head,1,n) ;
//释放顶点数组和边链表使用的内存
return 0 ;
}
//作用:向点from的边链表中增加一条有向从from到to长度为dis的边。
void addEdge(Point* from,Point* to,int dis){
//参数:边的起点、终点顶点的指针及边的权值
Edge *ed = new Edge ;
//分配一个边结构体
(ed -> v) = to ;
//表示这条边的终点是to顶点
(ed -> dis) = dis ;
//表示这条边的权值是dis
(ed -> next) = 0 ;
//表示这条边是链表最后一个结点,
//暂时没有后继节点,所以将next指针指向0
if(from -> s)(from -> e -> next) = ed ;
//如果在这之前已经有其他结点了,
//则将已存在最后一个结点的next指针指向当前结点
else from -> s = ed ;
//否则,这就是第一个结点,直接把链表指针s指向该结点
from -> e = ed ;
//指针e永远指向最后一个结点
return ;
}
//作用:释放顶点数组和边链表使用的内存
void del(Point* head,int from,int to){
//参数:顶点数组,
//使用到的第一个的顶点和最后一个顶点的编号
if(!head)return ;
//如果顶点数组为空,直接返回
while(from <= to){
if(!head[from].s){ ++ from ; continue ; }
//如果边链表为空,不进行操作
Edge *now = head[from].s,*next ;
//存储当前结点和当前结点的下一个节点
while(now != head[from].e){
//重复直到到达尾结点
next = now -> next ;
//先保存下一个结点
delete now ;
//释放当前结点
now = next ;
//准备释放下一个结点
}
delete head[from].e ;
//最后一个节点,即尾结点,直接释放
head[from].s = head[from].e = 0 ;
//指针清零
++ from ;
//遍历下一个结点
}
delete[] head ;
//释放顶点数组内存
return ;
}
尾声
如果你认真看到了这里——恭喜你,通过一篇万字好文透彻地学习了图的存储结构及图的构建、释放!你的编程水平又提升了一个阶梯。
想要学习更多关于“图论”的知识。
专栏:图论专栏
专栏导航:专栏导航