数据结构—图

目录

一、基本概念​​​​​​

1、图的定义

2、有向图和无向图

3、无权图和带权图

4、完全图

5、邻接点

6、顶点的度

7、路径

8、图的连通性

9、连通分量

10、生成树

11、最小生成树

二、图的存储结构

1、邻接矩阵

2、邻接表

3、十字链表

三、图的遍历

1、深度优先搜索遍历(DFS)

2、广度优先搜索遍历(BFS)

四、图的算法

1、生成树

2、最小生成树

3、最短路径

4、拓扑排序


一、基本概念​​​​​​

1、图的定义

     图是由顶点集V和边集E组成,记作G=(V, E)     G:Graphic 图形     V:Vertex 顶点     E:Edge 边

                          

2、有向图无向图

     如果边是有方向的则称为有向图,如果边没有方向则称为无向图

                                                       

3、无权图带权图

      对图中的边赋予具有一定意义的数值(路程、费用等等)的图称为带权图

                                        

4、完全图

        任意两个顶点之间都存在一条边

                无向完全图的边数为1/2*n*(n-1)

                有向完全图的边数为n*(n-1)

     稀疏图:有很少的边

     稠密图:有很多的边,最稠密的图就是完全图

5、邻接点

     如果顶点vi、vj之间存在一条边,则称它们互为邻接点

6、顶点的度

      对于无向图,顶点v的度定义为和v相关联的边数

      对于有向图,顶点v的度分为入度出度

7、路径

     若顶点vp和vq可以由若干条边连通,则称vp到vq存在一条路径

                无权图的路径长就是路径上经过边数

                带权图的路径长要乘以每条边的权

     简单路径:除了起点和终点可以为同一个顶点外,其余顶点均不相同

                起点和终点为同一个顶点的简单路径称为回路

                   

                   

8、图的连通性

     如果从顶点vi到顶点vj有路径,则称vi和vj是连通的。

     如果图中任意两个顶点都是连通的,则称该图是连通图

     对于有向图,如果图中的任意两个顶点vi和vj是双向连通的,则该图是强连通图

9、连通分量

     无向图的连通子图称为连通分量,有向图的强连通子图称为强连通分量

     连通分量的概念是图的难点:首先连通分量是一个子图,其次这个子图是(强)连通图

     【推论】

     a、无向连通图的连通分量只有一个,就是它自身,称为极大连通子图

     b、无向非连通图的连通分量有多个

     c、有向强连通图的强连通分量只有一个,就是它自身,称为极大强连通子图

     d、有向非强连通图的强连通分量有多个

        连通分量通过遍历图来求出,从图中某个顶点v出发遍历图,如果能够遍历到所有顶点,则该图是连通的;如果遍历不到所有顶点,则图是非连通的,要在未被遍历的顶点中选择新的出发点,直到所有顶点都被遍历到,那么每次遍历得到的就是图的一个连通分量。

10、生成树

      在图论中,树被定义为没有回路的连通图,生成树的研究对象是连通图

      连通图G中包含所有顶点的极小连通子图(边最少)称为G的一棵生成树         

  【推论】

       a、一个有n个顶点生成树有且仅有n-1条边

       b、一个连通图的生成树不唯一

11、最小生成树

       采用不同的遍历方法可以得到不同的生成树,从不同的顶点出发进行遍历也可以得到不同的生成树,所以图的生成树是不唯一的。对于连通的带权图G,其生成树也是带权的。我们把生成树各边的权值总和称为该树的权,把权值最小的生成树称为图的最小生成树。

二、图的存储结构

1、邻接矩阵

     邻接矩阵是表示图中顶点之间相邻关系的矩阵。设G=(V,E)是具有n个顶点的图,则G的邻接矩阵是具有如下定义的n阶方阵:      

A[i][j]=\left\{\begin{matrix} 1& (vi,vj)\subseteq E(G)\\ 0& (vi,vj)\nsubseteq E(G) \end{matrix}\right.

// 邻接矩阵表示法的数据结构定义
#define    MaxVertexNum    50                        // 最大顶点数
typedef struct _MGraph{
    VertexType    vexs[MaxVertexNum];                // 顶点数组
    int           arcs[MaxVertexNum][MaxVertexNum];  // 邻接矩阵
} MGraph;

无向图的邻接矩阵:对称矩阵             

 有向图的邻接矩阵:各行之和是出度,各列之和是入度                   

 带权图的邻接矩阵

2、邻接表

     邻接表是一种链式存储结构,类似于链表数组

// 邻接表表示法的数据结构定义
#define    MaxVertexNum    50

// 邻接表结点
typedef node {
    int          vex_id;    // 顶点序号
    struct node *next;      // 指向下一个邻接点
} EdgeNode;

// 顶点表
typedef vnode {
    VertexType    vexs;    // 顶点信息
    EdgeNode     *link;    // 邻接表头
} VNode;

// 图
VNode    ALGraph[MaxVertexNum];

无向图的邻接表          

有向图可以用两个邻接表表示,出边表叫邻接表,入边表叫逆邻接表               

3、十字链表

邻接表对于有向图的表示分为邻接表和逆邻接表,我们无法从一个表中获取某顶点的出度和入度情况,所以有人提出了十字链表的存储方式。

顶点表:           

firstin:入边表头指针,指向顶点入边表的第一个节点

firstout:出边表头指针,指向顶点出边表的第一个节点

边表:       

tailvex是边起点在顶点表的下标,headvex边终点在顶点表的下标

headlink入边表指针,指向终点相同的下一条入边;taillink出边表指针,指向起点相同的下一条出边           

三、图的遍历

图的遍历是从某个顶点出发,沿着某条路径对图中每个顶点做且仅做一次访问。遍历图的算法是求解图的连通性、图的拓扑排序等算法的基础。

为了避免顶点的重复访问,可设一个布尔数组visisted[0...n-1],用来标记某个顶点是否被访问过,初始值均为假,若该顶点已被访问过,则以顶点序号为下标的数组元素置为真。

1、深度优先搜索遍历(DFS)

从图G中任选一顶点v作为初始出发点,首先访问出发点v,并将其标记为已访问过;然后依次搜索v的每个邻接点w,若w未曾访问过,则以w作为新的出发点,继续进行深度优先遍历,直到图中所有和v有路径相通的顶点都被访问到;若此时仍有顶点未被访问到(非连通图),则另选一个未访问过的顶点作为起点,重复上述过程,直到图中所有顶点都被访问到为止。

(1)以邻接矩阵为存储结构的DFS递归算法

int visited[MaxVertexNum] = {0};

//@param    G 要遍历的图
//          i 顶点序号
//          n 顶点总数
void DFS(MGraph G, int i, int n)
{
    printf("v%d ", i); // 首先访问顶点vi
    visited[i] = 1;

    for (int j = 0; j < n; j++) { // 依次遍历vi的每个邻接点
        // 若(vi,vj)属于E(G)且vj未访问过
        if (G.arcs[i][j] == 1 && !visited[j])
            DFS(G, j, n);
    }
}

(2) 以邻接表为存储结构的DFS递归算法

int visited[MaxVertexNum] = {0};

//@param    G 要遍历的图
//          i 顶点序号
void DFS(VNode G[], int i)
{
    printf("v%d ", i); // 首先访问vi
    visited[i] = 1;

    EdgeNode *p;
    int j;

    p = G[i].link;
    while (p != NULL) { // 依次遍历vi的每个邻接点
        j = p->vex_id;
        if (!visited[j]) // 若邻接点vj未被访问过
            DFS(G, j);

        p = p->next;
    }
}

 (3)以邻接矩阵为存储结构的DFS非递归算法

int visited[MaxVertexNum] = {0}

//@param    G 要遍历的图
//          i 出发点vi
//          n 顶点总数
void DFS(MGraph G, int start, int n)
{
    printf("v%d ", start);
    visited[start] = 1;

    // 当前正在访问的顶点
    int cur = start;

    // 用顺序栈保存访问过的顶点,以便回溯访问过的顶点的未被访问的邻接点
    SeqStack S;
    S.top = -1;

    // 当前顶点是否有未被访问过的邻接点
    bool done = false;

    for (int k = 0; k < n; k++) {
        if (G.arcs[cur][k] == 1 && !visited[k]) {
            done = true;
            break;
        }
    }

    while (!StackEmpty(&S) || done) {
        int i = 0;
        while (i < n) {
            if (G.arcs[cur][i] == 1 && !visited[i]) {
                // 把当前顶点的未被访问过的邻接点vi压入栈中
                S.top += 1;
                S.data[S.top] = i;

                // 访问该邻接点
                printf("v%d ", i);
                visited[i] = 1;

                // 以该邻接点为出发点进行遍历
                cur = i;

                i = 0; // 从第一个顶点开始重新搜索
            }
            else
                i++;
        }

        // 如果栈非空
        if (!StackEmpty(&S) {
            cur = S.data[S.top]; // 以栈顶顶点为出发点继续遍历
            S.top -= 1;
        }
    }
}

2、广度优先搜索遍历(BFS)

广度优先搜索是一种按层次遍历的方法,基本思想是:从图G中任选一顶点Vi作为初始出发点,首先访问出发点Vi,接着依次访问Vi的所有未被访问过的邻接点Vi1,Vi2,...,Vit并均标记为已访问过,然后再按照Vi1,Vi2,...,Vit的次序,访问每一个顶点的所有未被访问过的邻接点并均标记为已访问过,依次类推,直到图中所有和Vi有路径相通的顶点都被访问到;若此时仍有顶点未被访问到(非连通图),则另选一个未访问过的顶点作为起点,重复上述过程,直到图中所有顶点都被访问到为止。

(1)以邻接矩阵为存储结构的广度优先搜索遍历算法

int visited[MaxVertexNum] = {0};

//@param    G 要遍历的图
//          i 顶点序号
//          n 顶点总数
void BFS(MGraph G, int i, int n)
{
    CirQueue Q;           // 定义一个队列保存已访问过的顶点

    InitQueue(&Q);        // 初始化队列
    printf("v%d ", i);    // 首先访问顶点vi
    visited[i] = 1;       // 标记vi已访问过

    EnQueue(&Q, i);       // 将已访问过的顶点入队

    int j, k;

    while (!QueueEmpty(&Q)) {
        k = DeQueue(&Q);  // 删除队首元素

        // 搜索vk的每个邻接点
        for (int j = 0; j < n; j++) {
            if (G.arcs[k][j] == 1 && !visited[j]) {
                printf("v%d ", j);
                visited[j] = 1;
                EnQueue(&Q, j);
            }
        }
    }
}

(2)以邻接表为存储结构的广度优先搜索遍历算法 

int visited[MaxVertexNum] = {0};

//@param    G 要遍历的图
//          i 顶点序号
void BFS(VNode G[], int i)
{
    CirQueue Q;           // 定义一个队列保存已访问过的顶点

    InitQueue(&Q);        // 初始化队列
    printf("v%d ", i);    // 首先访问顶点vi
    visited[i] = 1;       // 标记vi已访问过

    EnQueue(&Q, i);       // 将已访问过的顶点入队

    int j, k;
    EdgeNode *p;

    while (!QueueEmpty(&Q)) {
        k = DeQueue(&Q);
        p = G[k].link;

        while (p != NULL) {
            j = p->vex_id;
            if (!visited[j]) {
                printf("v%d ", j);
                visited[j] = 1;
                EnQueue(&Q, j);
            }

            p = p->next;
        }
    }
}

四、图的算法

1、生成树

关于生成树的概念上文中已有叙述,那么对于给定的连通图,如何求得其生成树呢?

设图G=(V,E)是一个具有n个顶点的连通图,从G的任一顶点出发,做一次深度优先搜索或广度优先搜索,就可以将图中的所有n个顶点都访问到。显然,在这两种遍历搜索方法中,从一个已访问过的顶点vi搜索到另一个未访问过的顶点vj必定经过G中的一条边(vi,vj),而这两种搜索方法对图中的n个顶点都仅访问一次,因此除初始出发点外,对其余n-1个顶点的访问一共要经过G中的n-1条边,这n-1条边将G中n个顶点连接成包含G中所有顶点的极小连通子图,可见它是G的一棵生成树。

通常,我们把由深度优先搜索所得的生成树称之为深度优先生成树,简称DFS生成树;而由广度优先搜索所得的生成树称之为广度优先生成树,简称BFS生成树。

无向图的DFS和BFS生成树

2、最小生成树

最小生成树以无向图为研究对象

     Prim算法

       假设G=(V,E)是一个具有n个顶点的连通网,T=(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,U和TE的初值均为空。

       算法开始时,首先从V中任取一个顶点(假设取v1)并入U中,此时U={v1}。然后只要U是V的真子集,就从那些一个端点已在T中,另一个端点仍在T外的所有边中,找一条权值最小的边,假定为(vi,vj),其中vi∈U,vj∈V-U,并把该边(vi,vj)和顶点vj并入T的边集TE和顶点集U中,如此进行下去,每次往生成树中并入一个顶点和一条边,直到把所有n个顶点都并入生成树T的顶点集中,此时U=V,TE中包含n-1条边,T就是最后得到的最小生成树。

Prim算法最小生成树构造过程

       在算法中设一个辅助数组minedge[MaxVertexNum],记录从U到V-U权值最小的边,数组元素包含两个域,ver存储依附在U中的顶点,lowconst存储该边上的权值,对每个顶点v∈V-U在辅助数组中都存在一个分量minedge[v]。

// 权值类型
typedef int VRType;
// 辅助数组
typedef struct {
    VertexType    vex;
    VRType        lowcost;
} minedge[MaxVertexNum];

//@param    G 待求解的图
//          u 最小生成树的初始顶点(树根)
//          n 图的顶点总数
void Prim(MGraph G, VertexType u, int n)
{
    int k = VtxNum(u);        // 顶点u的下标

    for (int i = 0; i < n; i++) {
        minedge[i].vex = u;
        minedge[i].lowcost = G.arcs[k][i];
    }

    minedge[k].lowcost = 0;    // 代价为0表示并入U中

    for (int i = 1; i < n; i++) {
        k = min(minedge[i]);   // 1<=i<=n-1,找满足条件的最小边(u,k),u∈U,k∈V-U
        printf("(%d,%d) ", minedge[k].ver, G.vexs[k]);

        minedge[k].lowcost = 0; // 第k个顶点并入U中

        // 由于U中新并入了顶点,更新权值最小的边
        for (int j = 0; j < n; j++) {
            if (G.arcs[k][j] < minedge[i].lowcost) {
                minedge[j].ver = G.vexs[k]
                minedge[j].lowcost = G.arcs[k][j];
            }
        }
    }
}

     Kruskal算法

      假设G=(V,E)是一个具有n个顶点的连通网,T=(U,TE)是G的最小生成树,U的初值等于V,即包含G中的全部顶点。T的初始状态是只含有n个顶点的而无边的森林T=(V,Φ)。

      该算法的基本思想是:按权值从小到大依次选取图G中的边,若选取的边使生成树T不形成回路,则把它并入TE中,保留作为T的一条边;若选取的边使生成树T形成回路,则将其舍弃,如此进行下去直到TE中包含n-1条边位置,此时的T即为最小生成树。

Kruskal算法最小生成树构造过程

3、最短路径

     Dijkstra算法

最短路径问题研究有向帯权图,比如两地是否有路径相通,哪一条最近,哪一条会费最少等等。

 在上面的有向图中,假定以v1为源点,到其他各顶点的最短路径如下表所示:

v1到其他各顶点的最短路径表
源点最短路径终点路径长度
v1v1,v3,v2v25
v1v1,v3v33
v1v1,v3,v2,v4v410
v1v1,v3,v5v518

       设有向图G=(V,E),其中V={1,2,...,n},cost是表示G的邻接矩阵,cost[i][j]表示有向边<i,j>的权。若不存在有向边<i,j>,则cost[i][j]为无穷大。设S是一个集合,每个元素表示一个顶点,从源点到这些顶点的最短距离已经求出。设v1为源点,集合S的初态只包含v1。数组dist记录从源点到其他各顶点当前的最短距离,初始值为dist[i]=cost[v1][i],i=2,...,n。

       从V-S中选出一个顶点w,使dist[w]最小。从源点到达w只通过S中的顶点,把w加入集合S中并调整dist中记录的从源点到V-S中每个顶点的距离,即从原来的dist[v]和dist[w]+cost[w][v]中选择较小的作为新的dist[v]。直到S中包含V中其余顶点的最短路径。

//@param    G  待求解的图
//          v1 源点
//          n  图的顶点总数
void Dijkstra(MGraph G, int v1, int n)
{
    int inf = 32767    // 表示无穷大
    int mind;

    // 源点到其余各顶点的最短路径是否已求得
    bool S[MaxVertexNum];

    // 源点到其余各顶点最短路径初始值
    int  dist[MaxVertexNum];

    // 保存最短路径上顶点的前趋顶点
    int path[MaxVertexNum];

    for (int v = 1; v < n; v++) {
        S[v] = false;
        dist[v] = G.arcs[v1][v];
    }

    // S初始只有源点
    dist[v1] = 0;
    S[v1] = true;

    for (int i = 2; i < n; i++) {
        mind = inf;
        // 源点到其余各顶点的最短路径
        for (int w = 1; w < n; w++) {
            if (!S[w] && dist[w] < mind) {
                v = w;
                mind = dist[w];
            }
        }

        // 标记为已求得
        S[v] = true;

        // 更新最短路径
        for (int w = 1; w < n; w++) {
            if (!S[w] && (dist[v] + G.arcs[v][w] < dist[w])) {
                dist[w] = dist[v] + G.arcs[v][w];
                path[w] = v;
            }
        }
    }
}

4、拓扑排序

       通常,在实现一项较大的工程时,经常会将该工程划分为若干个子工程,我们把这些子工程称为活动。在整个工程中,有些子工程没有先决条件,可以安排在任何时间开始;而有些子工程必须在其他相关子工程完成之后才能开始。

       为了形象地反映出整个工程中各个子工程之间的先后关系,可用一个有向图来表示,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,我们把这种有向无环图称为顶点活动网,简称AOV网。注意,AOV网中不应该有有向环(即回路),因为这意味着某项活动以自己为先决条件,这样的设计是有问题的!

       我们看这样一个实际应用,计算机专业的同学需要修完一系列课程才能毕业,但是这些课程是有先后顺序的,比如“高等数学”是基础课,不需要先修其他课程;而“数据结构”则要先修完“离散数学”和“算法语言”才能学习,等等。这些课程的先后关系我们用下面的表格来表示:

计算机专业必修课
课程编号课程名称先修课程
C1计算机基础
C2高等数学
C3数据结构C4,C7
C4算法语言C1
C5操作系统C3,C6
C6计算机原理C9
C7离散数学C1,C2
C8编译技术C3,C4
C9大学物理C2

      我们构建一个AOV网如下图:

       对于一个有向无环图,若将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若<u,v>∈E(G),则u在线性序列中出现在v之前,这样的线性序列称为拓扑序列。也就是说,在AOV网中,若不存在回路(即环),所有活动可排成一个线性序列,使得每个活动的所有前趋活动都排在该活动的前面,此序列就是拓扑序列。由AOV网构造拓扑序列的过程称为拓扑排序

       对给定的AOV网,应首先判定是否存在环?检测的方法是对有向图构造其顶点的拓扑序列,如果网中所有顶点都在它的拓扑序列中,则该AOV网必定不存在环

       AOV网的拓扑序列不是唯一的,那么拓扑排序的基本思想描述如下:

  1. 在有向图中选一个没有前趋(入度为0)的顶点,且输出之
  2. 从有向图中删除该顶点及其与该顶点有关的所有边
  3. 重复执行上述两个步骤,直到全部顶点都已输出或图中的顶点没有前趋(入度为0)为止
  4. 输出剩余的无前趋顶点
课程表AOV网拓扑排序过程

     以C1作为第一个输出的拓扑序列为:C1,C4,C2,C7,C9,C3,C6,C5,C8

     以C2作为第一个输出的拓扑序列为:C2,C1,C9,C7,C4,C3,C6,C8,C5

       在拓扑排序算法中,需要设置一个数组indegree保存所有顶点的入度值,再设一个栈来保存所有入度为0的顶点,以后每次选入度为0的顶点时,只需要出栈即可。算法中删除顶点及其所有边只需要检查顶点vi的出边表,把每条出边<vi,vj>的终点vj所对应的入度indegree[j]减1,如果vj的入度为0,则将j入栈。

#define VNUMS    20

//@param    G 要遍历的图
//          n 顶点总数
void TopuSort(VNode G[], int n)
{
    int i, j, m = 0;
    EdgeNode *p;
    
    // 顶点入度数组
    int indegree[VNUMS] = {0};

    // 遍历入边表,统计每个顶点的入度值
    for (i = 0; i < n; i++) {
        p = G[i].link;
        while (p) {
            indegree[p->vex_id]++;
            p = p->next;
        }
    }

    SeqStack S;
    InitStack(&S);
    // 把入度为0的顶点入栈
    for (i = 0; i < n; i++)
        if (indegree[i] == 0)    Push(&S, i);

    while (!StackEmpty(&S)) {
        i = Pop(&S);      // 遍历栈顶入度为0的顶点
        printf("v%d ", i);
        m++;

        p = G[i].link;
        while (p) {
            j = p->vex_id;
            indegree[j]--; // 终点的入度减1,相当于删除边
            if (indegree[j] == 0)
                Push(&S, j);

            p = p->next;
        }
    }

    // 输出的顶点数小于图中的顶点数,说明图有回路,排序失败
    if (m < n)
        printf("The Graph has a cycle!");
}
  • 18
    点赞
  • 80
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值