图的基本概念
两个元素: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;
代码实现:
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;