【数据结构】十、图的存储方式以及BFS、DFS遍历算法

一、图的概念

图具有非常复杂的数据结构,每个存储数据的结构称为节点,连接每个节点的结构称为,这个边可以出现在任意的两个节点之间,并且这个边带有长度,称为权值,表示从一个节点到另一个节点需要付出的“代价”。

比如现在有一份地图,其中节点代表地点的名称,边代表连接两个顶点的路径,则权值表示两个地点之间的距离(km)。

(这个图将作为本次的实验测试用图)

1)线性表、树、和图

线性表的个数据之间只存在线性关系,每个元素只有一个前驱和一个后继;

中不仅存在着线性关系,还存在着层次关系,每个元素有一个前驱但是可以有多个后继;

而在中,数据之间的线性关系更为复杂,每个数据之间不仅可以有多个前驱,还可以有多个后继,任意两个元素之间都可能存在着关系。

2)图的分类

按有无权值分,图可以分为带权图和不带权图。

按有无方向分,图可以分为又向图和无向图。本节我们主要讨论的是无向带权图。

二、图的存储方式

1)邻接矩阵法

使用邻接矩阵存储,需要用到一个一维数组(用来存储节点)和一个二维数组(用来存储边)

如果是无向图,则边数组矩阵对称,如果是不带权图,边数组中使用1表示存在边。

结构体定义为:

typedef struct {
    LinList node; //存放节点
    int num; //存放边的数目
    int **edge;  //存放边
}AdjMGraph;    //邻接矩阵结构体

typedef struct {
    int list[MaxSize];  //使用数组来存储元素
    int size;           //记录存储的元素个数
}LinList;

这里的LinList是一个线性表,其基本操作和函数可跳转:

【数据结构】一、线性表-CSDN博客

Question

对于现有的一个图,我们要这么保存到这个结构体里面呢?

下面提供一个方法:将图的数据保存到文件中,再通过读取文件的方式逐顶点逐边存入图结构体。

文件中的数据:

其中第一行代表顶点的数据,0为结束位,其后每一行代表边的弧头弧尾和权值,代码实现如下:

#define MAXWEIGHT 1000      //表示无穷大
/*
* @brief 初始化邻阶矩阵
* @param 图结构体的地址
* @return None
*/
static void AdjM_Init(AdjMGraph* G)
{
    G->num = 0;
    LinearList_Init(&G->node);
}


/*
* @brief 从文件中读取,使用邻接矩阵存储
*/
void AdjM_Creat(AdjMGraph *G)
{
    AdjM_Init(G);                         //初始化节点线性表

    FILE* fp = fopen("Graph.txt", "r");  //打开文件
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    char ch;                         //逐字节读取节点
    while (ch = fgetc(fp))
    {
        if (ch == '0') break;

        LinearList_Insert(&G->node, G->node.size, ch); //插入到线性表中
    }

    int len = G->node.size;                 //根据读取到的节点的数目,分配邻接矩阵内存
    G->edge = (int**)calloc(len, sizeof(int*));
    if (G->edge == NULL)
    {
        printf("内存分配失败!1\n");
        return;
    }
    for (int i = 0; i < len; i++)
    {
        G->edge[i] = (int*)calloc(len , sizeof(int));
        if (G->edge[i] == NULL)
        {
            printf("内存分配失败!\n");
            return;
        }
    }

    for (int i = 0; i < len; i++)                 //初始化邻接矩阵
    {
        for (int j = 0; j < len; j++)
        {
            G->edge[i][j] = (i == j) ? 0 : MAXWEIGHT;
        }
    }

    char start, end;
    int weight;
    while (fscanf(fp, "\n%c %c %d", &start, &end, &weight) == 3)   //输入边的数据
    {
        G->edge[start - 'A'][end - 'A'] = weight;
        G->edge[end - 'A'][start - 'A'] = weight;
        G->num += 2;
    }//因为我们的节点使用大写字母表示,从文件中读取后转换成存储到线性表的下标中


    fclose(fp);
    fp = NULL;
}

//打印邻接矩阵
void AdjM_Print(AdjMGraph G)
{
    for (int i = 0; i < G.node.size; i++)
    {
        for (int j = 0; j < G.node.size; j++)
        {
            printf("%4d ", G.edge[i][j]);
        }
        printf("\n");
    }
}

输出结果如下:

2)邻接表法

邻接表中使用一个数组来存储节点(如下图左边的一列),数组的类型为一个结构体,包括节点的数据和一个链表指针,与该顶点相连的节点通过链表引出,每个链表节点包含了节点序列与顶点相连的边的权值指向与顶点相连的下一个顶点的指针

结构体定义:

typedef struct EdgeList{
    char end_node;             //指向的节点
    int weight;           //边的权重
    struct EdgeList* next; //指针域
}EdgeList_t;              //边结构体

typedef struct NodeList{
    char start_node;       //该顶点的数据
    EdgeList_t* edge;     //该顶点所连接的定点链表
}NodeList_t;              //顶点结构体

typedef struct {
    NodeList_t* node;          //节点数组
    int edgenum,nodenum;       //顶点和边的个数
}AdjAGraph;              //邻接表结构体

这个结构体比较复杂,我来解释一下:

第一个结构体表示链表的一个节点

第二个结构体表示一个顶点和与它相连的顶点组成的一条链表

第三个结构体表示节点的数组,此外还加上了顶点和边的个数计数。

Question:

这里介绍另一种输入图的方法:手动键盘输入,使用邻接表存储

static void AdjA_Init(AdjAGraph* G)
{
    G->edgenum = G->nodenum = 0;
    G->node = NULL;
}

//手动输入图表数据
//先输入一个顶点,然后输入该顶点所连接的顶点和他们的边的权值,输入0表示结束
void AdjA_Creat(AdjAGraph* G)
{
    AdjA_Init(G);
    G->node = (NodeList_t*)calloc(10, sizeof(NodeList_t));  //开辟十个节点的内存
    if (G->node == NULL) return;

    char ch1,ch2;           //顶点
    int weight = 0;         //权值
    for (int i = 0; i < 10; i++)
    {
        while (scanf("%c", &ch1) != EOF)          //限制节点使用大写字母表示,避免空格、回车等字符造成影响
        {
            if ((ch1 >= 'A' && ch1 <= 'Z') || ch1 == '0') break;
        }
        if (ch1 == '0')break;     //表示输入顶点结束

        G->node[i].start_node = ch1;
        G->node[i].edge = (EdgeList_t*)calloc(1, sizeof(EdgeList_t));
        if (G->node[i].edge == NULL) return;
        else G->node[i].edge->next = NULL;
        G->nodenum++;                                      //添加节点并初始化
        while (1)
        {
            while (scanf("%c", &ch2) != EOF)
            {
                if ((ch2 >= 'A' && ch2 <= 'Z') || ch2 == '0') break;
            }
            if (ch2 == '0')break;   //输入邻接顶点结束

            scanf("%d", &weight);
            EdgeList_t* newedge = (EdgeList_t*)calloc(1, sizeof(EdgeList_t));
            if (newedge == NULL)return;
            else
            {
                newedge->end_node = ch2;
                newedge->weight = weight;
                newedge->next = NULL;
            }                   //添加到链表中

            EdgeList_t* k = G->node[i].edge;
            while (k->next != NULL)
            {
                k = k->next;
            }
            k->next = newedge;
            G->edgenum++;                                //添加边
        }
    }
}

//输入样例:
A B 3 C 5 0
B A 3 D 1 E 7 0
C A 5 D 7 G 2 0
D B 1 C 7 G 9 F 5 E 4 0
E B 7 D 4 F 3 H 4 0
F D 5 E 3 I 2 0
G C 2 D 9 J 5 0
H E 4 I 3 0
I F 2 H 3 0
J G 5 0
0

  //打印邻接表
void AdjA_Print(AdjAGraph G)
{
    for (int i = 0; i < G.nodenum; i++)
    {
        printf("%c -> ", G.node[i].start_node);
        EdgeList_t* p = G.node[i].edge->next;
        while (p != NULL)
        {
            printf("%c %d -> ", p->end_node, p->weight);
            p = p->next;
        }
        printf("nullptr\n");
    }
}

打印结果如下:

三、图的遍历

图的遍历方式分为深度优先算法(DFS)广度优先算法(BFS),其中深度优先算法类似于树的前序遍历,使用递归实现,广度优先算法类似于树的层序遍历,使用队列实现。

tip:以下的遍历均以邻接矩阵为例。

1)深度优先算法(DFS算法)

算法思想:

  1. 访问顶点v并标记顶点v已访问;

  2. 查找顶点v的第一个邻接顶点w;

  3. 若w存在,则继续执行,否则算法结束;

  4. 若顶点w未访问,则访问w,递归到第一步;

  5. 查找v的w顶点的下一个邻接顶点,转到第三步。

//深度优先算法
void DFS(AdjMGraph G, int v, int visit[])
{
    Visit(G.node.list[v]);   //访问该顶点
    visit[v] = 1;            //标记已访问

    for (int i = 0; i < G.node.size; ++i) {
        if (G.edge[v][i] != MAXWEIGHT && G.edge[v][i] != 0 && visit[i] != 1) {  //寻找存在且未访问的
            DFS(G, i, visit);  //递归
        }
    }
}

2)广度优先算法(BFS算法)

算法思想:(出入队列的都是存储节点的数组代表节点的下标)

  1. 访问初始顶点v并标记访问,v入队列;

  2. 若队列非空,则继续执行,否则算法结束;

  3. 队头顶点出队列,并访问;

  4. 将队头的顶点的所有未被访问的邻接顶点入队列;

//广度优先算法
void BFS(AdjMGraph G,int visit[])
{
    SeqQueue Q;
    Queue_Init(&Q);

    Queue_Add(&Q, 0);
    while (!Queue_Empty(Q))
    {
        int temp = Queue_Pop(&Q);
        if(visit[temp] != 1)
        {
            Visit(G.node.list[temp]);
            visit[temp] = 1;
        }

        for (int i = 0; i < G.node.size; ++i) {
            if (G.edge[temp][i] != MAXWEIGHT && G.edge[temp][i] != 0 && visit[i] != 1) {
                Queue_Add(&Q, i);
                visit[temp] = 1;
            }
        }
    }
    printf("\n");
}

最后感谢你观看完我的文章,如果文章对你有帮助,可以点赞收藏评论,这是对作者最好的鼓励!不胜感激🥰🥰

  • 26
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值