【数据结构】图的创建与遍历

本文详细介绍了图数据结构的创建,包括邻接矩阵、邻接表、十字链表和邻接多重表的存储结构及代码实现。此外,还探讨了图的遍历方法,包括深度优先遍历和广度优先遍历的算法思想和实现过程,对比了两种遍历策略的适用场景。


图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

线性表:线性关系,由直接前驱和直接后继组成。
:层次关系,由父结点和孩子结点组成,每个结点最多有一个父结点(根结点无父结点)。
:结点的关系是任意的,任意两个结点都有可能有联系。


图的创建

图中存储的数据称为顶点,无向图连接顶点之间关系的称为,有向图连接顶点的称为,弧的起点为弧尾,终点为弧头
图可以根据边有无方向,分为无向图有向图,只要存在有方向的边,则为有向图,全部为无方向边的图,则为无向图。

无向图和有向图

如果图的边或弧带有权值,则称图为网。


一、邻接矩阵

图可以用G = {V, {E}}表示,V为顶点的集合,E为边或弧的集合。

上图中,无向图G1={V1,{E1}}G1 = \{V1, \{E1\}\}G1={V1,{E1}}
其中
V1={S,A,B,C,D}V1 = \{S, A, B, C, D\}V1={S,A,B,C,D}
E1={(S,A),(S,B),(S,C),(S,D),(A,B),(A,D),(B,C),(C,D)}E1 = \{(S,A), (S,B), (S,C), (S,D), (A,B), (A,D), (B,C), (C,D)\}E1={(S,A),(S,B),(S,C),(S,D),(A,B),(A,D),(B,C),(C,D)}

有向图G2={V2,{E2}}G2 = \{V2, \{E2\}\}G2={V2,{E2}}
其中
V2={S,A,B,C,D}V2 = \{S, A, B, C, D\}V2={S,A,B,C,D}
E2={<A,S>,<S,B>,<S,C>,<D,S>,<A,B>,<B,A>,<A,D>,<D,A>,<B,C>,<C,B>,<C,D>,<D,C>}E2 = \{<A,S>, <S,B>, <S,C>, <D,S>, <A,B>, <B,A>, <A,D>, <D,A>, <B,C>, <C,B>, <C,D>, <D,C> \}E2={<A,S>,<S,B>,<S,C>,<D,S>,<A,B>,<B,A>,<A,D>,<D,A>,<B,C>,<C,B>,<C,D>,<D,C>}


1、存储结构

我们将边用一个二维数组表示,如果两个顶点有边,则数组对应的位置为1,顶点与其自身的位置为0,没有边的位置为无穷大∞\infty

边因为没有方向,因此其数组显然是一个对称矩阵,读入数据时,只需要读取一半的数据,而弧是有方向的,因此需要读取所有弧的信息。

邻接矩阵


2、代码编写

使用C语言构造邻接矩阵存储的结构:

typedef char VertexType; //顶点数据类型
typedef int EdgeType; //边的权重数据类型
#define MAXVEX (100)  //最大顶点数
#define INFINITY (65535) //2^16 - 1

typedef struct MGraph
{
    VertexType vexs[MAXVEX];  //顶点表
    EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵
    int numVertexes, numEdges;  //顶点数和边数
}MGraph;

创建图的邻接矩阵结构:

/**********************************************************\
*function: 创建图的邻接矩阵结构
*input: GraphAdjList *G
*output: none
*return: void
\**********************************************************/
void CreateMgraph(MGraph *G)
{
    int i, j, k, w;
    printf("输入顶点数和边数:\n");
    scanf(" %d,%d", &G->numVertexes, &G->numEdges);

    for (i = 0; i < G->numVertexes; i++)
    {
        printf("输入顶点%d信息:\n", i);
        scanf(" %c", &(G->vexs[i]));
    }

    for (i = 0; i < G->numVertexes; i++)
    {
        for (j = 0; j < G->numVertexes; j++)
        {
             if (i != j)
            {
                G->arc[i][j] = INFINITY; 
            }
            else
            {
                G->arc[i][j] = 0; //矩阵正对角线上为0
            }
        }
    }

    for (k = 0; k < G->numEdges; k++)
    {
        printf("选择边或弧(0 ? 1): ");
        scanf(" %d", &ch);
        if (0 == ch)
        {
            printf("输入边(vi,vj)上的下标i,下标j和权w:\n");
            scanf(" %d, %d, %d", &i, &j, &w);
            G->arc[i][j] = w;
            G->arc[j][i] = G->arc[i][j];
        }
        else
        {
            printf("输入弧(vi,vj)上的下标i,下标j和权w:\n");
            scanf(" %d, %d, %d", &i, &j, &w);
            G->arc[i][j] = w;
        }
    }
}


二、邻接表

因为邻接矩阵的大小是取决于顶点的数量,因此如果不是完全图,会造成很大的空间浪费。
我们知道链表的数据结构会很合理的根据实际数据量来增加或减少空间占用量。

我们将边/弧数组与链表结合的存储方式称为邻接表

1、无向图

我们创建两类结点,顶点表结点和边表结点:

顶点表结点存储顶点数据data和指向第一个边表结点的指针firstedge,边表结点存储邻接顶点的下标adjvex和指向下一个边表结点的指针next。如果边有权值,则可以在边表结点添加权值域。

无向图邻接表


结点定义代码编写

typedef struct EdgeNode //边表结点
{
    int adjvex; //邻接点域
    EdgeType weight; //权值
    struct EdgeNode *next; //链域,指向下一个邻接点
}EdgeNode;

typedef struct VertexNode //顶点表结点
{
    VertexType data; //顶点域
    EdgeNode *firstedge; //边表头指针
}VertexNode;

typedef struct
{
    int numVertexes,numEdges; //顶点和边数
    VertexNode  adjList[MAXVEX]; //顶点数组

}GraphAdjList;

邻接表结构图创建

思路:
(1)首先图的顶点表初始化:输入顶点信息(如S、A),将firstedge指针置为空指针。

(2)构建顶点之间的边关系:输入边两端的顶点(Vi, Vj)下标序号i,j,因为边是无方向的,因此需要进行2步操作:
(a)将Vj看做Vi的邻接点,创建一个表示Vj的边表结点,插入到Vi的边表链中;
(b)将Vi看做Vj的邻接点,创建一个表示Vi的边表结点,插入到Vj的边表链中。

/**********************************************************\
*function: 创建图的邻接表结构
*input: GraphAdjList *G
*output: none
*return: void
\**********************************************************/
void CreateALGraph(GraphAdjList *G)
{
    int i, j, k;
    EdgeNode *e;
    printf("输入顶点数和边数:\n");
    scanf(" %d, %d", &G->numVertexes, &G->numEdges);
    for (i = 0; i < G->numVertexes; i++)
    {
        printf("输入顶点信息adjList[%d].data: ", i);
        scanf(" %c", &G->adjList[i].data);
        G->adjList[i].firstedge = NULL;
    }

    for (k = 0; k < G->numEdges; k++)
    {
        printf("输入边(vi,vj)上的顶点序列:\n");
        scanf(" %d,%d",&i, &j);

        /*  */
        e = (EdgeNode *)malloc(sizeof(EdgeNode)); //为顶点Vi的边表结点申请内存
        e->adjvex = j;
        e->next = G->adjList[i].firstedge; //e->next指向当前顶点指向的结点
        G->adjList[i].firstedge = e;  //当前顶点的指针指向e

        e = (EdgeNode *)malloc(sizeof(EdgeNode)); //为顶点Vj的边表结点申请内存
        e->adjvex = i;
        e->next = G->adjList[j].firstedge;
        G->adjList[j].firstedge = e;
    }

}

2、有向图

有向图的邻接表结构是类似的,但是因为其是有方向的,因此,我们对于每个顶点表结点设置2个指针,一个是以顶点为弧尾时,指向弧头的指针outfirstedge;一个是以顶点为弧头时,指向弧尾的指针infirstedge。可以方便的确定顶点的入度和出度。

向图邻接表

思路:
程序的结构也是和无向图的结构是类似的,只不过在读取弧信息时,需要区别新边表结点插入在出边表链还是入边表链中。

/**********************************************************\
*function: 创建有向图的邻接表结构
*input: DirGraphAdjList *G
*output: none
*return: void
\**********************************************************/
void CreateDirALGraph(DirGraphAdjList *G)
{
    int i, j, k;
    EdgeNode *e;
    printf("输入顶点数和弧数:\n");
    scanf(" %d, %d", &G->numVertexes, &G->numEdges);
    for (i = 0; i < G->numVertexes; i++)
    {
        printf("输入顶点信息adjList[%d].data: ", i);
        scanf(" %c", &G->adjList[i].data);
        G->adjList[i].infirstedge = NULL;
        G->adjList[i].outfirstedge = NULL;
    }

    for (k = 0; k < G->numEdges; k++)
    {
        printf("输入弧<vi,vj>上的顶点序列:\n");
        scanf(" %d,%d",&i, &j);
        //出方向 Vi -> Vj

        /* 插入Vi的出边表链 */
        e = (EdgeNode *)malloc(sizeof(EdgeNode)); //边表结点申请内存
        e->adjvex = j;
        e->next = G->adjList[i].outfirstedge; //e->next指向当前顶点指向的结点
        G->adjList[i].outfirstedge = e;  //当前顶点的指针指向e

        /* 插入Vj的入边表链 */
        e = (EdgeNode *)malloc(sizeof(EdgeNode)); //边表结点申请内存
        e->adjvex = i;
        e->next = G->adjList[j].infirstedge;
        G->adjList[i].infirstedge = e;


    }

}


三、十字链表

1、存储结构

观察上图的有向图的邻接表结构图,发现虽然实现了对出度和入度问题的解决,但是其中存在较多的边表结点的重复,顶点Vi的某一出边一定也是某个顶点Vj的一个入边,造成一定存储空间的浪费。

因此这里讲有向图的一种改进存储方法:十字链表

我们需要对边表结点修改一下结构:

tailvexheadvexheadlinktaillink
弧尾顶点弧头顶点指向下一个弧头顶点指向下一个弧尾顶点

什么意思呢?指针headlink链接的链表,其实就是顶点的入边表链,同有向图邻接结构图中蓝绿色部分。增加的弧尾顶点tailvex入边表结点的顶点下标。

将之前的结构图修改一下,如下图所示:

十字链表

比如观察S的入边表结点,infirstedge指针指向A的第一个出边结点(红色虚线),其弧尾顶点为1,即表示A->S。

该边结点的headlink指向D的第一个出边结点,其弧尾顶点为4,即表示D->S,此时边结点的headlink为空指针,S的入边表链结束。


2、代码编写

首先是建立结点的结构,相比之前的只修改了边表结点。

typedef struct CroEdgeNode //边表结点
{
    int tailvex; //弧尾顶点
    int headvex; //弧头顶点
    EdgeType weight; //权值
    struct CroEdgeNode *headlink; //指向下一个弧头顶点
    struct CroEdgeNode *taillink; //指向下一个弧尾顶点
}CroEdgeNode;


typedef struct CroVertexNode //有向图顶点表结点
{
    VertexType data; //顶点域
    CroEdgeNode *outfirstedge; //以顶点为弧尾时,边表头指针
    CroEdgeNode *infirstedge; //以顶点为弧头时,边表头指针
}CroVertexNode;


typedef struct
{
    int numVertexes,numEdges; //顶点和边数
    CroVertexNode  adjList[MAXVEX];

}CroGraphAdjList;

十字链表创建思路

(1)初始化步骤和前面一样;

(2)在读取一个弧时,创建一个边表结点,将弧头和弧尾顶点数据填入;

(3)将该结点插入Vi的出边表链中,再将其插入Vj的入边表链中。

/**********************************************************\
*function: 创建有向图的十字链表结构
*input: CroGraphAdjList *G
*output: none
*return: void
\**********************************************************/
void CreateCroALGraph(CroGraphAdjList *G)
{
    int i, j, k;
    CroEdgeNode *e;
    printf("输入顶点数和弧数:\n");
    scanf(" %d, %d", &G->numVertexes, &G->numEdges);
    for (i = 0; i < G->numVertexes; i++)
    {
        printf("输入顶点信息adjList[%d].data: ", i);
        scanf(" %c", &G->adjList[i].data);
        G->adjList[i].infirstedge = NULL;
        G->adjList[i].outfirstedge = NULL;
    }

    for (k = 0; k < G->numEdges; k++)
    {
        printf("输入弧<vi,vj>上的顶点序列:\n");
        scanf(" %d,%d",&i, &j);

        /* 插入Vi的出边表链 */
        e = (CroEdgeNode *)malloc(sizeof(CroEdgeNode)); //边表结点申请内存
        e->tailvex = i;
        e->headvex = j;

        e->taillink = G->adjList[i].outfirstedge;
        G->adjList[i].outfirstedge = e;

        e->headlink = G->adjList[j].infirstedge;
        G->adjList[j].infirstedge = e;
    }

}


四、邻接多重表

1、存储结构

邻接多重表是对无向图邻接表的优化,改造方法和有向图的十字链表相似。

重新定义边表结构:

ivexilinkjvexjlink
边的某一端顶点下标指向ivex顶点的下一个边结点边的另一边顶点下标指向jvex顶点的下一个边结点

首先将所有顶点和所有边画出来,即下图左右两列,再将link指针连接到与其vex相同的边表结点vex,最后一个link为空指针。

邻接多重表

如S的边表结点,如红色虚线连接所示,可以看出与A、B、C、D都有边连接。

这种存储结构对边的删减操作很方便,比如删除S与A的边,则先将S的firstedge指针指向{0, ilink, 2, jlink}结点,然后删除边表结点{0, ilink, 1, jlink}

只需要对一个表结点进行删除操作,而邻接表结构是需要删除两个表结点。


2、代码编写

首先定义结点的数据结构:

typedef struct MuEdgeNode //边表结点
{
    int ivex, jvex; //邻接点域
    EdgeType weight; //权值
    struct MuEdgeNode *ilink, *jlink; //链域,指向下一个邻接点
}MuEdgeNode;


typedef struct MuVertexNode //顶点表结点
{
    VertexType data; //顶点域
    MuEdgeNode *firstedge; //边表头指针
}MuVertexNode;


typedef struct
{
    int numVertexes,numEdges; //顶点和边数
    MuVertexNode  adjList[MAXVEX];

}MuGraphAdjList;

邻接多重表创建函数:

/**********************************************************\
*function: 创建无向图的邻接多重表结构
*input: MuGraphAdjList *G
*output: none
*return: void
\**********************************************************/
void CreateMuALGraph(MuGraphAdjList *G)
{
    int i, j, k;
    MuEdgeNode *e;
    printf("输入顶点数和边数:\n");
    scanf(" %d, %d", &G->numVertexes, &G->numEdges);
    for (i = 0; i < G->numVertexes; i++)
    {
        printf("输入顶点信息adjList[%d].data: ", i);
        scanf(" %c", &G->adjList[i].data);
        G->adjList[i].firstedge = NULL;
    }

    for (k = 0; k < G->numEdges; k++)
    {
        printf("输入边(vi,vj)上的顶点序列:\n");
        scanf(" %d,%d",&i, &j);

        e = (MuEdgeNode *)malloc(sizeof(MuEdgeNode)); //边表结点申请内存

        e->ivex = i;
        e->jvex = j;

        e->ilink = G->adjList[i].firstedge; //e->next指向当前顶点指向的结点
        G->adjList[i].firstedge = e;  //当前顶点的指针指向e

        e->jlink = G->adjList[j].firstedge;
        G->adjList[j].firstedge = e;
    }

}


五、边集数组

边集数组是由两个一位数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。

边数组结构如下:

beginendweight
edges[0]101
edges[1]122
edges[2]213
edges[3]234
edges[4]325

有向图

创建比较简单,代码就不做讨论了。

边集数组主要用于对边依次进行处理从操作,而不适合对顶点相关的操作。



图的遍历

一、深度优先遍历

从图的某个顶点V出发,访问此顶点数据,然后从顶点V的未被访问的邻接点出发,遍历图的结点,直到图中所有和V有路径相通的顶点都被访问到。如果图中仍有顶点未被访问,则选择其中一个顶点作为起始点,重复上述步骤,直至图的全部顶点都被访问过。

深度遍历结构图

思路:

(1)遍历方法和树的前序遍历方法一样,使用递归的方法访问各个结点。

(2)如上图所示,假如A-K依次存储在顶点数组的0-10位中,选择A为起始点,先打印A点数据,然后寻找A点的未被访问的邻接点,优先选择下标小的顶点,因此遍历B结点,然后C
当遍历到H时,H的所有邻接点全部都被访问过,此时递归函数开始出栈,结点回溯,一直回到D点,发现有一个邻结点I未被访问,访问后,因I没有未被访问的邻接点,继续回溯,直到返回A结点,此时DFS函数的递归结束。

(3)DFSTraverse函数中遍历所有顶点的访问数组visit,如果存在未访问的顶点,则继续调用DFS函数从该顶点开始遍历。如上图,将会从J顶点开始遍历,结束后,所有顶点均被访问,深度优先遍历结束。

typedef enum Bool
{
    FALSE = 0,
    TRUE = 1
}bool;

bool visited[MAXVEX]; //对应每个顶点的访问数组,FALSE表示未被访问


/* 邻接矩阵的深度优先递归算法 */
static void DFS(MGraph G, int i)
{
    int j;
    visited[i] = TRUE; //此顶点标志为已访问
    printf("%c ", G.vexs[i]);

    for (j = 0; j < G.numVertexes; j++)
    {
        if (G.arc[i][j] != 0 && G.arc[i][j] != INFINITY && !visited[j])
        {
            DFS(G, j);
        }
    }
}

/* 邻接矩阵的深度遍历操作  */
void DFSTraverse(MGraph G)
{
    int i;
    for (i = 0; i < G.numVertexes; i++)
    {
        visited[i] = FALSE;  //初始所有顶点均为未访问状态
    }

    for (i = 0; i < G.numVertexes; i++)
    {
        if (!visited[i])
        {
            DFS(G, i);
        }
    }
}

图的临接矩阵结构遍历,也是类似的,只需要更改一下DFS中访问的参数即可。

/* 邻接表的深度优先递归算法 */
static void DFSAL(GraphAdjList GL, int i)
{
    EdgeNode *p;
    visited[i] = TRUE; //此顶点标志为已访问
    printf("%c ", GL.adjList[i].data); //打印顶点

    p = GL.adjList[i].firstedge;
    while(p)
    {
        if (!visited[p->adjvex])
        {
            DFSAL(GL, p->adjvex);
        }
        p = p->next;
    }
}


二、广度优先遍历

广度优先遍历类似树的层序遍历,以图的某一顶点V为起始点,先遍历邻接顶点,再遍历邻接顶点的邻接顶点,以此类推,直至遍历此连通图的全部顶点。如果仍有为访问的顶点,则再选择其中一未访问的顶点作为起始点,重复上述步骤,直至图的所有顶点均被访问。

广度优先遍历结构

那么如何以层序的方式来遍历图呢?

可以利用队列的先进先出,并能够存储一段数据的特性,来依次遍历某一顶点的下一层顶点。

上图广度优先遍历的队列进出情况入下所示,每次从队列取出一个顶点,查找其下一层未被访问的顶点,将其存入队列。

出队列的数据队列存储数据入队列的数据
A
AB F
B F
BFC I G
FC I GE
CI G ED
IG E D
GE DH
ED H
DH
H

代码如下:

/* 邻接矩阵的广度遍历算法 */
void BFSTraverse(MGraph G)
{
    int i, j;
    sqQueue Q;
    for (i = 0; i < G.numVertexes; i++)
    {
        visited[i] = FALSE;
    }

    InitQueue(&Q); //队列初始化

    for (i = 0; i < G.numVertexes; i++) //对每个顶点做循环
    {
        if (!visited[i])
        {
            visited[i] = TRUE;
            printf("%c ", G.vexs[i]); //打印起始点,如果是连通图,则只执行一次

            PutQueue(&Q, i); //将起始点的下标存入队列

            while(!GetQueueLength(Q)) //检查队列是否为空
            {
                GetQueue(&Q, &i); //取出队列中的一个数据,赋给i
                for (j = 0; j < G.numVertexes; j++) //以i顶点为中心,查找没有访问过的邻接点
                {
                    if (G.arc[i][j] != 0 && G.arc[i][j] != INFINITY && !visited[j])
                    {
                        visited[j] = TRUE;
                        printf("%c ", G.vexs[j]);//打印顶点信息
                        PutQueue(&Q, j); //将访问的顶点下标存入队列
                    }
                }
            }
        }
    }
}

邻接表的代码和临接矩阵的代码基本是相同的,只是存储结构不同,使得遍历结点的部分代码有所变化。

/* 邻接表的广度遍历算法 */
void BFSALTraverse(GraphAdjList GL)
{
    int i;
    EdgeNode *p;
    sqQueue Q;

    for (i = 0; i <  GL.numVertexes; i++)
    {
        visited[i] = FALSE;
    }

    InitQueue(&Q);

    for (i = 0; i < GL.numVertexes; i++)
    {
        if (!visited[i])
        {
            visited[i] = TRUE;
            printf("%c ", GL.adjList[i].data);
            PutQueue(&Q, i);

            while (!GetQueueLength(Q))
            {
                GetQueue(&Q, &i);
                p = GL.adjList[i].firstedge;
                while(p) //遍历GL->adjList[i].data的所有邻接顶点
                {
                    if (!visited[p->adjvex]) // 此顶点未被访问
                    {
                        visited[p->adjvex] = TRUE;
                        printf("%c ", GL.adjList[p->adjvex].data);
                        PutQueue(&Q, p->adjvex);

                    }
                    p = p->next; //访问下一个邻接点
                }
            }
        }
    }
}

对比图的深度优先遍历和广度优先遍历算法,可以发现它们的事假复杂度是相同的,仅仅是对顶点的访问顺序不同,对于n个顶点e条边的图来说,时间复杂度均为O(n+e)

深度优先遍历是无差别的遍历图,更适合寻找目标顶点明确的情况;广度优先遍历时以一个顶点为起始点,向外层不断扩大遍历范围,更适合寻找相对最优目标顶点的情况。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值