图-数据结构

图的介绍

如果你有学过《离散数学》,那么对图的概念一定不陌生,在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列连接(结对)。顶点用圆圈表示,边就是这些圆圈之间的连线。注意:顶点也称为节点或交点,边也称为链接。

同时边可以是双向的,也可以是单向的,不一定所有的顶点中间一定要有边,如图:

大家都或多或少听说过最短路径算法,那你知道生活中有哪些场景出现过它的应用吗?没错,就是平常使用的地图导航软件,如图:

在上图中起点、终点、每一个交叉路口都可以看作是一个顶点,而连接它们的绿色通道就是边。

另外可以从图上看出,从一个地方到另一个地方有着多种路径,影响因素有距离、红绿灯、拥堵情况、事故。对于多种因素可以使用边的权重来表示,根据情况的不同给边分配数值。

我们之前学的树和链表其实都是图的特例。

图的表示方法

邻接列表

每一个顶点都会保存着与它相邻的边,并且这个边的起点是这个顶点,即只描述指向外部的边。例如下图中,B应该保存着三条边,C保存一条边,因为B有三条指向外面,而C只有一条指向外面。

而它的邻接列表表示为:

 按照这样的存储方式,想必查找两个顶点之间关系或者权重时会有点费时。

邻接矩阵

邻接矩阵由二维数组表示,行和列都表示顶点,两个顶点所对应的矩阵元素数值表示两个顶点是否相连( 0 表示不相连,非 0 表示相连以及权重值)。

将上面的图用邻接矩阵表式则为(在这里边上无权重,所有1表示的是相连):

这邻接矩阵查找就特别快了,但是往里面添加顶点的成本很高,因为要重新按照新的行和列创建一个新的矩阵,然后将已有的数据复制到新矩阵中去。

使用哪个更好

 操作       邻接列表邻接矩阵
存储空间O(V+E)O(V^2)
添加顶点O(1)O(V^2)
添加边O(1)O(1)
检查相邻性O(1)O(1)

其中V表示图中顶点的个数,E表示边的个数。

结论:大多数情况选择邻接列表。除非图比较密集,即每个顶点都与大部分顶点相邻,并且个数不多,那么就选择邻接矩阵。

图的算法实现

结构体定义

首先要理清楚结构关系,这里的图中有三种结构,分别是边、顶点、邻接链表,以下是它们的定义。

#define MaxSize 1024

typedef char DateElem; //顶点中存储的元素
//边
typedef struct _EdgeNode
{
	int adjvex; //与之相邻的节点的位置
	int weight; //边的权重
	struct _EdgeNode* next; //指向下一条与这个顶点相邻的边
}EdgeNode;


//顶点
typedef struct _VertexNode
{
	DateElem date;				//顶点的数据
	struct _EdgeNode* first;    //指向第一条与之相邻的边
}VertexNode,AdjList;

//邻接链表 
typedef struct _AdjListGraph
{
	AdjList* adjlist; //数组,保存着所有的顶点
	int vex; //顶点数
	int edge;//边数
}AdjListGraph;

邻接链表结构体中有着一个数组,数组保存着各顶点,顶点中又保存着与之相邻的边,整体是这样的结构。

图的初始化

其中 visited 数组是为了接下来的遍历操作,防止重复访问节点,因为在初始化里面出现了,那就一起写上。

bool visited[MaxSize]; //节点是否被访问过,被访问过为true
void Init(AdjListGraph& G)
{
	G.adjlist = new AdjList[MaxSize]; //像哈希表的结构
	G.edge = 0;
	G.vex = 0;

	for (int i = 0; i < MaxSize; i++)
	{
		visited[i] = false;
	}
}

图的创建

因为创建一条边,需要知道起始的顶点和结束的顶点的位置,所以使用自己定义的 Location 函数,通过你输入的顶点数据,找到这个顶点在数组中的位置。

int Location(AdjListGraph& G, DateElem c);

void Create(AdjListGraph& G)
{
	cout << "请输入顶点和边的个数:";
	cin >> G.vex >> G.edge;

	cout << "请依次输入顶点的数据:";
	
	for (int i = 0; i < G.vex; i++)
	{
		cin >> G.adjlist[i].date;
		G.adjlist[i].first = NULL;
	}


	cout << "请依次输入边的起始和终点:";
	DateElem v1, v2;
	int i1 = 0 , i2 = 0;
	
	for (int i = 0; i < G.edge; i++)
	{
		cin >> v1 >> v2;

		i1 = Location(G, v1);
		i2 = Location(G, v2);

		if (i1 != -1 && i2 != -1)
		{
			EdgeNode* tmp = new EdgeNode;
			tmp->adjvex = i2;
			tmp->next = G.adjlist[i1].first;
			G.adjlist[i1].first = tmp;
		}
	}
}

//通过顶点保存的数据找到顶点在图中的位置
int Location(AdjListGraph& G, DateElem c)
{

	for (int i = 0; i < G.vex; i++)
	{
		if (G.adjlist[i].date == c)
		{
			return i;
		}
	}

	return -1; //没找到
}

其中插入节点的操作为下图所示:

相当于链表的前插法。

图的遍历

如下图,这也是一个图结构,不过这让我想到树结构,是因为图的遍历方式和树的遍历方式有很多相似之处。

深度遍历

也就是以一个未被访问过的顶点作为起始顶点,沿当前顶点的边去访问未被访问过的顶点。

当没有未被访问过的顶点时,就回退到上一个顶点,访问别的顶点,直到所有的顶点都被访问过。

深度遍历其实迷宫问题中就出现过了,也就是这条路能走就一直走,不撞南墙不回头。

深度遍历和树的前序遍历很类似,假如起始顶点是 A,深度遍历可以是:A D F C B E,也可以是A B E F C D,只是因为输入边时的顺序不同。

//深度遍历一个节点
void DFS(AdjListGraph& G, int v)
{

	if (visited[v] == true)
	{
		return;
	}

	cout << G.adjlist[v].date << " ";
	visited[v] = true;

	EdgeNode* temp = G.adjlist[v].first;
	int cur = 0;
	while (temp != NULL)
	{
		cur = temp->adjvex; //当前节点的位置
		if (visited[cur] == false)
		{
			DFS(G, cur);
		}
		temp = temp->next;
	}

}
//深度遍历全部节点
void DFS_All(AdjListGraph& G)
{
    for (int i = 0; i < MaxSize; i++)
    {
	    visited[i] = false;
    }
	for (int i = 0; i < G.vex; i++)
	{
		if (visited[i] == false)
		{
			DFS(G, i);
		}
	}
}

广度遍历

首先以一个未被访问过的顶点作为起始顶点,访问其相邻的所有顶点。

然后对每个相邻的顶点,再访问它们相邻的未被访问过的顶点,直至所有顶点都被访问过,遍历结束。

广度遍历有点像树的层序遍历,例如 A 为起始顶点,那么广度遍历的结果可能是:A D C B F E 或者是:A B C D E F 。

//广度优先遍历 需要包含头文件 <queue> 
void BFS(AdjListGraph& G, int v)
{
	queue<int> q;
	q.push(v);

	int cur = -1;
	while (!q.empty())
	{
		cur = q.front(); //取队头元素
		q.pop(); //出队

		if (visited[cur] == false)
		{
			cout << G.adjlist[cur].date << " ";
			visited[cur] = true;
		}

		
        //将与 cur 顶点相邻的顶点都入队
		EdgeNode* tmp = G.adjlist[cur].first; 
		while (tmp != NULL)
		{
			q.push(tmp->adjvex);
			tmp = tmp->next;
		}


	}
}
void BFS_All(AdjListGraph& G)
{
	for (int i = 0; i < MaxSize; i++) //初始化
	{
		visited[i] = false;
	}

	for (int i = 0; i < G.vex; i++)
	{
		if (visited[i] == false)
		{
			BFS(G, i);
		}
	}
}

 

用上图的图作为演示,得到运行结果。

其实从它们的叫法中也能看出一些来。

全部代码

#include <iostream>
#include <queue>
using namespace std;

#define MaxSize 1024
typedef char DateElem;
//边
typedef struct _EdgeNode
{
	int adjvex; //与之相邻的节点
	int weight; //边的权重
	struct _EdgeNode* next; //指向下一条与这个顶点相邻的边
}EdgeNode;


//顶点
typedef struct _VertexNode
{
	DateElem date;				//顶点的数据
	struct _EdgeNode* first;//指向第一条与之相邻的边
}VertexNode,AdjList;

//邻接链表 
typedef struct _AdjListGraph
{
	AdjList* adjlist; //数组,保存着所有的顶点
	int vex; //顶点数
	int edge;//边数
}AdjListGraph;



bool visited[MaxSize]; //节点是否被访问过,被访问过为true
//图的初始化
void Init(AdjListGraph& G)
{
	G.adjlist = new AdjList[MaxSize]; //像哈希表
	G.edge = 0;
	G.vex = 0;

	for (int i = 0; i < MaxSize; i++)
	{
		visited[i] = false;
	}
}


int Location(AdjListGraph& G, DateElem c);
//图的创建
void Create(AdjListGraph& G)
{
	cout << "请输入顶点和边的个数:"<<endl;
	cin >> G.vex >> G.edge;

	cout << "请输入顶点:" << endl;
	
	for (int i = 0; i < G.vex; i++)
	{
		cin >> G.adjlist[i].date;
		G.adjlist[i].first = NULL;
	}


	cout << "请输入边:" << endl;
	DateElem v1, v2;
	int i1 = 0 , i2 = 0;
	
	for (int i = 0; i < G.edge; i++)
	{
		cin >> v1 >> v2;

		i1 = Location(G, v1);
		i2 = Location(G, v2);

		if (i1 != -1 && i2 != -1)
		{
			EdgeNode* tmp = new EdgeNode;
			tmp->adjvex = i2;
			tmp->next = G.adjlist[i1].first;
			G.adjlist[i1].first = tmp;
		}
	}
}

//通过顶点保存的数据找到顶点在图中的位置
int Location(AdjListGraph& G, DateElem c)
{

	for (int i = 0; i < G.vex; i++)
	{
		if (G.adjlist[i].date == c)
		{
			return i;
		}
	}

	return -1; //没找到
}


//图的遍历,深度和广度

//深度遍历一个节点
void DFS(AdjListGraph& G, int v)
{

	if (visited[v] == true)
	{
		return;
	}

	cout << G.adjlist[v].date << " ";
	visited[v] = true;

	EdgeNode* temp = G.adjlist[v].first;
	int cur = 0;
	while (temp != NULL)
	{
		cur = temp->adjvex; //当前节点的位置
		if (visited[cur] == false)
		{
			DFS(G, cur);
		}
		temp = temp->next;
	}

}
//深度遍历全部节点
void DFS_All(AdjListGraph& G)
{
	for (int i = 0; i < MaxSize; i++)
	{
		visited[i] = false;
	}

	for (int i = 0; i < G.vex; i++)
	{
		if (visited[i] == false)
		{
			DFS(G, i);
		}
	}
}


//广度优先遍历 包含头文件 <queue> 
void BFS(AdjListGraph& G, int v)
{
	queue<int> q;

	q.push(v);

	int cur = -1;
	while (!q.empty())
	{
		cur = q.front(); //取队头元素
		q.pop();

		if (visited[cur] == false)
		{
			cout << G.adjlist[cur].date << " ";
			visited[cur] = true;
		}

		
		EdgeNode* tmp = G.adjlist[cur].first; 
		while (tmp != NULL)
		{
			q.push(tmp->adjvex);
			tmp = tmp->next;
		}


	}
}
void BFS_All(AdjListGraph& G)
{
	for (int i = 0; i < MaxSize; i++)
	{
		visited[i] = false;
	}

	for (int i = 0; i < G.vex; i++)
	{
		if (visited[i] == false)
		{
			BFS(G, i);
		}
	}
}

int main(void)
{

	AdjListGraph G;

	//图的初始化
	Init(G);

	//图的创建
	Create(G);

	//图的遍历:深度遍历
	cout << "深度遍历:";
	DFS_All(G);

	cout << endl;

	//图的遍历:广度遍历
	cout << "广度遍历:";
	BFS_All(G);
	return 0;
} 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/237076.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Design patterns--代理模式

设计模式之代理模式 我们使用Qt开发大型应用程序时&#xff0c;经常遇见大型程序启动时需要加载一些配置信息、用户末次操作信息&#xff0c;以及算法模型等数据时比较费时&#xff0c;笔者在程序启动时设计欢迎页或加载页等窗体来提示用户程序正在加载某些数据&#xff0c;加载…

【Java数据结构 -- 顺序表】

List和ArrayList与顺序表 一. List1.1 List介绍2.1 常见接口介绍3.1 List的使用 二. ArrayList与顺序表1.线性表2.顺序表2.1 接口的实现2.2 顺序表的创建2.3 顺序表的打印2.4 顺序表的插入2.5 顺序表的按索引位置插入数据2.6 判断顺序表是否包含某个数2.7 返回顺序表某个数的索…

谁会成为第一个MoE大模型基座呢?重磅!Mixtral MoE 8x7B!!!

文章目录 谁会成为第一个MoE大模型基座呢&#xff1f;重磅&#xff01;Mixtral MoE 8x7B&#xff01;&#xff01;&#xff01;前言重磅&#xff01;Mixtral MoE 8x7B&#xff01;&#xff01;&#xff01;Mixtral是啥模型介绍模型结构长啥样&#xff1f;表现如何&#xff1f;可…

Python从入门到精通五:Python数据容器

数据容器入门 为什么学习数据容器 思考一个问题&#xff1a;如果我想要在程序中&#xff0c;记录5名学生的信息&#xff0c;如姓名。 如何做呢&#xff1f; 学习数据容器&#xff0c;就是为了批量存储或批量使用多份数据 Python中的数据容器&#xff1a; 一种可以容纳多份…

HCIA-H12-811题目解析(10)

1、【单选题】DHCP客户端在租期到达哪个比例时第一次发送续租报文&#xff1f; 2、【单选题】在WLAN中用于标识无线网络&#xff0c; 区分不同的无线网络的是&#xff1f; 3、【单选题】我们在笔记本电脑上搜索可接入无线网络时&#xff0c;显示出来的网络名称实际是 4、【单…

哪些原因导致MES管理系统实施项目失败

在制造业中&#xff0c;实施MES管理系统是一种提高生产效率、降低成本、提升质量的重要手段。然而&#xff0c;许多MES管理系统实施项目并未取得预期的成功&#xff0c;甚至失败。本文将探讨导致MES管理系统实施项目失败的原因。 1、需求不明确 在MES实施项目中&#xff0c;需…

Java-异常(一)-异常的概述和常见异常的举例

&#x1f436;b站视频 124-异常处理-异常的概述与常见异常的举例_哔哩哔哩_bilibili 目录 b站视频 5.1 异常概念 5.2 Error 示例代码 5.3 Exception异常划分 ❓面试题&#xff1a;常见的异常有哪些&#xff1f;举例说明 &#x1f436;5.1 异常概念 在使用计算机语言进行…

基于SSM的校园心理健康网站的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

PostGIS学习教程十二:地理

PostGIS学习教程十二&#xff1a;地理 坐标为"地理&#xff08;geographics&#xff09;“形式或者说是” 纬度&#xff08;latitude&#xff09;/经度&#xff08;longitude&#xff09;"形式的数据非常常见。 与Mercator&#xff08;墨卡托&#xff09;、UTM&…

【jitterbuffer】2:OnCompleteFrameCallback 送去FrameBuffer 处理的流程

【jitterbuffer】2:OnCompleteFrameCallback 送去FrameBuffer 处理的流程 基于m98版本。 WebRtc Video Receiver(六)-FrameBuffer原理 大神有个详细的论述。 Finder的FID设计 H.264 没有FID,使用RtpSeqNumOnlyRefFinder ,比较复杂,要做出决定 RtpSeqNumOnlyRefFinder cla…

canvas 有趣的弹簧效果

先上效果 两个小球之间有一根弹簧&#xff0c;这里有一条线表示&#xff0c;其中左球固定&#xff0c;在点击开始后&#xff0c;右球开始做自由落体 思路 先做受力分析 经过受力分析可以发现&#xff0c;整个系统一共有三个力在起作用&#xff0c;我们分别把他们求出来并合成…

鸿蒙原生应用再添新丁!同花顺入局鸿蒙

鸿蒙原生应用再添新丁&#xff01;同花顺入局鸿蒙 来自 HarmonyOS 微博12月11日消息&#xff0c;同花顺已完成#鸿蒙原生应用#beta版本&#xff0c;并正在进行全量版本开发&#xff0c;进一步丰富了#鸿蒙原生应用#的覆盖领域。同花顺作为股民和券商首选的一站式金融理财服务平台…

搜集怎么绘制三维曲线和曲面?

1、针对函数对象是单一变量、两个函数的情况。用plot3函数&#xff1b;&#xff08;三维曲线&#xff09; 看一下matlab官方的例子&#xff1a; t 0:pi/50:10*pi; st sin(t); ct cos(t); plot3(st,ct,t) 绘制出来的曲线&#xff1a; 几个比较关键的点&#xff1a; &…

Linux系统编程(一):基本概念

参考引用 Unix和Linux操作系统有什么区别&#xff1f;一文带你彻底搞懂posix Linux系统编程&#xff08;文章链接汇总&#xff09; 1. Unix 和 Linux 1.1 Unix Unix 操作系统诞生于 1969 年&#xff0c;贝尔实验室发布了一个用 C 语言编写的名为「Unix」的操作系统&#xff0…

nginx中的正则表达式及location和rewrite

目录 常用的Nginx 正则表达式 location和rewrite的区别 location location 大致可以分为三类 location 常用的匹配规则 location 优先级 location 示例说明 location优先级的总结 rewrite rewrite的功能 rewrite实现跳转的条件 rewrite的执行顺序 rewrite的语法格式…

C# 任务的异常和延续处理

写在前面 当Task在执行过程中出现异常或被取消等例外的情况时&#xff0c;为了让执行流程能够继续进行&#xff0c;可以使用延续方法实现这种链式处理&#xff1b;还可以针对前置任务不同的执行结果&#xff0c;选择执行不同的延续分支方法。子任务执行过程中的任何异常都会被…

【收获】成长之路

目录 一、前言二、计算机方面三、专业知识方面四、总结 一、前言 四年&#xff0c;对于一个人的成长来说&#xff0c;是一个相当重要的阶段。在这段时间里&#xff0c;我经历了许多挑战、收获了许多成就&#xff0c;也在不断地成长和改变。回首这四年的点点滴滴&#xff0c;我深…

linux docker 怎么更换镜像源

要设置Docker镜像&#xff0c;您可以按照以下步骤进行&#xff1a; 1. 打开终端并登录到Docker主机上。 运行以下命令来编辑 Docker 的配置文件 "/etc/docker/daemon.json"&#xff08;如果不存在则新建&#xff09;&#xff1a; sudo nano /etc/docker/daemon.js…

Django系列之Celery异步框架+RabbitMQ使用

在Django项目中&#xff0c;如何集成使用Celery框架来完成一些异步任务以及定时任务呢&#xff1f; 1. 安装 pip install celery # celery框架 pip install django-celery-beat # celery定时任务使用 pip install django-celery-results # celery存储结果使用2. Django集成…

URIBuilder与SSRF

在使用一个静态扫描工具时&#xff0c;报了一个SSRF的问题&#xff0c;经过数据流的分析&#xff0c;导致此工具报SSRF的原因是在调用URIBuilder的setPath函数时&#xff0c;参数是从请求里获取的&#xff0c;导致了数据流被污染&#xff0c;因此认为由URIBuilder构造的URL也被…