图(数据结构)自用

图的基本概念

两个元素:1.顶点 2.边

有向图:顶点有序,边有方向

无向图:顶点无序,边无方向

完全图:每个顶点与其他所有的顶点之间有边

简单路径:路径上各顶点均不同

连通分量:非联通图的极大联通子图

强联通与强联通分量:针对与有向图

生成树:一个连通图的生成树是它的极小连通子图

图的存储结构

无法在存储位置上反映数据元素之间的联系,所以没有顺序存储结构

如果用多重链表存储,结点的结构难以确定,因为每个顶点的度不同,当差别大时,容易浪费空间

常用:邻接矩阵和邻接表

邻接矩阵:

无向图的邻接矩阵

两部分:一个数组存顶点,一个矩阵(二维数组)存边

矩阵一定是一个对称矩阵

矩阵中两种元素,0和1,0代表两点之间没有边,1代表有边。

类模板:

包含  :      顶点 vertexes(一维数组) 

                   边 arcs[ ][ ](二维数组)

                   当前节点数目:vexNum

                    允许最大顶点个数:vexMaxNum

                    当前边的个数:arcNum

代码实现时的难点:

1.构造函数:

   课本上构造二维数组的方法值得记住:

template<class ElemType>
AdjMatrixUndirGraph<ElemType>::AdjMatrixUndirGraph(int vertexMaxNum)//传入参数最大顶点个数的构造函数
{
   //......
  arcs=(int **)new int *[vexMaxNum];
  for(int v=0;v<vexMaxNum;v++)
    arcs[v]=new int [vexMaxNum];//构造一个二维数组的邻接矩阵vexMaxNum*vexMaxNum大小
}

2. 插入顶点d

修改四个部分       1.顶点域加入顶点d

                              2.邻接矩阵长宽加一并且去赋值

                              3.tag数组

                               4.顶点个数加一

vertexes[vexNum]=d;
tag[verNum0=UNVISITED;
for(int v=0;v<=verNum;v++)
{
  arcs[verNum][v]=0;
  arcs[v][verNum]=0;
}
vexNum++;

3.插入边

    邻接矩阵要改两个边值,将0改为1(对阵矩阵)

4.删除顶点

  首先查找要删除的顶点是否存在,存在的话,先删除与顶点相关的边,接着删除顶点。

  删除与顶点相关的边时候就是删除了邻接矩阵中的一个十字,与顶点相关的一行和一列。然后将    矩阵的最后一行和一列移到这个位置,来减少矩阵元素的大范围移动。

template<class ElemType>
void AdjMatrixUndirGraph<ELemType>::DeleteVerx(const ElemType &d)
{
  int v;//记录要删除顶点下标
  for(int v=0;v<vexNum;v++)
{ 
  if(vertexes[v]==d)
     break;
  if(v==vexNum)
     throw Error("图中不存在次节点");
}

for(int u=0;u<vexNum;u++) //删除边
{
 arcNum--;
 arcs[v][u]=0;
 arcs[u][v]=0;
}
vexNum--;

if(v<vexNum)//移动
{
 vertexes[v]=vertexes[vexNum];
 tag[v]=tag[verNum];
 for(int u=0;u<vexNum;u++)
     arcs[v][u]=arcs[vexNum][u];
  for(int u=0;u<vexNum;u++)
     arcs[u][v]=arcs[u][vexNum];
}
}

有向图的临界矩阵

实现思路大致相同,但是有向图的临界矩阵不是对称的。

有权和无权

区分主要在临界矩阵元素不同表示

无权用0,1代表有无边即可

有权时,权值代表有边和边权值大小,对角线统一为0,没有边用无穷表示(自己定义一个值为100000的符号即可)

邻接表

首先要明白为什么要用邻接表,其实邻接表是邻接矩阵的一个改进,当图中边很少时,邻接矩阵会有大量的0元素,浪费空间。邻接表就是只关注有边的那些,作为节点。并且邻接表的边节点的链入顺序是任意的,所以在有些时候会有些差异。

邻接表一般用顺序结构存储节点,在每个节点后以链表的形式去将节点加入。

无向邻接表

因为一条边关联两个顶点,所以每个邻接表中如果顶点有x个,那么边的节点有2x个,并且有两个边节点表示的是同一条边。

对于带有权重的。只需要在构造边节点时,加入weight即可。

有向邻接表

有向邻接表用到的比较多,也比较重要。

在有向图的邻接表中,一条弧(有向)在邻接表中只出现一次,如果想统计顶点的出度,那么直接看顶点的弧节点的个数就可以了,但是如果想统计入度,可以构建逆邻接表(以进入顶点的弧作为边节点构建邻接表)

有向邻接表中顶点类模板(struct AdjListNetWorkVex)

数据成员:

 ElemType data;//数据元素值

 AdjListNetworkArc<WeightType> * firstarc;//指向邻接表边节点的指针

有向邻接表中弧节点类模板(struct AdjListNetworkArc)

数据成员:

int adjVex; //弧头顶点序号

WeightType weight;//边的权值

AdjListNetworkArc<WeightType> *nextarc; //下一条边节点的指针

有向邻接表类模板

数据成员:

int vexNum,vexMaxNum,arcNum; //顶点数目,允许最大顶点数目,边数

AdjListNetWorkVex<ElemType,WeightType> *verTable; //一维数组存放顶点

(mutable的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。
在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中)

 mutable  Status *tag ;

功能函数:

1.插入弧:

   用p保存原先的第一个边节点,在将新的边节点插入到顶点与原来第一个边的中间。

  第一次没看明白代码:

AdjListNetworkArc<WeightType> *p;
	p = vexTable[v1].firstarc; // 将v1的第一个边结点保存在p中
    vexTable[v1].firstarc = new AdjListNetworkArc<WeightType>(v2, w, p); // 将新的边结点插入到v1的邻接表中
	arcNum++; // 更新边的数量

2.删除弧:

  在删除弧时候有特殊处理,当删除顶点的第一个节点时,要将firstarc指向null

代码如下:

//删除从v1到v2的弧
//v1,v2,异常或v1=v2时,抛出异常省略
AdjListNetworkArc<WeightType> *p,*q;
p=vexTable[v10.firstarc;
while(p!=NULL&&p->adjvex!=v2)
{
   q=p;
   p=p->nextarc;
}
if(p!=NULL)
{
   if(vexTable[v1].firstarc==p)
      vexTable[v1].firstarc=p->nextarc;//特殊处理
   else
       q->nextarc=p->nextarc;
    delete p;
    arcNum--;
 }
}

图的遍历与连通性

两种遍历方法:DFS和BFS

深度优先遍历

邻接矩阵的DFS是固定的,而邻接表不固定,因为邻接表的边的链接没有顺序,DFS一般递归实现或者用栈去模拟。

DFS搜索函数:

思路:1.从 顶点v出发,访问节点v,并将其置为以访问的状态。

           2.取v的第一个邻接节点w

           3.若w不存在,则结束。否则进行4

           4.若w未被访问,则访问w,并置为已访问

           5.使w为v相对于原先w的下一个邻接节点,转到3

void DFS(const AdjMatrixUndirGraph &g,int v)
{
  ElemType e;
  g.SetTag(v,VISITED);
  g.GetElem(v,e);
  for(int w=g.FirstAdjVex(v);w!=-1;w=g.NextAdjVex(v,w)
    if(g.GetTag(w)==UNVISITED)
       DFS(g,w);
}
  

DFS遍历:

先将每个节点设置为未被访问,然后从每一个未被访问的节点进行深度优先搜索即可。

BFS搜索函数

思路:(类似与层次遍历)

 1. 访问节点v ,并标记v已经访问,同时将v如队列。

 2.当队列为空时结束,否则执行3

 3.队头顶点出队列为v

 4.取v的第一一个邻接顶点w

 5.若顶点不存在,转3

 6.若顶点w未访问,访问w,设置为VISITED,将w入队,否则进行7

 7.使w为v相对于原来w的下一个邻接顶点,转5

void BFS(const AdjMatrixUndirGraph &g,int v)
{
 LinkQueue<int> q;
 int u,w;
 ElemType e;
 g.SetTag(v,VISITED);
 q.Enqueue(v);//入队
 while(!q.IsEmpty())
{
  q.DelQueue(u);//队头出队,元素存为u
  for(w=g.FirstAdjVex(u);w!=-1;w=g.NextAdjVex(u,w))
{
   if(g.GetTag(w)==UNVISITED)
     {
        g.SetTag(w,VISITED);
        q.EnQueue(w);
      }
}
}

联通分量:

     判断依据,从一个点进行DFS或者BFS,如果,所有节点都访问过,说明联通,反之不联通。

最小生成树MST

一个联通图的生成树是原图的极小联通子图,连通图的生成树不唯一,对于有权的图,找到一个生成树,使各个边的权值总和最小

克鲁斯卡尔

基本思想:

 先构造包含所有顶点的一个森林,然后将最小的边加入

  因为要判断这条边能不能使连通分量-1,所以要用到并查集,又因为要判断边的大小,所以要用 

到最小堆对边进行堆排序

实现:

template <class ElemType, class WeightType>
void MiniSpanTreeKruskal(const AdjMatrixUndirNetwork<ElemType, WeightType> &g)
// 初始条件:存在网g
// 操作结果:用Kruskal算法构造网g的最小代价生成树
{
	int count, VexNum = g.GetVexNum();
    KruskalEdge<ElemType, WeightType> KEdge;//边
 	MinHeap<KruskalEdge<ElemType, WeightType> > ha(g.GetEdgeNum());
    ElemType  *kVex, v1, v2;
	kVex = new ElemType[VexNum];	// 定义顶点数组,存储顶点信息 
	for (int i = 0; i < VexNum; i++)
	    g.GetElem(i, kVex[i]);
	UFSets<ElemType> f(kVex,VexNum);// 根据顶点数组构造并查集 
	for (int v = 0; v < g.GetVexNum(); v++)
		for (int u = g.FirstAdjVex(v); u >= 0; u = g.NextAdjVex(v, u))
			if (v < u)			{	// 将v < u的边插入到最小堆 
                g.GetElem(v, v1); 
                g.GetElem(u, v2);
                KEdge.vertex1 = v1;
                KEdge.vertex2 = v2;
                KEdge.weight = g.GetWeight(v,u);
				ha.Insert(KEdge);
			}
	count = 0;					    // 表示已经挑选的边数

	while (count < VexNum - 1)	{	
        ha.DeleteTop(KEdge);        // 从堆顶取一条边
		v1 = KEdge.vertex1;
        v2 = KEdge.vertex2;
		if (f.Differ(v1, v2))	{	// 边所依附的两顶点不在同一棵树上
			cout << "边:( " << v1 << ", " << v2 << " ) 权:" << KEdge.weight << endl ; // 输出边及权值
			f.Union(v1, v2);		// 将两个顶点所在的树合并成一棵树
			count++;
		}
	}
}

普里姆算法

基本思想:

与克鲁斯卡尔算法不同,普里姆算法是先从一个节点出发,找到与其相关的最小权值的边和点加入图中,再以这个整体去找下一个权值最小的边和顶点,将其加入图中。

在构造最小生成树中,要设置一个辅助数组closearc[ ],以记录现在的部分图到剩下图中的顶点具

有权值最小的边,如图,每个顶点有两个域,lowweight和nearvertex,lowweight记录的是顶点到剩下部分最小的边的权值,nearvertex记录这条边连接的另个顶点的下标

实现算法思路:

1. 初始化辅助数组closearc[ ]

 2.执行3,4n-1次

 3.将最小边(lowweight!=0)加入

 4.修改加入过后的权值 ,对于剩下的每个顶点j,如果j原来的权值大于j到刚加入新顶点的权值,就对权值进行一个更新loweright=arc[v][j]; nearvertex=v;

85855c08c1d94dd58f7207257587cf6c.jpeg

代码实现:

template <class ElemType, class WeightType>
void MiniSpanTreePrim(const AdjMatrixUndirNetwork<ElemType, WeightType> &g, int u0)
// 初始条件:存在网g,u0为g的一个顶点
// 操作结果:用Prim算法从u0出发构造网g的最小生成树
{
   WeightType min;
   ElemType v1, v2;
   int vexnum = g.GetVexNum();
   CloseArcType<ElemType, WeightType> * closearc;

   if (u0 < 0 || u0 >= vexnum)
      throw Error("顶点u0不存在!"); // 抛出异常

	int u, v, k;						// 表示顶点的临时变量 
	closearc = new CloseArcType<ElemType, WeightType>[vexnum];	// 分配存储空间
	for (v = 0; v < vexnum; v++)	{	// 初始化辅助数组adjVex,并对顶点作标志,此时U = {v0}
		closearc[v].nearvertex = u0;
		closearc[v].lowweight = g.GetWeight(u0, v);
	}
	closearc[u0].nearvertex = -1;
	closearc[u0].lowweight = 0;
	
	for (k = 1; k < vexnum; k++) {	// 选择生成树的其余g.GetVexNum() - 1个顶点
		min = g.GetInfinity();
        v = u0;// 选择使得边<w, adjVex[w]>为连接V-U到U的具有最小权值的边
		for (u = 0; u < vexnum; u++)
           if (closearc[u].lowweight != 0 && closearc[u].lowweight < min) {
			  v = u;
              min = closearc[u].lowweight;
	       }
 	    if (v != u0) {
           g.GetElem(closearc[v].nearvertex, v1);
           g.GetElem(v, v2);     
		   cout << "边:( " << v1 << ", " <<  v2 << " ) 权:" << min << endl ; // 输出边及权值
		   closearc[v].lowweight = 0;		// 将w并入U
		   for (u = g.FirstAdjVex(v); u != -1 ; u = g.NextAdjVex(v, u)) 	// 新顶点并入U后重新选择最小边
			  if (closearc[u].lowweight != 0 && (g.GetWeight(v, u) < closearc[u].lowweight))	{	// <v, w>为新的最小边
				closearc[u].lowweight = g.GetWeight(v, u);
				closearc[u].nearvertex = v;
			  }
		}
		
	}
	delete []closearc;			// 释放存储空间  
}

破圈法

最短路径问题

寻找带权有向图中两个顶点之间路径长度最短的路径

Dijkstra算法——权值非负单源点问题

按路径长度的递增次序逐条产生最短路径

BF(贝尔曼福特)算法

Floyd算法

活动网络

通过不同代表活动的方式,将活动网络划分为顶点代表活动网络,和边表示活动网络

用顶点表示活动网络(AOV网)

计划,施工工程,生产流程,程序流程这类工程类的项目,经常用这种活动网络

不能出现有向环,因为这样这个顶点就不能执行(自己的开始与否以自己为判断条件,逻辑矛盾)

拓扑排序

检测是否有有向环——对AOV进行拓扑排序的构造:

如果通过拓扑排序能将AOV网络所有顶点都排入一个拓扑排序中,那么就说明一定不含有环,反之说明含有有向环。

拓扑排序的有序序列不一定唯一

拓扑排序思路:

1. 在AOE网中选一个入度为0,即没有前驱的顶点v,并输出v。

2.在图中删去顶点,同时删去所有从改顶点出发的弧。

3.重复1和2,直到所有入度为0即没有前驱的顶点全部输出,如果AOV网所有顶点都已经输出,则说明AOV网中没有有向回路

实现思路:

AOV网用邻接表存储,为了方便查找入度为0的顶点,设置了一个顶点入度的数组InDegree[ ],在构造时候,首先将每个InDegree[ ]=0,以后每输入一个弧<i,j>,都将顶点j的入度加一

代码:

template<class ELemType>
void StatInDegree(const AdjListDirGraph<ElemType> &g,int *InDegree)
{
  for(int v=0;v<g.GetVexNum();v++)
  {
    InDegree[v]=0;
  }
  for(int v=0;v<g.GetVexNun();v++)//如果有边,那么就给<v,u>边中的u的顶点入度加一
  {
    for(int u=g.FirstAdjVex(v);u!=-1;u=g.NestAdjVex(v,u))
       InDegree[u]++;
  }
}

拓扑算法:

1.建立入度为0的顶点的栈

2.当栈为空时转6,否则转3

3.顶点栈中栈顶元素出栈,并输出顶点v

4.从AOV网中删去顶点v和所有从v出发的弧<v,j>,并将j的入度减一

5.如果顶点j的入度减为0,将顶点入栈,转2

6.如果输出顶点个数少于AOV网顶点个数,输出有环的消息,结束

算法实现:

在实现是,可以直接利用顶点入度数组InDegree[ ]中,入度为0的元素,建立入度为0的静态链栈,设置一个栈顶指针top,指向当前栈顶位置,,初始化为top=-1,表示空栈,当顶点v进栈时:

InDegree[v]=top;top=v;

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值