图的四种存储结构
邻接矩阵
有一个存储顶点的顺序表和一个存储边/弧的二维数组。
存储结构
#define MaxInt 32767
#define MVNum 100 //最大顶点数
typedef struct {
VerTexType vexs[MVNum]; //顶点顺序表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum, arcnum; //图的顶点数和边数
} AMGraph;
特点
- 无向图的邻接矩阵是对称的。
- 有向图的邻接矩阵第i行表示以顶点i为头结点的弧,第i列表示以顶点i为尾结点的弧;第i行元素之和为顶点i的出度,第i列元素之和为顶点i的入度;第i行元素之和加第i列元素之和等于顶点i的度。
- 完全图的邻接矩阵对角元素全为0, 其他元素全为1.
- 初始化时,图全置为0,网全置为∞。
- 容易实现求顶点度、找邻接点、判断两点是否直接相连等操作,但对于稀疏图非常浪费空间,适合存储稠密图。空间复杂度为O(n²)(与边数无关)。
邻接表
图的链式存储结构,只保存图中存在的边的信息。
对每个结点建立一个带头结点的单链表(边链表),放入与其相邻的结点,再把所有边链表的头结点用顺序表存储。
存储结构
#define MVNum 100
typedef struct ArcNode {
int adjvex; // 该边依附的结点编号
struct ArcNode* nextarc; // 指向下一条边的指针
OtherInfo info;
} ArcNode; // 边
typedef struct VNode {
VertexType data;
ArcNode* firstarc; // 指向第一条依附该顶点的边
} VNode, AdjList[MVNum]; // 顶点结点
typedef struct {
AdjList vertices; // 顶点表
int vexnum, arcnum;
} ALGraph;
特点
- 对于每个顶点,与其相关的边连入表中的顺序任意,所以邻接表不唯一。
- 有向图的邻接表存储的是每个结点的出边,逆邻接表存储的是入边。邻接表和逆邻接表结合可以便于算出点度。
- 无向图空间复杂度为O(n + 2e),有向图空间复杂度为O(n + e).
有向图:十字链表
存储结构
typedef struct ArcBox {
int tailvex, headvex; //弧的尾和头的位置
struct ArcBox* hlink, * tlink;// 指向同起点第一条弧和同终点的第一条弧
InfoType* info;
} ArcBox; // 弧结点
typedef struct VexNode {
VertexType data;
ArcBox* firstin, * firstout; // 指向第一条出弧和第一条入弧
} VexNode; // 顶点结点
typedef struct {
VexNode xlist[MAX_VERTEX_NUM]; // 顶点表
int vexnum, arcnum; // 顶点数和弧数
} OLGraph;
特点
可以看作结合了有向图的邻接矩阵和逆邻接矩阵。
无向图:多重邻接表
存储结构
typedef struct EBox {
VisitIf mark;
int ivex, jvex; //该边依附的两个顶点
struct Ebox* ilink, * jlink; // 与两个相关结点(分别)有关的下一条边
InfoType* info;
} EBox; // 边结点
typedef struct VexBox {
VertexType data;
EBox* firstedge; // 第一条依附于该顶点的边
} VexBox; // 顶点结点
typedef struct {
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum, edgenum; // 顶点数和边数
} OLGraph;
特点
相对于邻接表,邻接多重表用一个结点存储一条边,操作更方便。
图的最短路径
最小生成树与最短路径
最小生成树
- 包含图中的所有点。
- 能保证整个树中的所有路径之和最小。
最短路径
- 不一定包含图中的所有结点。(有向图,部分结点无法以最短路方式被加入)
- 能保证从一点到图中其他点的路径最小。
Dijkstra迪杰斯特拉算法
Dijkstra按路径长度递增的次序产生最短路径,最终得到的是从图中某个顶点到所有其他(存在最短路)的顶点的最短路径。
算法步骤
- 把图中所有顶点分为两组:已求出最短路的和未求出最短路的,前者初始时只包含源点v0;
- 按与源点的最短路长度依次递增的顺序,逐个把未求出最短路的集合中的顶点加入已求出最短路的结点,同时每加入一个新结点,都要对其他结点的最短路长度进行更新。
- 最终所有结点都被加入已求出最短路的结点。
辅助数据结构
- 一维数组
S[i]
用于标记结点是否已经被求出了最短路径。 - 一维数组
Path[i]
记录每个结点的直接前驱结点的编号。 - 一维数组
D[i]
记录从源点到每个结点的当前最短路长度。
如果加入新结点后,发现某些结点以新加入的结点作为中转结点会使其到源点的最短路径比当前已求出的最短路径更小,则需要对其进行更新。更新后再选择D中值最小的结点,重复上述步骤。
算法实现
void ShortestPath_DIJ(AMGraph G, int v0) {
n = G.vexnum;
for(v = 0; v < n; v++) {
S[v] = false; D[v] = G.arcs[v0][v];
Path[v] = (D[v] < MaxInt) ? v0 : -1; // 不与源点直接相连的 最短路长度初始化为-1
}
S[v0] = true; D[v0] = 0; // 加入源点 下面是主循环
for(i = 1; i < n; i++) {
min = MaxInt;
for(w = 0; w < n; w++)
if(!S[w] && D[w] < min) {v = w; min = D[w];}
S[v] = true;
for(w = 0; w < n; w++) {
if(!S[w] && (D[v] + G.arcs[v][w] < D[w]))
// 加入新结点后 对之前求出的其它结点最短路径长度进行更新
D[w] = D[v] + G.arcs[v][w], Path[w] = v;
}
}
}
算法分析
时间复杂度为O(n²)。
即使是只想知道从源点到某个特定结点的最短路,也需要完整执行一次Dijkstra算法。
Floyd弗洛伊德算法
Floyd算法用于求每一对结点之间的最短路径,也可以调用n次Dijkstra算法解决,但Floyd更优雅。两者时间复杂度都是O(n³)。
就是遍历图中的所有结点,每次都把遍历到的当前结点作为中转结点进行插入(就是考察如果加上这个新结点,现有的所有最短路径长度会不会更新)。
算法步骤
- 在两顶点间插入一个中转结点(也就是给二维数组赋值),通过比较两种方式的路径长度,得到两顶点间的最短路径长;
- 不断重复上述步骤,每插入一个新结点,就更新一次所有的最短路径值。
辅助数据结构
- 二维数组
D[i][j]
:记录两顶点间最短路长度。 - 二维数组
Path[i][j]
:记录两顶点间的中转结点,便于中间过程进行比较更新。
算法实现
void ShortestPath_Floyd(AMGraph G) {
for(i = 0; i < G.vexnum; i++) // 初始化
for(j = 0; j < G.vexnum; j++) {
D[i][j] = G.arcs[i][j];
Path[i][j] = (D[i][j] < MaxInt && i != j) ? i : -1;
}
for(k = 0; k < G.vexnum; k++)
for(i = 0; i < G.vexnum; i++)
for(j = 0; j < G.vexnum; j++)
if(D[i][k] + D[k][j] < D[i][j]) {
// 通过比较两顶点间的 当前最短路径长度 与 经过中转结点的最短路径长度
// 得到更小的一个作为新的最短路径长度
D[i][j] = D[i][k] + D[k][j];
Path[i][j] = Path[k][j];
}
}
算法分析
可以处理有负权边的图,但图中不得出现负权回路(环)。
图的最小生成树
最小生成树:包含图的所有n个顶点,且有足以构成一个连通图的(n - 1)条边的极小连通子图。 但满足上述条件的极小连通子图不一定是最小生成树。
下述两个算法用于求无向图的最小生成树。有向图的最小生成树称为最小树形图,此处不做讨论。
Prim普里姆算法
Prim算法,也叫“加点法”,通过从一个结点连接到下一个结点的方式寻找图的最小生成树。
算法步骤
- 将图中所有结点分成AB两类,A表示已包含在树中的,B表示还没包含在树中的;
- 任选一个结点加入A中,作为起始点;
- 在B中所有结点中找到到A中结点的边权值最小的结点,并将其移到A中;
- 直到B中没有结点,此时A中包含的所有结点和走过的所有边就构成图的最小生成树。
辅助数据结构
辅助数组closedge[]
用于记录从A到B具有最小权值的边。
其中储存的每个元素包含两个域:
lowcost
存储最小边的权值, adjvex
存储最小边在A中的(那一个)结点。
算法实现(严蔚敏 《数据结构》)
void MiniSpanTree_Prim(AMGraph G, VerTexType u) {
k = LocateVex(G, u); // 定位到u的下标
for(j = 0; j < G.vexnum; j++)
if(j != k) closedge[j] = {u, G.arcs[k][j]}; // 初始化为到A中结点的边信息
closedge[k].lowcost = 0;
for(i = 1; i < G.vexnum; i++) {
k = Min(closedge); // closedge[k]存的是当前最小边
u0 = closedge[k].adjvex; v0 = G.vexs[k];
cout << u0 << " " << v0; // 分别为最小边的两个结点
closedge[k].lowcost = 0; // 第k个结点并入A
for(j = 0; j < G.vexnum; j++) // 并入后找新的最小边
if(G.arcs[k][j] < closedge[j].lowcost)
closedge[j] = {G.vexs[k], G.arcs[k][j]};
}
}
算法分析
第一个循环(初始化)时间复杂度O(n);
第二个循环遍历其余(n-1)个结点,时间复杂度O(n);
第二个大循环有两个内循环,一个是求当前最小边,一个是并入新结点后,重新寻找最小边。
所以时间复杂度为O(n²)。
与图的边数无关,适用于处理稠密图。
Kruskal克里斯卡尔算法
Kruskal算法,也叫“加边法”,通过逐步增加生成树的边来求得最小生成树。
算法步骤
- 初始化:将图中所有边按从小到大排序,可以认为每个顶点各自形成一个连通分量;
- 选择图中最小的边,如果此边的顶点不与已经有的部分生成树形成回路,就把这条边加入;否则舍弃这条边,选择下一个最小的边;
- 重复第二步,直到图中所有结点都位于同一连通分量上,就得到了最小生成树。
辅助数据结构
- 结构体数组
Edge[]
:存储边的起点终点和权值。
struct {
VerTexType Head, Tail;
ArcType lowcost;
} Edge[arcnum];
Vexset[]
:标记各结点所属的连通分量。
初始时Vexset[i] = i
,表示各个结点各成一个连通分量。
算法实现
void MiniSpanTree_Kruskal(AMGraph G) {
Sort(Edge); // 从小到大排序
for(i = 0; i < G.vexnum; i++) Vexset[i] = i; // 初始化 每个结点各成一个连通分量
for(i = 0; i < G.vexnum; i++) {
v1 = LocateVex(G, Edge[i].Head), v2 = LocateVex(G, Edge[i].Tail);
vs1 = Vexset[v1], vs2 = Vexset[v2];
if(vs1 != vs2) { // 当前最小边与之前已确定的部分所属的连通分量不同
cout << Edge[i].Head << " " << Edge[i].Tail;
for(j = 0; j < G.vexnum; j++)
if(Vexset[j] == vs2) Vexset[j] = vs1; // 把当前最小边合并到已有的连通分量(树)中
}
}
}
算法分析
最耗时的循环是合并两个连通分量。
时间复杂度是O(elog2(e)),与边数相关。适合处理稀疏图。
拓扑排序 & 关键路径
拓扑排序
AOV网
- DAG图:有向无环图
- AOV(Activities On Vertex Network)网:用顶点表示活动,用弧表示活动间的优先关系的网.AOV网中不会出现自环(也不会出现环),因为不会存在以自己为前提的活动,也不会存在几个活动互为前提。
拓扑排序
按照优先顺序对AOV网中的顶点进行排序 使之形成一个线性序列。
算法步骤
- 选择一个无前驱结点并输出;
- 删除这个结点和所有以其为起点的弧;
- 重复上述两步直至不存在无前驱结点。此时如果已输出的顶点数小于图中顶点数,说明图中存在环,否则输出的顶点序列就是一个拓扑序列。
辅助数据结构
- 一维数组
indegree[i]
删除顶点及以其为尾的弧时,不用调整图的存储结构,只需要将弧头结点的入度减1. - 栈
s
暂存所有入度为0的顶点,避免重复扫描。 - 一维数组
topo[i]
记录拓扑序列的顶点序号。
算法实现
void TopologicalSort(ALGraph G, int topo[]) {
FindInDegree(G, indegree); // 求各顶点初始化入度
for(int i = 0; i < G.vexnum; i++)
if(!indegree[i]) s.push(i); // 暂存入度为0的
int cnt = 0; // 记录输出的顶点个数
while(!s.isEmpty()) {
s.pop(); topo[cnt] = i; cnt++;
p = G.vertices[i].firstarc; // 第一条依附该结点的边 指向第一个邻接点
while(p != NULL) {
k = p->adjvex; indegree[k]--; // 每个邻接点入度减1
if(!indegree[k]) s.push(k);
p = p->agjvex; // 指向下一条边
}
}
}
算法分析
求各顶点入度:O(e) + 建立零入度顶点栈O(n) = O(n + e)
关键路径
AOV网
- AOV(Activity On Edge)网,用边表示活动,顶点表示事件,权值表示活动持续时间的带权有向无环图。
- 一些定义
- 源点:(唯一的)入度为0的点
- 汇点:(唯一的)出度为0的点
- 带权路径长度:一条路上的权值和
- 关键路径:长度最长的路径(可能不只一条)
- 关键活动:关键路径上的活动,对整个工程时间影响最大
- 性质
- 只有在进入某顶点的所有活动都完成后,该顶点的活动才开始。
- 只有某顶点的活动结束后,以其为起点的各顶点活动才开始。
- 所有活动都完成后才能到达终点。
- 从起点到终点所必需的时间(最短工期)就是关键路径长度。
求解过程
- 对顶点进行排序,用拓扑排序算法求出每个事件的最早发生时间;
- 逆拓扑排序求出每个事件的最迟发生时间;
- 求每个活动的最早开始时间(从起点向终点算)和最晚开始时间(从终点向起点算);
- 最早开始时间等于最晚开始时间的活动就是关键活动。
算法分析
时间复杂度为O(n + e)