数据结构C语言 Part6 图

版权声明:版权所有,欢迎相互学习与交流 https://blog.csdn.net/qq_42229034/article/details/84729011

学习目标:

1.掌握:图的基本概念及相关术语和性质

2.熟练掌握:图的邻接矩阵和邻接表两种存储表示方法

3.熟练掌握:图的两种遍历方法DFS和BFS

4.熟练掌握:最短路径算法(Dijkstra算法)

5.掌握:最小生成树的两种算法及拓扑排序算法的思想

基本概念储备:

Graph=(V,E),and graph is generally divided into directed and undirected graph,according to the property of edge。 

完全图:任意两个点都有一条边相连。对于有向无向图其Number of edges can be solved。

稀疏图:有很少边或弧的图。 稠密图:有较多边或弧的图。

网:边/弧带的图。

路径:接续的边构成的顶点序列。 路径长度:路径上边或弧的数目/权值之和。

回路(环):第一个顶点和最后一个顶点相同的路径。

简单路径:顶点均不重复的路径。

简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径。

强连通图:在无(有)向图G=( V, {E} )中,若对任何两个顶点 v、u 都存在从v 到 u 的路径,则称G是连通图(强连通图)。

极大连通子图 & 极小连通子图

图的存储结构:

顺序存储结构:邻接矩阵数组表示法

链式存储结构:邻接表链式表示法

无向图的邻接矩阵一定是对称的.并且对于顶点i而言,它的度等于第i列或者第i行中1的个数。

为什么我们用无穷来代表无边呢?某萌妹如是说:

至于采用邻接矩阵构造无向网(带权),我们的算法分四步 1)输入总顶点数和总边数   2)依次输入顶点的信息存入顶点表中 3)初始化邻接矩阵,权值赋值为极大值,比如Limit.h的INT_MAX 4)构造邻接矩阵。

//邻接矩阵的构造方式:
#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)
{ 
    //采用邻接矩阵表示法,创建无向网G 
    scanf(“%d”,&G.vexnum);
     scanf(“%d”,&G.arcnum); 	//输入总顶点数,总边数 
    for(i = 0; i<G.vexnum; ++i)    
      scanf(“%d”,&G.vexs[i]);                        	//依次输入点的信息 
    for(i = 0; i<G.vexnum;++i) 	//初始化邻接矩阵,边的权值均置为极大值
       for(j = 0; j<G.vexnum;++j)   
         G.arcs[i][j] = MaxInt;   
    for(k = 0; k<G.arcnum;++k){                     //构造邻接矩阵 
      scanf(“%d, %d, %d”, &v1,&v2,&w);    //输入一条边依附的顶点及权值 
      i = LocateVex(G, v1);  j = LocateVex(G, v2);  //确定v1和v2在G中的位置
      G.arcs[i][j] = w; //边<v1, v2>的权值置为w 
      G.arcs[j][i] = G.arcs[i][j];              //置<v1, v2>的对称边<v2, v1>的权值为w 
   }//for 
   return OK; 
}//CreateUDN 

int LocateVex(MGraph G,VertexType u)
 {//存在则返回u在顶点表中的下标;否则返回-1
   int i;
   for(i=0;i<G.vexnum;++i)
     if(u==G.vexs[i])
       return i;
   return -1;
 }

然后我们看看邻接表的链式表示方法:

邻接表的链式存储表示:

算法思想:1)输入总顶点数和总边数 2)依次输入各个点的信息存入顶点表中,每个表头节点的指针域初始化为NULL 3)创建邻接表

#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;              		//图的当前顶点数和边数 
}ALGraph; 

Status CreateUDG(ALGraph &G){ 
  //采用邻接表表示法,创建无向图G 
   scanf(“%d, “%d”, &G.vexnum,&G.arcnum);               	//输入总顶点数,总边数 
    for(i = 0; i<G.vexnum; ++i){          	//输入各点,构造表头结点表 
       scanf(“%d”,&G.vertices[i].data);           	//输入顶点值 
       G.vertices[i].firstarc=NULL;       	//初始化表头结点的指针域为NULL 
    }//for 
    for(k = 0; k<G.arcnum;++k){        		//输入各边,构造邻接表 
       scanf(“%d, %d”, &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;  
      //将新结点*p1插入顶点vi的边表头部 
      p2=new ArcNode; //生成另一个对称的新的边结点*p2 
    p2->adjvex=i;                   			//邻接点序号为i 
    p2->nextarc= G.vertices[j].firstarc;  G.vertices[j].firstarc=p2;  
      //将新结点*p2插入顶点vj的边表头部 
    }//for 
    return OK; 
}//CreateUDG 

十字链表:略

图的遍历:从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。

遍历实质:找每个顶点的邻接点的过程。

图的特点:图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。

如何避免重复访问?:解决思路:设置辅助数组 visited [n ],用来标记每个被访问过的顶点。 初始状态为0 i 被访问,改 visited [i]为1,防止被多次访问

DFS:深度优先算法

DFS算法设计:

void DFS(AMGraph G, int v)	//图G为邻接矩阵类型 
{        	
  printf(“%d”, v);  visited[v] = true;  		//访问第v个顶点
  for(w = 0; w< G.vexnum; w++)  	//依次检查邻接矩阵v所在的行  
        if((G.arcs[v][w]!=0)&& (!visited[w]))  
            DFS(G, w); 
      //w是v的邻接点,如果w未访问,则递归调用DFS 
} 
void DFS(ALGraph G, int v){        		//图G为邻接表类型 
  printf(“%d”,v);  visited[v] = true;    		//访问第v个顶点
  p= G.vertices[v].firstarc;     //p指向v的边链表的第一个边结点 
while(p!=NULL){              	//边结点非空 
  w=p->adjvex;               	//表示w是v的邻接点 
  if(!visited[w])  DFS(G, w); 	//如果w未访问,则递归调用DFS 
  p=p->nextarc;                	//p指向下一个边结点 
 } 
} 

BFS:广度优先搜索

简单归纳: 在访问了起始点v之后,依次访问 v的邻接点; 然后再依次访问这些顶点中未被访问过的邻接点; 直到所有顶点都被访问过为止。

广度优先搜索是一种分层的搜索过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有回退的情况。 因此,广度优先搜索不是一个递归的过程,其算法也不是递归的。

(1)从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后将v进队。

(2)只要队列不空,则重复下述处理。

           ① 队头顶点u出队。  

           ② 依次检查u的所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true,然后将w进队。

void BFS (Graph G, int v)
{ 
    //按广度优先非递归遍历连通图G 
    printf(“%d”, v); visited[v] = true;     		//访问第v个顶点
    InitQueue(Q);              			//辅助队列Q初始化,置空         
    EnQueue(Q, v);            			//v进队 
    while(!QueueEmpty(Q)){   		//队列非空 
       DeQueue(Q, u);        			//队头元素出队并置为u 
       for(w = FirstAdjVex(G, u); w>=0; w = NextAdjVex(G, u, w)) 
       if(!visited[w]){               	//w为u的尚未访问的邻接顶点 
             printf(“%d”, w); visited[w] = true;	EnQueue(Q, w); //w进队 
          }//if 
    }//while 
}//BFS 

如何求最小生成树 ?

Prim算法: 归并顶点,与边数无关,适于稠密网

Kruskal算法:归并边,适于稀疏网

补充:

贪心算法原理:以当前情况为基础作最优选择,而不考虑各种可能的整体情况,所以贪心法不要回溯。  算法优点:因为省去了为寻找解而穷尽所有可能所必须耗费的大量时间,因此算法效率高。 注意:贪婪算法的精神就是“只顾如何获得眼前最大的利益”,有时不一定是最优解。比如找零钱是先用大面值再用小面值让我们情感上认为花的钞票的钞票数最小;又比如0-1背包问题.

最短路径问题:在带权有向图中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。

两种常见的最短路径问题:

一、 单源最短路径—用Dijkstra(迪杰斯特拉)算法 ,一个顶点到其余各个顶点

二、所有顶点间的最短路径—用Floyd(弗洛伊德)算法,任意两点之间

Dijkstra算法:

我们将带全网N=(V,E)分为两个集合,一个是S(已经求出的最短路径的终点集合),一个是V-S。

1.初始化:先找出从源点v0到各终点vk的直达路径(v0,vk),即通过一条弧到达的路径。

2.选择:从这些路径中找出一条长度最短的路径(v0,u)。

3.更新:然后对其余各条路径进行适当调整:

              若在图中存在弧(u,vk),且(v0,u)+(u,vk)<(v0,vk),

              则以路径(v0,u,vk)代替(v0,vk)。

              在调整后的各条路径中,再找长度最短的路径,依此类推。

算法流程:

① 初始化:

●    将源点v0加到S中,即S[v0] = true;

●    将v0到各个终点的最短路径长度初始化为权值,即D[i] = G.arcs[v0][vi],(vi∈V − S);

●    如果v0和顶点vi之间有弧,则将vi的前驱置为v0,即Path[i] = v0,否则Path[i] = −1。

② 选择下一条最短路径的终点vk,使得:     D[k] = Min{D[i]|vi∈V − S}

③ 将vk加到S中,即S[vk] = true。

④ 更新从v0出发到集合V − S上任一顶点的最短路径的长度,同时更改vi的前驱为vk。 若S[i]=false 且 D[k]+G.arcs[k][i]<D[i],则D[i]=D[k]+ G.arcs[k][i]; Path [i]=k;。

⑤ 重复②~④ n − 1次,即可按照路径长度的递增顺序,逐个求得从v0到图上其余各顶点的最短路径。

上面的描述大可不看,看看算法代码会更加直观:

void ShortestPath_DIJ(AMGraph G, int v0){ 
    //用Dijkstra算法求有向网G的v0顶点到其余顶点的最短路径 
    n=G.vexnum;                    		//n为G中顶点的个数 
    for(v = 0; v<n; ++v){             	//n个顶点依次初始化 
       S[v] = false;                  	//S初始为空集 
       D[v] = G.arcs[v0][v];           	//将v0到各个终点的最短路径长度初始化 
       if(D[v]< MaxInt)  Path [v]=v0; //v0和v之间有弧,将v的前驱置为v0 
       else Path [v]=-1;               	//如果v0和v之间无弧,则将v的前驱置为-1 
      }//for 
      S[v0]=true;                    	//将v0加入S 
      D[v0]=0;                      		//源点到源点的距离为0 	
/*―开始主循环,每次求得v0到某个顶点v的最短路径,将v加到S集―*/ 
      for(i=1;i<n; ++i){               	//对其余n−1个顶点,依次进行计算 
        min= MaxInt; 
        for(w=0;w<n; ++w) 
          if(!S[w]&&D[w]<min)  
              {v=w; min=D[w];}         	//选择一条当前的最短路径,终点为v 
        S[v]=true;                   		//将v加入S 
        for(w=0;w<n; ++w) 	//更新从v0出发到集合V−S上所有顶点的最短路径长度 
        if(!S[w]&&(D[v]+G.arcs[v][w]<D[w])){ 
             D[w]=D[v]+G.arcs[v][w];   	//更新D[w] 
             Path [w]=v;              		//更改w的前驱为v 
        }//if 
    }//for       
}//ShortestPath_DIJ 

猜你喜欢

转载自blog.csdn.net/qq_42229034/article/details/84729011