目录
用邻接矩阵实现图
邻接矩阵实现的图结构定义如下
typedef struct graph *Graph; //Graph是一个类型,为graph型指针
struct graph
{
WItem NoEdge;//无边标记
int n; //顶点数
int e; //边数
WItem **a; //邻接矩阵,a是一个二级指针
}AWDgraph;
函数GraphInit(n,noEdge)创建一个有n个孤立顶点的图
Graph GraphInit(int n,WItem noEdge) //返回指向一个图的指针
{
Graph G=malloc(sizeof*G); //G是变量名,sizeof要加*
G->n=n;
G->e=0;
G->a=Make2DArray(G->n+1,G->n+1,noEdge); //创建一个权值全为noEdge的矩阵
//不赋权图将上一句的NoEdge改为0
return G;
}
函数GraphVerticles(G)和GraphEdges(G)分别返回图G的顶点数和边数
int GraphVerticles(graph G)
{
return G->n;
}
int GraphEdges(graph G)
{
return G->e;
}
函数GraphExits(i,j,G)判断当前图G中的边(i,j)是否存在
int GraphExits(int i,int j,graph G)
{
if(i<1||j<1||i>G->n||j>G->n||G->a[i][j]==G->NoEdge) return 0;
//输入不在合法范围内或该边值为NoEdge
//不赋权图把if条件中的NoEdge改为0
return 1;
}
函数GraphAdd(i,j,w,G)在图G中加入边(i,j)
void GraphAdd(int i,int j,WItem w,Graph G)
{
if(i<1||j<1||i>G->n||j>G->n||i==j||G->a[i][j]!=G->NoEdge)
//若输入不在合法范围或在对角线上或该边已经存在,则报错
//不赋权图把if条件中的NoEdge改为0
Error("Bad input");
G->a[i][j]=w;
//G->a[j][i]=w; 无向图加一句这个
//不赋权图只需要将上述语句的w改成1
G->e++; //更新边数
}
函数GraphDelete(i,j,G)在图G中删除边(i,j)
void GraphDelete(int i,int j,graph G)
{
if(i<1||j<1||i>G->n||j>G->n||G->a[i][j]==G->NoEdge)
//输入不在合法范围内或待删边本不存在
//不赋权图把if条件中的NoEdge改为0
Error("Bad input");
G->a[i][j]=NoEdge;
//G->a[j][i]=NoEdge; 无向图加一句这个
//不赋权图只需要将上述语句的w改成0
G->e--; //更新边数
}
函数InDegree(i,G)返回图G中顶点度数
int InDegree(int i,graph G)
{
int sum=0;
/*可单独作为求入度的方法*/
for(int j=0;j<G->n;j++)
{
if(G->a[i][j]!=NoEdge) //无向图改NoEdge为0
sum++;
}
/*如果是无向图加上以下代码*/
/*也可单独作为求出度的方法*/
for(int j=0;j<G->n;j++)
{
if(G->a[j][i]!=NoEdge) //无向图改NoEdge为0
sum++;
}
/**/
return sum;
}
用邻接表实现图
邻接表结点结构定义
typedef struct lnode *glink; //glink是一个类型,为lnode型指针
struct lnode
{
int v; //边的另一个顶点
glink next; //邻接表指针
//若是赋权图,加上w属性
//WItem w;
};
创建一个新的邻接表结点
glink NewLNode(int v,glink next) //返回指向lnode的指针
{
glink x=malloc(sizeof*x); //x是变量名,sizeof取变量名长度要加*
x->v=v;
x->next=next;
//若是赋权图
//x->w=w;
return x;
}
用邻接表实现图的结构定义
typedef struct graph *Graph;
struct graph
{
int n; //顶点数
int e; //边数
glink *adj; //glink型的一维数组,数组中的成员都是指针
}Lgraph;
函数GraphInit(n)创建一个用邻接表表示的有n个孤立顶点的图
Graph GraphInit(int n)
{
Graph G=malloc(sizeof*G) //G是变量名,sizeof前加*
G->n=n;
G->e=0;
G->adj=malloc(n+1)*sizeof(glink)) //adj是一个指针数组,成员都是glink指针,v此时是0
for(int i=0;i<=n)
G->adj[i]=0;
return G;
}
函数GraphVerticles(G)和函数GraphEdge(G)分别返回图的顶点数和边数同邻接矩阵的写法,略。
函数GraphExist(i,j,G)判断当前有向图G中的边(i,j)是否存在
int GraphExist(int i,int j,graph G)
{
if(i<1||j<1||i>G->n||j>G->n) return 0;
glink p=G->adj[i];
while(p&&p->v!=j) p=p->next; //不走到最后且不为目标,则继续向前
if(p) return 1;
else return 0;
}
函数GraphAdd(i,j,G)通过在顶点i的邻接表表首插入顶点j来实现向图中加入一条边(i,j)的操作
void GraphAdd(int i,int j,Graph G) //赋权图加上WItem w
{
if(i<1||j<1||i>G->n||j>G->n||i==j||GraphExits(i,j,G))
Error("Bad input");
G->adj[i]=NewLNode(j,G->adj[i]); //直接取代G->a[i]
//无向图加上下一句
//G->adj[j]=NewLNode(i,G->adj[j]);
//赋权图加上w
//G->adj[i]=NewLNode(j,w,G->adj[i]);
G->e++;
}
函数GraphDelete(i,j,G)删除有向图G中的边(i,j)
void GraphDelete(int i,int j,Graph G)
{
glink p,q;
if(i<1||j<1||i>G->n||j>G->n||!GraphExists(i,j,G))
{
Error("Bad input");
}
p=G->adj[i];
if(p->v==j) //如果第一个节点就是待删除节点,直接令其成为邻接表数组中的成员
{
G->adj[i]=p->next;
free(p);
}
else
{
while(p&&p->v!=j) p=p->next; //若未走到最后且未找到目标则继续向前
if(p) //若找到目标
{
q=p->next;
p->next=q->next;
free(q);
}
}
//有向图加上下面的代码
/*
p=G->adj[j];
if(p->v==i) //如果第一个节点就是待删除节点,直接令其成为邻接表数组中的成员
{
G->adj[j]=p->next;
free(p);
}
else
{
while(p&&p->v!=i) p=p->next; //若未走到最后且未找到目标则继续向前
if(p) //若找到目标
{
q=p->next;
p->next=q->next;
free(q);
}
}
*/
G->e--;
}
函数OutDegree(i,G)通过计算顶点i的邻接表长,返回有向图G中顶点的出度
int OutDegree(int i,Graph G)
{
glink p;
int sum=0;
if(i<1||i>G->n) Error("Bad input");
p=G->adj[i];
while(p)
{
sum++;
p=p->next;
}
return sum;
}
函数InDegree(i,G)返回有向图G中顶点i的入度
int InDegree(int i,Graph G)
{
int sum=0;
for(int j=0;j<G->n;j++)
{
if(GraphExists(i,j,G))
sum++;
}
return sum;
}
广度优先搜索
//用邻接矩阵实现的无向图G中的广度优先搜索算法bfs描述
bfs(G,i)
{
/*从顶点v开始,广度优先搜索图G的算法*/
标记顶点i;
用顶点i初始化顶点队列Q;
while(!QueueEmpty(Q))
{
i=DeleteQueue(Q);
设j是i的邻接顶点;
while(j)
{
if(j未标记)
{
标记顶点j;
EnterQueue(j,Q);
}
j=i的下一个邻接顶点
}
}
}
===================================================================
//具体实现时,用一个数组pre来记录搜索到的顶点的状态。初始时对所有顶点v有pre[v]=0。
//用一全局变量cnt记录算法对图中顶点的访问次序。算法结束后,数组pre[i]中的值是算法访问顶点i的序号。
void bfs(Graph G,int i)
{
Queue Q=QueueInit();
EnterQueue(i,Q); //顶点先入队
while(!QueueEmpty(Q))
{
if(pre[i=DeleteQueue(Q)]==0) //未访问过
{
pre[i]=cnt++; //访问该结点并标记
for(int j=1;j<=G->n;j++)
{
if(G->adj[i][j]&&pre[j]==0) //若边存在且未访问过,入队
EnterQueue(j,Q);
}
}
}
}
//图中可能不止一个连通分量,遍历全图算法如下
void GraphSearch(Graph G)
{
cnt=1;
for(int i=1;i<=G->n;i++) pre[i]=0;
for(int i=1;i<=G->n;i++)
{
if(pre[i]==0)bfs(G,i); //对每个连通分量调用一次bfs
}
}
//天勤上面用邻接表表示图的bfs写法
int visit[MAXSIZE]=0; //访问状态数组
void bfs(Graph *G,int v)
{
ArcNode *p;
visit[v]=1;
//Visit(v);
Queue<int> q;
q.push(v);
while(!IsEmpty(q))
{
int t=q.top();
q.pop();
p=G->adj[t].firstarc; //p指向当前结点第一条边
while(p)
{
if(visit[p->adjvex]==0)
{
visit[p->adjvex]=1;
//Visit(p->adjvex);
q.push(p->adjvex); //若当前邻接顶点未被访问过,则置为已访问,并压入队列
}
p=p->nextarc; //否则,p指向当前结点的下一条邻边
}
}
}
void BFS(Graph *G)
{
for(int i=0;i<G->n;i++)
if(visit[i]==0)
bfs(G,i);
}
深度优先搜索
//用邻接矩阵实现的无向图G中的深度优先搜索算法dfs如下
void dfs(Graph G,int i)
{
pre[i]=cnt++; //初始化时所有结点pre[i]=0,当pre[i]不为0使表示该结点已访问过,pre[i]的值为算法访问结点i的序号
for(int j=1;j<=G->n;j++) //顺序遍历与结点i相邻的结点,
{
if(G->[i][j]) //若边<i,j>存在
{
if(pre[j]==0)dfs(G,j); //若结点j还未被访问过,对j递归调用dfs,若访问过,j++探测下一个相邻结点
}
}
}
//用邻接表实现的有向图G中的深度优先搜索算法dfs如下
void dfs(Graph G,int i)
{
glink p;
pre[i]=cnt++;
for(p=G->adj[i];p;p=p->next)
{
if(pre[i]==0)dfs(G,p->v); //若p指向的结点已访问过,结束当前层次的递归,返回上一层递归,执行p=p->next探测下一个相邻结点
}
}
void GraphSearch(Graph G)
{
cnt=1;
for(int i=1;i<=G->n;i++) pre[i]=0;
for(int i=1;i<=G->n;i++) if(pre[i]==0) dfs(G,i);
}
//天勤上面用邻接表表示图的dfs写法
int visit[MAXSIZE]=0; //全局变量用来保存访问状态
int dfs(Graph *G,int v)
{
ArcNode *p;
visit[v]=1; //置当前结点为已访问
Visit(v);
p=G->adj[v].firstarc; //p指向顶点v的第一条边
while(p)
{
if(visit[p->adjvex])==0 //若当前结点未被访问过,则往深处继续访问
{
dfs(G,p->adjvex)
}
p=p->nextarc; //否则检测下一个与v邻接的结点
} //执行到这一步时,所有与v邻接的结点已经检查完毕,该层dfs也执行到了最后一句,自动返回上一层递归
}
void DFS(Graph *G)
{
for(int i=0;i<G->n;i++)
if(visit[i]==0)
dfs(G,i);
}
//天勤上判断图G是否是树的算法,基于dfs
int vc=0,ec=0; //定义全局变量vc和ec,就不用在函数里面传了
int visit[MAXSIZE]=0;
void dfs(Graph *G,int v)
{
ArcNode *p;
visit[v]=1; //置当前结点为已访问
vc++; //结点数自增1
p=G->adj[v].firstarc;
while(p)
{
if(visit[p->adjvex]==0) //若当前顶点的邻接点未被访问过,则访问它,并对ec自增1
{
ec++;
dfs(G,p->adjvex);
}
p=p->next;
}
}
int IsTree(Graph *G)
{
dfs(G,1);
if(vc==G->n&&(ec/2)==G->e) //若vc等于图中顶点数,ec等于图中边数的2倍
return 1;
return 0;
}
//由于每次访问顶点ec都会累加上之前所有与访问过的结点关联的边数,因此遍历完之后,相当于ec中的数值是图的度数,(一条边2个度),因此ec/2就是访问过的边数。
最短路径
Dijkstra算法(单源最短路径)
算法思想:
设置一个顶点集合S,并不断作贪心扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已经。初始时,S中仅含源。设u是G的某个顶点,把从源到u且中间只经过S中顶点的路径成为源到u的特殊路径,并用数组dist来记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路径长度的顶点u,将u添加到集合S中,同时对数组dist作必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到其他所有顶点之间的最短路径长度。
算法描述:
步骤1:初始化dist[v]=a[s][v]
对于所有与s邻接的顶点v置pre[v]=s;
对于其他顶点u置pre[u]=0;
建立表L包含所有pre[v]≠0的顶点v //即L中初始时都是与源s直接相邻的顶点
步骤2:若表L空,则算法结束,否则转步骤3
步骤3:从表L中取出dist值最小的顶点v //即与源s最近的顶点
步骤4:对于顶点v的所有邻接顶点u置dist[u]=min{dist[u],dist[v]+a[v][u]}
若dist[u]改变,即加入顶点v后特殊路径更短,则置pre[u]=v;且若u不在表L中,将u加入L;转至步骤2
//在用邻接矩阵实现的赋权有向图中,单源最短路径问题的Dijkstra算法实现如下
void Dijkstra(int s,WItem dist[],int prev[],Graph G)
{
int i,j;
List L=ListInit(); //初始化L表
if(s<1||s>G->n) Error("Out of bounds") //处理非法输入
/*初始化dist,prev和L*/
for(int i=1;i<=G->n;i++)
{
dist[i]=a[s][i]; //顶点i到源点s的初始距离为a[s][i]的值
if(dist[i]==G->NoEdge) prev[i]=0; //若顶点i与源点s之间不存在边,则置i的前驱结点为0
else{prev[i]=s;ListInsert(0,i,L);} //否则,置i的前驱结点为s,并将顶点i加入L表,参数啥意思?
}
dist[s]=0; //源点s的前驱为0
/*修改dist和prev*/
while(!ListEmpty(L))
{
/*找L中具有最小dist值得顶点v*/
/*将顶点v从表L中删除,并修改dist的值*/
i=ListDelMin(L,dist); //顶点i是L表中所有已知特殊路径长度的结点中路径最短的
for(int j=1;j<=G->n;j++) //遍历所有与顶点i相邻的顶点
{
if(G->a[i][j]!=G->NoEdge&&(!prev[j]||dist[j]>dist[i]+G->a[i][j])) //若边(i,j)存在,且加入结点i之后,结点j到源点s的距离缩短
{
/*dist减少*/
dist[j]=dist[i]+G->a[i][j]; //更新结点j到源点s的特殊路径距离
/*顶点j插入表L*/
if(!prev[j]ListInsert(0,j,L)) //结点j的特殊路径长度已知,加入表L
prev[j]=i; //更新结点j的前驱
}
}
}
}
//天勤上的写法,我觉得更好理解一些
void Dijkstra(Graph G,int v,int dist[],int path[]) //dist是v到某点vu的距离,path是vu到v最短路径上前驱结点的下标,dist是一维数组,因为将它看作二维数组时,有一维是不变的,下标都是v
{
int sex[MAXSIZE]; //set记录顶点是否被加入路径,取值为1则为加入,0为未加入
/*以下是对各数组进行初始化*/
for(int i=0;i<G.n;i++)
{
dist[i]=G.edges[v][i];
path[i]=v;
set[i]=0;
}
set[v]=1;
/*初始化完毕*/
/*以下是找到剩余结点中与已生成的路径距离最短的结点*/
int min=INF;
for(int i=0;i<G.n;i++)
{
if(set[i]==0&&dist[i]<min)
{
u=i;
min=G.dist[i];
}
}
set[u]=1;
/*找到该结点后,加入路径中*/
/*加入顶点u到路径中之后,更新dist和path*/
for(int i=0;i<G.n;i++)
{
if(set[i]==0&&dist[u]+G.edges[u][i]<dist[i])
{
dist[i]=dist[u]+G.edges[u][i];
path[i]=u; //u为i在路径上的前驱
}
}
}
Floyd算法(任意两点最短路径)
算法思想:
设置一个矩阵c,初始时c[i][j]=a[i][j]。然后再矩阵c上做n次迭代,经第k次迭代后,c[i][j]的值是从顶点i到顶点j,且中间不经过编号大于k的顶点的最短路径长度。在c上做第k次迭代时,c[i][j]=min{c[i][k]+c[k][j],c[i][j]},要计算c[i][j]只要比较当前c[i][j]与c[i][k]+c[k][j]的大小。当前c[i][j]的值表示从顶点i到j,中间顶点编号不大于k-1的最短路径长度;而c[i][k]+c[k][j]表示从顶点i到k,再从k到j,且中间不经过顶点编号大于k的顶点的最短路径长度。
二维数组path用来记录最短路径。当k是算法中使c[i][j]取得最小值的整数时,就置p[i][j]=k。当path[i][j]=0时,表示从顶点i到j的最短路径就是从i到j的边。在计算出c[i][j]的值后,容易由path记录的信息,找出相应的最短路径。
void Floyd(WItem **c,int **path,Graph G)
{
/*初始化c[i][j]*/
for(int i=1;i<=G->n;i++)
for(int j=1;j<=G->n;j++)
{
if(i==j)
c[i][j]=0;
else
{
c[i][j]=G->a[i][j];
path[i][j]=0; //path=0表示当前顶点i到顶点j的最短路径就是它们之前的边
}
}
for(int k=1;k<=G->n;k++)
for(int i=1;i<=G->n;i++)
for(int j=1;j<G->n;j++)
{
if(c[i][k]!=NoEdge&&c[k][j]!=NoEdge&&(c[i][j]==NoEdge||c[i][j]>c[i][k]+c[k][j]))
//确保加入的顶点k与顶点i,顶点k与顶点j之前是可达的,当顶点i到顶点j无边或小于顶点i到k加上顶点k到j的距离时,更新顶点i到j的路径长度
{
c[i][j]=c[i][k]+c[k][j];
p[i][j]=k; //加入的点k是使c[i][j]取最小值的数
}
}
}
最小生成树
Prim算法 时间复杂度0(n^2)
算法思想:
首先设S={1},然后,只要S是V的真子集(就是还没有包含所有结点),就作如下的贪心选择:选取集合S中的一点i,V-S中的一点j,且a[i][j]最小的边,并将顶点j添加到集合S中。这个过程一直进型到S=V时为止。(不需要判断是否有回路,因为边的两个顶点来自不同集合)
如何找出满足条件的i和j,需要设置两个数组closet和lowcost。对于一个V-S中的顶点j,closest[j]是j在S中的一个邻接顶点,它与j在S中的其他邻接顶点k相比较都有a[j][closest[j]]<=a[j][k]。lowcost[j]的值就是a[j][closest[j]]
在Prim算法执行过程中,先找出V-S中使lowcost最小的顶点j,然后根据数组closest选取边(j,closest[j]),最后将j添加到S中,并对closest和lowcost作必要的修改。
void Prim(WItem *lowcost,int *closest,Graph G)
{
/*初始化*/
int *s; //集合S,值为1表示在集合中,为0表示不在集合中
s=malloc((G->n+1)*sizeof(int));
for(int i=1;i<=G->n;i++)
{
lowcost[i]=a[1][i]; //先设置顶点1在S中
closest[i]=1;
s[i]=0; //除1以外所有点都不在集合S中
}
s[1]=1; //把顶点1加入集合S
for(int i=1;i<=G->n;i++) //执行n次
{
min=G->NoEdge; //要找出最小值,先把min设为最大
j=1; //j用来记录到集合S距离最短的顶点序号,一开始先设为1作为初始值
for(int k=2;k<=G->n;k++) //k=2是因为一开始只有1在S中,V-S要从2开始
{
if(lowcost[k]<min)&&(!s[k]) //k到集合S最短距离小于min且k不在集合S中,即k在V-S中
{
min=lowcost[k]; //更新最小值,j为当前V-S中里S最近的顶点,现在令j为k
j=k;
}
}
//此时j是到集合S距离最短的顶点序号
s[j]=1; //把j加入集合S
for(int k=2;k<=G->n;k++) //j加入集合S后,要修改原来的closest和lowcost
{
if((G->a[k][j]<lowcost[k])&&(!s[k])) //若K不在集合S且顶点j和k之间距离小于j加入集合S前顶点k到集合S的最短距离
{
//更新closest,lowcost
closest[k]=j;
lowcost[k]=G->a[k][j]
; }
}
}
}
//V-S中的顶点为焦点向S中的顶点逐个连线找最短,j是通过k来更新的
//利用closest还能找到最短路径
//while(k>0)
//p=closest[k];
//k=p;
Kruskal算法 时间复杂度O(eloge)
首先将G的n个顶点看成n个孤立的连通分支。将所有的边按权从小到大排序,从第一条边开始,依边权递增的顺序查看每一条边,当查看到第k条边(v,w)时,若端点v和w分别是当前两个不同的连通分支T1和T2的顶点时,就用边(v,w)将T1和T2连接成一个连通分支,然后继续查看第k+1条边,否则直接查看第k+1条边,这个过程一直进行到只剩下一个连通分支时为止。
上述构造最小生成树的Kruskal算法需要按权的递增顺序查看图G的所有边。为此需要将G的所有边按边权值排序。存储每条边的结构定义为:
typedef struct edge
{
int u;
int v;
WItem w;
}Edge;
/*函数EDGE(u,v,w)创造一条权威w的边(u,v)*/
Edge EDGE(int u,int v,WItem w)
{
Edge e;
e->u=u;
e->v=v;
e->w=w;
return e;
}
/*函数Edges(a,G)抽取图G的所有边到赋权边数组a中,并返回图G的边数*/
int Edges(Edges a[],Graph G)
{
int k=0;
for(int i=1;i<=G->n;i++)
for(int j=1;j<=G->n;j++)
if(G->a[i][j]!=G->NoEdge)
a[k++]=EDGE(i,j,G->a[i][j]);
return k;
}
void Kruskal(Edge mst[],Graph G)
{
Edge a[maxE];
UFset U; //并查集u
int e=Edges(a,G); //抽取G的所有边,e为边数
quicksort(a,0,e-1); //对边数组a排序,参数意义为对数组a从元素0到元素e-1进行排序
U=UFinit(G->n); //初始化并查集U
for(int i=0,int k=0;i<e&&k<G->n-1;i++) //k记录循环次数,执行n-1次结束
{
int s=UFind(a[i].u,U); //a[i].u是当前最短边的一个端点,找出它所属的连通分支
int t=UFind(a[i].v,U); //a[i].v是当前最短边的另一个端点,找出它所属的连通分支
if(s!=t) //若s和t不是同一个连通分支
{
mst[k++]=a[i]; //更新最小生成树
UFunion(s,t,U); //合并连通分支s和t
}
}
}