目录
[2.1 邻接矩阵](#2.1 邻接矩阵)
[2.2.1 邻接矩阵的简介](#2.2.1 邻接矩阵的简介)
[2.1.2 邻接矩阵的简单实现](#2.1.2 邻接矩阵的简单实现)
[2.2 邻接表](#2.2 邻接表)
[2.2.1 邻接表的简介](#2.2.1 邻接表的简介)
[2.2.2 邻接表的简单实现](#2.2.2 邻接表的简单实现)
1.图的基本概念
- 图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:顶点集合V = {x|x属于某个数据对象集}是有穷非空集合;E = {(x,y)|x,y属于V}或者E = {<x, y>|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。
- (x, y)表示x到y的一条双向 通路,即(x, y)是无方向 的;Path(x, y)表示从x到y的一条单向 通路,即Path(x, y)是有方向的。
- 顶点和边:图中结点称为顶点,第i个顶点记作vi。
- 两个顶点vi和vj相关联 称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。
- 有向图和无向图 :在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G1和G2为无向图。
- 注意:无向边(x, y)等于有向边<x, y>和<y, x>。

- 完全图 :在有n个顶点的无向图中,若有n * (n-1)/2 条边 ,即任意两个顶点之间有且仅有一条边, 则称此图为无向完全图 ,比如上图G1;在n个顶点的有向图中,若有 n * (n-1) 条边 ,即任意两个 顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如上图G4。
- 邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依 附于顶点u和v;在有向图G中,若是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶 点u,并称边与顶点u和顶点v相关联。
- 顶点的度 :顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图 中,顶点的度等于该顶 点的入度与出度之和 ,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度 是以v为起始点的有向边的条数,记作outdev(v)。因此: dev(v) = indev(v) + outdev(v) 注 意:对于无向图 ,顶点的度等于该顶点的入度和出度 ,即 dev(v) = indev(v) = outdev(v)
- 路径 :在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶 点序列为从顶点vi到顶点vj的路径。
- 路径长度 :对于不带权的图,一条路径的路径长度是指该路径上的边的条数 ;对于带权的图,一 条路 径的路径长度是指该路径上各个边权值的总和

- 简单路径与回路 :若路径上各顶点v1,v2,v3,...,vm均不重复 ,则称这样的路径为简单路径 。若路 径上第一个顶点v1和最后一个顶点vm重合 ,则称这样的路径为回路或环

- 子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。

- 连通图 :在无向图 中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
- 强连通图 :在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。
- 生成树 :一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n顶点和n - 1条边。
2.图的存储结构
因为图中既有节点,又有边(节点与节点之间的关系)
因此,在图的存储中,只需要保存:节点和边关系即可
图的存储结构有 邻接矩阵和邻接表
2.1 邻接矩阵
2.2.1 邻接矩阵的简介
因为节点与节点之间的关系就是连通与否,即为0(不连通)或者1(连通)或者附带权值(连通),
因此邻接矩阵(二维数组)即是:
先用一个数组将顶点保存,然后采用矩阵来表示节点与节点之间的关系

注意:
- 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。
有向图的邻接矩阵不一 定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。
- 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,
如果两个顶点不通,则使用无穷大代替

- 邻接矩阵存储图
优点是能够快速知道O(1)两个顶点是否连通,适合稠密图
缺陷 是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间 ,并且不是很好求两个节点之间的路径
2.1.2 邻接矩阵的简单实现
namespace matrix
{
//邻接矩阵
template<class V, class W, W W_MAX = INT_MAX, bool Direction = false>
class graph
{
public:
//手动初始化
graph(const V* a, size_t n){}
int GetIndexOfVertexes(const V& v){}
void AddEdge(const V& src, const V& dst, const W& w){}
void Print(){}
private:
vector<V> _vertexes;//顶点集合
unordered_map<V, int> _index;//顶点映射下标
vector<vector<W>> _matrix;//邻接矩阵存放权值
};
(1)创建命名空间,防止与后续的邻接表实现发生命名冲突
(2)模板参数中,V是顶点类型,W是权值类型,权值默认给INT_MAX,默认无向图
(3)三个成员:_vertexes 存储顶点,_index 映射顶点对应的下标,_matrix 就是邻接矩阵了
(4)成员函数:
这里为了方便测试,选择构造函数手动初始化顶点集合等
另外的重点就是通过给定两顶点,添加边以及为了方便观察,打印邻接矩阵
-
构造函数
graph(const V* a, size_t n)
{
_vertexes.reserve(n);
for (int i = 0; i < n; ++i)
{
_vertexes.push_back(a[i]);
_index[a[i]] = i;
}
//n行n列全部初始化为最大值
_matrix.resize(n, vector<int>(n, W_MAX));
}
依据顶点数组及其元素个数,初始化顶点集合及映射关系
邻接矩阵初始化为n行n列,每条边对应的权值都是最大值
-
添加边
int GetIndexOfVertexes(const V& v)
{
auto it = _index.find(v);
if (it == _index.end())
throw invalid_argument("无效参数");return it->second;
}
为了方便后续通过顶点直接找到边,这里单独写一个函数返回顶点所对应的下标
(1)首先,判断所给顶点的有效性,即在映射关系中查找顶点是否存在
(2)存在即返回下标,不存在就及时抛异常
void AddEdge(const V& src, const V& dst, const W& w)
{
//得到起点和终点的下标
int srci = GetIndexOfVertexes(src);
int dsti = GetIndexOfVertexes(dst);
//标识矩阵中对应元素
_matrix[srci][dsti] = w;
//无向图
if (Direction == false)
{
_matrix[dsti][srci] = w;
}
}
(1)对于给定的两个顶点,先找到其对应的下标
(2)然后根据始末位置找到边所对应的矩阵元素进行赋值操作
(3)如果是无向图,再到对称位置的矩阵元素进行赋值
-
打印
void Print() { //打印 下标及其对应的顶点值 for (int i = 0; i < _vertexes.size(); ++i) { cout << '[' << i << "] -> " << _vertexes[i] << endl; } //横下标 cout << setw(6) << 0; for (int i = 1; i < _matrix.size(); ++i) { cout << setw(5) << i; } cout << endl; for (int i = 0; i < _matrix.size(); ++i) { //列下标 cout << i; for (int j = 0; j < _matrix[i].size(); ++j) { //矩阵元素 if(_matrix[i][j] != W_MAX) cout << setw(5) << _matrix[i][j]; else cout << setw(5) << '*'; } //打印完一行要换行 cout << endl; } }
(1)先打印下标及下标对应的顶点值
(2)再打印横下标,这里使用setw控制数据宽度(右对齐)
(3)对于矩阵的每一行,先打印行数,再打印权值,并且为了好看,当两顶点无边,即权值为
INT_MAX时,我们选择打印 '*'
2.2 邻接表
2.2.1 邻接表的简介
- 邻接表:使用数组表示顶点的集合(数组下标直接对应顶点编号),使用链表表示边的关系
- 将边视为一个结构体,存储起点、终点、权值、指向下一条边的指针
- 类似于哈希桶,起点的数组下标决定了边在邻接表中的挂载位置

- 注意:无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点 vi边链表集合中结点的数目即可

注意:有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就 是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链 表,看有多少边顶点的dst取值是i
邻接表存储图优点是能够更快查找一个顶点连接出去的边,适合稀疏图
缺陷 是不适合确定两个顶点是否相连及其权值
2.2.2 邻接表的简单实现
namespace link_table
{
template<class W>
struct Edge
{
int _srci;//起点
int _dsti;//终点
W _w; //权值
Edge<W>* _next;//连接到邻接表中去
Edge(int srci, int dsti, const W& w)
:_srci(srci),_dsti(dsti),_w(w)
,_next(nullptr)
{ }
};
//邻接表
template<class V, class W, bool Direction = false>
class graph
{
typedef Edge<W> Edge;
public:
graph(const V* a, size_t n){}
int GetIndexOfVertexes(const V& v){}
void AddEdge(const V& src, const V& dst, const W& w){}
void Print(){}
private:
vector<V> _vertexes;//顶点集合
unordered_map<V, int> _index;//顶点映射下标
vector<Edge*> _tables;//邻接表 存放边的集合
};
}
(1)创建命名空间,防止与后续的邻接表实现发生命名冲突
(2)正如前面介绍的一样,将边视为一个结构体
存储起点、终点、权值、指向下一条边的指针
(3)图的模板参数就不用权值的最大值了
(3)三个成员:_vertexes 存储顶点,_index 映射顶点对应的下标,_tables 就是邻接表
这里只实现了出度表
(4)成员函数:
这里为了方便测试,选择构造函数手动初始化顶点集合等
另外的重点就是通过给定两顶点,添加边以及为了方便观察,打印邻接表
-
构造函数
graph(const V* a, size_t n)
{
_vertexes.reserve(n);
for (int i = 0; i < n; ++i)
{
_vertexes.push_back(a[i]);
_index[a[i]] = i;
}_tables.resize(n);
}
邻接表顶点集合与下标映射关系的初始化与邻接矩阵一致
邻接表的_tables初始化顶点个(n个)空间
-
添加边
void AddEdge(const V& src, const V& dst, const W& w)
{
//得到起点和终点的下标
int srci = GetIndexOfVertexes(src);
int dsti = GetIndexOfVertexes(dst);
//构造边
Edge* eg = new Edge(srci, dsti, w);
//头插
eg->_next = _tables[srci];
_tables[srci] = eg;
if (Direction == false)
{
//构造边
Edge* eg = new Edge(dsti, srci, w);
//头插
eg->_next = _tables[dsti];
_tables[dsti] = eg;
}
}
(1)这里根据顶点获得下标的函数与邻接矩阵的一致,这里不再赘述
(3)获得下标后,根据始末位置及权值构造边
(3)单链表头插效率更高
(4)如果是无向图,则还需要链接一次
-
打印
void Print() { //打印 下标及其对应的顶点值 for (int i = 0; i < _vertexes.size(); ++i) { cout << '[' << i << "] -> " << _vertexes[i] << endl; } cout << endl; //打印邻接表 for (int i = 0; i < _tables.size(); ++i) { //打印起点 cout << "[ " << _vertexes[i] << " : " << i << " ] -> "; //起点对应的边 Edge* cur = _tables[i]; while (cur) { cout << "[ " << _vertexes[cur->_dsti] << " : " << cur->_dsti << " : " << cur->_w << " ]" << " -> "; cur = cur->_next; } cout << "nullptr" << endl; } }
(1)先打印下标及下标对应的顶点值
(2)针对邻接表的每个位置
先打印起点:下标,再打印起点到达的位置