八.图
①定义 图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
②特点 图最基本的单元是顶点Vertex,图就是由很多这样的顶点组成。线性表和树可以是空的,但是图不能是空的,图不允许没有顶点。
图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边类表示。
③图的术语
1.无向边: 若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi,vj)来表示。
2.无向图: 图中任意两个顶点之间的边都是无向边
3.有向边: 若从顶点vi到vj的边有方向,则称这条边为有向边,也叫弧 Arc,用有序偶对<vi,vj>来表示,其中vi为弧尾Tail,vj为弧头Head.
4.有向图: 图中任意两个顶点之间的边都是有向边。有向图由两个集合组成:顶点集合和顶点间的弧集合,即G2=(V2,{E2})
5.简单图: 图中不存在顶点到其自身的边,且同一条边不重复。
6.无向完全图: 任意两个顶点之间都存在边。
7.有向完全图: 任意两个顶点之间都存在方向互为相反的两条弧。
8.网: 图的边或者弧如果带有权值,则这个图叫网
9.邻接点: 两个点之间有连接则两个点互为邻接点
10.边与两个点相关联 : 两个点之间的边依附于两个点,也叫这个边与两个点相关联
11.度: 顶点V的度是指和这个顶点v相关联的边的数目TD(V)
12.邻接到/自: 弧<v,v'>,v邻接到v'; v'邻接自v
13.顶点的入度ID:指向v的弧的个数
14.顶点的出度OD:从v发出的弧的个数
15.路径:从一个点到另一个点的路径是一个顶点序列
16.回路/环: 第一个顶点到最后一个顶点相同的路径称为回路或环。
17.简单路径: 序列中顶点不重复出现的路径。
18.简单回路/简单环: 除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路。
④连通图术语
1.连通图: 在无向图中,如果任意两个顶点都是想通的,则称这个图为连通图
2.连通分量: 无向图中的极大连通子图:子图必须含有极大顶点数
3.强连通图: 在有向图中,两个点之间互相都存在路径。
4.有向图的强连通分量: 有向图中的极大强连通子图
5.连通图的生成树: 一个极小的连通子图,含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
6.有向树:一个有向图恰有一个顶点的入度为0,其余顶点的入度都是1. 其实为0的那个就是一根节点,其余入度为1说明其双亲只有一个。
7.有向图生成的森林 :由若干棵有向树组成,含有图中全部的顶点,但只有足以构成若干棵不相交的有向树的弧。
⑤图的边和顶点的数值关系
1.含有n个顶点的无向完全图有n*(n-1)/2条边。
2.一个图顶点数为n,边数为e,则无向图e<=n*(n-1)/2; 有向图e<=n*(n-1)
⑥图应该有的基本操作
创建图,销毁图, 返回图中某一个顶点的位置,返回图中某顶点的值,将图中顶点赋值,返回某顶点的一个邻接顶点,
返回顶点相对于另一个顶点的下一个邻接顶点,在图中增加新点,删除图中顶点和它的弧,在图中增加弧,
在图中删除弧,对图进行深度优先遍历,在遍历过程中对每一个顶点调用。对图进行广度优先遍历,遍历过程中对每个顶点调用。
⑦图的五种存储结构
1.邻接矩阵
①定义: 用两个数组表示图,一个一维数组存储图中顶点信息,一个二维数组存图的边或弧信息。
②特点: 二维数组中,某点的入度是该点所在列上各数之和。某点的出度是该点所在行的各数之和。
③代码表示
//邻接矩阵结构体表示 typedef char VertexType; //顶点的数值形式 typedef int EdgeType; //边上的权值 #define MAXVEX 100 //最大顶点数 #define INFINITY 65535 //代表无穷大 typedef struct { VertexType vexs[MAXVEX];//顶点表 EdgeType arc[MAXVEX][MAXVEX];//连接矩阵,就是边的表 int numVertexes,numEdges;//当前图的顶点数和边数 }MGraph; //利用邻接矩阵构造一个图 void CreateMGraph(MGraph *G){ int i,j,k,w; printf("输入顶点数和边数:\n"); scanf("%d,%d",G->numVertexes,G->numEdges);//输入顶点数和边数 for(i=0;i<G->numVertexes;i++) scanf(&G->vexs[i]); //输入numVertexes个顶点 for(i=0;i<G->numVertexes;i++) //邻接矩阵初始化,每个元素值都设置为无穷大 for(j=0;j<G->numVertexes;j++) G->arc[i][j]=INFINITY; for(k=0;k<G->numEdges;k++){ printf("输入边(vi,vj)上的下标i,下标j和权w \n"); scanf("%d,%d,%d",&i,&j,&w);//给两个点vi,vj之间的边arc[i][j]赋值为w G->arc[i][j]=w; G->arc[j][i]=G->arc[i][j];//对称原则,自动把另一半矩阵填完。 } }
2.邻接表:解决在边数少时邻接矩阵大量空间浪费的问题
①定义: 数组与链表相结合的存储方法,每个顶点后面都跟着一个链表,链表中的结点就是顶点依次排列的所有邻接点。数组存放所有n个顶点,n个链表存放每个顶点的邻接情况
②有向图的邻接表: 每个顶点后面跟着的链表中的元素依次是这个顶点为弧尾的弧的所有弧头。这样出度很明显,为了能得到这个顶点的入度,同时建立一个有向图的逆邻接表,就把弧方向都调头以后就可以很明显求入度了。
③代码表示
//邻接链表 //存放所有顶点的数组 typedef char VertexType;//顶点类型 typedef int EdgeType;//权值类型 typedef struct EdgeNode{//链表结点 int gindex;//本结点在数组中的下标 EdgeType weight;//权值 struct EdgeNode *next;//指向下一个结点的指针 }EdgeNode; //顶点数组中的结点 typedef struct VertexNode{ VertexType data;//顶点的值,即这个顶点是谁 EdgeNode *firstedge;//每个顶点后面都跟着一个链表,这个指针指向那个链表的第一个有效结点, //这个指针可以消除链表的空的头指针,使得链表全是有效指针,相当于顶点特殊的next指针 bool isVisited;//是否访问过 }VertexNode,AdjList[MAXVEX];//所有顶点放入一个数组中 //最终邻接链表图的结构 typedef struct{ AdjList adjList;//顶点都在数组中 int numVer,numEdg;//该图顶点总数和边总数 }GraphAdjList; //邻接表的创建方法 void CreateALGraph(GraphAdjList *G){ printf("输入该图顶点总数和边总数:\n"); scanf("%d,%d",G->numVer,G->numEdg);//输入顶点数和边数 int i,j,vi,vj,w; for(i=0;i<G->numVer;i++){ scanf("%d",&G->adjList[i].data); G->adjList[i].firstedge=NULL; } for(j=0;j<G->numEdg;j++){ printf("输入边vi,vj的两个顶点序号值,这个序号值就是上面输入时对应顶点的顺序值:\n"); scanf("%d,%d",vi,vj); //创建一个新节点存放vi为弧尾vj为弧头的边 EdgeNode* n=(EdgeNode*)malloc(sizeof(EdgeNode)); n->gindex=vj; n->next=G->adjList[vi].firstedge;//头插法,新结点的下一个结点是原来链表的第一个有效结点 G->adjList[vi].firstedge=n;//当前的新分配的结点成为了第一个有效结点 //对称的再创建一个新结点,存放vj为弧尾vi为弧头的边 EdgeNode* p=(EdgeNode*)malloc(sizeof(EdgeNode)); p->gindex=vi; p->next=G->adjList[vj].firstedge; G->adjList[vj].firstedge=p; } }
3.十字链表:邻接表还是有缺陷,无法同时获知某个点的出度和入度,需要多余的逆连接表才能表示全,解决方法就是十字链表
4.边集数组:
①定义 两个一维数组组成,一个存储顶点信息,一个存储边的信息,这个边数组每个数据元素由一条边的起点下标begin,终点下标end和权值w构成。
由于这种结构当想找一个顶点的度时需要扫描整个边数组,所以效率不高,但是适合对边需要依次进行处理的操作。而不适合点的操作。
②代码
//边集数组结构体表示 typedef struct{ int begin; int end; int weight; }Edge;
⑧图的遍历 从图中某一顶点出发遍访图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历
1.深度优先遍历DFS
定义:从图中某个顶点V出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径想通的顶点都被访问到
代码:
//图的遍历 //深度优先遍历,邻接矩阵实现 bool hasVisted[MAXVEX];//影子数组,辅助记录顶点是否被遍历过了 void DFS(MGraph *G,int i){ hasVisted[i]=true; printf("%c",G->vexs[i]);//前序遍历的标志,遍历到一个有效点,先打印一下它的值 for(int j=0;j<G->numVertexes;j++){//遍历矩形的另一条边,看看i都有哪些边 if(G->arc[i][j]&&!G->vexs[j]){//找到当前顶点的下一个没遍历过的点 DFS(G,j);//继续遍历 } } } void DFSTravel1(MGraph *G){ for(int i=0;i<G->numVertexes;i++){ hasVisted[i]=false;//把每个顶点在对应状态数组中的值都置为false说明一开始所有顶点都没被遍历过 } for(int i=0;i<G->numVertexes;i++){ if(!G->vexs[i]){ DFS(G,i); } } } //深度优先遍历,邻接表实现 void DFS(GraphAdjList *G,int i){ G->adjList[i].isVisited=true; printf("%c",G->adjList[i].data); EdgeNode* p=G->adjList[i].firstedge; while(p){ if(!G->adjList[p->gindex].isVisited){//顶点链表第一个结点不为空,而且没有访问过 DFS(G,p->gindex); }else{ p=p->next;//进入下一个结点 } } } void DFSTravel2(GraphAdjList *G){ for(int i=0;i<G->numVer;i++){ G->adjList[i].isVisited=false; } for(int i=0;i<G->numVer;i++){ if(!G->adjList[i].data){ DFS(G,i); } } }
2.广度优先遍历BFS
定义:如果DFS类似树的前序遍历的话,那么BFS就类似树的层次遍历,每次一个顶点从队列中出来,对应着这个出来的顶点的所有邻接点进去。
代码:
//广度优先遍历 //邻接矩阵实现 void BFSTravel1(MGraph *G){ for(int i=0;i<G->numVertexes;i++){ hasVisted[i]=false;//把每个顶点在对应状态数组中的值都置为false说明一开始所有顶点都没被遍历过 } SqQueue* q; for(int i=0;i<G->numVertexes;i++){//遍历所有顶点 hasVisted[i]=true; printf("%c",G->vexs[i]); InitQueue(q);//初始化辅助队列 EnQueue(q,i);//把当前点入队列 while(QueueLength(*q)){//只要队列还有元素 DeQueue(q,&i);//取出一个顶点的角标赋值给i for(int j=0;j<G->numVertexes;j++){//遍历i所有的邻接点 if(G->arc[i][j]&&!G->vexs[j]){ hasVisted[j]=true; printf("%c",G->vexs[j]); EnQueue(q,j);//把还没遍历到的邻接点入队列 } } } } } //邻接表实现 void BFSTravel2(GraphAdjList *G){ for(int i=0;i<G->numVer;i++){ G->adjList[i].isVisited=false;//把每个顶点在对应状态数组中的值都置为false说明一开始所有顶点都没被遍历过 } SqQueue* q; for(int i=0;i<G->numVer;i++){//遍历所有顶点 G->adjList[i].isVisited=true; printf("%c",G->adjList[i].data); InitQueue(q);//初始化辅助队列 EnQueue(q,i);//把当前点入队列 while(QueueLength(*q)){//只要队列还有元素 DeQueue(q,&i);//取出一个顶点的角标赋值给i EdgeNode* p=G->adjList[i].firstedge; while(p){ if(!G->adjList[p->gindex].isVisited){//顶点链表第一个结点不为空,而且没有访问过 G->adjList[p->gindex].isVisited=true; printf("%c",G->adjList[p->gindex].data); EnQueue(q,p->gindex);//把还没遍历到的邻接点入队列 }else{ p=p->next;//进入下一个结点 } } } } }
13.最小生成树
①定义 构造连通网的最小代价生成树。一个连通图的生成树是一个极小的连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。
②算法代码
#define INFINITY 65535 //最小生成树算法之:Prim普里姆算法 void MiniSpanTree_Prim(MGraph G){ int min,i,j,k; int adjvex[MAXVEX]; //保存相关顶点下标 int lowcost[MAXVEX];//保持相关顶点间边的权值 lowcost[0]=0;//初始化第一个权值为0,即Vo加入生成树,凡是数组某元素值为0都代表这个元素已经加入了生成树了 adjvex[0]=0; //初始化第一个顶点下标为0 for(i=1;i<G.numVertexes;i++){//遍历除第0个顶点之外的所有顶点 lowcost[i]=G.arc[0][i];//把所有Vo顶点相关的边的权值保存到数组,没有边则其权值为一个极大值,因此这里可以统一遍历一次。 adjvex[i]=0;//初始化都为Vo的下标 } for(i=1;i<G.numVertexes;i++){//遍历除第0个顶点之外的所有顶点 min=INFINITY;//初始化最小权值为正无穷,目的是为了之后找到一定范围内的极小值 j=1;k=0;//j用来做顶点下标循环变量,k用来保存最小权值的顶点下标 while(j<G.numVertexes){//循环全部顶点,找和上次相关点邻接点间边最小的那个点 if(lowcost[j]!=0&&lowcost[j]<min){//lowcost[j]!=0表示此点还不是生成树的顶点,且此点和上一次的点相连 min=lowcost[j];//循环过程中不断修改min值为当前lowcost数组中的最小值,找上一次点的邻接点的最小边 k=j; //并用k保留此最小值边指向的顶点下标 } j++;//下一个顶点 } /**这里是两次找最短边的分割:, 上面是找已纳入点的邻接点中,边权值最小的那个点, 下面是找到这个点后,再寻找这个点发出的所有边中,权值最小的邻接点 如此反复直到纳入所有的点,纳入点的次序存于数组adjvex中。 ---->先和组织联系上,然后再发展壮大组织!!!! **/ printf("%d,%d",k,adjvex[k]); lowcost[k]=0;//将当前顶点k的权值设为0,表示这个顶点已经纳入最小生成树中了 for(j=1;j<G.numVertexes;j++){ if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j]){//找从刚被纳入的顶点出发的最小边 lowcost[j]=G.arc[k][j];//找到那个边,记录对应顶点的权值 adjvex[j]=k;//找到的目标点入组,这个数组主要就是记录入组次序 } }//循环完毕后,找到了下一个还没入组且有最小边的目标顶点 } } //最小生成树算法二 克鲁斯卡尔 Kruskal 算法 #define MAXEDGE 255 void MiniSpanTree_Kruskal(MGraph G){//MGraph 邻接矩阵结构 int i,n,m; Edge edges[MAXEDGE];//定义边集数组,省略对G转化为edges的步骤,边集数组按照边权值从小到大排列 int parent[MAXVEX]; //定义一维数组用来判断多个边是否会形成回环。 for(i=0;i<G.numVertexes;i++) parent[i]=0;//初始化数组值为0,大家都没加入生成树 for(i=0;i<G.numEdges;i++){//由于G中邻接矩阵在转换为边集数组edges时,默认边按照权值从小到大排列,所以遍历也是这个顺序 n=Find(parent,edges[i].begin);//看以当前边为起点最远能到达那个点,通过parent数组使n和m值不断的延伸到其他顶点 m=Find(parent,edges[i].end); //看以当前边尾点最远能到达那个点, //一开始由于parent里都是0,所以返回都是自己本身。 if(n!=m){//说明边两端的点各自延伸后最终没有形成回环,在构造边集数组时 parent[n]=m;//顶点m被纳入生成树了,放在数组第n个值处 } } } /****总体来说,算法就是在不形成环的前提下,不断把最小边收到树中来*****/ int Find(int *parent,int f){//以顶点f为起点,如果p[f]>0,那么下次循环顶点就是p[f]的数值,如果p[p[f]]>0,继续这个循环,向邻接点延伸 while(parent[f]>0) f=parent[f]; return f; }