数据结构与算法--图
0. 重要概念
图是常用的重要的一类数据结构,上一章的树可以看成是图的特例,树中每个数据元素至多允许一个前驱,只能反映数据元素之间一对多的关系,而图中没有该限制,允许数据元素可以有多个前驱,因此可以反映数据元素之间多对多的关系。
- 边和顶点的关系
设n为顶点数, e为边或弧的条数
无向图: 0 <= e <= n(n-1)/2
有向图: 0 <= e <= n(n-1) - 完全图
无向完全图:边数为 n(n-1)/2 的无向图
有向完全图:弧数为 n(n-1) 的有向图 - 顶点的度TD(V)
无向图:为依附于顶点V的边数
有向图:等于以顶点V为弧头的弧数(称为V的入度,记为ID(V))与以顶点V为弧尾的弧数(称为V的出度,记为OD(V) )之和。即: TD(V) = ID(V) + OD(V) - 顶点连通
若顶点v到顶点v’ 有路径,则称顶点v与v’ 是连通的 - 连 通 图 : 包括无向连通图和有向连通图
无向图:若图中任意两个顶点vi,vj都是连通的,则称该图是连通图(vi<>vj)
有向图:若图中任意两个顶点vi,vj,都存在从vi到vj和从vj到vi的路径,则称该有向图为强连通图(vi<>vj) - 连通分量:
无向图:无向图中极大连通子图,称为连通分量
有向图:有向图中极大强连通子图,称为强连通分量
1.邻接矩阵
1.1 无权图的邻接矩阵
设图G=(V, {E})有n个顶点,则G的邻接矩阵定义为n阶方阵A。由于比较简单,直接举例如下:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/666606c8bbbd3675c002f99d10c0cd39.png)
有向图和无向图的数组表示法
- 邻接矩阵的特点:
- 判定两个顶点Vi与Vj是否关联, 只需判A[i,j]是否为1
- 求顶点的度容易。
1.2 顶点的度
- 无向图
即顶点Vi的度等于邻接矩阵中第i行(或第i列)的元素之和(非0元素个数)。 - 有向图
1.3 带权图的邻接矩阵
例如:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/189ca2e45a98bf054cba705de7c03bd7.png)
1.4 邻接矩阵代码实现
//网络的邻接矩阵的定义:
#define MaxVerterNum 100
typedef char VerterType;
typedef int EdgeType;
typedef struct{
VerterType vexs[MaxVerterNum];//存储顶点的一维数组
EdeType edges[MaxVerterNum][MaxVerterNum];//
int n,e; //图当前的顶点数和边数
} MGragh;
//建立无向网络邻接矩阵的算法:
Void createMGragh(MGragh *G){
int i,j,k,w;
scanf(“%d%d”,&G->n,&G->e); //读入顶点数和边数
for(i=0;i<G->n;i++) //读入顶点信息,建立顶点表
G->vexs[i]=getchar( );
for(i=0;i<G->n;i++) //邻接矩阵初始化
for(j=0;i<G->n;j++)
G->edges[i][j]=0;
for(k=0;k<G->e;k++){ //读入e条边,建立邻接矩阵
scanf(“%d%d”,&i,&j); //读入边<vi,vj>上的权w
G->edges[i][j]=1;
G->edges[j][i]=1;
}
}
2. 邻接表
2.1 无向图邻接表
对图中每个顶点Vi建立一个单链表,链表中的结点表示依附于顶点Vi的边,每个链表结点(弧结点)为两个域。
AdjVex(邻接点域) | NextArc (链域) |
---|---|
记载与顶点Vi邻接的顶点信息 | 指向下一个与顶点Vi邻接的链表结点 |
每个链表附设一个头结点(顶点结点),头结点结构为:
VexData | FirstArc |
---|---|
存放顶点信息(姓名、编号等) | 指向链表的第一个结点 |
代码如下:
//弧结点
struct edge
{
int v;
struct edge *nextarc;
};
//顶点结点
struct vex
{
ElemType data;
struct edge *firstarc;
}
举例1:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/95eb675611abecb379310b2298d280c8.png)
举例2:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/fd036eebf00f42880bd6a122493b2a39.png)
无向图邻接表特点:
- n个顶点, e条边的无向图, 需n个头结点和2e个链表结点
- 顶点Vi的度 TD(Vi) = 链表i中的链表结点数
2.2 有向图邻接表
2.2.1 有向图邻接表
与无向图的邻接表结构一样。只是在第i条链表上的结点是以Vi为弧尾的各个弧头顶点,即出边表。
举例:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/677df5d100e788d3b1694ca0696cdfb2.png)
有向图邻接表特点:
- n个顶点, e条弧的有向图,需n个表头结点, e 个链表结点
- 第i条链表上的链表结点数,为Vi的出度(求顶点的出度易,求入度难)
2.2.2 有向图逆邻接表
与无向图的邻接表结构一样。只是在第i条链表上的结点是以Vi为弧头的各个弧尾顶点,即入边表。
举例:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/54305849bd57f0f2de9d6fd9ec038b04.png)
此时,第i条链表上的结点数,为Vi的入度。
2.2.3 代码实现
//邻接表的结构定义和建立算法:
typedef struct node{ //边表结点
int adjtex; //邻接点域
struct node *next; //链域
}EdgeNode; //若也表示边上的权,增加一个数据域
typedef struct vnode{ //顶点表结点
VertexType vertex; //顶点域
EdgeNode *firstedeg; // 边表头指针
} VertexNode;
typedef VertexNode AdjList[MaxNodeNum];
typedef struct{
AdjList adjlist;// 邻接表
int n,e; //顶点数和边数
}ALGraph; //对于简单应用无需定义此类型,直接使用AdjList类型。
//建立无向图的邻接表
void CreateALGraph(ALGraph *G){
int i,j,k;
EdgeNode *s
scanf(“%d%d”, &G->n, &G->e); //读入顶点数和边数
for(i=0; i<G->n; i++)
{ //建立顶点表
G->adjlist[i].vertex = getchar( ); //读入顶点信息
G->adjlist[i].firstedge = NULL;
} //边表置空
for(k=0; k<G->e; k++)
{ //建立边表
scanf(“%d%d”, &i, &j); //读入边(vi,vj)的顶点对序
s = (EdgeNode *)malloc(sizeof(EdgeNode)); //生成边表结点
s->adjvex = j; //邻结点的序号为j
s->next = G-> adjlist[i].firstedge;//前插入
G-> adjlist[i].firstedge = s; //将新结点*s插入vi头部
s= (EdgeNode *)malloc(sizeof(EdgeNode));
s->adjvex = i; //邻接序号为i
s->next = G-> adjlist[j].firstedge;
G-> adjlist[j].firstedge = s; //将新结点*s插入vj头部
}
}
3. 图的邻接矩阵与邻接表的比较
- 一个图的邻接矩阵表示是唯一的;邻接表表示不唯一。
邻接表中各边表结点的次序取决于建立算法和及输入边的次序。 - 邻接表(逆邻接表)中,每个边表对应邻接矩阵中的一行(或一列);边表中结点的个数等于邻接矩阵中的一行(或一列)非0元素的个数。
- 邻接表或逆邻接表的空间复杂度为S(n,e)=O(n+e)。若图中的边数e远远小于n2,称为稀疏图,其邻接表比邻接矩阵要节省存储空间。当边数e接近n2 (无向图:e接近n(n-1)/2);有向图:e接近 n(n-1)时,称为稠密图,考虑链域占空间,应选择邻接矩阵存储为宜。
- 求有向图顶点的度,采用邻接矩阵比邻接表结构方便。 在邻接表(出边表)结构中,求顶点的出度容易,入度困难;逆邻接表(入边表)中, 求顶点的入度容易,出度困难。
- 判断边,邻接矩阵比邻接表容易;求边数:邻接矩阵中花费的时间复杂度为O(n2),邻接表中花费的时间复杂度为O(n+e)。
4. 十字链表
对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道。反之,逆邻接表解决了入度,却不了解出度的情况。有没有可能把邻接表和逆邻接表结合起来呢?
答案是肯定的,就是把它们整合在一起。这种存储有向图的方法是:十字链表(Orthogonal List).
我们重新定义顶点结点结构为:
Data | FirstIn | FirstOut |
---|---|---|
数据域 | 入边表头指针 | 出边表头指针 |
- FirstIn 表示入边表头指针,指向该顶点的入边表中第一个结点
- FirstOut 表示出边表头指针,指向该顶点的出边表中的第一个结点
重新定义的弧结点结构如下表:
TailVex | HeadVex | HeadLink | TailLink |
---|---|---|---|
弧起点在顶点表的下标 | 弧终点在顶点表中的下标 | 入边表指针域 | 出边表指针域 |
- HeadLink 是指入边表指针域,指向弧终点(弧头)相同的下一条边。
- TailLink 是指出边表指针域,指向弧起点(弧尾)相同的下一条边。
- 如果是网,还可以再增加一个Weight域来存储权值。
其中,下图中的横向(实线)箭头所构成的表相当于出边表,纵向(虚线)箭头则相当于入边表,所以十字链表由此得名。
5. 图的遍历
5.1 深度优先搜索
5.1.1 深度优先搜索法遍历图的过程
1). 访问指定的某顶点V,将V作为当前顶点
2). 访问当前顶点的下一未被访问过的邻接点,并将该顶点作为当前顶点
3). 重复2,直到当前顶点的所有邻接点都被访问过
4). 沿搜索路径回退,退到尚有邻接点未被访问过的某结点,将该结点作为当前结点,重复2,直到所有顶点被访问过的为止
举例:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/8596af5003a46174bc9a5e78d6e633db.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/db4557a63a74d320812a0ac631f13452.png)
5.1.2 递归法实现深度优先遍历算法
举例:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/0d852c6828388669789f951fac3110d1.png)
代码如下:
//从顶点v0出发深度优先遍历g中能访问的各个顶点
void dfs(int v0)
{
visited[v0]=1; /*访问标志置为 1,表示已被访问*/
w=firstadj(g,v0); /* w是vo的第一个邻接点 */
while (w!=0)
{
if(visited[w]==0)
dfs(w); /*顶点未被访问,则递归的进行深度遍历 */
w=nextadj(g,v0,w); /*顶点已访问,则取顶点v0在w后面的下一个邻接点 */
}
}
5.2 广度优先搜索
5.2.1 广度优先搜索过程
举例:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/9121965696a806e499842f48d999d10e.png)
1). 首先访问指定顶点v1,将v1作为当前顶点;
2). 访问当前顶点的所有未访问过的邻接点,并依次将访问的这些邻接点作为当前顶点;
3). 重复2, 直到所有顶点被访问为止。
5.2.2 具体算法
对每一个当前访问顶点,一次性访问其所有的邻接点,采用队列来实现。
Void bfs(Graph g,int v0) { /* 从v0出发广度优先遍历图g */
visited[v0]=1;
Enqueue(Q,v0);
While (!Empty(Q)){
v=Dlqueue(Q); /* 取队头元素v */
w=Firstadj(g,v); /* 求v的第一个邻接点 */
while(w!=0){
if(visited[w]==0){
visited[w]=1;
Enqueue(Q,w);
}
w=Nextadj(g,v,w); /* 求下一个邻接点 */
}
}
}