目录
1. 图的基本概念
2. 图的存储结构
2.1 邻接矩阵
2.1.1私有成员变量
2.1.2类模板的声明
2.1.3构造函数
2.1.4获取顶点下标
2.1.5添加边的信息
2.1.6打印图
2.1.7测试用例
2.2邻接表
2.2.1私有成员变量
2.2.2Edge类
2.2.3类模板的声明
2.2.4构造函数
2.2.5获取顶点下标
2.2.6添加边的信息
2.2.7打印图
2.2.8测试用例
1. 图的基本概念
图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:
顶点集合V = {x|x属于某个数据对象集}是有穷非空集合;
E = {(x,y)|x,y属于V}或者E = {|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>和<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中,若<u,v>是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 邻接矩阵
因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。
注意:
1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一 定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。
2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替。
3. 用邻接矩阵存储图的优点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路 径不是很好求。
2.1.1私有成员变量
private: vector<V> _vertexs;//顶点集合 map<V, int> _indexMap;//顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵
我们的私有成员变量有三个,一个是存储顶点的vector,一个是存储顶点与下标映射关系的map,还有一个就是存储边信息的二维数组。V和W是我们类模板声明出来的变量,分别代表顶点和边的类型。
2.1.2类模板的声明
template<class V,class W,W MAX_W = INT_MAX , bool Direction = false>
V,W我们已经说过了,MAX_W就是我们设置的无穷大变量,用来标识两点不连通时的情况,Direction变量的意思是我们的图是有向的还是无向的,我们给它个缺省值为false,默认它是无向的。
2.1.3构造函数
class Graph { typedef Graph<V, W, MAX_W, Direction> Self; public: //图的创建 //1、IO输入 -- 不方便测试,oj更适合 //2、图结构关系写到文件,读取文件 //3、手动添加边 Graph() = default; Graph(const V* a,size_t n) { _vertexs.reserve(n); for (size_t i = 0; i < n; i++) { _vertexs.push_back(a[i]); _indexMap[a[i]] = i; } _matrix.resize(n); for (size_t i = 0; i < _matrix.size(); i++) { _matrix[i].resize(n, MAX_W); } } }
我们先将这个图重命名一下,方便我们后面使用,为了方便我们测试,我们采用的是手动添加边的方式。无参构造我们将它设置成默认,就是让编译器自己生成,然后我们的带参构造,我们需要把顶点数组里面的元素全部填入顶点vector里面,并且记录顶点与下标的映射关系,然后我们先将二维数组进行初始化,用MAX_W来表达他们未联通的状态。
2.1.4获取顶点下标
size_t GetVertexIndex(const V& v) { auto it = _indexMap.find(v); if (it != _indexMap.end()) { return it->second; } else { throw invalid_argument("顶点不存在"); return -1; } }
这个操作很简单,我们只需要在map里面找就可以了,找到了就返回它的下标,找不到就说明这个参数无效,我们将它用std::invalid_argument类抛异常,错误描述信息为"顶点不存在"并返回-1。
2.1.5添加边的信息
void _AddEdge(size_t src, size_t dst, const W& w) { _matrix[src][dst] = w; //无向图 if (Direction == false) { _matrix[dst][src] = w; } } void AddEdge(const V& src, const V& dst, const W& w) { size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); _AddEdge(srci, dsti, w); }
_AddEdge是AddEdge的子函数,我们从下往上看,要想添加边,我们需要的参数是这条边的起点和终点,还有边的权值。在有了这些参数之后,我们要获得这两个顶点的下标,然后我们就可以在二维数组里面将他们的边的权值填上,倘若是无向的,那么我们还要把他们的起始点和终点的位置交换一下,再给他们也填一下边的权值。
2.1.6打印图
void Print() { //顶点 for (size_t i = 0; i < _vertexs.size(); i++) { cout << "[" << i << "]" << "->" << _vertexs[i] << endl; } cout << endl; //打印下标 cout << " "; for (size_t i = 0; i < _vertexs.size(); ++i) { //cout << i << " "; printf("%4d", i); } cout << endl; //矩阵 for (size_t i = 0; i < _matrix.size(); i++) { cout << i << " "; for (size_t j = 0; j < _matrix.size(); j++) { if (_matrix[i][j] == MAX_W) { //cout << "*" << " "; printf("%4c", '*'); } else { //cout << _matrix[i][j] << " "; printf("%4d", _matrix[i][j]); } }cout << endl; } cout << endl; }
我们先将顶点与它下标的对应关系打印出来,然后我们打印矩阵,我们先将第一行、第一列用下标来填充,然后再将边的信息打印出来,不连通的点我们用符号'*'来代替。
2.1.7测试用例
matrix::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();
这里我们用的是有向图,运行截图如下:
2.2邻接表
邻接表:使用数组表示顶点的集合,使用链表表示边的关系。
1. 无向图邻接表存储
注意:无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点 vi边链表集合中结点的数目即可。
2. 有向图邻接表存储
注意:有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就 是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链 表,看有多少边顶点的dst取值是i。
我们就简单实现一下入边就可以了。
2.2.1私有成员变量
private: vector<V> _vertexs;//顶点集合 map<V, int> _indexMap;//顶点映射下标 vector<Edge*> _tables; //邻接表 };
唯一不同的就是把邻接矩阵二维数组换成了vector链表。
2.2.2Edge类
template<class W> struct Edge { int _dsti;//目标点的下标 W _w;//权值 Edge<W>* _next; Edge(int dsti,const W& w) :_dsti(dsti) ,_w(w) ,_next(nullptr) {} };
Edge节点存的是我们边的信息,它包括我们的目标点下标,权值,next节点指针,我们再实现一下带参构造就可以了。
2.2.3类模板的声明
template<class V, class W, bool Direction = false>
这里我们就不需要最大值来填充不连通的点了,因为不连通的点根本就不会出现在我们的邻接表中。
2.2.4构造函数
typedef Edge<W> Edge; public: //图的创建 //1、IO输入 -- 不方便测试,oj更适合 //2、图结构关系写到文件,读取文件 //3、手动添加边 Graph(const V* a, size_t n) { _vertexs.reserve(n); for (size_t i = 0; i < n; i++) { _vertexs.push_back(a[i]); _indexMap[a[i]] = i; } _tables.resize(n); }
我们先将节点重命名一下,方便我们后续编写代码,带参构造中,我们先将顶点存好,下标映射关系对好,我们先将邻接表初始化空间。
2.2.5获取顶点下标
size_t GetVertexIndex(const V& v) { auto it = _indexMap.find(v); if (it != _indexMap.end()) { return it->second; } else { throw invalid_argument("顶点不存在"); return -1; } }
这个操作跟我们的邻接矩阵是一模一样的。
2.2.6添加边的信息
void AddEdge(const V& src, const V& dst, const W& w) { size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); //1->2 Edge* eg = new Edge(dsti, w); eg->_next = _tables[srci]; _tables[srci] = eg; //2->1 if (Direction == false) { Edge* eg = new Edge(srci, w); eg->_next = _tables[dsti]; _tables[dsti] = eg; } }
我们还是要获取起点和终点的下标,然后创建节点,我们采用头插的方式将节点边的关系创建好,倘若是无向的,我们还要进行跟邻接矩阵同样的操作。
2.2.7打印图
void Print() { //顶点 for (size_t i = 0; i < _vertexs.size(); i++) { cout << "[" << i << "]" << "->" << _vertexs[i] << endl; } cout << endl; for (size_t i = 0; i < _tables.size(); i++) { cout << _vertexs[i] << "[" << i << "]->"; Edge* cur = _tables[i]; while (cur) { cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->"; cur = cur->_next; } cout << "nullptr" << endl; } }
2.2.8测试用例
string a[] = { "张三", "李四", "王五", "赵六" }; link_table::Graph<string, int,true> g1(a, 4); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 30);
运行截图如下: