数据结构中图的邻接表构建及其操作(C++语言)
此篇和上一篇:数据结构中图的邻接矩阵构建及其操作(C++语言)都是实现图的构建以及相关操作,这里主要放上图结构的邻接表的建立及相关操作的总体代码。学习的参考书是:吴艳等编著的《数据结构(用C++语言描述)》,但是书中代码个别部分有一些错误,我修改了,所以下面的代码与原书中有稍微不同。(这些代码是我一边理解原理一边参考书敲出来的,在vs上敲完之后进行过简单地测试,基本能够实现每个模块的功能,里面的注释有的是书上的,有的是我认为不好理解自己额外添加的。这里主要是一个总体代码的汇总,有时间的话我可能会把里面的算法代码分开放置成文章,添加原理解释部分)
以下以邻接表形式存储的图代码主要包括:
1、以邻接表的形式构建图结构,以及许多相关的基本操作(如顶点或插入删除,获取边或顶点等等)。
2、图的遍历算法,包括:深度优先遍历算法(DFS)(深度优先搜索图中所有顶点,保证所有连通分量均被搜索)以及广度优先搜索算法(BFS)(广度优先搜索方法没有探空和回溯的过程,而是一个逐层遍历的过程。)
3、AOV网络与拓扑排序。
4、AOE网及关键路径算法。
#pragma once
#define DEFAULTVALUE 20
#include<iostream>
using namespace std;
template<class T, class E> class Graph; //图的类定义预先声明。T为顶点类型,E为弧的权值类型
template<class T, class E>
class Edge {
private:
int dest; //一条边的另一个邻接点
E cost; //权值
Edge<T, E> *link; //另一条边的指针链
public:
Edge() {}
Edge(int d, E c, Edge<T, E> * next = NULL) : dest(d), cost(c), link(next) {}
friend class Graph<T, E>;
}; //定义边表结构
template<class T, class E>
struct Vertex {
T data;
int indegree=0; //顶点的入度,辅助进行aov网构建及拓扑排序
Edge<T, E> * adj; //第一个 指针
}; //顶点储存结构
template<class T, class E>
class Graph {
private:
Vertex<T, E> * NodeTable;
int numOfVertices; //顶点个数
int numOfArcs; //边数
int direction; //图的类型:0:无向图, 1:有向图
int maxSize; //顺序表存储的最大空间数
int visited[DEFAULTVALUE]; //是否被访问过的标志
int FindVertex(const T vertex) {
for (int i =0;i < numOfVertices; i++)
if (vertex == NodeTable[i].data) { return i; }
return -1; //查找失败
}
public:
Graph(int d = 0, int sz = DEFAULTVALUE); //初始化构造函数
~Graph(); //析构函数
int NumOfVertices() { return numOfVertices; } //去定点个数
int NumOfArcs() { return numOfArcs; } //取边数
E GetWeight(int v1, int v2)const { return v1 != -1 && v2 != -1 ? Edge[v1][v2] : 0; } //在成员函数中圆括号之后所带的关键字const表明,在该函数中不能改变类中的zhi数据成员的值。否则报错
bool InsertVertex(const T vertex); //插入一个顶点
bool InsertEdge(const T v1, const T v2, const int weight); //添加一条边
bool DeleteVertex(const int pos, T & vertex); //删除一个顶点,传入位置(也就是顶点表中的序号)与对应的值
bool DeleteEdge(const int v1, const int v2); //删除一条边
Edge<T, E> * GetFirstNeighbor(const int v); //取得序号为v的顶点的第一个邻接点
Edge<T, E> * GetNextNeighbor(const int v1, const int v2); //取得v1的下一个邻接点
void CreateGraph(int n, int e); //建立图表
void Print();
//深度优先搜索(DFS)函数
void DFSTraverse();
void DFS(int v);
//广度优先搜索(BFS)
void BFSTraverse();
//AOV网络与拓扑排序,采用栈存储入度为0的顶点
bool TopoSort();
//AOE网及关键路径算法
void CriticalPath();
};
//图的初始化操作,设置一个空表,为顶点表开辟一个连续的存储空间,设置direction和maxSize的值,结点个数和边数均为0
template<class T, class E>
Graph<T, E>::Graph(int d, int sz)
{
maxSize = sz; NodeTable = new Vertex<T, E>[maxSize]; //创建存储空间
numOfVertices = 0; numOfArcs = 0;
direction = d;
}
//图的销毁操作,即定义析构函数, 必须将每个顶点边表中节点释放,最终释放顶点表空间
template<class T, class E>
Graph<T, E>::~Graph()
{
for (int i = 0; i < numOfVertices; i++)
{
Edge<T, E> * p = NodeTable[i].adj; //将第i个顶点的firstlink取出来给p
while (p)
{
NodeTable[i].adj = p->link; //相当于将第二个边表地址给firstlink
delete p; //释放p结点,也就是释放第第一个边表
p = NodeTable[i].adj; //之后再将第二个结点地址给p,之后判断p是否为空,空:停止,否则继续
}
}
delete []NodeTable; //最后删除整个顶点表,使用new申请的内存,释放时用delete,使用new [ ]申请的内存释放时要用delete [ ]才行,delete只调用了一次析构函数
}
// 插入顶点操作,只需要在顶点表后增加一个新的顶点信息,顶点个数加一,没有增加此顶点的边
template<class T, class E>
bool Graph<T, E>::InsertVertex(const T vertex) {
if (numOfVertices == maxSize) { return false; } //顶点个数溢出,书中为numOfVertices==maxSize-1,经验证不对,取不到最大顶点个数。改成了numOfVertices==maxSize
NodeTable[numOfVertices].data = vertex; //实际存储时是从NodeTable[0]开始存储,在插入新顶点前,只存储到nodeTable[numOfVertices-1]
NodeTable[numOfVertices++].adj = NULL; //首先将新顶点的邻接点置位空, 再将numOfVertices顶点个数加一(i++ :先引用后增加,先在i所在的表达式中使用i的当前值,后让i加1。)
return true;
}
/*增加边,此操作需要区分图的类型,若为无向图,插入一条边时,既要在顶点为v1的边表中加入边结点(其数据域为顶点v2在顶点表中的序号,v2类型为T,不一定是相应序号),
同理也要在v2的边表中 加入一个边结点,与上面操作相同。若为有向图, 只要在顶点为v1的边表中加入边结点(其数据域为顶点v2在顶点表中的序号)*/
template<class T, class E>
bool Graph<T, E>::InsertEdge(const T v1, const T v2, const int weight)
{
int p1 = FindVertex(v1), p2 = FindVertex(v2); //找出v1和v2对应的顶点的序号
if (p1 == -1 || p2 == -1) { return false; } //只要有一个顶点不存在,返回错误
if (direction == 0) //如果是无向图,插入指定边(每次插入边结点到头结点之后)
{
NodeTable[p1].adj = new Edge<T, E>(p2, weight, NodeTable[p1].adj); //插入前adj指向的是旧的第一个边结点,现在要将新节点插入到头结点与旧的第一个边结点中间,初始化新的第一个边结点时,用adj来初始化其的nextlink指针,同时将新的第一个结点地址赋给头结点的adj
NodeTable[p2].adj = new Edge<T, E>(p1, weight, NodeTable[p2].adj);
}
else
{ //有向图较为简单,只需要上面第一个操作
NodeTable[p1].adj = new Edge<T, E>(p2, weight, NodeTable[p1].adj);
NodeTable[p2].indegree++; //书中未添加,用来统计拓扑排序中有向图顶点的入度情况
}
numOfArcs++; //边数增加一,(无向图虽然插入了两次,但是只是同一条边)
return true;
}
/*删除顶点操作, 不仅要就删除此顶点的pos(顶点表中的序号), 还要删除以该顶点为邻接点的边(包括有向图与无向图)
算法思路:
①对于有向图来说,要删除第pos条链上所有边结点(出弧)和别的链上边结点序号为pos的边结点(入弧);对于无向回来说,要删除第Pos条链上所有边结点以及相应的对称边,对称边只能算成一条边)。
②顶点数numOfVertices减1。
③如果删除的顶点不是最后一个顶点,则将最后一个顶点移至pos号位置,称为第pos条链,并将所有边结点中序号为numofvetices(删除前最后一个顶点序号)的边结点的序号改成pos。*/
template<class T, class E>
bool Graph<T, E>::DeleteVertex(const int pos, T & vertex)
{
if (pos <0 || pos >numOfVertices - 1) { return false; } //索引超出范围
Edge<T, E> * p = NodeTable[pos].adj, *q; //把该节点的firstlink给p
while (p)
{
NodeTable[pos].adj = p->link; //即将执行删除第一个边结点操作
delete p;
--numOfArcs; //边或弧减一
p = NodeTable[pos].adj; //将下一个结点赋给p,p为空则停止操作
}
for (int i = 0; i < numOfVertices; i++) //删除入弧或者对称边
{
p = NodeTable[i].adj;
if (p && p->dest == pos) //dest为当前边的另一个邻接点,并且该邻接点只在该顶点边表中只出现一次,因此只需执行一次删除操作
{
NodeTable[i].adj = p->link;
delete p;
}
else
{
if (p)
{
q = p->link;
while (q && q->dest != pos)
{
p = q; q = q->link;
}
if (q) //q不为空,说明找到了pos,删除他
{
p->link = q->link;
delete q;
}
}
}
if (direction) { numOfVertices--; } //若为对称边则不减少(因为上面减过一次,此对称边其实就是上面删过的边), 若为入弧(有方向,与上面删掉的不是同一条)则减少一
}
numOfVertices--; //顶点数减一
if (pos != numOfVertices) //最后一个顶点上移到被删顶点位置,修正最后顶点中的链接
{
NodeTable[pos].data = NodeTable[numOfVertices].data;
NodeTable[pos].adj = NodeTable[numOfVertices].adj;
for (int j = 0; j < numOfVertices; j++)
{
if (j != pos) { p = NodeTable[j].adj; } //自身边表中不用替换
while (p) //别的链上的边结点上序号为最后一个顶点序号一律换成pos
{
if (p->dest == numOfVertices) { p->dest = pos; break; }
else { p = p->link; }
}
}
}
return true;
}
/*删除边或弧操作,若为弧,则在v1的顶点的边表中找到序号为v2的边结点删除即可。若为边,不仅要删除上述结点,还要删除该结点的对称边。删除后,边数或弧数减一*/
template<class T, class E>
bool Graph<T, E>::DeleteEdge(const int v1, const int v2)
{
if (v1<0 || v1>numOfVertices - 1 || v2 <0 || v2 >numOfVertices - 1) { return false; }
Edge<T, E> *p = NodeTable[v1].adj, *q;
if (p->dest == v2) { NodeTable[v1].adj = p->link; delete p; } //很幸运,第一个边结点就是,删除它
else
{
q = p->link;
while (q && q->dest != v2) { p = q; q = q->link; }
if (q) { p->link = q->link; delete q; } //在边表中扎到了,干掉他
}
numOfArcs--;
if (direction == 0) //删除 无向图中的对称边,与前面操作基本相同
{
p = NodeTable[v2].adj;
if (p->dest == v1)
{
NodeTable[v2].adj = p->link;
delete p;
}
else
{
q = p->link;
while (q && q->dest != v1)
{
p = q;
q = q->link;
}
if (q) { p->link = q->link; delete q; }
}
}
return true;
}
//获取顶点为v的第一个邻接点操作,若有则返回第一个邻接点点的地址,若无,则返回NULL
template<class T, class E>
Edge<T, E> * Graph<T, E>::GetFirstNeighbor(const int v) //v为顶点在顶点表中的序号
{
if (v<0 || v> numOfVertices-1) { return NULL; }
return NodeTable[v].adj ? NodeTable[v].adj : NULL;
}
/*获取顶点v1的邻接点v2的下一个邻接点, 该操作的主要执行过程是在以v1顶点为头结点的单向链表中找到值v2顶点序号值的边结点,
若该边结点存在,并且其下一个结点链接非空,则返回该邻接点指针值(地址);否则,返回NULL。 */
template<class T, class E>
Edge<T, E> * Graph<T, E>::GetNextNeighbor(const int v1, const int v2)
{
if (v1<0 || v1>numOfVertices - 1 || v2<0 || v2>numOfVertices - 1) { return NULL; }
Edge<T, E> * p = NodeTable[v1].adj;
while (p && p->dest != v2) { p = p->link; }
if (p)
{
if (p->link) { return p->link; }
else { return NULL; }
}
else { return NULL; }
}
/*开始建立图,在建立一个图的邻接表时,首先,调用增加顶点函数InsertVertex添加图中所有顶点信息;然后,再根据给定的每条边或弧信息(边或弧的两个定义以及相应的权值),
调用增加边或弧函数InsertN8e添加图中所有边或弧。*/
template<class T, class E>
void Graph<T, E>::CreateGraph(int n, int e) //n个顶点, e条边
{
T vertex, vertex1, vertex2; E weight; int i, k;
cout << "input vertex:" << endl;
for (i = 0; i < n; i++) //插入n个顶点
{
cout << i + 1 << ":";
cin >> vertex;
InsertVertex(vertex);
}
cout << "input Edge:" << endl;
for (k = 1; k <= e; ++k) //插入e条边
{
cout << "input two vertex and weight:";
cin >> vertex1 >> vertex2 >> weight;
InsertEdge(vertex1, vertex2, weight);
}
}
//开始输出,oh my god
template<class T, class E>
void Graph<T, E>::Print() //以链表形式输出顶点和达成弧信息
{
for (int i = 0; i < numOfVertices; ++i)
{
cout << i + 1 << "data:" << NodeTable[i].data <<",indegree:"<<NodeTable[i].indegree<< "--->";
Edge<T, E> *p = NodeTable[i].adj;
while (p) //输出与该顶点相关的所有边成弧的信息
{
cout << p->dest +1 << "(data:" << NodeTable[p->dest].data << ",weight:" << p->cost << ")--->";
p = p->link;
}
cout << endl;
}
}
//深度优先遍历函数(DFS)深度优先搜索图中所有顶点,保证所有连通分量均被搜索
template<class T, class E>
void Graph<T, E>::DFSTraverse()
{
int v;
for (v = 0; v < numOfVertices; v++) { visited[v] = 0; }
for (v = 0; v < numOfVertices; v++) //采取循环方式,个人认为是为了包含进,有两个以上圈的情况
{
if (!visited[v]) { DFS(v); }
}
cout << endl;
}
template<class T, class E>
void Graph<T, E>::DFS(int v) //开始搜索以v为开始的连通分量
{
visited[v] = 1;
cout << NodeTable[v].data << " ";
Edge<T, E> * p = GetFirstNeighbor(v); //从v的第一个邻接点深度优先搜索
for (; p; p = GetNextNeighbor(v, p->dest)) //与v相关的邻接点均深度优先搜索遍历,相当于从根节点开始的其他分支,等第一个分支递归完成后再依次处理剩下分支
{
int w = p->dest;
if (!visited[w]) { DFS(w); }
}
}
//广度优先搜索(BFS)广度优先搜索方法没有探空和回溯的过程,而是一个逐层遍历的过程。
template<class T, class E>
void Graph<T, E>::BFSTraverse()
{
int i;
for (i = 0; i < numOfVertices; i++) { visited[i] = 0; } //用前初始化
int queue[DEFAULTVALUE + 10], front=-1, rear = -1; //初始化顺序队列
for (i = 0; i < numOfVertices; i++) //遍历所有顶点
{
if (!visited[i])
{
visited[i] = 1;
cout << NodeTable[i].data << " ";
queue[++rear] = i; //先将rear加一后,再运行queue[rear],将已遍历的顶点进入队列
while (front != rear) //队列非空,继续搜索
{
int v = queue[++front]; //出队列,开始搜索v的所有邻接点
Edge<T, E> * p = GetFirstNeighbor(v);
while (p)
{
int w = p->dest;
if (!visited[w])
{
visited[w] = 1;
cout << NodeTable[w].data << " ";
queue[++rear] = w;
}
p = GetNextNeighbor(v, w);
}
}
}
}
cout << endl;
}
//AOV网络与拓扑排序,原书中加入此功能时未修改InserEdge()函数,否则得不到正确结果
template<class T, class E>
bool Graph<T, E>::TopoSort()
{ //若有向图无回路,则输出图的拓扑序列并返回true,否则返回false。
int i, k, count = 0; Edge<T, E> *p; int stack[20], top = -1; //定义一个顺序栈
for (i = 0; i < numOfVertices; i++) //所有入度为0的顶点进栈
{
if (!NodeTable[i].indegree) { stack[++top] = i; }
}
while (top != -1)
{
i = stack[top--];
cout << NodeTable[i].data; ++count;
for (p = NodeTable[i].adj; p; p = p->link) //作用主要是遍历顶点i的边表,边表中每个顶点入度减一后,若为0,就入栈
{
k = p->dest;
if (!(--NodeTable[k].indegree)) { stack[++top] = k; } //如果k的入度减一后为0,则进栈
}
}
if (count < numOfVertices) { cout << "此有向图有回路,拓扑排序失败\n"; return false; }
else { cout << "为一个拓扑排序。\n"; return true; }
}
//AOE网及关键路径算法,此博客原理讲解:https://blog.csdn.net/u011587070/article/details/82773820
template<class T, class E>
void Graph<T, E>::CriticalPath()
{
int i, k;
Edge<T, E> *p;
int e, l, *ve = new int[30], *vl = new int[30];
for (i = 0; i < numOfVertices; i++) { ve[i] = 0; }
for (i = 0; i < numOfVertices; i++) //计算每个事件的最早发生时间
{
p = NodeTable[i].adj;
while (p) //按拓扑排序次序,修正每个顶点边表中顶点的最早发生时间
{
k = p->dest;
if (ve[i] + p->cost > ve[k]) //更新使顶点k的发生的最长的一条路径,例如有很多弧指向一个顶点,必须所有弧都完成才能发生,因此最长的路径完成时间即为事件发生的最早时间
{
ve[k] = ve[i] + p->cost;
}
p = p->link;
}
}
//测试
cout << "ve:";
for (i = 0; i < numOfVertices; i++)
{
cout << ve[i] << endl;
}
for (i = 0; i < numOfVertices; i++) //vl数组初始化,ve数组的逆序排列
{
vl[i] = ve[i]; //TND,书上好像又写错了
}
for (i = numOfVertices - 1; i >= 0; i--) //按拓扑排序逆序次序计算每个事件的最迟发生时间vl
{
int temp = 0;
p = NodeTable[i].adj; //从最后一个顶点的边表开始取
if (p)
{
temp = vl[p->dest] - p->cost;
}
while (p) //修正迟发生时间
{
k = p->dest;
if (temp >vl[k] - p->cost||temp==vl[k]-p->cost)
{
temp = vl[k] - p->cost; //书上代码似乎有问题,修改了代码。v[i]取v[k]-p->cost中值较小的。也就是必须在最小的那个时刻开始,否则延误最终结果。
vl[i] = temp;
}
p = p->link;
}
}
//测试
cout << "vl:";
for (i = 0; i < numOfVertices; i++)
{
cout << vl[i] << endl;
}
for (i = 0; i < numOfVertices; i++) //输出关键路径
{
p = NodeTable[i].adj;
while (p)
{
k = p->dest;
e = ve[i]; //e为边《i,k》的最早开始时间,等于起始顶点i的最早开始时间
l = vl[k] - p->cost; //l为边《i,k》的最迟开始时间,等于终点顶点k的最迟开始时间减去边的耗时
if (e == l)
{
cout << "<" << NodeTable[i].data << "," << NodeTable[k].data << ">" << "是关键活动" << endl;
}
p = p->link;
}
}
}