0
点赞
收藏
分享

微信扫一扫

数据结构—图

爱上流星雨 2022-04-29 阅读 91

一 图的基本概念

图(Graph)G由顶点集合V(G)和边集合E(G)构成在这里插入图片描述

  • 注意:线性表可以是空表,树可以是空树,但图不能是空图。就是说图不能一个顶点也没有,但边集可以为空,即V一定非空,E可以为空。

1 无向图

在图G中,如果代表边的顶点对是无序的,则称G为无向图。边记为(v,w),v,w互为邻接点。
在这里插入图片描述

2 有向图

如果表示边的顶点对是有序的,则称G为有向图。有向边(弧)记为<v,w>,其中v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从v到w的弧。表示为:G1 = (V1,E1) V1 = {123}

3 顶点的度

无向图:顶点 i 连接的边数称为该顶点的度。(全部顶点的度的和=边数的2倍,因为每条边关联两个顶点)
有向图:顶点 i 的入度与出度之和为顶点的度。(全部顶点的入度和=全部顶点的出度和=边数)

  • 入度:以顶点 i 为终点的入边的数目称为该顶点的入度。
  • 出度:以顶点 i 为始点的出边的数目,称为该顶点的出度。

4 完全图

无向图:每两个顶点之间都存在着一条边,称为完全无向图,包含有n(n-1)/2条边。
有向图:每两个顶点之间都存在着方向相反的两条边,称为完全有向图,包含有**n(n-1)**条边。
在这里插入图片描述

5 稠密图、稀疏图

当一个图接近完全图时,则称为稠密图。
相反,当一个图含有较少的边数(即当e << n(n-1))时,则称为稀疏图。

6 子图

设有两个图G=(V,E)和G’=(V’,E’),若V’是V的子集,即V’⊆V,且E’是E的子集,即E’⊆E,则称G’是G的子图。
在这里插入图片描述

7 路径和路径长度和回路

在一个图G=(V,E)中,从顶点i到顶点j的一条路径为顶点序列 i,i1,i2,…,im,j。
路径长度指一条路径上经过的边的数目。
第一个顶点和最后一个顶点相同的路径称为回路

8 简单路径、简单回路

路径序列中,顶点不重复出现的路径称为简单路径。

除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。

9 连通、连通图、连通分量

无向图中,若从顶点i到顶点j有路径,则称顶点i和j是连通的。

若图中任意两个顶点都连通,则称为连通图,否则称为非连通图。

无向图G中的极大连通子图称为G的连通分量。显然,任何连通图的连通分量只有一个,即本身,而非连通图有多个连通分量。
在这里插入图片描述

10 强连通图、强连通分量

在有向图中,若从顶点i到顶点j有路径,则称从顶点i到j是连通的。

若图G中的任意两个顶点i和j都连通,即从顶点i到j和从顶点j到i都存在路径,则称图G是强连通图。

有向图G中的极大强连通子图称为G的强连通分量。显然,强连通图只有一个强连通分量,即本身。非强连通图有多个强连通分量。
在这里插入图片描述

  • 注意:在无向图中讨论连通性,在无向图中讨论强连通性。

11 权和网

图中每一条边都可以附带有一个对应的数值,这种与边相关的数值称为权。边上带有权的图称为带权图,也称作网。

12 距离

如果从顶点u出发到顶点v的最短路径存在,则称此路径长度为u到v的距离,若u到v无路径,则距离记为∞

二 图的存储和基本操作

一图的存储

图主要有两种存储结构:邻接矩阵、邻接表。

邻接矩阵

邻接矩阵是表示顶点之间相邻关系的矩阵。设G=(V,E)是具有n (n>0)个顶点的图,顶点的编号依次为0~n-1。则图G的邻接矩阵A表示为n×n的二维数组。
在这里插入图片描述

  • A[i][j] = 1表示<i,j>∈E(G)或(i,j)∈E(G)。

  • 带权图 A[i][j] = wij表示(i,j)∈E(G)或<i,j>∈E(G),0或∞表示(i,j)∉E(G)或<i,j>∈E(G)

const int N = 100;		//顶点数目
typedef struct {
    int no;				//顶点编号
    infoType info;		//顶点其他信息
}VertexType;
typedef struct {
    int edges[N][N];	//邻接矩阵
    int n,e;			//顶点数、边数
    VertexType v[N];	//存放顶点信息
}MGraph;
  • 适合稠密图

邻接表

对图中每个顶点i建立一个单链表,将顶点i的所有邻接点链起来。
在这里插入图片描述在这里插入图片描述

typedef struct ANode		
{     
    int adjvex;				//该边的终点编号
    struct ANode *nextarc;	// 下一条边的指针
    //InfoType info;		//该边的权值等信息
}ArcNode;					//边结点类型

typedef struct Vnode		
{
    Vertex data;			//顶点信息
    ANode *firstarc;		//指向第一条边的指针
}VNode;						//头结点类型

typedef struct 
{     
    VNode adjList[MAXV] ;	//邻接表
    int n,e;				//图中顶点数n和边数e
}AdjGraph;					

二 图的基本操作

对邻接矩阵操作很简单,这里介绍邻接表的基本操作

void CreateAdj(AdjGraph *&G) //创建图的邻接表
{
	int i,j,k,w;
	ArcNode *p;
	G = (AdjGraph *)malloc(sizeof(AdjGraph)); 
	cout << "请输入图的顶点数和弧的数目:";
	cin >> G->n >> G->e;
	for (i=0; i < G->n; i++){
        //给邻接表中所有头结点的指针域置初值
		G->adjList[i].firstarc = NULL;
		G->adjList[i].data = i;
	}
	cout<<"请输入每条边的起始点和终止点的编号及边的权值,以空格隔开:\n";
    for(k = 0; k < G->e; ++k){      			//输入各边,构造邻接表
       	cin >> i >> j >> w;              		//输入一条边依附的两个顶点的编号
       	p=(ArcNode *)malloc(sizeof(ArcNode)); 	//生成一个新的边结点*p
	   	p->adjvex=j; 							// 邻接点序号为j
	   	p->weight=w;							//头插入到边结点链表中
	   	p->nextarc = G->adjList[i].firstarc; 
       	G->adjList[i].firstarc=p;
    }
}
void DispAdj(AdjGraph *G)	//输出邻接表G
{      int i;
       ArcNode *p;
       for (i=0;i < G->n; i++)
       {	
           	p = G->adjList[i].firstarc;
			printf("%3d: ",i);
			while (p != NULL)
			{       
                printf("%3d[%d]→",p->adjvex,p->weight);
	         	p = p->nextarc;
			}
			printf("\n");
       }
}
void DestroyAdj(AdjGraph *&G)   //销毁邻接表
{      
    int i; ArcNode *pre,*p;
    for (i=0;i < G->n;i++)		//扫描所有的单链表
    {	
        pre=G->adjList[i].firstarc;//p指向第i个单链表的首结点
		if (pre != NULL)
		{  
            p = pre->nextarc;
	        while (p != NULL)	//释放第i个单链表的所有边结点
	        {	
                free(pre);
				pre = p;
                p = p->nextarc;
	         }
	         free(pre);
		}
    }
    free(G);			//释放头结点数组
}

三 图的遍历

从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次,这个过程称为图的遍历,图的遍历得到的顶点序列称为图遍历序列。

深度优先搜索(DFS)

设置一个visited[]全局数组,visited[i] = false表示顶点i没有访问; visited[i] = true表示顶点i记经访问过。

(1)从图中某个初始顶点v出发,首先访问初始顶点v。

(2)选择一个与顶点v相邻且没被访问过的顶点w,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。
在这里插入图片描述

bool visited[MAX_V];			  	//记录顶点访问信息
void DFS(AdjGraph *G,int v){	   //从顶点v出发,开始遍历
    ArcNode *p; 
    int w;
    visit(v); 						//访问结点v的数据域
	visited[v] = true; 			 	//该节点已被访问
    
    p = G->adjList[v].firstarc;    //p指向顶点v的第一条边结点
    while (p != NULL) 			   
    {
        w = p->adjvex;			  //取出p中下一个节点的编号
		if(visited[w] == 0) 	 //如果w没被访问过,则访问
            DFS(G,w);   		//若w顶点未访问,递归访问它
		p = p->nextarc;  		//继续寻找下一个节点
    }
}
void DFSTravel(AdjGraph * G){
    for(int i = 0;i < G->n;i++) 
        if(visited[i]) DFS(G,i);
}

广度优先搜索(BFS)

1)访问初始点v,接着访问v的所有未被访问过的邻接点v0,v1, v2,…,Vn
(2)按照v0,v1,…,vn的次序,访问每一个顶点的所有未被访问过的邻接点。
(3)依次类推,直到图中所有和初始点v有路径相通的顶点都被访问过为止。
在这里插入图片描述

bool visited[MAX_V];
void BFS(AdjGraph * G,int v){    
    ArcNode *p;
    int w,i;	
    visit(v);				//访问结点v的数据域
    visited[v] = true;		//置节点已被访问
    push(qu,v);				//将节点v入队
    
    while(!IsEmpty(qu)){ 	//队列不为空则循环
        pop(qu.v);			//取出队头元素
        for(p = G->adjList[v].firstarc;p != NULL; p = p->nextarc){	//遍历v的所有邻接点
        	w = p->adjvex;
            if(!visited[w]){  //如果节点w没有被访问过~没有入队
                visit(w);	  //访问该节,并入队
                visited[w] = true;
                push(qu,w);	 
            }
        }
    }
}
void BFSTravel(AdjGraph * G){
    InitQueue(qu);			//初始化队列qu	
    for(int i = 0;i < G->n;i++) 
        if(visited[i]) BFS(G,i);
}

四 生成树和最小生成树

生成树

一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n-1)条边
在这里插入图片描述

  • 在一课生成树上任意添加一条边,必定会构成一个环

由深度优先遍历得到的生成树称为深度优先生成树
由广度优先遍历得到的生成树称为广度优先生成树在这里插入图片描述

  • 注意:一个连通图的生成树不是唯一的

最小生成树

对于带权连通图G(每条边上的权均为大于零的实数),可能有多棵不同生成树。

每棵生成树的所有边的权值之和可能不同。

其中权值之和最小的生成树称为图的最小生成树

求最小生成树算法有Prim、Kruskal两个算法。

Prim算法

此算法可以称为"加点法”,每次迭代选择权值最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。

过程

  • 1、初始化集合u = {s},s到其他顶点的边作为后选边
  • 2、重复以下步骤n-1次,使得其他n-1个顶点被加入到u中
    • 2.1 从候选边中挑选出权值最小的边(u0,v0),该边在v = V-U(v是所有没有被加入到u的顶点的集合)中对应顶点为v0,将v0加入u中
    • 2.2 枚举v0中所有边(v0,l),如果l在v中,且权值小于(u0,l),则利用(v0,l)更新(u0,l)
const int INF = 0x3f3f3f3f;			//定义正无穷
bool st[N];
int prim(MGraph G){					
    memset(dist,0x3f,sizeof dist);	//将所有候选边初始化为正无穷
    dist[1] = 0;					//将顶点1加入到集合u中
	st[1] = true;					//表示顶点1已被加入u中
    int res = 0;					//最小生成树权值之和
    for(int i = 2;i <= n;i++) dist[i] = G[1][i];	//用顶点1去更新dist(即初始化1到其邻接点边的权值)
    
    //循环n-1次
    for(int i = 1;i < n;i++){
        int minimum = INF;				//用来记录候选边集合dist中权值最小的边
        int t = -1;						//t记录v中到u权值最小的顶点
        
        //2.1 找到dist中权值最小的边
        for(int k = 2;k <= n;k++){	
            if(!st[k] && dist[k] < temp){	//如果k不在u中,且k到u的距离小于minimum就将下标赋给t
                minimum = dist[k];			//候选边中最小权值更新为dist[k]
                t = k;
            }
        }
        
        //如果t==-1,意味着在集合v找不到边连向集合u,该图不是连通图,没有最小生成树,返回INF
        if(t == -1) return INF;	
        //否则,将顶点t加入u
        st[t] = true;
        res += minimum;			
        
        //2.2 利用t去更新dist
        for(int k = 2;k <= n;k++) dist[k] = min(dist[k],G[t][k]);
    }
    
    return res;		
}
Kruskal算法

此算法可称为"加边法",将图中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边构成环路,就可以选择它组成最小生成树。

过程:

  • 1、置u的初值等于V(即包含有G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个连通分量)。
  • 2、将图G中的边按权值从小到大的顺序依次选取之后
    • 若选取的边未使生成树T形成回路,则加入TE
    • 否则,将该边舍弃,一直到TE包含n-1条边(n个顶点)为止
struct Edge{
	int a,b,w;	//一条边有两个顶点,一个权值
}edges[M];

void kruskal(){
    int res = 0;	//最小生成树的权值之和
    int cnt = 0;    //当前边的数量
    //将所有边升序排序
    sort(edges,edges+m);
    
    for(int i = 1;i <= n;i++) p[i] = i;
    
    for(int i = 0; i < m;i++){
        int a = edges[i].a;b = edges[i].b,w = edges[i].w;
        //利用并查集查找顶点a与顶点b是否在同一个集合中
        a = find(a),b = find(b);	
        //如果在同一个集合中,就不能选该边,否则就会构成环路
        if(a != b){
            p[a] = b;	//将a所在的集合和b所在的集合进行合并
            res += w;
			cnt++;		//边数+1
        }
    }
    //如果边数不等于n-1的话,此图不适连通图,没有最小生成树
    if(cnt < n - 1) res = INF;		
    return res;
}
举报

相关推荐

0 条评论