有向图的十字链表存储;深度优先、广度优先遍历

1:图的存储

数组表示法—–邻接矩阵
邻接表———-邻接表与逆邻接表结合成十字链表,邻接表求入度需要整体遍历,不方便
十字链表——-有向图
邻接多重表—-无向图
这里先介绍最常用也比较复杂的十字链表

2:十字链表

定义:邻接表与逆邻接表相结合的一种链式储存结构,拥于邻接表易求出度的特点,又有逆邻接表易求入度的特点。每一条弧有一个结点,每一个顶点也有一个结点
《有向图的十字链表存储;深度优先、广度优先遍历》
《有向图的十字链表存储;深度优先、广度优先遍历》
先弄清楚几个概念(有向图):
《有向图的十字链表存储;深度优先、广度优先遍历》
弧结点各元素含义:

tailvex:弧尾的位置(是个int型变量)
headvex:弧头的位置(是个int型变量)
hlink:弧头相同的下一条弧(都在同一条链表上)
tlink:弧尾相同的下一条弧(都在同一条链表上)
info:存放弧的相关信息,比如权值(网)

顶点结点各元素含义:

data:存储顶点相关信息,比如顶点的名字(A,B,C····)
firstin:指向该顶点为弧头的第一条弧
firstout:指向该顶点为弧尾的第一条弧

这很像像稀疏矩阵的十字链表,但是有一个很大的不同:
(1):弧与顶点是相互独立的,弧总体是由两条链表构成(弧头相同的链表,弧尾相同的链表)
(2):顶点正常来讲是只需要data域的(所有顶点其实存放在一个数组中的),但是却多了两条链域(firstin,firstout),弧头(尾)的第一条弧 —直接把存放data的数组和两条链域相联系,形成一个整体。

代码如下:
//十字链表储存结构
#define MAX_VERTEX_NUM 20 //顶点数目最多20个,顶点放在数组中

//弧结点 
typedef struct ArcBox
{
    int             tailvex, headvex;           //弧的尾与头位置
    struct ArcBox   *hlink, *tlink;             //弧头相同的链域,弧尾相同的链域
}ArcBox;

//顶点结点 
typedef struct 
{
    int nodenum;                                //为广度优先遍历获取顶点在数组的位置
    char    data;                               //顶点数据
    ArcBox  *firstin, *firstout;                //分别指向该顶点的第一条入弧和出弧
}VexNode;

typedef struct {
    VexNode xlist[MAX_VERTEX_NUM];              //表头向量
    int     vexnum, arcnum;                     //有向图的顶点数与弧数
}OLGraph;

3:构建有向图十字链表

基本步骤:
(1):顶点的创建(相当于数组赋值,先和两条链表没有联系)
1:输入顶点值(字母)
2:记录顶点位置
3:firstin,firstout置空
(2):弧结点的创建
1:输入弧的两个顶点(两个字母)
2:获取弧的两个结点位置
3:分配空间并赋值
4:修改顶点(数组)的firstin,firstout

注意事项

这里输入的是字母%c,采用scanf()输入会有几个问题。
(1):输入字母后的回车会被下一次scanf()读进去,导致会跳一次输入(跳过这次其实是回车)!!!
解决方法:%c前面务必要有一个空格
scanf(“(空格)%c”, &G.xlist[i].data);
(2):连续读入两个字母,如果输入有空格,必须用getchar()把空格读走,即用来吃掉上一句的enter!!!
《有向图的十字链表存储;深度优先、广度优先遍历》
(3):如果你阅读过C Primer Plus的话,会很清楚的知道scanf的%c读入的特殊性。
《有向图的十字链表存储;深度优先、广度优先遍历》

代码如下:
//创建有向图的十字链表
int CreatDG(OLGraph &G, int vexnum, int arcnum)
{
    for (int i = 0; i < vexnum; i++) {
        printf("Please enter %d data:", i+1);

        scanf(" %c", &G.xlist[i].data);                                         //输入顶点值

        G.xlist[i].nodenum = i;                                                 //记录顶点的位置
        G.xlist[i].firstin = NULL;                                              //初始化指针
        G.xlist[i].firstout = NULL;
    }

    char v1, v2;                                                                //一条弧的两个顶点
    int i, j;                                                                   //弧尾位置,弧头位置

    for (int k = 0; k < G.arcnum; k++) {
        printf("Please enter %d arc(tail -> head): ", k + 1);
        scanf_s(" %c", &v1);                                                    //输入弧的第一个顶点,这里%c前面必须要有一个空格!
        getchar();                                                              //把输入的空格读走
        v2 = getchar();                                                         //输入弧的第二个顶点
        i = LocateVex(G, v1);                                                   //获取弧的第一个节点位置
        j = LocateVex(G, v2);                                                   //获取弧的第二个节点位置

        ArcBox *p = (ArcBox *)malloc(sizeof(ArcBox));                           //分配弧结点空间
        p->tailvex = i;     p->tlink = G.xlist[i].firstout;                     //i放进tailvex,tlink(尾巴出去firstout)
        p->headvex = j;     p->hlink = G.xlist[j].firstin;                      //j放进headvex,hlink(头进来firstin)

        G.xlist[j].firstin = G.xlist[i].firstout = p;                           //入弧与出弧的插入(数组的firstin firstout有了指向)
    }
    return 0;
}

4:深度优先遍历

基本定义:顾名思义是按深度优先,即一直往下走 ,而广度优先是一层一层走完在向深处走。
核心思想:递归调用。
基本步骤:
(1):把所有结点标记为false
(2):对没有访问过的结点深度优先遍历
《有向图的十字链表存储;深度优先、广度优先遍历》
《有向图的十字链表存储;深度优先、广度优先遍历》

代码如下:
/****************************************************** ** DFS深度优先遍历 *******************************************************/
bool DFSvisted[20];                                             //这里定义最多有20个结点
void DFSTraverse(OLGraph &G)
{
    for (int i = 0; i < G.vexnum; i++)                          //先把每个结点都标记为假,没有访问过
        DFSvisted[i] = false;
    for (int i = 0; i < G.vexnum; i++) {
        if (!DFSvisted[i])                                      //对没有访问的结点,按深度优先遍历
            DFS(G, i);                          
    }
}


void DFS(OLGraph &G, int v)
{
    DFSvisted[v] = true;                                        //访问结点标识--真
    printf("%c\t",G.xlist[v].data);
    ArcBox *DFStemp = G.xlist[v].firstout;                      //用DFStemp来存储该节点的第一个出去的弧

    while (DFStemp)
    {
        if (!DFSvisted[DFStemp->headvex])                       //访问此结点第一个出去的弧
            DFS(G, DFStemp->headvex);
        DFStemp = DFStemp->tlink;                               //该结点还有指向其他的弧
    }
}

5: 广度优先遍历

基本定义:广度优先是一层一层走完在向深处走。就像跑道(由内而外)一圈走完再往下一圈走
核心思想:这里没有用很明显的递归调用,因为这里需要用队列记录访问过结点的先后顺序
几个难点:
(1):循环结束时即要队列空,也要for循环G.vexnum次
(2):访问过的结点入队列
(3):获取出队的元素所在位置,以便在十字链表中找到该节点为弧尾的第一个结点
《有向图的十字链表存储;深度优先、广度优先遍历》

******************************************************
**  BFS广度优先遍历
*******************************************************/
bool BFSvisted[20];                                                     //这里定义最多有20个结点
void BFSTraverse(OLGraph &G)
{
    for (int i = 0; i < G.vexnum; i++)                                  //先把每个结点都标记为假,没有访问过
        BFSvisted[i] = false;
    LinkQueue Q;                                                        //初始化队列
    InitQueue(Q);
    ArcBox *BFStemp;                                                    
    char HeadQueue;                                                     //出队元素(队头)第一次会出现一进队就出队
    int headnum = 0;                                                    //队头元素的位置,才好找到十字链表的下一个

    for (int i = 0; i < G.vexnum; i++) {
        if (!BFSvisted[i]) {
            BFSvisted[i] = true;
            printf("%c\t", G.xlist[i].data);                            //访问元素
            EnQueue(Q, G.xlist[i].data);                                //把访问的元素都入队
            while (!QueueEmpty(Q)) {
                DeQueue(Q, HeadQueue);                                  //出队
                //printf("出队:%c\t", DeQueue(Q, HeadQueue));

                for (int j = 0; j < G.vexnum; j++) {
                    if (G.xlist[j].data == HeadQueue)
                        headnum = G.xlist[j].nodenum;                   //获取队头位置
                }
                BFStemp = G.xlist[headnum].firstout;                    //用BFStemp来存储该节点的第一个出去的弧

                while (BFStemp) {
                    if (!BFSvisted[BFStemp->headvex]) {
                        BFSvisted[BFStemp->headvex] = true;             //把结点都标记为真
                        printf("%c\t", G.xlist[BFStemp->headvex].data); //访问元素
                        EnQueue(Q, G.xlist[BFStemp->headvex].data);     //把访问的元素都入队
                    }
                    BFStemp = BFStemp->tlink;
                }
            }
        }
    }
}

下面是遍历与队列情况
BFSTraverse:
A 出队:A D C 出队:D E B 出队:C 出队:E 出队:B F 出队:F G H 出队:G 出队:H I 出队:I
《有向图的十字链表存储;深度优先、广度优先遍历》

6: 主函数

#include "stdafx.h"
#include "graph.h"
#include "queue.h"
#pragma warning(disable:4996)

int main()
{
    OLGraph G;
    printf("Please enter vexnum and arcnum: ");

    scanf("%d %d", &G.vexnum, &G.arcnum);                           //输入结点数,弧数
    CreatDG(G, G.vexnum, G.arcnum);                                 //创建有向图

    printf("\nDFSTraverse:\n");
    DFSTraverse(G);                                                 //深度优先遍历
    printf("\nBFSTraverse:\n");
    BFSTraverse(G);                                                 //广度优先遍历
    printf("\n");


    return 0;
}

7: 输出结果

《有向图的十字链表存储;深度优先、广度优先遍历》
《有向图的十字链表存储;深度优先、广度优先遍历》

8: 后记

(1):数据结构自己尽量用C语言完成,方便更多的同学看懂核心。BFS广度优先遍历需要队列支持,所以自己临时构建了一个队列及实现队列基本功能。而C++直接有队列库,比较简单。
(2):里面有一个很重要的就是字母的输入scanf的%c用法,希望能牢记,如果用C++的string会更简单些,C语言没有这个库
(3):感谢以下博主文章对我的启发与帮助
https://blog.csdn.net/jkay_wong/article/details/6957336
https://blog.csdn.net/qiye005/article/details/46650933
(4):实验代码百度云链接(VS2017)
链接: https://pan.baidu.com/s/14xjFP51xeaDTA1xR_btVSg 密码: y226
(5):若你觉得文章对你有启发,请注明出处。

    原文作者:数据结构之图
    原文地址: https://blog.csdn.net/weixin_39956356/article/details/80371735
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞