数据结构之图(上)

图的定义和概念:

      图的结构是任意两个数据对象之间都可能存在某种特定关系的数据结构
      图(Graph)是由两个集合构成的,一个是非空但有限的顶点集合V(Vertex),另一个是描述顶点之间的关系——边的集合E(Edge)
     所以图可以表示为G=(V,E),每一条边每一条边都是一顶点对(v,w),均属于V之中

注意点:在图中,把数据对象称作顶点。且至少要求要有一个顶点,但边的集合可以是空的。

图的相关术语:

     无向图:顶点之间的边是没有方向的。即(v,w)=(w,v)。用"( )"表示无向边
     有向图:顶点之间所有的边都是有方向的,即(v,w)≠(w,v)。用"< >"表示有向边,方向不可随意颠倒。
     完全图:任意两个顶点之间都有一条边相连。
     097180ce866c4dfea57c54f5ab10bb72.png注意:无向完全图N个顶点(N-1)N/2条边,有向完全图N个顶点N(N-1)条边。
      稀疏图:边或者弧(有向边)很少的图,即满足E(边数)<N(Log2)N,N为顶点数
      稠密图:边或者弧(有向边)较多的图。
      网:边或者弧上面带权值。
      邻接:有边或者弧相连的两个顶点之间的关系,就是两个顶点之间有或者弧。
                 注意存在(vi,vj)则称vi和vi互为邻接点。存在<vi,vj>则称vi邻接到vj,vj邻接于vi
      关联(依附):边或者弧与顶点的关系。存在(vi,vj)/<vi,vj>,则称边/弧关联与vi和vj。
      顶点的度:与顶点相关联的边的数目,记TD(v)。
                        在有向图中又分为入度出度,有向图中顶点的度为顶点处入度出度之和。
      顶点v的入度:以顶点v为终点的有向边的条数,记作ID(v)。
      顶点v的出度:以顶点v为起点的有向边的条数,记作OD(v)。
a6b6497d413e4892afdc6b1354efd643.png
       出度是从这个顶点出去的,入度是到这个顶点的。
       路径:接续的边(就是若干条连续的边)所构成的顶点序列
       路径长度:路径上边或者弧的数目或者边上的权值之和
       回路(环):第一个顶和最后一个顶点是相同的路径。
       简单路径:除了起点和终点可以相同外,其余顶点均不相同的路径。就是路径没有重复走过。
       简单回路(简单环):除了起点和终点相同外,其余顶点均不相同的路径。
        29fa40c57712467caf52314ff2128e71.png
        连通图(强连通图):在有(无)向图G=(V,{E})中,对任何两个顶点v,u都存在从v到u的路径。则称G为连通图(强连通图)。注意是路径不是边。  
0ea0e3f3b4e94c31824575840c07125b.png有向图的连通图是强连通图
       7b9a80e980a145e4b4ec10ac3027150f.png
        极大连通子图:该子图是G的连通子图,将G的任何不在该子图的顶点加入,子图不在连通。就是说加了个顶点他就不连通了。
        连通分量(强连通分量):在有向图中称强连通分量,无向图(有向图)的极大连通子图,称为G的连通分量。就是将一个非连通图拆成两个连通的子图。
94f705789c5043898395a803de4b78b2.png
         极小连通子图:对于一个图G的连通子图,删除一条边就不连通的图。
         生成树:包含了极小连通子图的所有顶点的图。
         生成森林:对非连通图,各个连通分量的生成树的集合。
349de55d4e5c4a088b92d54d86b65426.png
        连通图和完全的区别:举个例子,四个顶点的完全图有6条边,也就是四条边加上2条对角线;而连通图可以只包含周围四条边就可以了。完全图一定是连通图,但连通图不一定是完全图。连通图只要存在任意两个顶点之间的路径就好了。

一些性质:

     对于一个具有n(n>0)个顶点的无向连通图,它包含的连通分量的个数为:最少1个,就是它本身;最多n个,该图没有边,每个顶点构成一个连通分量。
    已知无向图G含有16条边,其中度为4的顶点个数为3,度为3的顶点个数为4,其他顶点的度均小于3。图G所含的顶点个数至少是:无向图边数的两倍等于各顶点度数的总和。由于其他顶点的度均小于3,可以设它们的度都为2,设它们的数量是x,可列出这样的方程4*3+3*4+2*x=16*2,解得x=4。4+3+3=11

    若无向图 G = (V, E) 中含 7 个顶点,则保证图 G 在任何连边方式下都是连通的,则需要的边数最少是:
     可以看到,同样是给出 7 点 6 边,第二种情况并不能连通,也就不符合题目中“任何情况”的要求。由此我们可以分析到,要让 7 个点都连通,那么先让 6 个点完全连通,所谓完全就是每个点能够支出的边是满的,这样 6 个点的情况下,边和点的关系是满的。其边的数量由公式 n*(n-1)/2 得出(无向完全连通图),也就是 6*5/2=15;那么此时,我多了一个点,7 号点,只需要在那 6 个点的图中连一根边过来,7 号点就可以访问任意 6 点图中的点了。

d5d82ab981704aed85390f371f88e06f.png81981924078940dda355685b63c8f241.png
        

   如果G是一个有15条边的非连通无向图,那么该图顶点个数最少为:n(n-1)/2=15,则n=6,非连通再加一个顶点,即6+1=7。
    
用邻接矩阵法存储图,占用的存储空间数只与图中结点个数有关,而与边数无关。
    如果无向图G必须进行两次广度优先搜索才能访问其所有顶点,则G中一定有回路。
    不一定还可能存在2个连通分量
    如果无向图G必须进行两次广度优先搜索才能访问其所有顶点,则G一定有2个连通分量

如何表示一张图:

     利用邻接矩阵或者邻接表去表示。

    邻接矩阵:

     方便检查任意一对顶点是否存在边,方便找任意一顶点的所有邻接点(有边直接相邻的顶点),方便计算任意一顶点的度。
f6b2be18e85a48daa461235a0abc623e.png邻接矩阵中,认为行是起点列是终点,例如A[1][3]就标志1到3是存在的。对于一个顶点,例如A[3][3],那就看他所在的行和列,所在行为1是出度,从这个点出去到另一个点的边,所在列为1是入度,表示另一个点到这个点的边。注意主对角线的值一定为0,因为不可以有自回路。且有向图不一定对称。
      顶点的出度是第i行的元素和,顶点的入度是第i列的元素和。有向图的顶点的度=入度+出度
      81f87693e96e416491d09b9f9e6d5087.png
有权图的邻接矩阵对于不存在的边表示为无穷。
空间复杂度:T=O(N²)
缺点是:
不方便对元素的增加行列或者删除行列,对于稀疏图来说浪费空间。
                统计稀疏图中边的个数比较麻烦,要遍历所有边。

图的定义结构:

struct GNode {
	int VertexNum;//记录定点数
	int EdgeNum;//记录边数
	char* VertexData;//顶点存放数组,一维数组表示顶点,因为有时候定点不是数字
	int** WeightEdge;//带权重的边,没权重默认01表示有无
};
typedef struct GNode* Graph;//定义指向图结点的指针,这样方便传入指针对图直接修改

初始化一张图:

//初始化一张图
Graph CreateGraph(int Vertex) {
	Graph MyGraph = (Graph)malloc(sizeof(struct GNode));
	MyGraph->EdgeNum = 0;
	MyGraph->VertexNum = Vertex;
	MyGraph->VertexData = (char*)malloc(Vertex * sizeof(char));//分配顶点数个空间数组
	MyGraph->WeightEdge = (int**)malloc(Vertex * sizeof(int*));//二维数组分配空间,相当于分配了列
	for (int i = 0; i < Vertex; i++) {
		MyGraph->WeightEdge[i] = (int*)malloc(Vertex * sizeof(int));//给每行分配空间
		for (int j = 0; j < Vertex; j++) {
			MyGraph->WeightEdge[i][j] = 0;
		}
	}
	return MyGraph;//返回初始化的图
}

插入边:

//往图中插入边
//对于边,定义边的结构,方便函数的多次操作
struct ENode {
	int V1, V2;//边对应的顶点
	int Weight;//边上边的权重
};
typedef struct ENode* Edge;//之所以是指针是因为再其他函数调用中如果不是指针那么可能造成边的数据丢失
void InsertEdge(Graph GG, Edge EE) {
	GG->WeightEdge[EE->V1 - 1][EE->V2 - 1] = EE->Weight;//V1到V2这个点上的边获得权重
	//如果是无向图,则另个一边也要获得权重
	GG->WeightEdge[EE->V2 - 1][EE->V1 - 1] = EE->Weight;//因为数组的从0,0开始的
}

生成一张图:

//生成一张图
Graph BuildGraph() {
	int VertexNum;//存取图的顶点数
	printf("请输入顶点数:");
	scanf("%d", &VertexNum);
	Graph GG = CreateGraph(VertexNum);
	//往图中插入边
	printf("\n请输入插入的边数:");
	scanf("%d", &GG->EdgeNum);
	Edge EE;//定义一个边的变量用来存放边
	printf("\n请输入的边的参数:");
	if (GG->EdgeNum != 0) {
		//只要边的数目不为0
		EE = (Edge)malloc(sizeof(struct ENode));
		for (int i = 0; i < GG->EdgeNum; i++) {
			scanf("%d %d %d", &EE->V1, &EE->V2, &EE->Weight);//把变得参数传入到边变量中
			InsertEdge(GG, EE);//利用函数插入边
		}
	}
	//如果顶点有数据就对顶点数据处理
	printf("\n请输入顶点数据");
	for (int j = 0; j < GG->VertexNum; j++) {
		scanf("%s", &GG->VertexData[j]);
	}
	return GG;//放回图的地址
}

定位顶点下标:

int LocateTag(Graph GG, char VertexData) {
	int tag = -1;
	for (int i = 0; i < GG->VertexNum; i++) {
		if (GG->VertexData[i] == VertexData) {
			tag = i;
			break;
		}
	}
	return tag;
}

执行测试代码:

int main() {
	Graph GG = BuildGraph();
	for (int i = 0; i < GG->VertexNum; i++) {
		for (int j = 0; j < GG->VertexNum; j++) {
			printf("%d", GG->WeightEdge[i][j]);
		}
		printf("\n");
	}
	printf("\n顶点数据");
	for (int k = 0; k < GG->VertexNum; k++) {
		printf("%c", GG->VertexData[k]);
	}printf("\n");
	printf("%d", LocateTag(GG, 'B'));
	return 0;
}

 

快速建立邻接矩阵:

int main() {
	int VertexNum;
	printf("请输入顶点数:");
	scanf("%d", &VertexNum);
	Graph GG = (Graph)malloc(sizeof(struct GNode));
	GG->VertexNum = VertexNum;
	GG->WeightEdge = (int**)malloc(sizeof(int*) * VertexNum);
	for (int i = 0; i < VertexNum; i++) {
		GG->WeightEdge[i] = (int*)malloc(VertexNum * sizeof(int));
		for (int j = 0; j < VertexNum; j++) {
			GG->WeightEdge[i][j] = 0;
		}
	}
	int EdgeNum;
	int v1, v2, weight;
	printf("请输入边数");
	scanf("%d", &EdgeNum);
	printf("请输入数据");
	for (int k = 0; k < EdgeNum; k++) {
		scanf("%d %d %d", &v1, &v2, &weight);
		GG->WeightEdge[v1 - 1][v2 - 1] = weight;
		GG->WeightEdge[v2 - 1][v1 - 1] = weight;
	}
	for (int i = 0; i < GG->VertexNum; i++) {
		for (int j = 0; j < GG->VertexNum; j++) {
			printf("%d", GG->WeightEdge[i][j]);
		}
		printf("\n");
	}
}

邻接表:

     邻接表:方便查找任一顶点的所有邻接点,可以节约稀疏图的空间。
                   需要N个头指针和2E个结点(每一条边被存了两次),每个结点值少两个域(顶点值和下                       一个位置)。方便计算无向图的度。对于有向图:只能计算出度(就是Vi往外指出去的                       边),要计算入度,还需要构造逆邻接表来记录指向自己的边

       用邻接表表示有N个顶点、E条边的图,则遍历图中所有边的时间复杂度为:O(N+E)

       1c122b8a90b14ce48ab86455c7f480ef.png
     邻接表就是一维数组形式的链表,每一个数组下标都对应着一条链表,里面记录的是这个顶点所对应的边的结点和数据。
     无向图邻接表的特点:邻接表不唯一,边可以无序;
                                         若无向图中有N个顶点,E条边,那么他的邻接表需要N个头结点2E个                                               边结点,适合存储稀疏图。
                                         空间复杂度:T=O(N+2E)
                                         无向图中顶点Vi的度为第i个单链表中的结点数

     有向图邻接表的特点:只记录顶点发出的边。
                                         空间复杂度:T=O(N+E)
                                         出度为第i个链表的结点个数。
                                         出度是整个单链表中邻接点值为i-1的结点数
                                         所以有向图的找入度容易出度难。

8411fa9c19814f2ea62136bafed9ff41.png
     

邻接表的结构:

/首先邻接表是单链表数组的集合,所以先建立一个单链表,然后数组化,链表里面是结点数据和下一个地址
struct ENode {
	//邻接点类型
	int AdjNodeTag;//邻接点对应数组的下标
	int Weight;//到这个点的边上面的权重
	struct ENode* Next;
};
typedef struct ENode* EList;
struct ALNode {
	//邻接表的数组信息
	EList EE;//邻接点指针,用来指向后面的边,可以理解为记录边的链表
	char Data;//记录顶点数据
};
typedef struct ALNode* AdjList;
//在做一个封装,记录总信息:总的顶点数边数和数组
struct GNode {
	int VerNum, EdgeNum;
	AdjList AG;
};
typedef struct GNode* Graph;//封装一个图

初始化:

//写一个初始化
Graph CreatGraph(int Vertex) {
	Graph GG = (Graph)malloc(sizeof(struct GNode));
	GG->EdgeNum = 0; GG->VerNum = Vertex;
	GG->AG = (AdjList)malloc(sizeof(struct ALNode) * Vertex);//f分配链表数组的空间
	for (int i = 0; i < GG->VerNum; i++) {
		GG->AG[i].EE = (EList)malloc(sizeof(struct ENode));//对于链表也要分配空间,因为里边有指针
		GG->AG[i].EE->Next = NULL;//注意下就是对链表数据的边的部分,初始化先指空
		GG->AG[i].EE->AdjNodeTag = GG->AG[i].Data = GG->AG[i].EE->Weight = 0;//一次性初始化
	}
	return GG;
}

插入边:

//往图中插入边:思路就是头插法
struct Enode {
	int V1, V2;
	int Weight;
};
typedef struct Enode* Edge;
void InsertEdge(Graph GG, Edge EE) {
	//建立一个中继结点来做头插,注意类型
	EList temp = (EList)malloc(sizeof(struct ENode));
	temp->AdjNodeTag = EE->V2;//接收边的终点下标
	temp->Weight = EE->Weight;
	//做头插
	temp->Next = GG->AG[EE->V1].EE->Next;
	GG->AG[EE->V1].EE->Next = temp;//注意EE->V1就是边的起点
	//对于无向图另一边也要插入
	temp = (EList)malloc(sizeof(struct ENode));//注意结点在生成不然同一地址会被替换
	temp->AdjNodeTag = EE->V1;//接收边的终点下标
	temp->Weight = EE->Weight;
	//做头插
	temp->Next = GG->AG[EE->V2].EE->Next;
	GG->AG[EE->V2].EE->Next = temp;//注意EE->V2就是边的起点
}

找下标:

//根据顶点数据找数组下标
int LocateTag(Graph GG, char data) {
	int num = GG->VerNum;
	if (num == 0) {
		printf("没有顶点");
		return 0;
	}
	for (int i = 0; i < num; i++) {
		if (GG->AG[i].Data == data) return i;
	}
}

建立图:

Graph BuildGraph() {
	int vertex;
	printf("请输入顶点数:");
	scanf("%d", &vertex);
	Graph GG = CreatGraph(vertex);
	printf("\n请输入顶点数据:");
	for (int j = 0; j < vertex; j++) {
		scanf("%s", &GG->AG[j].Data);
	}
	int edge;
	printf("\n请输入边的个数:");//这里写的插入是无向图的插入,所以填总边数的一半
	scanf("%d", &edge);
	GG->EdgeNum = edge;
	printf("\n请输入边的数据:");
	Edge EE = (Edge)malloc(sizeof(struct Enode));
	for (int i = 0; i < edge; i++) {
		getchar();//用来吸收回车,防止读入错误
		char v1, v2;
		scanf("%c %c %d", &v1, &v2, &EE->Weight);
		EE->V1 = LocateTag(GG, v1);
		EE->V2 = LocateTag(GG, v2);
		InsertEdge(GG, EE);
	}
	printf("\n数据录入完毕\n");
	return GG;
}

测试代码:

int main() {
	Graph GG = BuildGraph();
	//打印信息
	int num = GG->VerNum;
	printf("\n\n\n");
	printf("这张图的顶点数:%d\t总的边数为:%d\n", GG->VerNum, GG->EdgeNum);
	printf("输出各个链表结点的信息:\n");
	for (int i = 0; i < num; i++) {
		printf("下标:%d\t顶点:%c\t出度的边:", i, GG->AG[i].Data);
		EList flag = (EList)malloc(sizeof(struct Enode));
		flag = GG->AG[i].EE;
		while (flag->Next != NULL) {
			flag = flag->Next;
			printf(" %d", flag->AdjNodeTag);
		}
		printf("\n");
	}
	printf("\nOver\n");
	return 0;
}

邻接表和邻接矩阵得区别:

       对于任意确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),邻接表不是唯一的(链接次于与顶点无关)
     邻接矩阵得空间复杂度为O(N²),邻接表空间复杂度O(N+E),因此对于稀疏图适合邻接表。稠密图适合邻接矩阵

689459822cdd45dc8755bfe2d9de3a2c.png

十字链表:

         空间复杂度:O(V+E)
         用于有向图

操作理解:对于一个有向图定义顶点的时候增加一个入度指针和一个出度指针,用来读取入度边和出度边,因为是有向图所以顶点的顺序决定了边的方向,所以对于边结点的定义是,起点终点和起点相同的指针与终点相同的指针,这样对于一个顶点的出度就是访问的出度指针,然后就会指向以这个顶点为出发点的边,接着只要访问起点相同的指针到空为止,就能访问所有的出度了。
d3dbe031f2e14b9e9715993bd31f31db.png

 邻接多重表:

                      空间复杂度:O(V+E)
                      用于存储无向图

操作理解:因为是无向图,所以对于边结点的读入先后顺序无关,所以用邻接多重表存储无向图的边保证只被存一次,那么就需要构造一个用来存放边的结构体,这个结构体里面放着两个顶点的数据和两个用来代表是哪个顶点开头的指针,类似于十字链表。通过构造一个顶点结点,里面存放数据和一个指针用来指向边结点的地址,然后访问边结点的两个数据就是到是那边到那条边了。
              例如:有01和10两条边,则边结点就是0X1Y,其中X,Y就是指针,用来指向下一个边界点,X就是用来指向0开头的边结点的指针,Y就是用来指向1开头的边结点。
               无向图的边之用构造一次,但是每个存在边的顶点都要去连接她。      


a38b3194df89412c9cbace070ab0a0c9.png 560305f2c9ed4a688a85f6a4c83b1ab5.png

9117b54f9f304797a407d9fed12e5927.png

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值