一.概念
注:
几个简称字母的翻译:
G:graph,图表
V:vertex,顶点
E:edge,边
1.图的两要素


由上文可知,图由顶点和边 这两要素组成。而下图的边是一个双向通路。

当然,边上还需有权值,即边附带的数据信息。如下图这样:

2.有向与无向


3.完全图(重要)


4.邻接顶点(半重要)

5.顶点的度

例子:5的两个度:

6.路径


7.简单路径与回路

注:递归的时候一定要避免出现回路,不然会递归死循环!!!!!
8.子图(重要)

子图的顶点和边都是原图的子集。
9.连通图和生成树

二.图的存储结构
1.储存顶点
直接用一个**vector(_vertexs)**作为顶点集,储存顶点即可:

2.储存边
此时就有人说,用一个pair<V,V>就可以存下边的两个顶点,进而就能代表存储一条边了,但这是不可以的。因为忽略了无法用pair表示的细节:

下面来叙述正确的存储方式:
①.邻接矩阵(该存储方式适合稠密图)
注:以矩阵的行代表方向的起点,列代表终点。
相关特点
Ⅰ.由于用矩阵表示时,表示顶点的是其存储在_vertexs的下标,因此要用map建立一个顶点与其下标的映射关系:

Ⅱ.如下图的矩阵,如果没有权值 ,某条边对应两个顶点有关系(相连), 那么矩阵中对应位置的值为1。


Ⅲ.那么两个顶点相连(边存在 ),且有权值 的情况呢,答案是矩阵对应的值不再是1,而是权值 ,而边不存在,矩阵就存个标识值(0,∞等):

Ⅳ.上面两个图一个是无向图,一个是有向图,那么它们在矩阵中存储时有何区别呢?
无向图
以中轴线 为对称轴,无向图的存储矩阵是个对称矩阵:

毕竟无向图边的方向是双向的,(A,B)与(B,A)是同一条边,所以A到B与B到A都存在,且矩阵上对应值也是相同的。
有向图
而有向图由于有方向,所以有<1,3>,不代表有<3,1>,所以无法形成以中轴线为轴的对称。

优劣性

代码实现
在看代码之前先明晰一点,map的find接口返回的规则是:找得到这个key,就返回指向存储key键值对(pair)的迭代器,找不到,就返回end()迭代器。
#pragma once
#include<vector>
#include<map>
namespace matrix
{//邻接矩阵版本
template<class V, class W, W MAX_W = INT_MAX ,bool Direction = false>//用V表示顶点类型,用W,即weight表示权值,Direction这个非类型模板参数代表有无向
class Graph
{
public:
Graph(const V* a,size_t n)
{
_vertexs.reserve(n);
for (int i = 0; i < n; i++)
{
_vertexs.push_back(a[i]);//存顶点
_indexMap[a[i]] = i;//存映射值
}
_matrix.resize(n);//先开n列
for (int i = 0; i < _matrix.size(); i++)
{
_matrix[i].resize(n, MAX_W);//再开n行,并用MAX_W初始化
}
}
size_t GetVertexIndex(const V& v)//获取顶点下标
{
//先获取到的顶点是否就是这个图的顶点
auto it = _indexMap.find(v);
if (it != _indexMap.end())
{
return it->second;
}
else
{
throw invalid_argument("顶点不存在");
return -1;
}
}
void AddEdge(const V& src,const V& des,const W& w )
{
size_t srci = GetVertexIndex(src);
size_t desi = GetVertexIndex(des);
//连接首尾,添加边
_matrix[srci][desi] = w;
//无向图
if (Direction == false)
{
_matrix[desi][srci] = w;
}
}
void Print()
{
//打印顶点
for (int i = 0;i < _vertexs.size();++i)
{
cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
}
cout << endl;
//打印矩阵
cout << " ";
for (int i = 0; i < _matrix.size(); i++)
{
cout << i << " ";//打印列标
}
cout << endl;
for (int i = 0; i < _matrix.size(); i++)
{
cout << i << " ";//打印行号
for (int j = 0; j < _matrix[i].size(); ++j)
{
//cout << _matrix[i][j] << " ";
if (_matrix[i][j] == MAX_W)
{
cout << "* ";
}
else
{
cout << _matrix[i][j] << " ";
}
}
cout << endl;
}
cout << endl;
}
private:
vector<V> _vertexs;//用vector存储所有顶点,即_vertexs为顶点集合
map<V, int> _indexMap;//储存顶点映射的下标
vector<vector<W>> _matrix;//矩阵
};
void TestGraph()
{
Graph<char, int, INT_MAX, true> g("0123", 4);
g.AddEdge('0', '1', 1);
g.AddEdge('0', '3', 4);
g.AddEdge('1', '3', 2);
g.AddEdge('1', '2', 9);
g.AddEdge('2', '3', 8);
g.AddEdge('2', '1', 5);
g.AddEdge('2', '0', 3);
g.AddEdge('3', '2', 6);
g.Print();
}
}
②.邻接表(适合存储稀疏图)
相关特点
无向图

有向图
注:有向图存一个出边表即可


优劣性

代码实现
template<class V, class W, bool Direction = false>//用V表示顶点类型,用W,即weight表示权值,Direction这个非类型模板参数代表有无向
class Graph
{
typedef Edge<W> Edge;
public:
Graph(const V* a, size_t n)
{
_vertexs.reserve(n);
for (int i = 0; i < n; i++)
{
_vertexs.push_back(a[i]);//存顶点
_indexMap[a[i]] = i;//存映射值
}
_tables.resize(n,nullptr);//邻接表开n个空间
}
size_t GetVertexIndex(const V& v)//获取顶点下标
{
//先获取到的顶点是否就是这个图的顶点
auto it = _indexMap.find(v);
if (it != _indexMap.end())
{
return it->second;
}
else
{
throw invalid_argument("顶点不存在");
return -1;
}
}
void AddEdge(const V& src, const V& des, const W& w)
{
size_t srci = GetVertexIndex(src);
size_t desi = GetVertexIndex(des);
//1->2
Edge* eg = new Edge(desi,w);
//头插,先把数组元素给next,再让eg成为数组元素
eg->_next = _tables[srci];
_tables[srci] = eg;
if (Direction == false)
{//如果是无向图
//2->1
Edge* eg = new Edge(srci, w);
eg->_next = _tables[desi];
_tables[desi] = eg;
}
}
void Print()
{
//打印顶点
for (int i = 0; i < _vertexs.size(); ++i)
{
cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
}
cout << endl;
}
private:
vector<V> _vertexs;//用vector存储所有顶点,即_vertexs为顶点集合
map<V, int> _indexMap;//储存顶点映射的下标
vector<Edge*> _tables;//邻接表
};
}
三.图的遍历
注:遍历是指遍历图的顶点。
1.广度优先遍历(BFS)
①.算法说明
从第0层的A开始,自己先进队列,随后出队列,同时将第1层跟自己相关联 的几个元素带进队列,随后第1层的元素在出队列的同时,会把跟自己相关联的,无论哪一层的元素都带进队列,这就会把之前已经入过队列的上一层元素再带进来,导致重复。解决重复的方案就是再开一个数组,去标记已经进过上面队列的元素,待出现重复元素时,就不会让其进队列。
这样就能做到逐圈向外遍历。

②.代码实现

标记数组的下标就映射了具体的人名,每个数组元素无非有两个答案:true/false,当某个人初次进入队列,标记数组所对应该人的那个元素就会变成true。

代码
这里主要的难点是把抽象的图形如何具象化,有很多个存储图组成元素的容器,非常容易弄混。
void BFS(const V& src)
{
size_t srci = GetVertexIndex(src);//获取下标
//队列and标记数组
queue<int> q;
vector<bool> visited(_vertexs.size(), false);
//入队列
q.push(srci);
visited[srci] = true;
size_t n = _vertexs.size();
while (!q.empty())
{
int front = q.front();
q.pop();
cout << front << ":" << _vertexs[front];
for (int i = 0; i < n; ++i)//将front的裙带关系带进队列
{
if (_matrix[front][i] != MAX_W)
{
if (visited[i] == false)
{
q.push(i);
visited[i] = true;
}
}
}
}
cout << endl;
}
测试用例与结果

③.优化------逐层出队列------应对几度好友美团笔试题

此处还是基于上面张三,李四,王五的图进行优化:
代码:
void BFS(const V& src)
{
size_t srci = GetVertexIndex(src);//获取下标
//队列and标记数组
queue<int> q;
vector<bool> visited(_vertexs.size(), false);
//入队列
q.push(srci);
visited[srci] = true;
size_t n = _vertexs.size();
int levelSize = 1;
while (!q.empty())
{
for (size_t i = 0; i < levelSize; ++i)//多一轮循环,控制逐层出队列
{
int front = q.front();
q.pop();
cout << front << ":" << _vertexs[front];
for (int i = 0; i < n; ++i)//将front的裙带关系带进队列
{
if (_matrix[front][i] != MAX_W)
{
if (visited[i] == false)
{
q.push(i);
visited[i] = true;
}
}
}
}
cout << endl;
levelSize = q.size();
}
}
优化后的结果:

2.深度优先遍历(DFS)
遍历的图是连通图

递归展开图:

void _DFS(size_t srci, vector<bool>& visited)
{
cout << srci << ":" << _vertexs[srci] << endl;
visited[srci] = true;
for (int i = 0; i < _vertexs.size(); i++)
{
if (_matrix[srci][i] != MAX_W && visited[srci] = false)
{
_DFS(i, visited);
}
}
}
void DFS(const V& src)
{
size_t srci = GetVertexIndex(src);
vector<bool> visited(_vertexs.size(), false);
_DFS(srci, visited);
}
四.最小生成树



1.Kruskal算法
①.算法原理
并查集不清楚的可参见笔者c++高级数据结构之并查集。
用并查集去判断成环与否:
图G就是森林A增长的最终结果,如上图的图n。
②.代码实现
在找当下权值最小的边时 ,会把所有边扔进priority_queue进行查找 ,如果priority_queue不太清楚,请参见笔者栈和队列的实现这篇文章。
while循环的条件误区:
我们需要选出size =(n - 1)条边,因此要用while循环,但我们的目标是从图的所有边里,找到n - 1条边,因此while循环当是针对图的 ,图的边数肯定是大于 n - 1,因此假如是size--,意思就是从图的n - 1条边里,找n - 1条边,这肯定是找不全的,因此while的条件打死不能是size--。

代码
W Kruskal(Self& minTree)
{
size_t n = _vertexs.size();
minTree._vertexs = _vertexs;
minTree._indexMap = _indexMap;
minTree._matrix.resize(n);
for (size_t i = 0;i < n;++i)
{
minTree._matrix[i].resize(n, MAX_W);
}
priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
for (size_t i = 0; i < n; ++i)
{
for (size_t j = 0; j < n; ++j)
{
if (_matrix[i][j] != MAX_W && i < j)//i < j可以确保无向图的边不重复
{
minque.push(Edge(i, j, _matrix[i][j]));
}
}
}
//选n-1条边
size_t size = n - 1;
UnionFindSet ufs(n);//用其判断成环与否
W totalW = W();
while (!minque.empty())
{
Edge min = minque.top();
minque.pop();
if (!ufs.InSet(min._srci, min._dsti))
{//没有成环
minTree._AddEdge(min._srci, min._dsti, min._w);
ufs.Union(min._srci, min._dsti);
++size;
totalW += min._w;
}
}
if (size == n - 1)
{//n - 1条边找完
return totalW;
}
else
{//队列走完,依旧找不到n - 1条边
return W();//返回缺省值
}
}
2.Prim算法
①.算法原理
将某个端点作为起点,并将其放入集合X中,然后图剩下的端点就归入集合Y中,从Y中每次挑一个与X中端点相邻 的端点,使得**{X} ->{Y}的边权值最小** ,随后将这个端点归入集合X 中,并将其从Y中抹除。
这是一个局部的贪心算法,基于集合X里的端点,在集合Y里挑另一个端点,构成权值最小的边。这波操作下来,就不会出现构成回路的情况。 因为想要构成回路 ,那条边的两个顶点都是出自同一集合。


②.代码实现
先用两个vector代表集合X,Y,去存储端点。
为了效率,用优先级队列minq去存储边,当每个端点进入集合X时,就将与其相连的几条边存进minq里,然后筛选出权值最小的那条边(minq.top),将这条边添加进最小生成树minTree,最后将这条边pop出minq。
但这种实现方式的局限在于,它有些违背了不构成回路的初心,将与X中端点相连的边添加进minq,会把构成回路的边也添加进来,当这条边的权值在局部最小时,会被选中进入minTree,这时就违反了最小生成树不能出现回路的原则:

那么会导致成环的边有什么特征呢?答案是它们均属于集合X,而原算法形成的合法边的两个端点,是来自于不同集合X与Y的,所以原算法可以不必考虑成环,但这里无法规避。
所以在实现层面,我们还得添加防止成环的算法。
代码:
W Prim(Self& minTree,const W& src)//src为起点
{
//与上面算法一样,先对矩阵进行初始化
size_t srci = GetVertexIndex(src);
size_t n = _vertexs.size();
minTree._vertexs = _vertexs;
minTree._indexMap = _indexMap;
minTree._matrix.resize(n);
for (size_t i = 0; i < n; ++i)
{
minTree._matrix[i].resize(n, MAX_W);
}
//用两个vector作为集合
vector<bool> X(n,false);
vector<bool> Y(n,true);
X[srci] = true;
Y[srci] = false;
//去寻找权值最小的边
priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
for (size_t i = 0; i < n; ++i)
{//将与src(a)相连的边放进minq
if (_matrix[srci][i] != MAX_W)
{
minq.push(Edge(srci, i, _matrix[srci][i]));
}
}
int size = 0;
W totalW = W();//权值
while (!minq.empty())
{//取权值最小的边
Edge min = minq.top();
minq.pop();
if (X[min._srci] && X[min._dsti])
{//判断是否形成回路的算法
cout << "构成环:";
cout << _vertexs[min._srci] <<"->" << _vertexs[min._dsti] << ":" << min._w << endl;
}
else
{
minTree._AddEdge(min._srci, min._dsti, min._w);
X[min._dsti] = true;//b进X
Y[min._dsti] = false;//b出Y
++size;
totalW += min._w;
if (size == n - 1)
{//选n - 1条边
break;
}
for (size_t i = 0; i < n; ++i)
{//让与b连接的边进入minq
if (_matrix[min._dsti][i] != MAX_W && X[i] == 0)//有了ab边,那ba边就重复了,去重就是让与b相邻的端点(此时b在X里了)不出现在X集合里
minq.push(Edge(srci, i, _matrix[min._dsti][i]));
}
}
}
if (size == n - 1)
{//n - 1条边找完
return totalW;
}
else
{//队列走完,依旧找不到n - 1条边
return W();//返回缺省值
}
}