各位读者大佬好,我是落羽!一个坚持不断学习进步的学生。
如果您觉得我的文章还不错,欢迎多多三连分享交流,一起学习进步!
欢迎关注我的blog主页: 落羽的落羽
文章目录
一、图的基本概念
图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E);
- V是顶点集合
- E是顶点间关系的集合,也叫做边的集合
- 图中的结点称为顶点。如果两个顶点 vi 和 vj 之间相关联,称有一条边记<vi, vj>
图分为有向图和无向图:在有向图中,<x, y>称为顶点x到顶点y的一条边,<x, y>和<y, x>是两条不同的边。而在无向图中,<x, y>和<y, x>是同一条边。

图中边的数量与顶点数量的关系,可以用稠密和稀疏形容。
完全图:设一张图有n个顶点。对于无向图,若有n*(n-1)/2条边,即任意两点之间都有一条边,则称此图为无向完全图,也是最稠密的图;对于有向图,若有n*(n-1)条边,即任意两点之间都有两条方向相反的边,则称此图为有向完全图。

顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点v的度等于该顶点的入度与出度之和,入度是指以v为终点的有向边的条数,出度是指以v为起点的有向边的条数。在无向图中,顶点v的度等于出度等于入度。
路径:在图中,若从顶点 vi 出发有一组边可以使其到达顶点 vj,则称顶点 vi 到 vj 的顶点序列为顶点 vi 到 vj 的路径。
权值:边附带的数据信息,比如:长度,价值,亲密度

路径长度:对于不带权的图,一条路径的路径长度是该路径上的边数量;对于带权的图,一条路径的路径长度是该路径上的所有边的权总和。
简单路径与回路:若一条路径上的各顶点都不重复,则称这样的路径为简单路径。若路径上的第一个顶点和最后一个顶点重合,则称这样的路径为回路或环。

子图:设图G = {V, E}、G1 = {V1, E1},若V1属于V且E1属于E,则称G1是子图。即一个图的子图,所有的顶点和边都在原图中出现过。

连通图:对于无向图,若顶点v1到v2有路径,则称v1和v2是连通的。如果图中任意两个顶点都是连通的,则称此图为连通图。
强连通图:对于有向图,如果任意两顶点vi和vj之间,都存在一条vi到vj的路径和vj到vi的路径,则称此图为强连通图。
生成树:生成树是连通图(无向图)的一个子图,是一棵树,包含原图所有顶点且保持连通,有n个顶点的连通图的生成树有n个顶点和n-1条边。

二、图的存储结构
一个图既有顶点也有边,图的存储结构中就需要保存它们。顶点保存比较简单,只需要一个数组即可,关系边该怎么保存呢?
保存边的方式,有领接矩阵和领接表两种方式!
1. 领接矩阵
顶点与顶点之间是否连通,可以用0或1表示。领接矩阵就是一个二维数组,用矩阵来表示顶点之间的关系

每个结点可以用数组下标代表。例如,顶点A的下标是x,顶点B的下标是y。领接矩阵中[x][y]代表从A到B的边的权值,如果是无权图就用01表示该边是否存在即可,如果是有权图则填入权值或默认值(表示边不存在,一般可以用INT_MAX代表)。
注意,对于无向图,领接矩阵是左下右上对称的,即[x][y]和[y][x]内容一样!有向图则不是,[x][y]和[y][x]内容不一定相同。
cpp
// 图
namespace Matrix
{
// V顶点类型 W边权值类型 MAX_W表示边不存在的值 Direction表示图是否有向
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:
Graph(const V* vertexs, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(vertexs[i]);
_vIndexMap[vertexs[i]] = i;
}
// MAX_W 作为不存在边的标识值
// 初始化时默认没有边,边需要一条一条手动添加,用AddEdge函数
_matrix.resize(n);
for (auto& e : _matrix)
{
e.resize(n, MAX_W);
}
}
// 找到一个顶点的映射下标
size_t GetVertexIndex(const V& v)
{
auto ret = _vIndexMap.find(v);
if (ret != _vIndexMap.end())
{
return ret->second;
}
else
{
throw invalid_argument("不存在的顶点");
return -1;
}
}
// 添加一条边,src和dst代表两端顶点,w是权值
void AddEdge(const V& src, const V& dst, const W& w)
{
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
_matrix[srci][dsti] = w;
//如果是无向图,则[dsti][srci]也需添加边
if (Direction == false)
{
_matrix[dsti][srci] = w;
}
}
private:
map<V, size_t> _vIndexMap; // 顶点到下标的映射
vector<V> _vertexs; // 顶点集合
vector<vector<W>> _matrix; // 领接矩阵 存储边
};
}
领接矩阵适合存储稠密图,能O(1)判断两个顶点的关系,得到权值。但是如果要查找一个顶点连接的所有边,效率是O(n)
2. 领接表
领接表是一个链表数组。数组表示顶点的集合,链表表示边的关系。

领接表适合存储稀疏图,适合查找一个顶点连接出去的边,但是相对不适合判断两个点是否有边及其权值。
cpp
// 临接表
namespace LinkTable
{
// 定义边结构, W是权值类型
template<class W>
struct LinkEdge
{
int _srcIndex;
int _dstIndex;
W _w;
LinkEdge<W>* _next;
LinkEdge(const W& w)
: _srcIndex(-1)
, _dstIndex(-1)
, _w(w)
, _next(nullptr)
{ }
};
template<class V, class W, bool Direction = false>
class Graph
{
typedef LinkEdge<W> Edge;
public:
Graph(const V* vertexs, size_t n)
{
_vertexs.reserve(n);
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(vertexs[i]);
_vIndexMap[vertexs[i]] = i;
}
_linkTable.resize(n, nullptr);
}
size_t GetVertexIndex(const V& v)
{
auto ret = _vIndexMap.find(v);
if (ret != _vIndexMap.end())
{
return ret->second;
}
else
{
throw invalid_argument("不存在的顶点");
return -1;
}
}
// 添加边
void AddEdge(const V& src, const V& dst, const W& w)
{
size_t srcindex = GetVertexIndex(src);
size_t dstindex = GetVertexIndex(dst);
Edge* sd_edge = new Edge(w);
sd_edge->_srcIndex = srcindex;
sd_edge->_dstIndex = dstindex;
sd_edge->_next = _linkTable[srcindex];
_linkTable[srcindex] = sd_edge;
// 如果是无向图,还要反过来添加一次
if (Direction == false)
{
Edge* ds_edge = new Edge(w);
ds_edge->_srcIndex = dstindex;
ds_edge->_dstIndex = srcindex;
ds_edge->_next = _linkTable[dstindex];
_linkTable[dstindex] = ds_edge;
}
}
private:
map<string, int> _vIndexMap; // 顶点到下标的映射
vector<V> _vertexs; // 顶点集合
vector<Edge*> _linkTable; // 边的集合的领接表
};
}
本篇完,感谢阅读
