1. 图的基本概念
2. 图的存储结构
(1)邻接矩阵
一般来说,邻接矩阵所占空间与边数无关(不考虑压缩存储),适合于存储稠密图。为了反映一个图的全面信息,通常采用以下类型定义:
#define MAXVEX 100
typedef char VertexType; //定义VertexType为char类型
typedef struct vertex
{
int adjvex; //顶点编号
VertexType data; //顶点信息
}VType;
typedef struct graph
{
int n,e; //n为实际点数,e为实际边数
VType vexs[MAXVEX]; //顶点集合
int edges[MAXVEX][MAXVEX]; //边集合
}AdjMatrix;
(2)邻接表
#define MAXVEX 100
typedef char VertexType; //定义VertexType为char类型
typedef struct edgenode
{
int adjvex; //邻接点序号
int value; //边的权值
struct edgenode *next; //下一条边的顶点
}ArcNode; //每个顶点建立的单链表中节点的类型
typedef struct vexnode
{
VertexType data; //节点信息
ArcNode *firstarc; //指向第一条边节点
}VHeadNode; //单链表的头结点类型
typedef struct
{
int n,e; //n为实际点数,e为实际边数
VHeadNode adjlist[MAXVEX]; //单链表头结点数组
}AdjList;
3. 图的遍历
假定图以邻接表方式存储。有广度优先搜索和深度优先搜索这两种基本方法。
(1)BFS:首先访问初始点vi,并将其标记为已访问过,接着访问vi的所有未被访问过的邻接点vi1,vi2,…,vit,并均标记为已访问过,然后再按照vi1,vi2,…,vit的次序,访问每一个顶点的所有未被访问过的邻接点,并均标记为已访问过,依此类推。
访问vi
vi入队
while(队不空)
出队
其所有为被访问过的邻点被访问并入队
实现BFS的非递归算法为:
void BFS(AdjList *g,int vi)
{
int i,v,visited[MAXVEX];
int Qu[MAXVEX],front=0,rear=0; //环形队列
ArcNode *p;
for(i=0;i<g->n;i++)
visited[i]=0;
visited[vi]=1;
cout<<vi<<" "; //do something!!
rear=(rear+1)%MAXVEX;
Qu[rear]=vi; //初始顶点入队
while(front!=rear){
front=(front+1)%MAXVEX;
v=Qu[front]; //出队
p=g->adjlist[v].firstarc; //查找v的第一个邻接点
while(p!=NULL){ //查找v的所有邻接点
if(visited[p->adjvex]==0){ //未访问则访问之,并入队
visited[p->adjvex]=1;
cout<<p->adjvex<<" ";
rear=(rear+1)%MAXVEX;
Qu[rear]=p->adjvex;
}
p=p->next;
}
}
}
(2)DFS:从图G中某个顶点vi出发,访问vi,然后选择一个与vi相邻且为被访问过的顶点 v 访问,再从 v 出发选择一个与 v 相邻且为被访问过的顶点vj访问,依此类推。如果当前已访问过的顶点的所有邻接顶点都已被访问,则退回到已被访问的顶点序列中最后一个拥有未被访问的相邻顶点的顶点w,从w出发按同样方法向前遍历。
vi 入栈
while(栈不空)
取出栈顶
访问它
它的所有未被访问的邻点入栈
实现DFS的递归算法为:
void DFS(AdjList *g,int vi)
{
ArcNode *p;
visited[vi]=1;
cout<<vi<<" "; //访问vi节点
p=g->adjlist[vi].firstarc; //找vi的第一个邻接点
while(p!=NULL){ //找vi的所有邻接点
if(visited[p->adjvex]==0)
DFS(g,p->adjvex); //从vi未访问过的邻接点出发DFS
p=p->next
}
}
4. 最小生成树
产生一个图的最小生成树主要有两个算法,即Prim算法和Kruskal算法
(1)Prim算法:假设G=(V,E)是一个具有n个顶点的连通网,T=(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,U和TE的初值均为空。首先从v中任取一个起点(假定取v0),将它并入U中,然后只要U是V的真子集,就从那些其一个端点已在T中,另一个端点仍在T外的所有边中,找一条最短(即权值最小)边,假定为(vi,vj),其中vi在U中,而vj在V-U中,并把该边(vi,vj)和顶点vj分别并入T的边集TE和顶点集U中,依此类推。这是一个Greedy算法。
$ MST-Prim(G,w,r)
for each u \in V do
key[u] = infinity && pi[u] = NIL
key[r] = 0
Q = V
while Q 不空
u = Extract-Min(Q) //最小优先级队列
for each v \in Adj[u]
if v \in Q && w(u,v)<key[v]
pi[v] = u && key[v] = w(u,v)
Prim算法的性能取决于优先队列Q是如何实现的。如果用二叉最小堆来实现Q(详见堆排序),则可以用Build-Min-Heap来实现前面的初始化部分,时间为O(V).对Extract-Min的全部调用时间为O(VlgV),最后一句隐含了一个对最小堆进行Decrease-Key操作,时间为O(ElgV),故总时间为O(ElgV).
通过斐波那契堆,Prim算法可以进一步改善(详见clrs ch20)。
(2)Kruskal算法:假设G=(V,E)是一个具有n个顶点的连通网,T=(U,TE)是G的最小生成树,U的初值等于V,即包含有G中的全部顶点,TE的初值均为空。首先将图G中的边按权值从小到大的顺序依次选取,若选取的边使生成树T不形成回路,则把它并入TE中,保留作为T的边,若选取的边使T形成回路,则将其舍弃,依此类推。这也是一个Greedy算法。
$ MST-Kruskal(G,w)
A = 空集
for each v \in V
Make-Set(v)
sort the edges of E into nondecreasing order by weight w
for each (u,v) \in E, taken in nondecreasing order by weight
if Find-Set(u) != Find-Set(v)
A = A U {(u,v)}
Union(u,v)
5. 最短路径:待续
6. 拓扑排序
在AOV网中很重要,解决有向无回路图的“排序”问题。有两种实现方法:一种是计算节点入度的Greedy算法,另一种是DFS。
(1)计算节点入度的Greedy算法
step1:从有向图中选择一个没有前驱(即入度为0)的顶点并且输出它.
step2:从网中删去该顶点,并且删去从该顶点发出的全部有向边.
step3:重复上述两步,直到剩余的网中不再存在没有前驱的顶点为止.
对于给定的有向图,采用邻接表作为存储结构,修改邻接表定义中的VNode类型如下:
typedef struct
{
Vertex data; //顶点信息
int count; //存放顶点入度
ArcNode *firstarc; //指向第一条弧
}VNode;
在下面代码中,当某个顶点的入度为零shi就将此顶点输出,同时将该顶点的所有后继顶点的入度减1,为了避免重复检测入度为零的顶点,设立一个栈St,以存放入度为零的顶点。
void TopSort(VNode adj[],int n)
{
int j;
int St[MAXVEX],top=-1;
ArcNode *p;
for(int i=1;i<=n;i++){
if(adj[i].count==0){ //入度为零的顶点入栈
top++;
St[top]=i;
}
while(top>-1){
i=St[top];top--;
cout<<i<<" ";
p=adj[i].firstarc; //找第一个相邻顶点
while(p!=NULL){
j=p->adjvex;
adj[j].count--;
if(adj[j].count==0){ //入度为零的相邻顶点入栈
top++;
St[top]=j;
}
p=p->next; //找下一个相邻顶点
}
}
}
}
(2)DFS实现:O(E+V)
其实就是clrs中的算法。利用DFS,记录各点完成访问的时刻(完成时间),用DFS遍历一次整个图,得出各结点的完成时间,然后按完成时间倒序排列就得到了图的拓扑序列。
void DFSTopSort(G,i,T){ //i是搜索的出发点,T是栈
int j;
visited[i]=TRUE; //访问i
for(所有i的邻接点j) //即<i,j>∈E(G)
if(!visited[j])
DFSTopSort(G,j,T);
//以上语句完全类似于DFS算法
Push(&T,i); //从i出发的搜索已完成,输出i
}
7.SCC
8. 最大网络流:待续