图的存储、遍历、应用

1. 基本术语

图有有向和无向之分,在此基础上对应有不同的术语及结构。

(有向)完全图:每个点到其它点都有直接路径。
连通图:图中任意两点连通。
连通分量:无向图中的极大连通子图。
强连通图:有向图中任意两点都存在路径。
强连通分量:有向图中的极大强连通子图。

生成树:一个连通图的极小连通子图,它含有连通图的全部顶点(n),但只有构成树的n-1条边。
这里涉及到极小连通子图和极大连通子图,分别对应生成树和连通分量,极小的意思是包含全部顶点但是边数最少,极大也是包含全部顶点但是边数最多。

最小生成树:针对带权图中的代价最小的生成树。

2. 图的存储

图的存储结构应用比较多的有两种:邻接矩阵和邻接表。这两个结构都是以顶点为内容来进行存储的,当对图中边应用较多时,可以采用稍微复杂的十字链表(有向图)或邻接多重表(无向图)结构来存储,是的,操作更为方便。

邻接矩阵就是一个二维表,比较简单,需要注意的是,带权和不带权边的信息不一样,倘若i,j之间不存在边,不带权图中G[i][j]=0; 带权图中G[i][j]=MAX。

邻接表采用链表方式存储,但头结点采用数组存储的,对应到两种结点:表结点与头结点。定义式如下:

//弧结点:
typedef struct ArcNode {
    int adjvex;//弧指向的顶点
    struct ArcNode *nextarc;//指向下一条弧
}ArcNode;
//头结点
typedef struct VNode {
    VertexType data;//顶点数据,一般0,1,2...
    ArcNode *firstarc;//指向的第一条弧
}VNode, AdjList[MAX_NUM];

//一个图的完整定义
typedef struct {
    AdjList vertices;//表头结点数组
    int vexnum, arcnum;
    int kind;//图的种类
}ALGraph;

上面定义很清晰了。再谈谈比较少用但若应用需要会十分高效的十字链表和邻接多重表。
这两类表也是由弧节点和头结点构成,但是弧结点包含的信息更多,因为针对的是需要操作边的场合。邻接多重表的弧结点包含了两个顶点信息(一条弧的两个端点)以及两个指针域(分别指向与两端点连接的其他弧);十字链表的弧结点也有两个顶点信息(弧头和弧尾)和两个指针域(hlink, tlink),但是两个指针域的指向与前者不同,hlink指向与本弧中弧头相同的下一条弧, tlink指向与本弧中弧尾相同的下一条弧。
表头结点也不一样,由于有向图每个节点作为弧头和弧尾是不一样的,所以十字链表的头结点除了包含节点数据信息以外,还包括两个指针域,分别指向以该节点为弧头和弧尾的弧;邻接多重表的表头结点则只有一个链域,指向与头结点连接的弧。

3. 图的遍历

图的遍历无非两种:深度优先搜索(DFS)和广度优先搜索(BFS)。
给出这两个算法伪码实现(假设需要深度遍历图采用邻接矩阵存储,需要广度遍历的图采用邻接表存储,其实可以写个接口直接根据图类型调用不同函数即可,但这里为了使用刚刚介绍的存储结构):

//深度遍历,可以递归实现,当然可以用自己的栈实现
void DFS(int G[][], int v) {
    visited[v] = TRUE;//标记被访问过的顶点
    for(i=0; i<n; ++i) {
        w = G[v][i];
        if (i != v && w >= 0 && !visited[w])
            DFS(G, w); 
    } 
}

//广度遍历,存储结构是邻接矩阵,需要队列喽
void BFS(Graph G, int v) {
    visited[v] = TRUE;
    InitQueue(Q);
    EnQueue(Q, v);
    while(!QueueEmpty(Q)) {
        DeQueue(Q, u);
        for(w=G.vertices[u].firstarc; w>0; w=w->next) {
            if (!visited[w]) {
                visited[w] = TRUE;
                EnQueue(Q, w);
            }
        }
    }
}

4. 图的应用

应用上,图基本都是带权的,图的应用也分为有向图和无向图不同应用类型,无向图的应用有个最小生成树,有向图有关键路径以及最短路径问题。

4.1无向图应用

最小生成树的定义已经说过,在所有生成树中,代价最小的那个就是的。这个问题有两个解决方案:Prim和Kruskal。两者处理结果一致,但是形式上有所差别:前者以顶点着手,后者以边突破。这一差别导致应用上侧重点不同:前者对顶点少边数多的图(稠密图)比较适用,后者处理顶点多边数少(稀疏图)的图比较适用。这里有个记忆捷径:prim的单词数少啊,kruskal单词较长,类比点和边倒是极好记住的。

prim算法:现在只剩下顶点(边被删除),以其中任意一个v开始,找它的边,权值最小的边就连接上,基于这条边上的另外一个顶点w,再找与v、w相连接的边,边另外一个顶点不是v和w,找到权值最小的边连接上。。。以此类推直到所有的顶点都被连接就ok。伪码如下:

struct {
    VertexType adjvex;//用来记录边顶点,可以方便输出边
    VRType lowcost;
}closedge[n];
//G为邻接矩阵存储,有权图中不存在的边是MAX
void MST_Prim(int G[][], int v) {
    for(i=0; i<n; ++i)
        if(i != v)
            closedge[i] = {v, G[v][i]};//初始化
    closedge[v].lowcost = 0;//初始v
    for(i=1; i<n; ++i) {
        k = min(closedge);//找代价最小的边,对应顶点编号
        print(closedge[k].adjvex, k);//打印边
        closedge[k].lowcost = 0;
        for(j=0; j<n; ++j) {
            if (G[k][j] < closedge[j].lowcost)
                closedge[j] = {k, G[k][j]};
        }
    }
}

Kruskal算法:现在只剩下顶点(边被删除),总是选择最小的边添加进去,但是要保证添加边后不会形成环,终止条件是所有顶点都是连通的。判断是否连通要用到并查集。伪代码如下:

typedef struct {  //用来存储边信息
    int u, v;    //边顶点 
    int w;       //边的权值 
}Edge, edgelist[m];//m条边

void MST_Kruskal(int G[][], edgelist[]) {
    sort(edgelist);//从小到大权值排序
    int vexset[n]={0...n-1};//并查集归类,用于判断两顶点是否连通,初始值为节点编号,也就是每个顶点都是独立的。
    k=0;//边从0到n-2,共n-1条,n个节点的书当然是n-1条边
    j=0;//j从0到m遍历,edgelist[j]
    while(k<n-1) {
        //取两顶点所属集合sn1, sn2
        sn1 = vexset[edgelist[j].u];
        sn2 = vexset[edgelist[j].v];
        if (sn1 != sn2) { //不属于同一集合,那么把边加进去
            print(edgelist[j].u, edgelist[j].v);
            ++k;//加了一条边,k要自增
            for(i=0; i<n; ++i) {
                //把所有属于sn2的节点归并到sn1中,这就是并查集喽
                if(vexset[i] == sn2)
                    vexset[i] = sn1;
            }
        }
        ++j;//处理下一条边
    }
} 

4.2有向图应用

有向图的应用相对广泛,而且大多是基于拓扑序列完成各种算法,拓扑序列就是从有向图中逐步输出入度为零的顶点,每输出一个顶点,对应弧要删除,同时该弧头对应节点的入度减一,知道输出所有顶点,该输出序列即为该图的拓扑序列。从上面叙述可知,带环的图是不存在拓扑序列的。

基于拓扑序列,可以拓展求关键路径:在拓扑排序过程中,结点依次入栈,同时记录下每个节点最早开始时间:

//这里假设G是邻接表的存储方式
Status TopSort(Graph G, Stack &T) {
    InDegree(G, indegree);//求得个顶点的入度indegree[0..n-1],并把入度为零的顶点推到栈S  
    InitStack(T); count = 0; ve[0..n-1] = {0};//ve为各结点最早开始时间
    while(!StackEmpty(S)) {
        Pop(S, j); Push(T, j); ++count;
        for(w=G.vertices[j].firstarc; w; w=w->next) {
            k = w->adjvex;
            if (--indegree[k] == 0) 
                Push(S, k);
            //设info为弧结点对应的权值
            if (ve[j] + w->info > ve[k])
                ve[k] = ve[j] + w->info;
        }
    }
    if (count < n)
        return FALSE;
    return TURE;
}

Status CriticalPath(Graph G) {
    if (!TopSort(G, T))
        return FALSE;   
    vl[0..n-1] = ve[0..n-1];//初始化最晚开始时间
    while(!StackEmpty(T)) {
        Pop(T, j);
        for (w=G.vertices[j].firstarc; w; w=w->next) {
            k = w->adjvex;
            if (vl[j] + w->info < vl[k])
                vl[j] = vl[k] - w->info;
        }
    }
    for(i=0; i<n; ++i) {//输出关键路径上结点
        if(ve[i] == vl[i])
            print(i);
    }
}

经常也会遇到这样的应用:图中某点到其它点的最短路径。有了这个解答后,可拓展到任意两点的最短路径求解。
先看看单源最短路径问题:从v0开始,找到直达顶点的路径D并保存,从这些路径中找最短(s1)的对应顶点v1(v0到v1最短路径就是s1),然后从v1找直达路径x[..],如果s1加上这些路径x[..]比P中短,替换之,排除s1后从D中再找最短…直到所有顶点找完。伪代码如下:

void ShortPath_Dij(int G[][], int v0, PathMatrix &P, PathTable &D) {
    //v0到v的最短路径P[v]及其带权长度D[v]
    //若P[v][w]为TRUE,则w是从v0到v最短路径上的顶点
    //final[v]为TRUE,表示已经求得v0到v的最短路径
    for (v=0; v<n; ++v) {
        final[v] = FALSE; D[v] = G[v0][v];
        for (w=0; w<n; ++w)
            P[v][w] = FALSE;//设空路径
        if (D[v]<MAX) {
            P[v][v0] = TRUE;
            P[v][v] = TRUE;
        }
    }
    D[v0] = 0; final[v0] = TURE;

    for (i=1; i<n; ++i) {
        min = MAX;
        for (w=0; w<n; ++w)//找最短的边
            if(!final[w])
                if (D[w]<min) {
                    v = w;
                    min = D[w];
                }
        final[v] = TRUE;
        for (w=0; w<n; ++w) //更新最短路径以及距离
            if(!final[w] && min+G[v][w] < D[w]) {
                D[w] = min + G[v][w];
                P[w] = P[v];
                P[w][w] = TRUE;
            }
    }
} 
    原文作者:数据结构之图
    原文地址: https://blog.csdn.net/lh648365878/article/details/47020915
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞