数据结构中图的邻接表构建及其操作(C++语言)

数据结构中图的邻接表构建及其操作(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;
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值