图(Graph)是一种较线性表和树更为复杂的数据结构。
在线性结构中,数据元素之间仅存在线性关系。
在树型结构中,数据元素之间存在明显的一对多的层次关系。
而在图型结构中,结点之间是多对多的任意关系。
图:由顶点的有穷非空集合和顶点之间边的集合组成
G(Graph) = (V,R)
其中:V={lv∈Data0bject}, R=[VR}, VR={
谓词P(v,w)定义了弧
图是一种与具体应用密切相关的数据结构,基本操作有很大差别
ADT Graph
DataModel
顶点的有穷非空集合和顶点之间边的集合
Operation
CreatGraph:图的建立
DestroyGraph:图的销毁
DFTraverse:深度优先遍历图
BFTraverse:广度优先遍历图
endADT
从图中某一顶点出发访问图中所有顶,并且每个结点仅被访问一次抽象操作,可以是对顶点的各种处理,这里简化为输出顶点的数据
类似于树的前序遍历
算法:DFTraverse
输入:顶点的编号 v
输出:无
1. 访问顶点 v ; 修改标志visited[v] = 1;
2. w =顶点 v 的第一个邻接点;
3. while (w 存在)
3.1 if (w 未被访问) 从顶点 w 出发递归执行该算法;
3.2 w = 顶点 v 的下一个邻接点;
类似于树的层序遍历。
算法:BFTraverse
输入:顶点的编号 v
输出:无
1. 队列 Q 初始化;
2. 访问顶点 v ; 修改标志visited [v] = 1; 顶点 v 入队列 Q;
3. while (队列 Q 非空)
3.1 v = 队列 Q 的队头元素出队;
3.2 w = 顶点 v 的第一个邻接点;
3.3 while (w 存在)
3.3.1 如果 w 未被访问,则
访问顶点 w ; 修改标志visited[w] = 1; 顶点 w 入队列 Q;
3.3.2 w = 顶点 v 的下一个邻接点;
在图中,任何两个顶点之间都可能存在关系(边)->无法通过存储位置表示这种任意的逻辑关系->图无法采用顺序存储结构
图的邻接矩阵存储结构不是顺序存储结构
for (j = 0; j < n; j++)
if (edge[i][j] == 1)
顶点 j 是顶点 i 的邻接点
const int MaxSize = 10;
template <typename DataType>
class MGraph
{
public:
MGraph(DataType a[ ], int n, int e);
~MGraph( );
void DFTraverse(int v);
void BFTraverse(int v);
private:
DataType vertex[MaxSize];
int edge[MaxSize][MaxSize];
int vertexNum, edgeNum;
};
算法:CreatGraph(a[n], n, e)
输入:顶点的数据a[n],顶点个数n,边的个数e
输出:图的邻接矩阵
1. 存储图的顶点个数vertexNum和边的个数edgeNum ;
2. 将顶点信息存储在一维数组vertex中;
3. 初始化邻接矩阵edge;
4. 依次输入每条边并存储在邻接矩阵edge中:
4.1 输入边依附的两个顶点的编号i和j;
4.2 将edge[i][j]和edge[j][i]的值置为1;
template <typename DataType>
MGraph<DataType> :: MGraph(DataType a[ ], int n, int e)
{
int i, j, k;
vertexNum = n; edgeNum = e;
for (i = 0; i < vertexNum; i++) //存储顶点
vertex[i] = a[i];
for (i = 0; i < vertexNum; i++) //初始化邻接矩阵
for (j = 0; j < vertexNum; j++)
edge[i][j] = 0;
for (k = 0; k < edgeNum; k++) //依次输入每一条边
{
cin >> i >> j; //输入边依附的两个顶点的编号
edge[i][j] = 1; edge[j][i] = 1; //置有边标志
}
}
算法:DFTraverse
输入:顶点的编号 v
输出:图的深度优先遍历序列
1. 访问顶点 v; 修改标志visited[v] = 1;
2. j =顶点 v 的第一个邻接点;
3. while (j 存在)
3.1 if (j 未被访问) 从顶点 j 出发递归执行该算法;
3.2 j = 顶点 v 的下一个邻接点;
template <typename DataType>
void MGraph<DataType> :: DFTraverse(int v)
{
cout << vertex[v]; visited[v] = 1;
for (int j = 0; j < vertexNum; j++)
if (edge[v][j] == 1 && visited[j] == 0)
DFTraverse( j );
}
算法:BFTraverse
输入:顶点的编号 v
输出:图的广度优先遍历序列
1. 队列 Q 初始化;
2. 访问顶点 v ; 修改标志visited [v] = 1; 顶点 v 入队列 Q;
3. while (队列 Q 非空)
3.1 i = 队列 Q 的队头元素出队;
3.2 j = 顶点 v 的第一个邻接点;
3.3 while (j 存在)
3.3.1 如果 j 未被访问,则
访问顶点 j ; 修改标志visited[j] = 1; 顶点 j 入队列 Q;
3.3.2 j = 顶点 i 的下一个邻接点;
template <typename DataType>
void MGraph<DataType> :: BFTraverse(int v)
{
int w, j, Q[MaxSize]; //采用顺序队列
int front = -1, rear = -1; //初始化队列
cout << vertex[v]; visited[v] = 1; Q[++rear] = v; //被访问顶点入队
while (front != rear) //当队列非空时
{
w = Q[++front]; //将队头元素出队并送到v中
for (j = 0; j < vertexNum; j++)
if (edge[w][j] == 1 && visited[j] == 0 ) {
cout << vertex[j]; visited[j] = 1; Q[++rear] = j;
}
}
}
图采用邻接矩阵存储,查找每个顶点的邻接点所需时间为O(n),所以,深度优先和广度优先遍历图的时间复杂度均为O(n2),其中n为图中顶点个数。
邻接矩阵存储结构的空间复杂度是O(n2)
如果采用邻接矩阵存储稀疏图,会生成稀疏矩阵
类似于树的孩子表示法
邻接表存储的基本思想是:
边表(邻接表):顶点 v 的所有邻接点链成的单链表
顶点表:所有边表的头指针和存储顶点信息的一维数组
其中,vertex为数据域,存放顶点信息;firstEdge为指针域,指向边表的第一个结点;adivex为邻接点域,存放该顶点的邻接点在顶点表中的下标①;nxt为指针域,指向边表的下一个结点。对于网图,边表结点还需增设info域存储边上信息(如权值)。
存储结构定义
struct EdgeNode //定义边表结点
{
int adjvex; //邻接点域
EdgeNode *next;
} ;
template <typename DataType>
struct VertexNode //定义顶点表结点
{
DataType vertex;
EdgeNode *firstEdge;
};
p = adjlist[v].firstEdge; count = 0;
while (p != nullptr)
{
count++; p = p->next;
}
对于有向图,顶点 i 的出度等于顶点 i 的出边表中的结点个数;顶点 i 的入度等于所有出边表中以顶点 i 为邻接点的结点个数。
p = adjlist[v].firstEdge;
while (p != nullptr)
{
j = p->adjvex; //j是v的邻接点
p = p->next;
}
const int Maxsize 10; //图的最多顶点数
template <typename DataType>
class ALGraph{
public:
ALGraph(DataType a[l,int n,int e); //构造函数
~ALGraph () //析构函数
void DETraverse(int v); //深度优先遍历图
void BFTraverse(int v) //广度优先遍历图
private:
VertexNode<DataType>adjlist [Maxsize]; //存放顶点表的数组
int vertexNum,edgeNum; //图的顶点数和边数
};
算法:构造函数ALGraph
输人:顶点的数据信息a[n],顶点个数n,边的个数e
输出:图的邻接表
1.存储图的顶点个数和边的个数:
2,将顶点信息存储在顶点表中,将该顶点边表的头指针初始化为NULL:
3.依次输人边的信息并存储在边表中:
3.1输入边所依附的两个顶点的编号1和:
3.2生成边表结点s,其邻接点的编号为j:
3.3将结点s插人到第1个边表的表头:
template <typename DataType>
ALGraph<DataType>::ALGraph(DataType a[],int n,int e){
int i,j,k;
EdgeNode *s=nullptr;
vertexNum = n;edgeNum = e;
for(i=0;i<vertexNum;1++) //输人顶点信息,初始化顶点表
{
adjlist[i].vertex = a[i]; //adjlist[]顶点表数组
adjlist[i].firstEdge = nullptr;
for (k=0;k edgeNum;k++) //依次输人每一条边
{
cin>>i>>j; //输入边所依附的两个顶点的编号
s = new EdgeNode;s->adjvex =j; //生成一个边表结点s
s->next = adjlist[i].firstEdge; //将结点s插入表头
adjlist[i].firstEdge=s;
}
}
template <typename DataType>
ALGraph<DataType>::~ALGraph()
{
EdgeNode *p=nullptr,*q=nullptr;
for (int i=0;i<vertexNum;i++)
{
p=q=adjlist[i].firstEdge;
while (p !=nullptr)
{
p-p->next
delete q;q=p;
}
}
}
算法:DFTraverse
输入:顶点的编号 v
输出:无
1. 访问顶点 v; 修改标志visited[v] = 1;
2. j =顶点 v 的第一个邻接点;
3. while (j 存在)
3.1 if (j 未被访问) 从顶点 j 出发递归执行该算法;
3.2 j = 顶点 v 的下一个邻接点;
template <typename DataType>
void ALGraph<DataType>::DFTraverse(int v)
{
int j;
EdgeNode p=nullptr;
cout <<adjlist[v].vertex;visited[v]=1;
p=adjlist[v].firstEdge; //工作指针P指向顶点v的边表
while(p !nullptr) //依次搜索顶点ⅴ的邻接点
{
j=p->adjvex;
if(visited[j]==0)DFTraverse(j)
p=p->next;
}
}
算法:BFTraverse
输入:顶点的编号 v
输出:无
1. 队列 Q 初始化;
2. 访问顶点 v ; 修改标志visited [v] = 1; 顶点 v 入队列 Q;
3. while (队列 Q 非空)
3.1 i = 队列 Q 的队头元素出队;
3.2 j = 顶点 v 的第一个邻接点;
3.3 while (j 存在)
3.3.1 如果 j 未被访问,则
访问顶点 j ; 修改标志visited[j] = 1; 顶点 j 入队列 Q;
3.3.2 j = 顶点 i 的下一个邻接点;
template <typename DataType>
void ALGraph<DataType>::BFTraverse(int v)
int w,j,Q[Maxsize]; //采用顺序队列
int front =-1,rear =-1; //初始化队列
EdgeNode *p=nullptr;
cout <<adjlist[v].vertex;visited[v]=1;
Q[++rear]=v: //被访问顶点入队
while (front!=rear) //当队列非空时
{
w=Q[++front];
p=adjlist[w].firstEdge; //工作指针p指向顶点ⅴ的边表
while (p!=nullptr)
{
j=p->adjvex;
if (visited[j]==0){
cout<<adjlist[j].vertex; visited[j]=1;
Q[++rear]=j;
{
p=p->next;
}
}
}
图的邻接矩阵是一个n×n的矩阵,所以其空间代价是O(n2)。
邻接表的空间代价是O(n+e)。
在图的算法中访问某个顶点的所有邻接点是较常见的操作。如果使用邻接表,只需要检查此顶点的边表,即只检查与它相关联的边,平均需要查找O(e/n)次:如果使用邻接矩阵,则必须检查所有可能的边,需要查找O(n)次。
当图中每个顶点的编号确定后,图的邻接矩阵表示是唯一的;但图的邻接表表示不是唯一的,边表中结点的次序取决于边的输入次序以及结点在边表的插入算法。
图的邻接矩阵和邻接表虽然存储方法不同,但存在着对应关系。邻接表中顶点i的边表对应邻接矩阵的第行,整个邻接表可看作是邻接矩阵的带行指针的链接存储。
算法:Prim
输入:无向连通网G=(V,E)
输出:最小生成树T=(U,TE)
1. 初始化:U = {v}; TE={ };
2. 重复下述操作直到U = V:
2.1 在E中寻找最短边(i,j),且满足i∈U,j∈V-U;
2.2 U = U + {j};
2.3 TE = TE + {(i,j)};
void Prim(int v) /*假设从顶点v出发*/
{
int i, j, k, adjvex[MaxSize], lowcost[MaxSize];
for (i = 0; i < vertexNum; i++) /*初始化辅助数组shortEdge*/
{
lowcost[i] = edge[v][i]; adjvex[i] = v;
}
lowcost[v] = 0; /*将顶点v加入集合U*/
for (k = 1; k < vertexNum; i++) /*迭代n-1次*/
{
j = MinEdge(lowcost, vertexNum) /*寻找最短边的邻接点j*/
cout << j << adjvex[j] << lowcost[j];
lowcost[j] = 0; //顶点)加入集合0
for (i = 0; i < vertexNum; i++) /*调整数组shortEdge[n]*/
if (edge[i][j] < lowcost[i]) {
lowcost[i] = edge[i][j]; adjvex[i] = j;
}
}
}
}
MinEdge函数实现在数组lowcost中查找最小权值并返回其下标
Prim算法的时间复杂度为O(n^2^)
算法:Kruskal算法
输入:无向连通网G=(V,E)
输出:最小生成树T=(U,TE)
1. 初始化:U=V;TE={ };
2. 重复下述操作直到所有顶点位于一个连通分量:
2.1 在E中选取最短边(u,v);
2.2 如果顶点 u、v 位于两个连通分量,则
2.2.1 将边 (u,v) 并入TE;
2.2.2 将这两个连通分量合成一个连通分量;
2.3 在 E 中标记边 (u,v),使得 (u,v) 不参加后续最短边的选取;
struct//定义边集数组的元素类型
{
int from,to,weight;//假设权值为整数
}EdgeType;
vex1 = FindRoot(parent, i);
vex2 = FindRoot(parent, j);
if (vex1 != vex2) {
}
int FindRoot(int parent[ ], int v)
{
int t = v;
while (parent[t] > -1)
t = parent[t];
return t;
}
vex1 = FindRoot(parent, i);
vex2 = FindRoot(parent, j);
if (vex1 != vex2) {
parent[vex2] = vex1;
}
void Kruskal( )
{
int i, num = 0, vex1, vex2;
for (i = 0; i < vertexNum; i++)
parent[i] = -1;
for (num = 0, i = 0; num < vertexNum; i++)
{
vex1 = FindRoot(parent, edge[i].from);
vex2 = FindRoot(parent, edge[i].to);
if (vex1 != vex2) {
cout << edge[i].from << edge[i].to << edge[i].weight;
parent[vex2] = vex1;
num++;
}
}
}
const int MaxVertex = 10;
const int MaxEdge = 100;
template <typename DataType>
class EdgeGraph
{
public:
EdgeGraph(DataType a[ ], int n, int e);
~EdgeGraph( );
void Kruskal( );
private:
int FindRoot(int parent[ ], int v)
DataType vertex[MaxVertex];
EdgeType edge[MaxEdge];
int vertexNum, edgeNum;
} ;
最短路径(shortest path):在非网图中,是指两顶点之间经历的边数最少的路径。路径上的第一个顶点称为源点(source),最后一个顶点称为终点(destination)。在网图中,最短路径是指两顶点之间经历的边上权值之和最少的路径。
w
dist(v, vi):表示从顶点 v 到顶点 vi 的最短路径长度
Dijkstra算法按路径长度递增的次序产生最短路径
只可能经过已经产生终点的顶点
将顶点集合V分成两个集合,一类是生长点的集合S,包括源点和已经确定最短路径的顶点;另一类是非生长点的集合V一S,包括所有尚未确定最短路径的顶点,并使用一个待定路径表,存储当前从源点到每个非生长点v的最短路径。
(1)初始时,集合S中仅包含源点v0,集合V-S中包含除源点v0以外的所有顶点,V0到V-S中各顶点的路径长度或着为某个权值(如果它们之间有弧相连),或者为∞(没有弧相连)。
(2)按照最短路径长度递增的次序,从集合V-S中选出到顶点V0路径长度最短的顶点VK加入到S集合中。
(3)加入VK之后,为了寻找下一个最短路径,必须修改从V0到集合V-S中剩余所有顶点V的最短路径。若在路径上加入VK之后,使得V0到V1的路径长度比原来没有加入VK时的路径长度短,则修正V0到v1的路径长度为其中较短的。
(4)重复以上步骤,直至集合V-S中的顶点全部被加入到集合S中为止。
邻接矩阵
如何存储dist(v, vi)呢?
整型数组dist[n]:存储当前最短路径的长度
字符串数组path[n]:存储当前的最短路径,即顶点序列
①图的存储结构:因为在算法执行过程中,需要快速求得任意两个顶点之间边上的权值,所以,图采用邻接矩阵存储。
②辅助数组dist[n]:元素dist[i]表示当前所找到的从源点v到终点vi的最短路径长度。初态为:若从v到vi有弧,则dist[i]为弧上的权值:否则置dist[i]为∞。若当前求得的终点为vk,则根据下式进行迭代:
dist[i]=min {dist[i],dist[k]+edge[k][i]} 0 <= i <= n-1
若vi∈S,dist[i]表示源点到vi的最短路径长度
若vi∈V-S,dist[i]表示源点到vi的只包括S中的顶点为中间顶点的最短路径。
③辅助数组path[n]:元素path[i]是一个字符串,表示当前从源点v到终点vi的最短路径。初态为:若从v到u:有弧,则path[i]为"vvi",否则置path[]为空串。
伪代码
算法:Dijkstra算法
输入:有向网图 G=(V,E),源点 v
输出:从 v 到其他所有顶点的最短路径
1. 初始化:集合S = {v};dist(v, vi) = w, (i=1...n);
2. 重复下述操作直到 S == V
2.1 dist(v, vk) = min{dist(v, vj), (j=1..n)};
2.2 S = S + {vk};
2.3 dist(v, vj)=min{dist(v, vj), dist(v, vk) + w};
void Dijkstra(int v) /*从源点v出发*/
{
int i, k, num, dist[MaxSize]; string path[MaxSize];
for (i = 0; i < vertexNum; i++)
{
dist[i] = edge[v][i]; path[i] = vertex[v] + vertex[i];
}
for (num = 1; num < vertexNum; num++)
{
for (k = 0, i = 0; i < vertexNum; i++)
if ((dist[i] != 0) && (dist[i] < dist[k])) k = i;
cout << path[k] << dist[k];
for (i = 0; i < vertexNum; i++)
if (dist[i] > dist[k] + edge[k][i]) {
dist[i] = dist[k] + edge[k][i]; path[i] = path[k] + vertex[i];
}
dist[k] = 0;
}
}
时间复杂度是O(n2)。
求每一对顶点的最短路径问题
【想法 1】每次以一个顶点为源点调用Dijkstra算法。显然,时间复杂度为O(n3)
w
distk(vi, vj):从顶点 vi 到顶点 vj 经过的顶点编号不大于 k 的最短路径长度
dist-1(vi, vj) = w
distk(vi, vj) = min{distk-1(vi, vj), distk-1(vi, vk)+distk-1(vk, vj)}
算法:Floyd算法
输入:带权有向图 G=(V,E)
输出:每一对顶点的最短路径
1. 初始化:假设从 vi 到 vj 的弧是最短路径,即dist-1(vi, vj)=w;
2. 循环变量 k 从 0~n-1 进行 n 次迭代:
distk(vi, vj)=min{distk-1(vi, vj), distk-1(vi, vk)+distk-1(vk, vj)}
void Floyd( )
{
int i, j, k, dist[MaxSize][MaxSize];
for (i = 0; i < vertexNum; i++)
for (j = 0; j < vertexNum; j++)
dist[i][j] = edge[i][j];
}
for (k = 0; k < vertexNum; k++)
for (i = 0; i < vertexNum; i++)
for (j = 0; j < vertexNum; j++)
if (dist[i][k] + dist[k][j] < dist[i][j])
dist[i][j] = dist[i][k] + dist[k][j];
#C++实现
void Floyd( )
{
int i, j, k, dist[MaxSize][MaxSize];
for (i = 0; i < vertexNum; i++)
for (j = 0; j < vertexNum; j++)
dist[i][j] = edge[i][j];
for (k = 0; k < vertexNum; k++)
for (i = 0; i < vertexNum; i++)
for (j = 0; j < vertexNum; j++)
if (dist[i][k] + dist[k][j] < dist[i][j])
dist[i][j] = dist[i][k] + dist[k][j];
}
最小生成树有且仅有n-1条边,最短路径每条路径最多有n-1条边。
(1)从AOV网中选择一个没有前驱的顶点并输出;
(2)从AOV网中删去该顶点以及所有以该顶点为尾的弧;
(3)重复上述两步,直到全部顶点都被输出,或AOV网中不存在没有前驱的顶点。
算法:TopSort
输入:有向图G=(V,E)
输出:拓扑序列
1. 栈 S 初始化;累加器 count 初始化;
2. 扫描顶点表,将入度为 0 的顶点压栈;
3. 当栈 S 非空时循环
3.1 j = 栈顶元素出栈;输出顶点 j;count++;
3.2 对顶点 j 的每一个邻接点 k 执行下述操作:
3.2.1 将顶点 k 的入度减 1;
3.2.2 如果顶点 k 的入度为 0,则将顶点 k 入栈;
4. if (count<vertexNum) 输出有回路信息;
void TopSort( )
{
int i, j, k, count = 0, S[MaxSize], top = -1;
EdgeNode *p = nullptr;
for (i = 0; i < vertexNum; i++) /*扫描顶点表*/
if (adjlist[i].in == 0) S[++top] = i;
while (top != -1 ) /*当栈中还有入度为0的顶点时*/
{
j = S[top--]; cout << adjlist[j].vertex; count++;
p = adjlist[j].first;
while (p != nullptr) /*描顶点表,找出顶点j的所有出边*/
{
k = p->adjvex; adjlist[k].in--;
if (adjlist[k].in == 0) S[++top] = k; /*将入度为0的顶点入栈*/
p = p->next;
}
}
if (count < vertexNum ) cout << "有回路”;
}
时间复杂度为O(n+e)。
(1)只有在进入某顶点的各活动都已经结束,该顶点所代表的事件才能发生
(2)只有在某顶点所代表的事件发生后,从该顶点出发的各活动才能开始
算法:关键路径算法
输入:带权有向图 G=(V,E)
输出:关键活动
1. 计算各个活动的最早开始时间和最晚开始时间
2. 计算各个活动的时间余量,时间余量为 0 即为关键活动