首页 > 编程语言 >算法与数据结构——图的基础操作及图的遍历(广度优先与深度优先)

算法与数据结构——图的基础操作及图的遍历(广度优先与深度优先)

时间:2024-09-11 14:53:13浏览次数:10  
标签:优先 遍历 及图 邻接矩阵 vector 邻接 顶点

图的实现

基于邻接矩阵的实现

给定一个顶点数量为n的无向图:

  • 初始化:传入n个顶点,初始化长度为n的顶点列表vertices,使用O(n)时间;初始化 n * n 大小的邻接矩阵adjMat,使用O(n2)时间。
  • 添加或删除边:直接在邻接矩阵中修改指定的边即可,使用O(1)时间。而由于是无向图,因此需要同时更新两个方向的边。

  • 添加顶点:在邻接矩阵的尾部添加一行一列,并全部填0即可,使用O(n)时间。
  • 删除顶点:在邻接矩阵中删除一行一列。当删除首行首列时达到最差情况,需要将(n-1)2个元素“向左上移动”,从而使用O(n2)时间。
/*基于邻接矩阵实现的无向图类*/
class GraphAdjMat{
	vector<int> vertices;					// 顶点列表,元素代表顶点值,索引代表顶点索引
	vector<vector<int>> adjMat;		// 邻接矩阵,行列索引代表对应的顶点索引

public:
	/*构造方法*/
	GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges){
		// 添加顶点
		// 添加边
	}

	/*获取顶点数量*/
	int size(){
		return vertices.size();
	}
	/*添加顶点*/
	void addVertex(int val){
		int n = size();
		vertices.push_back(val);
		// 在邻接矩阵中添加一行0
		adjMat.push_back(vector<int>(n, 0));
		// 在邻接矩阵中添加一列0
		for (vector<int> &row : adjMat){
			row.push_back(0);
		}
	}
	/*删除顶点*/
	void removeVertex(int index){
		if (index >= size()){
			cout << "顶点不存在" << endl;
			return;
		}
		// 在顶点列表中删除索引为index的顶点
		vertices.erase(vertices.begin() + index);
		// 邻接矩阵中删除索引 index 的行
		adjMat.erase(adjMat.begin() + index);
		// 邻接矩阵中删除索引 index 的列
		for (vector<int> &row : adjMat){
			row.erase(row.begin() + index);
		}
	}
	/*添加边*/
	void addEdge(int i, int j){
		// 索引越界与相等处理
		if (i < 0 || j < 0 || i >= size() || j >= size() || i == j){
			cout << "顶点不存在" << endl;
			return;
		}
		adjMat[i][j] = 1;
		adjMat[j][i] = 1;
	}
	/*删除边*/
	void removeEdge(int i, int j){
		// 索引越界与相等处理
		if (i < 0 || j < 0 || i >= size() || j >= size() || i == j){
			cout << "顶点不存在" << endl;
			return;
		}
		adjMat[i][j] = 0;
		adjMat[j][i] = 0;
	}

};

基于邻接表的实现

设无向图的顶点总数为n,边总数为m,则可根据以下各图实现各种操作。

  • 初始化:在邻接表中创建n个顶点和2m条边,使用O(n+m)时间。

  • 添加边:在顶点对应链表的末尾添加边即可,使用O(1)时间。因为是无向图,所以需要同时添加两个方向的边。

  • 删除边:在顶点对应链表中查找并删除指定边,使用O(m)时间。在无向图中,需要同时删除两个方向的边。

  • 添加顶点:在邻接表中添加一个链表,并将新增顶点作为链表头节点,使用O(1)时间。

  • 删除顶点:需遍历整个邻接表,删除包含指定顶点的所有边,使用O(n+m)时间。

以下是邻接表的代码实现:

  • 为了方便添加与删除顶点,以及简化代码,我们使用列表(动态数组)来代替链表。
  • 使用哈希表来存储邻接表,key为顶点实例,value为该顶点的邻接顶点列表(链表)。

另外,我们在邻接表中使用Vertex类来表示顶点,这样做的原因是:如果与邻接矩阵一样,用列表索引来区分不同顶点,那么假设要删除索引为i的顶点,则需要遍历整个邻接表,将所有大于i的索引全部减1,效率很低。如果每个顶点都是唯一的Vertex实例,删除某一顶点后就无需改动其他顶点了。

/*顶点*/
class Vertex{
	int val;
};

/*基于邻接表实现的无向图类*/
class GraphAdjList{
public:
	// 邻接表; key:顶点,value:该顶点的所有邻接顶点
	unordered_map<Vertex*, vector<Vertex*>> adjList;

	/*构造方法*/
	GraphAdjList(const vector<vector<Vertex*>> &edges){
		// 添加所有顶点和边
		for (const vector<Vertex*> &edge : edges){
			addVertex(edge[0]);
			addVertex(edge[1]);
			addEdge(edge[0], edge[1]);
		}
	}

	/*在vector中删除指定节点*/
	void remove(vector<Vertex*> &vec, Vertex *vet){
		for (int i = 0; i < vec.size(); i++){
			if (vec[i] == vet){
				vec.erase(vec.begin() + i);
				break;
			}
		}
	}
	
	/*获取顶点数量*/
	int size(){
		return adjList.size();
	}
	/*添加边*/
	void addEdge(Vertex* vet1, Vertex* vet2){
		// 顶点不存在 或 两个顶点相同
		if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2){
			cout << "顶点不存在" << endl;
			return;
		}
		adjList[vet1].push_back(vet2);
		adjList[vet2].push_back(vet1);
	}
	/*删除边*/
	void removeEdge(Vertex* vet1, Vertex* vet2){
		// 顶点不存在 或 两个顶点相同
		if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2){
			cout << "顶点不存在" << endl;
			return;
		}
		remove(adjList[vet1], vet2);
		remove(adjList[vet2], vet1);
	}
	/*添加顶点*/
	void addVertex(Vertex* vet){
		if (adjList.count(vet)){
			cout << "顶点已存在" << endl;
			return;
		}
		// 在邻接表中添加一个新链表
		adjList[vet] = vector<Vertex*>();
	}
	/*删除顶点*/
	void removeVertex(Vertex *vet){
		// 顶点不存在 或 两个顶点相同
		if (!adjList.count(vet) ){
			cout << "顶点不存在" << endl;
			return;
		}
		// 在邻接表中删除顶点vet对应的链表
		adjList.erase(vet);
		// 遍历其他顶点的链表,删除所有包含 vet 的边
		for (auto &adj : adjList){
			remove(adj.second, vet);
		}
	}
};

效率对比

设图中共有n个顶点和m条边,下表对比了邻接矩阵和邻接表的时间效率和空间效率。

邻接矩阵 邻接表(链表) 邻接表(哈希表)
判读是否邻接 O(1) O(m) O(1)
添加边 O(1) O(1) O(1)
删除边 O(1) O(m) O(1)
添加顶点 O(n) O(1) O(1)
删除顶点 O(n2) O(n+m) O(n)
内存空间占用 O(n2) O(n+m) O(n+m)

由表,似乎邻接表(哈希表)的时间效率与空间效率最优。但实际上,在邻接矩阵中操作边的效率更高,只需要一次数组访问或赋值即可。综合来看,邻接矩阵体现了“以空间换时间”的原则,而邻接表体现了“以时间换空间”的原则。

图的遍历

树代表的是“一对多”的关系,而图具有更高的自由度,可以表示任意“多对多”关系。因此,我们可以把树看做图的一种特例。显然,树的遍历操作也是图的遍历操作的一种特例

图和树都需要应用搜索算法来实现遍历操作。图的遍历方式也可分为两种:广度优先遍历深度优先遍历

广度优先遍历

广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外扩张

如图,从左上角顶点出发,首先遍历该顶点的所有邻接顶点,然后遍历下一个顶点的所有邻接顶点,以此类推,直至所有顶点访问完毕。

算法实现

BFS通常借助队列来实现。队列具有“先入先出”的性质,这与BFS的“由远及近”思想异曲同工。

  • 将遍历起始顶点startVet加入队列,并开启循环。
  • 在循环的每轮迭代中,弹出队首顶点,并记录访问,然后将该顶点的所有邻接顶点加入到队尾部。
  • 循环上一步骤,直到所有顶点被访问完毕后结束。

为了防止重复遍历顶点,我们需要借助一个哈希表visited来记录哪些节点已被访问。

/*广度优先遍历*/
// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
vector<Vertex*> graphBFS(GraphAdjList &graph, Vertex *startVet){
	// 顶点遍历序列
	vector<Vertex*> res;
	// 避免重复哈希表 用于记录访问过的顶点
	unordered_set<Vertex*> visited;
	// 队列
	queue<Vertex*> que;
	que.push(startVet);

	while (!que.empty()){
		Vertex* vet = que.front();
		que.pop();
		res.push_back(vet);
		for (Vertex* v : graph.adjList[vet]){
			if (!visited.count(vet)){
				que.push(v);			// 只入队为访问的顶点
				visited.emplace(v); // 标记该顶点已被访问
			}
		}
	}
	return res;
}

注意:广度优先遍历的序列是否唯一?
不唯一。广度优先遍历只要求按“由近及远”的顺序遍历,而多个相同距离的顶点的遍历顺序允许被任意打乱。

复杂度分析

时间复杂度:所有顶点都会出队并入队一次,使用O(|V|)时间;在遍历邻接顶点的过程中,由于是无向图,因此所有边都会被访问2次,使用O(2|E|)时间,总体使用O(|V|+|E|)时间。

空间复杂度:列表res,哈希表visited,队列que中的顶点数量最多为|V|,使用O(|V|)空间。

深度优先遍历

深度优先遍历是一种优先走到底、无路可走再回头的遍历方式。如图所示,从左上角顶点出发,访问当前顶点的某个邻接顶点,直到走到尽头时返回,再继续走到尽头并返回,以此类推,直至所有顶点遍历完成。

算法实现

这种“走到尽头再返回”的算法范式通常基于递归来实现。与广度优先遍历类似,在深度优先遍历中,我们也需要借助一个哈希表visited来记录已被访问的顶点,以避免重复访问顶点。

/*深度优先遍历辅助函数*/
void dfs(GraphAdjList &graph, unordered_set<Vertex*> &visited, vector<Vertex*> &res, Vertex * vet){
	res.push_back(vet);		// 记录访问顶点
	visited.emplace(vet);		// 标记该顶点已被访问
	
	// 遍历该顶点的所有邻接顶点
	for (Vertex* adjVet : graph.adjList[vet]){
		if (visited.count(adjVet))
			continue; // 跳过已被访问的顶点
		// 递归访问邻接顶点
		dfs(graph, visited, res, adjVet);
	}
}
/*深度优先遍历*/
// 使用邻接表来表示图,以便获取指定顶点的邻接顶点
vector<Vertex*> graphDFS(GraphAdjList &graph, Vertex* startVet){
	// 顶点遍历序列
	vector<Vertex*> res;
	// 哈希表,用于记录被访问过的顶点
	unordered_set<Vertex*> visted;
	dfs(graph, visted, res, startVet);
	return res;
}

深度优先遍历的算法流程如图所示:

  • 直虚线代表向下递推,表示开启了一个新的递归方法来访问新顶点。
  • 曲虚线代表向上回溯,表示此递归方法已经返回回溯到开启此方法的位置。











深度优先遍历序列的顺序同样不是唯一的。给定某点,先往哪个方向探索都可以,即邻接顶点的顺序可以任意打乱,都是深度优先遍历。

复杂度分析

时间复杂度:所有顶点都会被访问一次,使用O(|V|)时间;所有边都会被访问两次,使用O(2|E|)时间;总体使用O(|V|+|E|)时间。
空间复杂度:列表res,哈希表visited顶点数量最多为|V|,递归最大深度为|V|,因此使用O(|V|)空间。

标签:优先,遍历,及图,邻接矩阵,vector,邻接,顶点
From: https://www.cnblogs.com/1873cy/p/18404762

相关文章

  • 1.5宽度优先搜索
    算法理解从一个点出发,遍历它的所有相邻点,一层一层往下遍历T1:(30min)bfs注意起点不一定在左上角,四个方向都要走T2:(40min)bfs注意山峰山谷有一个很重要的条件,周围的所有点高度必须大于或小于山峰山谷的高度T4:(1h)我打了一个SPFA,因为每一个点需要更新最小值并且可以重复入队(准确来讲......
  • Java中的线程优先级与调度:如何有效管理线程的执行顺序
    Java中的线程优先级与调度:如何有效管理线程的执行顺序大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在Java中,线程的优先级和调度策略对于高效管理线程执行顺序至关重要。通过合理地设置线程优先级和调度策略,可以有效地优化应用的性能和响应时间。本......
  • 【Python】排序算法及二叉树讲解(冒泡 选择 插入 二分查找 二叉树的广度优先和三种深
    排序算法​所谓排序,使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作​排序算法,就是如何使得记录按照要求排列的方法​排序算法在很多领域是非常重要​在大量数据的处理方面:一个优秀的算法可以节省大量的资源。​在各个领域中考虑到数据的......
  • C ++ 从单链表到创建二叉树到二叉树的遍历(结构体)
    首先我们要了解二叉树的数据结构是什么,本质上二叉树是一个有两个节点的链表,我们先了解的单链表的相关定义单链表创建一个朴素的单链表#include<iostream>usingnamespacestd;structNode{intval;Node*next;Node(intx):val(x),next(nullptr){}......
  • ASP.NET 8 AOT 遍历文件夹获取文件名称及时长
    朋友下了N多视频记不住文件夹下都有啥了…问批处理怎么导出文件夹下所有文件名及视频时长…批处理导出文件名好弄,获取视频时长真没用过…想到.net控制台程序AOT发布,不和批处理差不多效果…新建个控制台项目选下使用AOT首先要获取文件的时长,我们需要用下TagLibSharpInstall-P......
  • 前端css样式优先级问题
    一、常用选择器1.标签选择器(标签名{}),选中对应标签里的内容,例(div{})2.类选择器(.类名{}),选中对应类名的内容,例(.one{})   注:不可以数字开头,一个标签中可有多个类名3.id选择器(#id{}),选中对应id的内容,例(#one{})   注:不可以数字开头,一个标签里只能有一个id属性4.通配符选择器(*{}),......
  • 【408DS算法题】038进阶-图深度优先遍历DFS
    Index题目分析实现总结题目设计函数实现对图的深度优先遍历。分析实现类似于图的BFS的分析思路,图的DFS和二叉树的DFS思路相同,但需要额外考虑结点是否已经被访问过。此处同样用布尔数组visited来记录每个结点的访问情况,对于邻接矩阵存储方式的图的DFS,依照先序遍......
  • 【算法笔记】单源最短路问题——Dijkstra算法(无优化/优先队列/set优化)
    0.前言Dijkstra算法可在\(\mathcalO(m\logm)\)或\(\mathcalO(m\logn)\)的时间内求解无负权单源最短路问题。本文中,我们将详细介绍算法的原理、实现,以及常用的两种优化。另外,Dijkstra算法也不要乱用,比如说多源的最短路,用Dijkstra求解的复杂度只有\(\mathcalO(nm\logm)\),但......
  • 秒懂:进程优先级
    1.概念    简单来说,进程优先级是对于资源访问顺序来说的,谁先访问资源,谁的优先级就高。   注意:这和权限概念不一样,权限是能不能访问。2.情景引入进程的运行,是在CPU上执行,每次执行只能执行CPU的一个时间片,会有多个进程在run_Queue运行队列上等待CPU。 不同的......
  • 算法入门-深度优先搜索2
    第六部分:深度优先搜索104.二叉树的最大深度(简单)题目:给定一个二叉树root,返回其最大深度。二叉树的最大深度是指从根节点到最远叶子节点的最长路径上的节点数。示例1:输入:root=[3,9,20,null,null,15,7]输出:3示例2:输入:root=[1,null,2]输出:2第一种思路:感觉递......