非线性表-Graph(图-待完善)
1:图的知识点
关于连通子图 1)极小连通子图与极大连通子图是在无向图中进行讨论的 2)极大强连通子图是在有向图中进行讨论的
极大连通子图:
1.连通图只有一个极大连通子图,就是它本身。(是唯一的)
2.非连通图有多个极大连通子图。(非连通图的极大连通子图叫做连通分量,每个分量都是一个连通图)
3.称为极大是因为如果此时加入任何一个不在图的点集中的点都会导致它不再连通。
下图为非连通图,图中有两个极大连通子图(连通分量)。
极小连通子图:
1.一个连通图的生成树是该连通图顶点集确定的极小连通子图。(同一个连通图可以有不同的生成树,所以生成树不是唯一的)
(极小连通子图只存在于连通图中)
2.用边把极小连通子图中所有节点给连起来,若有n个节点,则有n-1条边。如下图生成树有6个节点,有5条边。
3.之所以称为极小是因为此时如果删除一条边,就无法构成生成树,也就是说给极小连通子图的每个边都是不可少的。
4.如果在生成树上添加一条边,一定会构成一个环。
也就是说只要能连通图的所有顶点而又不产生回路的任何子图都是它的生成树。
极大强连通子图:
1.强连通图的极大强连通子图为其本身。(是唯一的)
2.非强连通图有多个极大强连通子图。(非强连通图的极大强连通子图叫做强连通分量)极小强连通子图:不存在这个概念
(一)知识点
1)连通:顶点V到顶点V'有路径
2)连通分量:无向图中的极大连通子图(极大:1.顶点足够多;2.极大连通子图包含依附这些点的所有边)
3)强连通分量:有向图中的极大强连通子图
4)连通的生成树:包含图中的全部n个顶点,但只有n-1条边的极小连通子图
5)度:以该顶点为一个端点的边数目
6)入度(ID):以顶点为终点的有向边数目
7)出度(OD):以顶点为起点的有向边数目
8)连通图:在无向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该无向图为连通图。
9)强连通图:在有向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该有向图为强连通图。
10)连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
11)生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环
1)BFS(广度优先遍历):类似于树的层序遍历算法 邻接表:每个顶点入队一次,O(|V|),对于每个顶点,搜索其邻接点,所以访问该顶点所有边,O(|E|),所以总时间复杂度O(|V|+|E|)。 邻接矩阵:每个顶点入队一次,O(|V|),对于每个顶点,搜索其邻接点,所以遍历矩阵,O(|V|),所以总时间复杂度O(|V|2|)。 2)DFS(深度优先遍历):类似于树的先序遍历算法。 邻接表:每个顶点入队一次,O(|V|),对于每个顶点,搜索其邻接点,所以访问该顶点所有边,O(|E|),所以总时间复杂度O(|V|+|E|)。 邻接矩阵:每个顶点入队一次,O(|V|),对于每个顶点,搜索其邻接点,所以遍历矩阵,O(|V|),所以总时间复杂度O(|V|2|)。
(二)题
1)n个顶点的连通图最多 n(n-1)/2 ,最少 n-1;n个顶点的强连通图最多 n(n-1) ,最少 n-1. 2)n个顶点的有向图最多有 n(n-1) 条边 3)n个顶点的无向图,要连通全部顶点至少需要 n-1 条边 4)n个顶点的无向连通图,它所包含的连通分量个数为 1 5)n个顶点的强连通图至少有 n 条边 6)用DFS遍历一个无环有向图,并在DFS算法退栈返回时打印相应的顶点,则输出的顶点序列是 逆拓扑有序 的 7)关键路径是事件结点网络中的 从源点到汇点的最长路径 8)在AOE-网工程中,减少任一关键活动上的权值后,整个工期也就相应的减小;若网中有 n条关键路径时,仅减其中一条关键路径的权值并不能使整个工期减少 9)若图中包含k个连通分量,若按深度优先(DFS)搜索方法访问所有结点,则必须调用 k 次深度优先遍历算法。#一次深度优先搜索只能遍历一个连通分量# 10)若无向图G的顶点度数的最小值大于或等于 2 时,G至少有一条回路 11)设无向图G的顶点数为n,图G最少有0条边,最多有 n(n-1)/2 条边。 若G为有向图,有n个顶点,则图G最少有0条边,最多有 n(n-1) 条边。 具有n个顶点的无向完全图,边的总数为 n(n-1)/2 条; 而具有n个顶点的有向完全图中,边的总数有 n(n-1) 条。 12)设每个结点都有n-1条弧线从自己出发分别射向其它各个结点的话,则n个结点共有 n(n-1) 条有向弧线存在; 但是,如此一来任两个结点之间都会有两条相向而指的弧线存在,这就是所谓的有向完全图。 如果我们限定任意两个结点之间都有且仅有一条无向的连线存在,则整个图的连线总数就会比有向完全图的弧线总数刚好少一半,
即共有n(n-1)条边,也就是 (n-1) 条边,即无向完全图。
13)简述无向图和有向图有哪几种存储结构,并说明各种结构在图中的不同操作(图的遍历,有向图的拓扑排序等)中有什么样的优越性?
无向图的存储结构有邻接矩阵、邻接表和邻接多重表,有向图的存储结构有邻接矩阵、邻接表和十字链表。
a) 邻接矩阵:可判定图中任意两个顶点之间是否有边(或弧)相连,并容易求得各个顶点的度;此外,对于图的遍历也是可行的。
b) 邻接表:容易找到任一顶点的第一个邻接点和下一个邻接点;但要判断任意两个顶点之间是否有边或弧相连,则需搜索第i个及第j个链表,这不如邻接矩阵方便;
此外,对于图的遍历和有向图的拓扑排序也是可行的。
c) 十字链表:容易找到以某顶点为头或尾的弧,因此容易求得顶点的入度和出度;在有向图的应用中,十字链表是很有用的工具。
d) 邻接多重表:是无向图的一种非常有效的存储结构,在其中容易求得顶点和边的各种信息。
14)对于有n个顶点的无向图,采用邻接矩阵表示,如何判断以下问题(想法描述)
(1)图中有多少条边? (2)任意两个顶点i和j之间是否有边相连? (3)任意一个顶点的度是多少?
解:
(1)若有n个非零值,则边为n/2条边。
(2)设邻接矩阵为A,若aij=1,则i,j有边直接相连;若aik=1,akj=1则经过k有边直接相连。
(3)统计以该点为行的非零元素个数。
2:图的几种数据结构
1:邻接矩阵
typedef char VertexType;//顶点类型 typedef int EdgeType;//边权值类型 #define MAXVEX 100 //最大顶点数 #define INFINITY 65535 //用65535代表∞ typedef struct _Mgraph { VertexType vexs[MAXVEX];//顶点数组 EdgeType arc[MAXVEX][MAXVEX];//领接矩阵 int numVexs;//图中顶点数 int numEdges;//图中边数 }Mgraph;
2:邻接表
/** 1.图中顶点用一维数组存储,另外每个数据元素还需存储指向第一个邻接点的指针,以便于查找该顶点的信息。 2.图中每个顶点的vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储。无向图称为顶点vi的边表,有向图则称为vi作为弧尾的出边表。 */ typedef struct _EdgeNode//边节点 { int adjvex;//邻接点域存储该顶点对应下标 EdgeType weight;//用于存储权值,非网图可以不需要 struct _EdgeNode *next;//链域,指向下一个邻接点 }EdgeNode; typedef struct _VexNode//顶点结点 { VertexType data;//数据域,存储结点信息 struct _VexNode *firstedge;//指针域,存储指向第一个邻接点的指针(边表头指针) }VexNode,AdjList[MAXVEX]; typedef struct { AdjList adjlist; int numvexs;//顶点数 int numedge;//边数 }GraphAdjList;
3:十字链表--有向图的一种存储方法
/**十字链表 顶点表结构:firstin表示入边表头指针,指向该顶点的入边表中的第一个顶点(相当于逆邻接表),firstout表示出边表头指针,指向该顶点的出边表中的第一个结点(相当于邻接表)。 边表结构:tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下标,headlink是指入边表指针域,指向终点相同的·下一条边,taillink是指向起点相同的下一条边。如果是网,还可以在增加一个weight域来存储权值。 */ typedef struct _CEdgeNode { int tailvex; //弧起点(弧尾)在顶点表的下标 int headvex; //弧终点(弧头)在顶点表的下标 struct _EdgeNode *headlink; //入边表指针域,用来指向终点相同的下一条边 struct _EdgeNode *taillink; //出边表指针域,用来指向起点相同的下一条边 }EdgeNode; typedef struct _CVexNode { VertexType data; //用来存放顶点信息 EdgeNode *firstin; //指针域,用来指向入边表的第一个顶点(即该顶点做弧头) EdgeNode *firstout; //指针域,用来指向出边表的第一个顶点(即该顶点做弧尾) }VexNode,Adjvexs[MAXVEX]; typedef struct { Adjvexs adjvexs; //定义顶点数组 int numvexs; //该图的顶点数 int numedges; //该图的边数 }GraphCrossLinkList;
4:邻接多重表 ---无向图的一种存储方法
处理删除边类似这种操作,使用邻接多重表会更合适。
邻接多重表与邻接表的区别,仅仅是在于同一条边在邻接多重表中用一个结点表示,而在邻接表中用两个边表结点表示。ivex与jvex是与某条边依附的两个顶点在顶点表中下标。ilink是指依附顶点ivex的下一条边,jlink是指依附顶点jvex的下一条边。
邻接多重表的顶点结点和表结点的构成如图 所示:
表结点构成:
- mark 为标志域,作用是标记某结点是否已经被操作过,例如在遍历各结点时, mark 域为 0 表示还未遍历;mark 域为 1 表示该结点遍历过;
- ivex 和 jvex 分别表示该结点表示的边两端的顶点在数组中的位置下标; ilink 指向下一条与 ivex 相关的边;
- jlink 指向下一条与 jvex 相关的边;
- info 指向与该边相关的信息。
点结点构成:
- data 为该顶点的数据域;
- firstedge 为指向第一条跟该顶点有关系的边。
5:边集数组
由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,
这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
查找一个顶点的度需要扫描整个边数组,效率并不高,因此它更适合对边依次进行处理的操作。而不适合对顶点相关的操作。
2:图的遍历
1:邻接矩阵----深度优先
A
. .
. .
B .. G .. F
. . . . .
. . . . .
C .. I . H .
. . . . . .
. . . . . .
.. . . .
D ....... E
/*深度优先利用了递归 入栈/递归调用 A B C D E F G H H顶点所有连接点访问结束 出栈/H递归结束 G顶点所有连接点访问结束 出栈/G递归结束 F顶点所有连接点访问结束 出栈/F递归结束 E顶点所有连接点访问结束 出栈/E递归结束 退回到D 入栈/递归调用 I I顶点所有连接点访问结束 出栈/I递归结束 D顶点所有连接点访问结束 出栈/D递归结束 C顶点所有连接点访问结束 出栈/C递归结束 B顶点所有连接点访问结束 出栈/B递归结束 A顶点所有连接点访问结束 出栈/A递归结束 */ int visited[MAXVEX]; void DFS(Mgraph G,int i) { int j; visited[i]=1; printf("%c",G.vexs[i]);//顶点 for(j=0;j<G.numVexs;j++)//图中顶点数9 { //如果i点的邻接点j没有被访问过,则对该邻接点进行递归调用。 if(G.arc[i][j]==1&&!visited[j]) { DFS(G,j); } } } void DFSTraverse(Mgraph G) { int i; for(i=0;i<G.numVexs;i++) { visited[i]=0; } for(i=0;i<G.numVexs;i++) { if(!visited[i]) { DFS(G,i); } } }
2:邻接矩阵---广度优先
入队 A B F C I G E D H /*队列结构*/ typedef struct QNode { QElemType data; struct QNode *next; }QNode,*QueuePtr; typedef struct { QueuePtr front,rear; //队头队尾指针 }LinkQueue; //构造邻接矩阵 void create(Mgraph *G) { int i,j,k,w; printf("请输入顶点数和边数:\n"); scanf("%d%d",&G->numVertexes,&G->numEdges); fflush(stdin); for(i=0;i<G->numVertexes;i++) //建立顶点表 { printf("\n第%d个顶点",i+1); scanf("%c",&G->vexs[i]); getchar(); } 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(空格隔开):"); scanf("%d%d%d",&i,&j,&w); G->arc[i][j]=w; G->arc[j][i]=G->arc[i][j]; } } //输出邻接矩阵 void Output(MGraph *G) { int i,j,count=0; for(i=0;i<G->numVertexes;i++) printf("\t%c",G->vexs[i]); printf("\n"); for(i=0;i<G->numVertexes;i++) { printf("%4c",G->vexs[i]); for(j=0;j<G->numVertexes;j++) { printf("\t%d",G->arc[i][j]); count++; if(count%G->numVertexes==0) printf("\n"); } } } //创建空队列 Status InitQueue(LinkQueue &Q) { Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode)); if(!Q.front) exit(0); Q.front->next=NULL; return 1; } //入队列 //将s作为新元素加入队尾 Status EnQueue(LinkQueue &Q,int i) { QueuePtr s; s=(QueuePtr)malloc(sizeof(QNode)); if(!s)exit(0); s->data=i; s->next=NULL; Q.rear->next=s; Q.rear=s; return 1; } //检验是否为空 Status QueueEmpty(LinkQueue Q) { if(Q.front->next==NULL) return 0; else return 1; } //出队列 Status DeQueue(LinkQueue *Q,int *i) { QueuePtr p; if(Q->front==Q->rear) return 0; p=Q->front->next; //该写法值得商榷 //相当于p储存第一个结点 *i=p->data; Q->front->next=p->next; if(p==Q->rear) //若队头是队尾 ,删除后将rear指向头结点 Q->rear==Q->front; free(p); return 1; }
void BFSTraverse(MGraph G) { int i,j; LinkQueue Q; for(i=0;i<G.numVertexes;i++) visited[i]=FALSE; InitQueue(Q); //&的用法?? 初始化队列 for(i=0;i<G.numVertexes;i++) { if(!visited[i]) //未访问过 该顶点 { visited[i]=TRUE; printf("%c->",G.vexs[i]); EnQueue(Q,i); //将其入队列 while(!QueueEmpty(Q)) { DeQueue(&Q,&i); //将队中元素出队列,赋值给i for(j=0;j<G.numVertexes;j++) { if(G.arc[i][j]==1&&!visited[j]) //其他顶点与该顶点存在边 且未访问过 { visited[j]=TRUE; printf("%c",G.vexs[j]); EnQueue(Q,j); //入队列 } } } } } }
3:邻接表---深度优先
void DFS(ALGraph G,int v) { EdgeNode *p; if (v<0 || v>=G.nodeNum) return; else { visit[v] = 1; //已经访问标记 printf("%d ", G.adjlist[v].vertex); p = G.adjlist[v].edgenext; //p指向顶点v的第一条边 while (p != NULL) { if (visit[p->adjvex] != 1) DFS(G,p->adjvex); p = p->next; } } } void DFSTravel(ALGraph G) { int v; printf("深度优先遍历顺序为:"); for (v = 0; v < G.nodeNum; v++) visit[v] = 0; for (v = 0; v < G.nodeNum; v++) { if (!visit[v]) DFS(G, v); } }
4;邻接表--广度优先
int visitBFS[maxSize]; void BFS(ALGraph G,int v) { EdgeNode *p; int que[maxSize], front = 0, rear = 0; //队列定义的简单写法 int j; printf("%d ", G.adjlist[v].vertex); visitBFS[v] = 1; rear = (rear + 1) % maxSize; //当前顶点v进入队列 que[rear] = v; while (front!=rear) //队空时遍历完成 { front = (front + 1) % maxSize; //顶点出队 j = que[front]; p = G.adjlist[j].edgenext; //p指向出队顶点j的第一条边 while (p != NULL) { if (visitBFS[p->adjvex] == 0) { printf("%d", p->adjvex); visitBFS[p->adjvex] = 1; rear = (rear + 1) % maxSize; //该顶点进队 que[rear] = p->adjvex; } p = p->next; } } } void BFSTravel(ALGraph G) { int i; printf("\n广度优先遍历顺序为:"); for (i = 0; i < G.nodeNum; i++) { if (visitBFS[i] == 0) BFS(G, i); } } void main() { ALGraph *G = malloc(sizeof(ALGraph)); create_Graph(G); print_Graph(G); DFSTravel(G); system("pause"); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?