一、图的存储结构
1、邻接矩阵
用邻接矩阵表示法表视图,出了一个用于存储邻接矩阵的二维数组外,还需要用一个一位数组来存储顶点信息。
#define MaxInt 32767 //表示极大值
#define MVNum 100 //最大顶点数
typedef char VerTexType; //假设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型
typedef struct{
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前顶点数和变数
}AMGraph;
采用邻接矩阵创建无向图:
Status CreateUDN(AMGraph &G){
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(int i=0;i<G.vexnum;i++) //依次输入点的信息
cin>>G.vexs[i];
for(int i=0;i<G.vexnum;i++) //初始化邻接矩阵,边的权值均置为最大值
for(int j=0;j<G.vexnum;j++)
G.arcs[i][j]=MaxInt;
for(k=0;k<G.arcnum;k++) //构造邻接矩阵
{
cin>>v1>>v2>>w; //输入一条边依附的顶点及权值
i=LocateVex(G,v1); //确定v1,v2在G中的位置,即顶点数组的下标
j=LocateVex(G,v2);
G.arcs[i][j]=w; //变<v1,v2>的权值为w
//若为无向图则<v2,v1>的权值即<v1,v2>的权值,可以直接赋值
//若为有向图则必须不相同,需要再输入<v2,v1>的权值
G.arcs[j][i]=G.arcs[i][j];
}
return OK;
}
int LocateVex(AMGraph &G,VexTexType v){
for(int i=0;i<G.vexnum;i++){
if(G.vexs[i]==v)
return i;
}
return MaxInt;
}
2、邻接表
邻接表是图的一种链式存储结构。在邻接表中,对图中每一个顶点Vi建立一个单链表,把与Vi相邻接的顶点放在这个链表中,把与Vi相邻接的顶点放在这个链表中。邻接表中每个单链表的第一个结点存放有关顶点的信息,把这个看成链表的表头,其余结点存放有关边的信息,这样邻接表便有两部分组成:表头结点表和边表。
表头结点表:有所有表头结点一以顺序的形式存储,以便可以随机访问任一顶点的边链表。表头结点由数据域和链域两部分组成。
边表:由表示图中顶点间关系的2n个边链表组成。边链表中边结点包括:邻接点域、数据域和链域。邻接点域指示与顶点Vi邻接的点在图中的位置;数据域存储和边有关的信息,如权值等;链域指示与顶点Vi邻接的下一条边的结点。
#define MVNum 100; //最大顶点个数
typedef struct ArcNode //边结点
{
int adjvex; //该边所指向的顶点的位置
struct ArcNode *nextarc; //指向下一条边的指针
OtherInfo info; //和边相关的信息
}ArcNode;
typedef struct VNode //表头结点
{
VerTexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的边的指针
}VNode,AdjList[MVNum]; //AdjList表示邻接表类型
typedef struct //邻接表
{
AdjList vertices;
int vexnum,arcnum; //图当前顶点数和边数。
}
采用邻接表创建无向图:
status CreateUDG(ALGraph &G){
cin>>G.vexnum>>G.arcnum; //输入中顶点数和总边数
for(int i=0;i<G.vexnum;i++){
cin>>G.vertices[i].data; //输入顶点信息
G.vertices[i].firstarc=NULL; //初始化表头结点的指针域为NULL
}
for(k=0;k<G.arcnum;k++){ //输入各边,构造邻接表
cin>>v1>>v2; //输入边
i=LocateVex(G,v1); //确定边依附结点的位置
j=LocateVex(G,v2);
p1=new ArcNode; 生成一个新的边结点*p1
p1->adjvex=j; //邻接点序号为j
p1->nextarc=G.vertices[i].firstarc;
G.vertices[i].firstarc=p1;
//若为无向图需要把j->i的也表示出来,若为有向图则不用
p2=new ArcNode;
p2->adjvex=i;
p2->nextarc=G.vertices[j].firstarc;
G.vertices[j].firstarc=p2;
}
return OK;
}
int Locatevex(ALGraph &G,ArcNode v)
{
for(int i=0;i<G.vexnum;i++){
if(G.vertices[i].data==v)
return i;
}
return -1;
}
二、图的遍历
图的遍历是求解图的连通性问题、拓扑排序和关键路径的算法的基础。
图的遍历要比树的遍历复杂的多。因为图的任一顶点都可能和其余的顶点相邻接。所以访问某个顶点之后,可能沿着某条路径搜索之后,又回到该顶点上。为此,需要设一个辅助数组visited[n],其初始值置为‘false’或者0,一旦访问了该顶点,便置vistited[i]为“true”或1.
1、深度优先搜索(DFS)
(1)连通图的遍历
深度优先搜索类似于树的先序遍历,是树先序遍历的推广。
算法实现:
1、 从图中某个顶点V出发,访问V,并置visited[v]的值为true。
2、 依次检查v的所有邻接点w,如果visited[w]的值为true的值false,再从w出发进行递归遍历,直到图中所有的顶点都被访问过。
邻接矩阵的深度优先搜索:
void DFS_AM(AMGraph G.int v)
{
cout<<v; //输出v,并置v为的访问标志组visited[v]=true.
visited[v]=true;
for(w=0;w<G.vexnum;w++) //依次检查邻接矩阵v所在的行
if(G.arcs[v][w]!=0&&visited[w]!=true) //若是邻接点并未被访问则递归
DFS_AM(G,w);
}
邻接表的深度优先搜索:
void DFS_AL(ALGraph G,int v)
{
cout<<v; //输出v,并置访问标志组visited[v]为true。
visited[v]=true;
p=G.vertices[v].firstarc; //p指向v的边链表的第一个边结点
while(p!=NULL){
w=p->adjvex; //表示w是v的邻接点
if(visited[w]!=true) //如果w为为被访问,则递归调用DFS_AL
DFS_AL(G,w);
p=p->nextarc; //p指向下一个边结点
}
}
(2)非连通图的遍历
上面的算法是连通图的遍历,输入一个结点从这个结点开始遍历,只能遍历该结点所在的连通分支,其他连通分支无法便利,所以只适用于连通图的遍历。
若要对非连通图进行遍历需要循环调用上面的算法,(注意:对非连通图的遍历就不能指定从哪一个结点开始了,因为指定一个结点只能遍历该结点所在的连通分支)
非连通图的遍历:
void DFS(Graph G)
{
for(int i=0;i<G.vexnum;i++) //将所有访问标志组置为false
{
visited[v]=false;
}
sum=0;
for(int i=0;i<G.vexnum;i++) //循环遍历所有的结点
{
if(visited[i]!=true){ //若该结点没有被访问,则从该结点开始遍历,调用上面的算法
sum++;
//若为临界矩阵非连通的遍历,则调用邻接矩阵的遍历算法
DFS_AM(G,i);
//若为邻接表的非连通图的遍历,则调用邻接表的遍历的算法
DFS_AL(G,i);
}
}
cout<<"该图的连通分支数:"<<sum;
}
此外,因为从一个点遍历就可以遍历完该点所在的连通分支,所以这个算法还可以计算图的连通分支数:
在第二个循环里,if条件进入多少次就说明有多少个连通分支,即遍历算法调用了多少次就有多少连通分支。
2、广度优先搜索(BFS)
广度优先搜索遍历类似于树的按层次遍历的过程。
算法步骤:
- 从图中某个顶点v出发,访问v,并置visited[v]为true.然后将v进队
- 只要队列不空,则重复下述操作:
- 队头顶点u出队;
- 依次检查u所有的邻接点w,如果visited[w]的值为false,则访问w,并置w的值为true,然后w进队
(1)连通图的遍历
邻接矩阵连通图的的广度优先搜索
void BFS(AMGraph G,int v)
{
cout<<v; //访问第v个顶点,并置访问标志数组相应分量值为true。
visited[v]=true;
InitQueue(Q); //初始化队列Q,为空
EnQueue(Q,v); //v进队
while(!QueueEmpty(Q)) //队列非空
{
DeQueue(Q,u); //队头元素出队并置为u
for(int i=0;i<G.vexnum;i++)
if(visited[i]!=true&&G.arcs[u][i]!=0) //如果i未被访问,并且与u相邻
{
cout<<i; //输出i
visited[i]=true; //置访问标记符尾true
EnQueue(Q,i); //i进队
}
}
}
邻接表连通图的广度优先搜索:
void BFS_AL(ALGraph G,int v)
{
cout<<v; //访问第v个顶点,并置访问标志数组相应分量值为true。
visited[v]=true;
InitQueue(Q); //初始化队列Q,为空
EnQueue(Q,v); //v进队
while(!QueueEmpty(Q)) //队列非空
{
DeQueue(Q,u); //队头元素出队并置为u
p=G.vertices[u].firstarc; //将p置为u的第一个邻接点
while(p!=NULL){ //p不为NULL就循环
w=p->adjvex; //记录下该结点的位置序号
if(visited[w]!=true){ //如果该结点未被访问过
cout<<w; //输出该结点
visited[w]=true; //置访问标志为true
EnQueue(Q,w); //将该结点进队
}
p=p->nextarc; //指向下一个邻接点
}
}
}
(2)非连通图的广度优先搜索
非连通图的广度优先搜索的实现类似于非连通图的深度优先搜索,只需要将调用的深度优先搜索算法改为广度优先搜索算法即可
void BFS(Graph G)
{
for(int i=0;i<G.vexnum;i++) //将所有访问标志组置为false
{
visited[v]=false;
}
sum=0;
for(int i=0;i<G.vexnum;i++) //循环遍历所有的结点
{
if(visited[i]!=true){ //若该结点没有被访问,则从该结点开始遍历,调用上面的算法
sum++;
//若为临界矩阵非连通的遍历,则调用邻接矩阵的遍历算法
BFS_AM(G,i);
//若为邻接表的非连通图的遍历,则调用邻接表的遍历的算法
BFS_AL(G,i);
}
}
cout<<"该图的连通分支数:"<<sum;
}
同理,也可以得到连通分支数。
三、最短路径
1、从某个单源点到其余各顶点的最短路径——迪杰斯特拉算法
迪杰斯特拉算法的的实现要求以下的辅助数据结构:
(1)一维数组S[i]:记录从源点V0到终点Vi是否已被确定最短路径长度,true表示确定,false表示尚未确定。
(2)一维数组Path[i]:记录从源点V0到Vi的当前最短路径上Vi的直接前驱顶点序号。其初始值为:如果从V0代Vi有弧,则 Path[i]=V0,否则为-1。
(3)一维数组D[i]:记录从源点V0到终点Vi的当前最短路径长度。其初始值为:如果从V0到为Vi有弧,则D[i]为弧上的权值,都则为极大值。
算法步骤:
1、初始化:
将源点V0加到S中,即S[V0]=true。
将V0到各个顶点的最短路径长度初始化为权值,即D[i]=G.arcs[V0][Vi].
如果V0和Vi之间有弧,则将Vi的前驱置为V0,即Path[Vi]=V0,否则Path[Vi]=-1;
2、循环n-1次,执行一下操作:
选择下一条最短路径的终点Vk,使的:D[k]=Min{D[i] | i属于未被确定的顶点};
将Vk加到S中,即S[Vk]=true;
根据条件更新从V0出发到集合V-S上上任一顶点的最短路径的长度,若条件D[K]+G.arcs[k][i]<D[i]成立,则更新D[i]=D[k]+G.arcs[k][i],同时更改Vi的前驱为Vk,Path[i]=K.
void ShortestPath_DIJ(AMGraph G,int V0)
{
for(int i=0;i<G.vexnum;i++) //n个顶点依次初始化
{
S[i]=false; //S初始化为空集
D[i]=G.arcs[v0][i]; //将V0到各个终点的最短路径长度初始化为弧上的权值
if(D[i]!=MaxInt) //如果V0和i之间有弧,则将i的前置初始化为V0
Path[i]=V0;
else //如果V0和i之间无弧,则将i的前置初始化为-1
Path[i]=-1;
}
S[V0]=true; //将V0加入S
D[V0]=0; //V0到自己的最短路径为0
//------------初始化结束,开始主循环,每次求得V0到某个顶点V的最短路径,将V加入S集合---
for(int i=1;i<G.vexnum;i++) //对其余N-1个顶点,依次进行计算
{
int min=MaxInt; //用来求出一个当前最短的路径确定其终点
int locate; //记录终点的位置序号
for(int j=0;j<G.vexnum;j++) //循环n次
{
if(S[j]!=true&&D[j]<min) //找出未被确定的且路径最小的顶点
{
min=D[j];
locate=j; //记录其位置序号
}
}
S[locate]=true; //将该顶点加入S,即确定其最短路径
for(int j=0;j<G.vexnum;j++) //更新从V0出发到集合V-S上所有顶点的最短路径长度
{
if(S[j]!=true&&D[j]>D[locate]+G.arcs[locate][j])
{
D[j]=D[locate]+G.arcs[locate][j]; //更新D[j]
Path[j]=locate; //更新j的前驱为locate
}
}
}
}
迪杰斯特拉算法就是先初始化,然后N-1次循环,每次循环线确定当前最短的一条路径,将其终点加入S确定下来,然后由于确定了一个最短路径,需要更新其余的为确定的顶点的当前最短路径,如经过刚刚确定的顶点更短则更新他的当前最短路径D和其直接前驱Path。
2、每一对顶点之间的对短路径——弗洛伊德算法(Floyd算法)
Floyd算法
四、最小生成树
1、普里姆算法(Prim)——归并顶点,与边无关,适用于稠密图