探索数据结构:图(一)之邻接矩阵与邻接表


✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:数据结构与算法
贝蒂的主页:Betty’s blog

1. 图的定义

**图(Graph)**是数学和计算机科学中的一种基本结构,它由两个主要组成部分定义:顶点集(Vertex Set)V 边集(Edge Set)E。在一个图 G = ( V , E ) G=(V,E) G=(V,E)中,V 是顶点的有限非空集合,而 E是连接这些顶点之间的边的集合,即每条边都是两个顶点之间的关系。

  • 顶点(Vertices):顶点可以由字母变量表示,比如 V = { v 1 , v 2 , … , v n } V=\{v_1,v_2,\ldots,v_n\} V={v1,v2,,vn} n n n 表示顶点的数量,也称为图的阶或维度。
  • 边(Edges):边是表示顶点间关系的元素,通常表示为 ( u , v ) (u,v) (u,v),这意味着从顶点 u u u 到顶点 v v v 有一条边。 E E E 的大小,即 ∣ E ∣ |E| E,给出了图中边的数量。

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

2. 图的基本概念

2.1 有向图与无向图

  • 有向图中, < x , y > <x,y> <x,y>是有序的,被称为顶点x到顶点y的一条边, < x , y > <x,y> <x,y> < y , x > <y,x> <y,x>是两条不同的边。
  • 无向图中, ( x , y ) (x,y) (x,y)是无序的,称为顶点x和顶点y相关联的一条边,这条边没有特定方向, ( x , y ) (x,y) (x,y) ( y , x ) (y,x) (y,x)是同一条边。

2.2 完全图

在有n个顶点的无向图中,若有 n × ( n − 1 ) ÷ 2 n × ( n − 1 ) ÷ 2 n×(n1)÷2条边,即任意两个顶点之间都有直接相连的边,则称此图为无向完全图
在有n个顶点的有向图中,若有 n × ( n − 1 ) n × ( n − 1 ) n×(n1)条边,即任意两个顶点之间都有双向的边,则称此图为有向完全图

2.3 邻接顶点

  • 无向图中,若边 ( u , v ) (u, v) (u,v)存在于图中,则称 u u u v v v互为邻接顶点,并且边 ( u , v ) (u, v) (u,v)依附于顶点 u u u和顶点 v v v
  • 有向图中,若边 < u , v > <u, v> <u,v>是图中的一条边,则称顶点 u u u邻接到顶点 v v v,顶点 v v v邻接自顶点 u u u,同时称边 < u , v > <u, v> <u,v>与顶点 u u u和顶点 v v v相关联。

2.4 顶点的度

  • 有向图中,顶点的度等于该顶点的入度与出度之和,顶点的入度是以该顶点为终点的边的条数,顶点的出度是以该顶点为起点的边的条数。
  • 无向图中,顶点的度等于与该顶点相关联的边的条数,同时也等于该顶点的入度和出度。

2.5 路径与路径长度

  • 若从顶点 v i v_i vi出发有一组边使其可到达顶点 v j v_j vj ,则称顶点 v i v_i vi到顶点 v j v_j vj 的顶点序列为从顶点 v i v_i vi到顶点 v j v_j vj 的路径。
  • 对于不带权的图,一条路径的长度是指该路径上的边的条数;对于带权的图,一条路径的长度是指该路径上各个边权值的总和。其中权值指边附带的数据信息。

2.6 简单路径与回路

  • 若路径上的各个顶点 v 1 , v 2 , v 3 . . . v n v_1,v_2,v_3...v_n v1,v2,v3...vn均不相同,则称这样的路径为简单路径
  • 若路径上第一个顶点与最后一个顶点相同,则称这样的路径为回路

2.7 子图

设图 G = ( V , E ) G=(V,E) G=(V,E)和图 G 1 = ( V 1 , E 1 ) G_1=(V_1,E_1) G1=(V1,E1),若 V 1 ⊆ V V_1⊆V V1V E 1 ⊆ E E_1⊆E E1E,那么称 G 1 G_1 G1 G G G的子图。

2.8 连通图与强连通图

  • 无向图中,若从顶点 v 1 v_1 v1 到顶点 v 2 v_2 v2 有路径,则称顶点 v 1 v_1 v1 与顶点 v 2 v_2 v2 是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图
  • 有向图中,若每一对顶点 v i v_i vi v j v_j vj 之间都存在一条从 v i v_i vi v j v_j vj 的路,也存在一条从 v j v_j vj v i v_i vi 的路,则称此图是强连通图

2.9 生成树与最小生成树

  • 一个连通图的最小连通子图称为该图的生成树,有 n n n个顶点的连通图的生成树有 n n n个顶点和 n − 1 n - 1 n1条边。
  • 最小生成树指的是一个图的生成树中,总权值最小的生成树。

img

3. 图的存储结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。 下面我们将介绍两种常见的表示方法:邻接矩阵邻接表

3.1 邻接矩阵

邻接矩阵:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系 。
对于一个具有 n n n个顶点的图 G = ( V , E ) G=(V,E) G=(V,E),其中 V V V 是顶点集, E E E 是边集。其邻接矩阵 A A A是一个 n × n n\times n n×n 的矩阵,定义如下:

  • 如果顶点 i i i 和顶点 j j j 之间有边相连,那么 A [ i ] [ j ] = 1 A[i][j]=1 A[i][j]=1(对于无向图, A [ i ] [ j ] = A [ j ] [ i ] = 1 A[i][j]=A[j][i]=1 A[i][j]=A[j][i]=1)。
  • 如果顶点 i i i 和顶点 j j j 之间没有边相连,那么 A [ i ] [ j ] = 0 A[i][j]=0 A[i][j]=0(对于无向图, A [ i ] [ j ] = A [ j ] [ i ] = 0 A[i][j]=A[j][i]=0 A[i][j]=A[j][i]=0)。

例如,对于一个简单的无向图,有三个顶点 V = { v 1 , v 2 , v 3 } V=\{v_1,v_2,v_3\} V={v1,v2,v3},如果 v 1 v_1 v1 v 2 v_2 v2 之间有边相连, v 2 v_2 v2 v 3 v_3 v3 之间有边相连, v 1 v_1 v1 v 3 v_3 v3 之间没有边相连,那么这个图的邻接矩阵为: [ 0 1 0 1 0 1 0 1 0 ] \begin{bmatrix} 0 & 1 & 0\\ 1 & 0 & 1\\ 0 & 1 & 0 \end{bmatrix} 010101010
如果是对于具有 n n n 个顶点的带权图 G = ( V , E , W ) G=(V,E,W) G=(V,E,W),其中 V V V 是顶点集, E E E 是边集, W W W 是权值集合。其邻接矩阵 A A A 是一个 n × n n\times n n×n 的矩阵。

  • 如果顶点 v i v_i vi 和顶点 v j v_j vj 之间有边相连,那么 A [ i ] [ j ] A[i][j] A[i][j] 存放着该边对应的权值。
  • 如果顶点 v i v_i vi 和顶点 v j v_j vj 之间没有边相连,那么 A [ i ] [ j ] A[i][j] A[i][j] 通常被设为一个特定的标识值,比如无穷大或者一个特殊的标记,具体取决于应用场景和算法需求。

例如,有一个带权无向图,三个顶点分别为 v 1 v_1 v1 v 2 v_2 v2 v 3 v_3 v3 v 1 v_1 v1 v 2 v_2 v2 之间边的权值为 2, v 2 v_2 v2 v 3 v_3 v3 之间边的权值为 3, v 1 v_1 v1 v 3 v_3 v3 之间没有边相连。那么这个图的邻接矩阵为: [ 0 2 ∞ 2 0 3 ∞ 3 0 ] \begin{bmatrix} 0 & 2 & ∞\\ 2 & 0 & 3\\ ∞ & 3 & 0 \end{bmatrix} 0220330

3.2 邻接表

邻接表:使用数组表示顶点的集合,使用链表表示边的关系。
对于一个具有 n n n个顶点的图 G = ( V , E ) G=(V,E) G=(V,E),邻接表的主要思想是为每个顶点建立一个链表,链表中存储与该顶点相邻的其他顶点。
具体来说:

  • 对于无向图,图中的每一条边 ( u , v ) (u,v) (u,v),如果顶点 u u u的链表中还没有顶点 v v v,则将顶点 v v v添加到顶点 u u u的链表中;同理,如果顶点 v v v的链表中还没有顶点 u u u,则将顶点 u u u添加到顶点 v v v的链表中。
  • 对于有向图,图中的每一条边 < u , v > <u,v> <u,v>,只需要将顶点 v v v添加到顶点 u u u的链表中。

例如,对于一个具有 5 5 5个顶点的无向图,顶点分别为 v 1 , v 2 , v 3 , v 4 , v 5 v_1,v_2,v_3,v_4,v_5 v1,v2,v3,v4,v5,如果有边 ( v 1 , v 2 ) (v_1,v_2) (v1,v2) ( v 1 , v 3 ) (v_1,v_3) (v1,v3) ( v 2 , v 4 ) (v_2,v_4) (v2,v4) ( v 3 , v 4 ) (v_3,v_4) (v3,v4) ( v 3 , v 5 ) (v_3,v_5) (v3,v5),那么其邻接表表示如下:

  • 顶点 v 1 v_1 v1的链表: v 2 v_2 v2 v 3 v_3 v3
  • 顶点 v 2 v_2 v2的链表: v 1 v_1 v1 v 4 v_4 v4
  • 顶点 v 3 v_3 v3的链表: v 1 v_1 v1 v 4 v_4 v4 v 5 v_5 v5
  • 顶点 v 4 v_4 v4的链表: v 2 v_2 v2 v 3 v_3 v3
  • 顶点 v 5 v_5 v5的链表: v 3 v_3 v3

如果是对于一个具有 5 5 5个顶点的有向图,顶点分别为 v 1 , v 2 , v 3 , v 4 , v 5 v_1,v_2,v_3,v_4,v_5 v1,v2,v3,v4,v5,如果有边 < v 1 , v 2 > <v_1,v_2> <v1,v2> < v 1 , v 3 > <v_1,v_3> <v1,v3> < v 2 , v 4 > <v_2,v_4> <v2,v4> < v 3 , v 4 > <v_3,v_4> <v3,v4> < v 3 , v 5 > <v_3,v_5> <v3,v5>,那么其邻接表表示如下:

  • 顶点 v 1 v_1 v1的链表: v 2 v_2 v2 v 3 v_3 v3
  • 顶点 v 2 v_2 v2的链表: v 4 v_4 v4
  • 顶点 v 3 v_3 v3的链表: v 4 v_4 v4 v 5 v_5 v5
  • 顶点 v 4 v_4 v4的链表:没有任何边。
  • 顶点 v 5 v_5 v5的链表:没有任何边。

3.3 邻接矩阵与邻接表的优缺点

对于邻接矩阵:

优点:

  • 直观性强,邻接矩阵能够 O ( 1 ) O(1) O(1)的时间判断两个顶点是否相连,并获得相连边的权值。
  • 适合稠密图:图中的边越多,邻接矩阵的空间利用率就越高。

缺点:

  • 不合适稀疏图:会浪费大量空间。
  • 不适合查找一个顶点连接出去的所有边:需要遍历矩阵中对应的一行,该过程的时间复杂度是 O ( N ) O ( N ) O(N) ,其中 N N N表示的是顶点的个数。

对于邻接表:

优点:

  • 适合查找一个顶点连接出去的所有边:只需要遍历对应的链表即可。
  • 适合稀疏图:图中的边越少,邻接表存储的空间就越少。

缺点:

  • 不合适稠密图:一个顶点会链接大量数据,需要遍历顶点对应位置的链表来确定两点是否相连,该过程的时间复杂度是 O ( E ) O(E) O(E),其中 E E E表示从源顶点连接出去的边的数量。

4. 邻接矩阵与邻接表的实现

4.1 邻接矩阵

4.1.1 邻接矩阵的结构

为了支持所有类型,我们实现一个模版类。其中肯定有两个模版参数VW分别代表顶点与权值类型,MAX_W表示两个顶点之间没有直接相连的值,一般我们默认为INT_MAX,并且还需要一个bool类型的模版参数Direction代表是有向图还是无向图,false为无向,true为有向。
邻接矩阵的成员变量有三个分别为:数组_vertexs代表边的集合,哈希表_indexMap来映射不同类型与下标的关系,二维数组_matrix代表邻接矩阵。

template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:
    //构造函数
    Graph(const V*vertexs,int n);
    //获取对应顶点的下标
    int getVertexsIndex(const V& v);
    void addEdge(const V& src, const V& dest, const W& weight);
    //打印顶点集合和邻接矩阵
    void Print();
private:
    vector<V> _vertexs;//顶点集合
    unordered_map<V, int> _indexMap;//映射关系
    vector<vector<W>> _matrix;//邻接矩阵
};
4.1.2 邻接矩阵的初始化

我们首先顶点集合全部初始化,然后将邻接矩阵的值全设为INT_MAX,最后将顶点集合与下标建立映射关系。

//构造函数
Graph(const V*vertexs,int n)
    :_vertexs(vertexs, vertexs + n)
    , _matrix(n,vector<int>(n, MAX_W))
{
    //映射下标
    for (int i = 0; i < n; i++)
    {
        _indexMap[vertexs[i]] = i;
    }
}
4.1.3 添加边

添加边之前我们需要先找到对应顶点的下标,如果找不到需要抛异常。然后根据对应下标更新邻接矩阵,如果是无向图继续更新。

//获取对应顶点的下标
int getVertexsIndex(const V& v)
{
    auto it = _indexMap.find(v);
    if (it == _indexMap.end())
    {
        throw invalid_argument("不存在的顶点");
        return -1;
    }
    else
    {
        return it->second;//返回对应下标
    }
}
//添加边
void addEdge(const V& src, const V& dest, const W& weight)
{
    int srci = getVertexsIndex(src);//获取起始点下标
    int desti = getVertexsIndex(dest);//获取终点下标
    _matrix[srci][desti] = weight;
    if (Direction == false)
    {
        _matrix[desti][srci] = weight;//无向图
    }
}
4.1.4 打印邻接矩阵

最后我们打印邻接矩阵,方便测试程序的正误。

//打印顶点集合和邻接矩阵
void Print() 
{
	int n = _vertexs.size();
	//打印顶点集合
	for (int i = 0; i < n; i++) {
		cout << "[" << i << "]->" << _vertexs[i] << endl;
	}
	cout << endl;
	//打印邻接矩阵
	cout << "  ";
	for (int i = 0; i < n; i++) {
		printf("%4d", i);
	}
	cout << endl;
	for (int i = 0; i < n; i++) {
		cout << i << " "; //竖下标
		for (int j = 0; j < n; j++) {
			if (_matrix[i][j] == MAX_W) {
				printf("%4c", '*');
			}
			else {
				printf("%4d", _matrix[i][j]);
			}
		}
		cout << endl;
	}
}

4.2 邻接表

4.2.1 邻接表的结构

同样邻接表我们编写成一个模板类,相比与邻接矩阵我们可以不需要代表权值最大的MAX_W。并且为了方便描述我们需要首先编写一个关于边Edge的类。这个类包含起始与终点下标,已经对应的权值。
然后邻接表中有三个成员变量:数组_vertexs代表边的集合,哈希表indexMap来映射不同类型与下标的关系,数组_linkTable代表邻接矩阵。

template<class W>
//边
struct Edge
{
    int _srci;//起始下标
    int _desti;//终点下标
    W _w;//权值
    Edge<W>* _next;
    Edge(int srci,int desti,const W&w)
        :_srci(srci)
        ,_desti(desti)
        ,_w(w)
        ,_next(nullptr)
    {}
};
template<class V, class W, bool Direction = false>
class Graph
{
    typedef Edge<W> Edge;
public:
    //构造函数
    Graph(const V* vertexs, int n);
    //获取对应顶点的下标
    int getVertexsIndex(const V& v);
    //添加边
    void addEdge(const V& src, const V& dest, const W& weight);
    //打印顶点集合和邻接表
    void Print();
private:
    vector<V> _vertexs;//顶点集合
    unordered_map<V, int> _indexMap;//映射关系
    vector<Edge*> _linkTable;//邻接矩阵
};
4.2.2 邻接表的初始化

我们首先顶点集合全部初始化,然后将邻接表的值全设为nullptr,最后将顶点集合与下标建立映射关系;

//构造函数
Graph(const V* vertexs, int n)
    :_vertexs(vertexs, vertexs + n)
    , _linkTable(n, nullptr)
{
    //映射下标
    for (int i = 0; i < n; i++)
    {
        _indexMap[vertexs[i]] = i;
    }
}
4.2.3 添加边

添加边之前我们需要先找到对应顶点的下标,如果找不到需要抛异常。然后根据对应下标更新邻接矩阵,如果是无向图继续更新。

//获取对应顶点的下标
int getVertexsIndex(const V& v)
{
	auto it = _indexMap.find(v);
	if (it == _indexMap.end())
	{
		throw invalid_argument("不存在的顶点");
		return -1;
	}
	else
	{
		return it->second;//返回对应下标
	}
}
//添加边
void addEdge(const V& src, const V& dest, const W& weight)
{
	int srci = getVertexsIndex(src);//获取起始点下标
	int desti = getVertexsIndex(dest);//获取终点下标
	Edge* sre = new Edge(srci, desti, weight);
	//进行头插
	sre->_next = _linkTable[srci];
	_linkTable[srci] = sre;
	if (Direction == false)
	{
		Edge* dese = new Edge(desti, srci, weight);
		dese->_next = _linkTable[desti];
		_linkTable[desti] = dese;
	}
}
4.2.4 打印邻接矩阵

最后我们打印邻接表,方便测试程序的正误。

//打印顶点集合和邻接表
void Print()
{
	int n = _vertexs.size();
	//打印顶点集合
	for (int i = 0; i < n; i++) {
		cout << "[" << i << "]->" << _vertexs[i] << " ";
	}
	cout << endl << endl;

	//打印邻接表
	for (int i = 0; i < n; i++) {
		Edge* cur = _linkTable[i];
		cout << "[" << i << ":" << _vertexs[i] << "]->";
		while (cur) {
			cout << "[" << cur->_desti << ":" << _vertexs[cur->_desti] << ":" << cur->_w << "]->";
			cur = cur->_next;
		}
		cout << "nullptr" << endl;
	}
}

5. 源码

5.1 邻接矩阵

namespace Matrix
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph
	{
	public:
		//构造函数
		Graph(const V*vertexs,int n)
			:_vertexs(vertexs, vertexs + n)
			, _matrix(n,vector<int>(n, MAX_W))
		{
			//映射下标
			for (int i = 0; i < n; i++)
			{
				_indexMap[vertexs[i]] = i;
			}
		}
		//获取对应顶点的下标
		int getVertexsIndex(const V& v)
		{
			auto it = _indexMap.find(v);
			if (it == _indexMap.end())
			{
				throw invalid_argument("不存在的顶点");
				return -1;
			}
			else
			{
				return it->second;//返回对应下标
			}
		}
		//添加边
		void addEdge(const V& src, const V& dest, const W& weight)
		{
			int srci = getVertexsIndex(src);//获取起始点下标
			int desti = getVertexsIndex(dest);//获取终点下标
			_matrix[srci][desti] = weight;
			if (Direction == false)
			{
				_matrix[desti][srci] = weight;//无向图
			}
		}
		//打印顶点集合和邻接矩阵
		void Print() {
			int n = _vertexs.size();
			//打印顶点集合
			for (int i = 0; i < n; i++) {
				cout << "[" << i << "]->" << _vertexs[i] << endl;
			}
			cout << endl;
			//打印邻接矩阵
			cout << "  ";
			for (int i = 0; i < n; i++) {
				printf("%4d", i);
			}
			cout << endl;
			for (int i = 0; i < n; i++) {
				cout << i << " "; //竖下标
				for (int j = 0; j < n; j++) {
					if (_matrix[i][j] == MAX_W) {
						printf("%4c", '*');
					}
					else {
						printf("%4d", _matrix[i][j]);
					}
				}
				cout << endl;
			}
		}
	private:
		vector<V> _vertexs;//顶点集合
		unordered_map<V, int> _indexMap;//映射关系
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

5.2 邻接表

namespace LinkTable
{
	template<class W>
	struct Edge
	{
		int _srci;//起始下标
		int _desti;//终点下标
		W _w;//权值
		Edge<W>* _next;
		Edge(int srci,int desti,const W&w)
			:_srci(srci)
			,_desti(desti)
			,_w(w)
			,_next(nullptr)
		{}
	};
	template<class V, class W, bool Direction = false>
	class Graph
	{
		typedef Edge<W> Edge;
	public:
		//构造函数
		Graph(const V* vertexs, int n)
			:_vertexs(vertexs, vertexs + n)
			, _linkTable(n, nullptr)
		{
			//映射下标
			for (int i = 0; i < n; i++)
			{
				_indexMap[vertexs[i]] = i;
			}
		}
		//获取对应顶点的下标
		int getVertexsIndex(const V& v)
		{
			auto it = _indexMap.find(v);
			if (it == _indexMap.end())
			{
				throw invalid_argument("不存在的顶点");
				return -1;
			}
			else
			{
				return it->second;//返回对应下标
			}
		}
		//添加边
		void addEdge(const V& src, const V& dest, const W& weight)
		{
			int srci = getVertexsIndex(src);//获取起始点下标
			int desti = getVertexsIndex(dest);//获取终点下标
			Edge* sre = new Edge(srci, desti, weight);
			//进行头插
			sre->_next = _linkTable[srci];
			_linkTable[srci] = sre;
			if (Direction == false)
			{
				Edge* dese = new Edge(desti, srci, weight);
				dese->_next = _linkTable[desti];
				_linkTable[desti] = dese;
			}
		}
		//打印顶点集合和邻接表
		void Print()
		{
			int n = _vertexs.size();
			//打印顶点集合
			for (int i = 0; i < n; i++) {
				cout << "[" << i << "]->" << _vertexs[i] << " ";
			}
			cout << endl << endl;

			//打印邻接表
			for (int i = 0; i < n; i++) {
				Edge* cur = _linkTable[i];
				cout << "[" << i << ":" << _vertexs[i] << "]->";
				while (cur) {
					cout << "[" << cur->_desti << ":" << _vertexs[cur->_desti] << ":" << cur->_w << "]->";
					cur = cur->_next;
				}
				cout << "nullptr" << endl;
			}
		}

	private:
		vector<V> _vertexs;//顶点集合
		unordered_map<V, int> _indexMap;//映射关系
		vector<Edge*> _linkTable;//邻接矩阵
	};
}
  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Betty’s Sweet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值