目录
3.1 深度优先搜索(Depth_First Search——DFS)
3.2 广度优先搜索(Breadth_First Search——BFS)
一、图的定义和术语
图:两个集合构成的偶对
有向图:每条边都是有方向的
无向图:每条边都是无方向的
完全图:任意两个点都有一条边相连
稀疏图:有很少的边或弧的图(e<n*logn)
稠密图:有较多边或弧的图
网:边/弧带权的图
邻接:有边/弧相连的两个顶点之间的关系。
存在(vi,vj),则称vi和vj互为邻接点
存在<vi,vj>,则称vi邻接到vj,vj邻接于vi
关联(依附):边/弧与顶点之间的关系
存在(vi,vj)/<vi,vj>,则称该边/弧关联于vi和vj
顶点的度:与该顶点相关联的边的数目,记为TD(v)
在有向图中,顶点的度等于该顶点的入度和出度之和
顶点v的入度是以v为终点的有向边的条数,记作ID(v)
顶点v的出度是以v为起点的有向边的条数,记作OD(v)
有向树:当有向图中仅一个顶点的入度为0,其余顶点的入度均为1时,是一棵有向树
路径:接续的边构成的顶点序列
路径长度:路径上边或弧的数目/权值之和
环(回路):第一个顶点和最后一个顶点相同的路径
简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
简单回路(环):除路径起点和终点相同外,其余顶点均不相同的路径
连通图(强连通图):在无(有)向图G=(V,{E})中,若对任何两个顶点v,u都存在从v到u的路径,则称G是连通图(强连通图)
子图
连通分量(无向图)
强连通分量(有向图)
极小连通子图:一个子图是G的连通子图,在该子图中删除任何一条边,该子图不再连通,这个子图就是极小连通子图。
生成树:包含无向图G所有顶点的极小连通子图
生成森林:对非连通图,由各个连通分量的生成树的集合
二、图的存储结构
2.1 图的抽象数据类型定义
ADT Graph{
数据对象V:具有相同特性呃数据元素的集合,成为顶点集。
数据关系R:R={VR}
VR={<v,w>|<v,w>|v,w∈V ^ p(v,w)},
<v,w>表示从v到w的弧,P(v,w)定义了弧<v,w>的信息
}
2.2 图的存储结构
图的逻辑结构:多对多
图没有顺序存储结构,但可以借助二维数组来表示元素之间的关系。
数组表示法:邻接矩阵
链式存储结构:多重链表(邻接表,邻接多重表,十字链表)
2.2.1 邻接矩阵
邻接矩阵的类型定义
建立一个顶点表和一个邻接矩阵(两个顶点间有关系则为1,没有关系则为0)
邻接数组的存储表示:用两个数组分别存储顶点表和邻接矩阵
#define MaxInt 22222//表示极大值,即无穷
#define MVNum 100//最大顶点数
typedef char VerTexType;//设置顶点的数据类型
typedef int ArcType;//设置权值的类型
typedef struct{
VerTexType vexs[MVNum];//顶点表
ArcType arcs[MVNum][MVNum];//邻接矩阵
int vexnum,arcnum;//图的当前点数和边数
}AMGraph;//Adjacency Matrix Graph
用邻接矩阵创造无向图
步骤:
a.输入总顶点数和总边数;
b.依次输入点的信息,存入顶点表中;
c.初始化邻接矩阵,使每个权值初始化为极大值;
d.构造邻接矩阵。
Status CreateUDN(AMGraph &G){
cin>>G.vexnum>>G.arcnum;//输入总顶点数,总边数
for(i = 0;i<G.vexnum;i++)
cin>>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++){
cin>>v1>>v2>>w;//输入一条边所依附的顶点及边的权值
i = LocateVex(G,v1);
j = LocateVex(G,v2);//确定两个顶点在G中的位置
G.arcs[i][j] = w;//边<v1,v2>的权值置为w
G.arcs[j][i] = G.arcs[i][j];//<v1,v2>的对称边<v2,v1>的权值也为w
}
return OK;
}
在创建无向网的基础上,取建造无向图,有向网 ,有向图。
无向图:1.初始化邻接矩阵时,w = 0;2.构造邻接矩阵时,w = 1。
有向网:为G.arcs[i][j]赋值,不需要为G.arcs[j][i]赋值。
有向图:结合无向图和有向网。
邻接矩阵的优缺点
优点:
1.直观,简单,好理解
2.方便检查任意一队顶点间是否存在边
3.方便找任一顶点的所有邻接点
4.方便计算任一顶点的度
缺点:
1.不便于增加或删除
2.浪费空间:存储稀疏图时,边很少,其空间复杂度只与顶点数相关,是顶点数的平方
3.浪费时间:统计稀疏图的边数时,也需要n次
2.2.2 邻接表
1.邻接表表示法(链式)
头结点:数据域和指针域。数据域存储顶点的数据,指针域指向与顶点相关的边/弧。
表结点:数据域和指针域。数据域为前一个指针域相关的另一个顶点的顺序下标,指针域指向与头结点中顶点相关的下一个边/弧。
若为网,每个边/弧上有权值,则为表结点再增加一个数据域,存储其权值。
无向图的邻接表
特点:
1.邻接表不唯一,每个头结点的表结点可以互换位置。
2.若无向图由n个顶点,e条边,则其邻接表需要n个头结点和2e个表结点。适合存储稀疏图。
3.无向图中顶点vi的度为第i个单链表中的表结点个数。
4.空间复杂度:n+2e
有向图的邻接表
表结点只记录以表头顶点为弧的起点的弧(只存储出度)
空间复杂度:n+e
顶点vi的出度:为第i个单链表中的表结点个数。
顶点vi的入度:为全部表中邻接点域值为i-1的结点个数。
计算出度简单,入度难。
因此有逆邻接表,其结点的指针域存储以顶点为弧的终点的弧。
图的邻接表存储表示:
注:这里firstarc指针指向表结点,因此firstarc应是表结点类型的指针。
//邻接表的头结点
typedef struct VNode{
VerTexType data;//顶点信息
ArcNode *firstarc;//指向第一条依附该顶点的边的指针
}VNode,AdjList[MVNum];//AdjList表示邻接表类型
//AdjList x;相当于 VNode x[MVNum];都是在声明一个数组,数组存放的内容为VNode这个数据类型。
//邻接表的表结点(弧的结点结构)
typedef struct ArcNode{
int adjvex;//该弧指向的顶点的下标
struct ArcNode *nextarc;//指向下一条与头结点的顶点相关的弧的位置
int info;//其他信息,例如权值
}ArcNode;
//邻接表
typedef struct{
VNode vertices[MVNum];
int vexnum,arcnum;//图当前顶点数和弧数
}ALGraph;
采用邻接表创建无向网
Status CreateUDG(ALGraph &g){
cout<<"输入该无向网的弧数和顶点数";
cin>>g.arcnum>>g.vexnum;
for(int i=0;i<g.vexnum;i++){
cout<<"输入第"<<i<<"个顶点的数据内容\n";
cin>>g.vertices[i].data;
}
for(int j=0;j<g.vexnum;j++){
ArcNode *p;
p = g.vertices[j].firstarc;
int j_arcnum;
cout<<"输入与第"<<j<<"个顶点相关的弧数\n";
cin>>j_arcnum;
for(int m = 1;m<=j_arcnum;m++){
cout<<"输入与第"<<j<<"个顶点相关的第"<<m<<"条弧相关的顶点下标\n";
cin>>p->adjvex;
p++;
}
}
return 0;
}//邻接表创造无向图
注:输入全部的顶点数据和第一个顶点弧数后,输入第一条与该顶点相关的弧的另一个顶点下标后,会自动退出,没明白是为什么。
2.2.3 邻接表和邻接矩阵的关系
1.联系:邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。
2.区别:
a.对任一无向图,邻接矩阵是唯一的;但邻接表不唯一。
b.邻接矩阵的空间复杂度时O(n^2),邻接表的空间复杂度为O(n+e);
3.用途:邻接矩阵多用于稠密图;邻接表多用于稀疏图。
2.2.4 十字链表和邻接多重表
十字链表(Orthogonal List):是有向图的另一种存储结构。可以将其看成是有向图的邻接表和逆邻接表结合起来形成的一种链表。
有向图中的每一条弧对应十字链表中的一个弧结点,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点。
例:
顶点结点(用于存放顶点数据):有一个数据域存放该顶点的数据;一个firstin指针域,指向第一条指向这个顶点的弧;一个firstout指针域,指向第一条从这个顶点指出的弧。
弧结点(用于存放弧的信息):四个域,前两个为数据域,tailvex存储一条弧的出发点,headvex存储一条弧的目标点;以顶点a为例:hlink指向下一条指向a的弧,tlink指向下一条从a出发的弧。
邻接多重表(无向图的另一种链式存储结构)
由于在邻接表中,每条边都会出现两次,在进行一些操作时不方便(例如:删除一条边,需要找两个结点,删除两次),所以改进邻接多重表。
若是网,可以在info中存储边的权值,是图的时候可以不要。
三、图的遍历算法
图的遍历:从连通图的某一顶点出发,沿着边访问所有结点,且每个顶点只被访问一次,就叫做图的遍历。是找每个顶点的邻接点的过程。
为避免重复访问,设置辅助数组visited[n],用于标记每个被访问过顶点。
初始状态visited[i]=0;若顶点i被访问,改visited[i]为1,防止多次访问
3.1 深度优先搜索(Depth_First Search——DFS)
3.1.1 算法思想:
1.在访问图中某一起始顶点v后,由v出发,访问它的任一邻接顶点w;再从w出发,访问与w邻接但还未被访问过的顶点w;
2.然后再从W出发,进行类似的访问;
3.如此进行下去,直至到达所有的邻接顶点都被访问过的顶点u为止。接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点;
4.如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;
5.如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
(先沿着一条路走,走到没有路了,就返回,看有没有岔路)
3.1.2 算法实现
遍历无向图的邻接矩阵
typedef char VerTexType;//设置顶点的数据类型
typedef int ArcType;
typedef struct{
VerTexType vexs[MVNum];//顶点表
ArcType arcs[MVNum][MVNum];//邻接矩阵
int vexnum,arcnum;//图的当前点数和边数
}AMGraph;//Adjacency Matrix Graph
Status CreateUDN(AMGraph &G){
cout<<"输入总顶点数和总边数:";
cin>>G.vexnum>>G.arcnum;//输入总顶点数,总边数
for(int v = 0;v<G.vexnum;v++){
cout<<"第"<<v<<"个点的数据:";
cin>>G.vexs[v];}//依次输入点的信息
for(int i = 0;i<G.vexnum;i++){
for(int j = 0;j<G.vexnum;j++)
G.arcs[i][j] = 0;}//初始化邻接矩阵,将边的权值均设为极大值
for(int k = 1;k<=G.arcnum;k++){
int v1,v2;
cout<<"输入第"<<k<<"条边所依附的顶点:";
cin>>v1>>v2;//输入一条边所依附的顶点
G.arcs[v1][v2] = 1;
//G.arcs[v2][v1] = G.arcs[v1][v2];
}
return OK;
}//邻接矩阵构造无向图
void DFS(AMGraph g,int v,int visited[]){
//int visited[g.vexnum];
cout<<g.vexs[v];
visited[v]=true;
for(int w = 0;w<g.vexnum;w++){
if((g.arcs[v][w]!=0)&&(!visited[w]))
DFS(g,w,visited);
}
}
3.1.3 DSF算法效率分析
用邻接矩阵来表示图时,遍历图中每个顶点都到从头扫描该顶点所在行,时间复杂度为O(n^2)。
用邻接表来表示图时,有2e个表结点,但只需要扫描e个结点即可完成遍历,加上访问n个头结点,时间复杂度为O(n+e)。
稠密图适合用邻接矩阵进行深度遍历,稀疏图适合用邻接表进行深度遍历。
3.2 广度优先搜索(Breadth_First Search——BFS)
3.2.1 算法思想:一层层的访问。
从某一结点开始,依次访问该结点的所以邻接点;再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点。
重复此过程,直至所有顶点均被访问为止。
3.2.2 算法实现
无向图的邻接表。
创造一个队列。
首先0入队,visited[0] = 1,然后0就可以出队了,然后将其邻接点入队:1,2。
然后访问1和2,visited[1] = visited[2] = 0,然后出队一个,按顺序,出队的是1,然后将1的邻接点,没访问过的3和4入队。
....
注意的点:队列的头指针每次移动一个,即每次出队只出一个,然后将出队的这个点的邻接点入队。所以一次可以入队多个。
typedef struct{
QElemType *base;//初始化的动态分配存储空间
int frot;//头指针
int rear;//尾指针
}SqQueue;
#define MAXSIZE 100
Status InitQueue(SqQueue &q){
q.base = new QElemType[MAXSIZE];
if(!q.base ) exit(OVERFLOW);//存储分配失败
q.frot = q.rear = 0;//头指针尾指针置为0,队列为空
return OK;
}//队列的初始化
Status EnQueue(SqQueue &q,QElemType e){
if((q.rear + 1)%MAXSIZE == q.frot)
return ERROR;//队满
q.base[q.rear] = e;
q.rear = (q.rear + 1)%MAXSIZE;
return OK;
}//循环队列的入队
Status DeQueue(SqQueue &q,QElemType &e){
if(q.frot == q.rear)
return ERROR;//队空
e = q.base[q.frot];
q.frot = (q.frot+1)%MAXSIZE;
return OK;
}//循环队列的出队
int FirstAdjVex(AMGraph G,int v)
{
int i;
if(!G.vexs[v].firstarc)//判断是否有临界点
{
return 0;
}
return G.vexs[v].firstarc->adjvex;//如果有返回该点的位置
}
int NextAdjVex(AMGraph G,int u,int w){//表示u相对于w的下一个临界点w>=0 表示存在临界点
ArcNode *p;
p=G.vexs[u].firstarc;
while(p&&p->adjvex!=w)
p=p->nextarc;//找到w点的位置
p=p->nextarc;w位置的下一个位置
if(p)//如果该位置不为NULL说明和u位置还有相连的
{
return p->adjvex;
}
return -1;//该位置为NULL说明,与u位置有关系的点已经遍历完
}
void BFS(AMGraph g,int v,int visited[]){
cout<<g.vexs[v];
visited[v] = true;
SqQueue q;
InitQueue(q);
EnQueue(q,v);//第一个点入队
while(q.frot!=q.rear){
int u;
DeQueue(q,u);
for(int w = FirstAdjVex(g,u);w>=0;w = NextAdjVex(g,u,w))
if(!visited[w]){
cout<<g.vexs[w];
visited[w] = true;
EnQueue(q,w);
}
}
}
3.2.3 算法效率分析
使用邻接矩阵,则BFS对于每个被访问的顶点,都要循环检测矩阵中的整整一行,总的时间代价为O(n^2)。
使用邻接表,虽然有2e个结点,但之需要扫描e个结点即可完成遍历,加上n个头结点,时间复杂度为O(n+e)。
DFS和BFS相比较
空间复杂度相同,都是O(n)。
时间复杂度之与存储结构(邻接表和邻接矩阵)相关,与搜索路径无关。
四、最小生成树
生成树:所有顶点均由边连接在一起,但不存在回路的图。
生成树的特点:
顶点数和图的顶点数相同
生成树是极小连通子图,去掉一条边则非联通;
一个有n个顶点的连通图的生成树有n-1条边;
含有n个顶点,n-1条边的图,不一定是生成树。
在生成树中再加一条边必然形成回路。
生成树中任意两个顶点间的路径是唯一的。
如何得到生成树?
在遍历图的过程中,将访问过程中经历的边保存。
最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那颗生成树称为该网的最小生成树,也叫最小代价生成树。
prim算法
kruskal算法
prim算法和kruskal算法的比较