C语言手撕实战代码_图_邻接表_DFS_BFS_判路_判环_拓扑排序的代码实现详解

1.图的存储结构

1.1 图的临接矩阵存储结构

typedef enum GraphKind{DG,UDG,DN,UDN}GraphKind;   //不带权有向图DG  不带权有向图UDG 带权有向网DN 带权无向网UDN

#define INT_MAX 99999
#define INF INT_MAX
#define MAX_NUM 10
//图弧信息,图弧信息中,可能存在其他信息,如铁路作为一条边,客流量

typedef struct ArcCell
{
    int adj;  //权值
}ArcCell,AdjMatrix[MAX_NUM][MAX_NUM];


typedef int VertexType;
//图的拓扑信息不仅仅有弧信息,顶点信息,宏观的信息
typedef struct
{
    AdjMatrix arcs; //邻接矩阵,弧信息,二维数组
    VertexType vexs[MAX_NUM];//顶点信息,默认顶点编号从1开始
    int vexnum;   //点数
    int arcnum;   //边数
    GraphKind kind; //图的类型
}MGraph;

1.2 图的邻接表存储结构

一些需要记住的英语单词,方便更快速的写出图的结构

arc 弧 ---->ArcNode 弧结点 ----> nextArc 下一条弧–>AdjList[ ] 邻接链—>ALGraph 邻接链表,简称邻接表
adjacent 相邻的
vertices 顶点

图的邻接表存储结构的大体思路:
本质就是由多条带有头结点单链表组成,由于这个表是连续的,为了方便的使用下标访问,将每一条单链表的头结点封装成一个一维数组,就可以通过下标访问,最后将这个一维数组,和其他图的相关属性,如图的边数,结点数,图的类型最后封装起来,就构成了临接表的数据结构。

typedef int VertexType;  //头结点的类型,考试中头结点都是int类型,工程上可能是char类型,如A,B,C,D啥的
#define MAX_NUM 10
typedef struct ArcNode
{
    int adjvex; //该点头结点的索引值
    struct ArcNode * nextArc; //下一个结点
}ArcNode;

typedef struct VNode
{
    VertexType data; //头结点
    ArcNode *firstArc; //存储第一个结点的地址
}VNode,AdjList[MAX_NUM];

typedef struct ALGraph
{
    AdjList vertices;
    int vexnum;
    int arcnum;
    int kind; //类型
}ALGraph;

一个图的最初信息最直接的方式是知道有多少的顶点。

邻接矩阵:给一个最大值来存储所有可能的边情况

易错点:0可能用来代表权值,所以人为规定,图的边不存在不能用0,而不是用一个无穷大的数,99999999

2.图的存储结构代码相关实战

2.1 使用邻接表存储方式建立无向图G

在这里插入图片描述

构造函数,传入参数,空图,一维数组构成的点集合,点的数量,二维数组构成的边集合,边的数量
1.先将头结点,赋值给空值,将头结点所指向的指针置空,方便后续使用头插法。
2.读取每一条弧的信息,将弧的信息在表上体现

构造邻接表的代码:

void createALGraph(ALGraph &G,VertexType vList[],int VListLength,VertexType arcList[][2],int arcListLength)
{
    for(int i=1;i<=VListLength;i++)
    {
        G.vertices[i].data= vList[i];
        G.vertices[i].firstArc=NULL;
    }
    
    for(int i=0;i<arcListLength;i++)
    {
         VertexType v= arcList[i][0]; //v,w就是 arcList[i][0], arcList[i][1]
         VertexType w= arcList[i][1];
        
        //申请一个表结点,做v->w的插入,这个表结点的值是w,头插进入v
        ArcNode* p=(ArcNode *)malloc(sizeof(ArcNode));
        p->adjvex=w;
        p->nextArc=G.vertices[v].firstArc;
        G.vertices[v].firstArc=p;
        //申请一个表结点,做w->v的插入
        p=(ArcNode *)malloc(sizeof(ArcNode));
        p->adjvex=v;
        p->nextArc=G.vertices[w].firstArc;
        G.vertices[w].firstArc=p;
    }
    G.vexnum=VListLength;
    G.arcnum=arcListLength;
    G.kind=0; //0代表无向图,1代表有向图
}

测试代码:

int main()
{
    ALGraph G;
    VertexType vList[]={0,1,2,3,4,5,6};
    int VListLength=6;
    VertexType arcList[][2]={{1,2},{1,3},{1,4},{2,3},{3,5},{4,5},{4,6},{5,6}};
    int arcListLength=8;
    createALGraph(G, vList, VListLength, arcList, arcListLength);
    
    //遍历无向图
    for(int i=1;i<=G.vexnum;i++)
    {
        ArcNode *p=G.vertices[i].firstArc;
        while(p!=NULL)
        {
            printf("%d ",p->adjvex);
            p=p->nextArc;
        }
        printf("\n");
    }
    
    printf("\n");
}

在这里插入图片描述

2.2 计算有向图G中所有顶点的度

思路,就是每访问一条边<v,w>,每访问一条就是v++,w++

在这里插入图片描述

//设计算法计算有向图G中所有顶点的度
void getDegree4Vertex(ALGraph G,int degreeArra[]) //degreeArra存放图的点数
{
    for(int i=1;i<=G.vexnum;i++)
    {
        ArcNode *p=G.vertices[i].firstArc;
        VertexType v= G.vertices[i].data;
        while(p!=NULL)
        {
            VertexType w=p->adjvex;
            degreeArra[v]++;
            degreeArra[w]++;
            p=p->nextArc;
        }
    }
}

2.3 设计算法将邻接表方式存储的无向图G删除顶点v到顶点w的边

本质就是查找+单链表的删除+分情况

void deleteArc(VertexType v,VertexType w,ALGraph &G)  //有向的删除,如果是无向图,就调用两次
{
    //首先肯定找到v的头结点
    ArcNode *p=G.vertices[v].firstArc;
    printf("v的头结点是%d\n",p->adjvex);
    ArcNode *pre=NULL; //单链表的删除用pre指针
    
   
    
    //分情况讨论,如果删除的结点是就是头结点所指向的第一个结点
    if(p->adjvex==w)
    {
        G.vertices[v].firstArc=p->nextArc;
        G.arcnum--;
        free(p);
    }else{  //单链表中存在待删除的元素
        
        while(p!=NULL&&p->adjvex!=w)  //找到p
        {
            pre=p;
            p=p->nextArc;
        }
        if(p!=NULL)  //编译器太高级,必须得判断他是不是为空,考试写不写问题不大
        {
            pre->nextArc=p->nextArc;
            G.arcnum--;
            free(p);
        }
      
    }
}

测试代码:

  deleteArc(1, 4, G);
  deleteArc(4, 1, G);

在这里插入图片描述

2.4 将邻接表存储的有向图G转换成逆邻表存储(重点)

在这里插入图片描述

再设置一个逆邻接表rG,将G中的v,w,以w,v的形式插入到rG中

三步走:

  1. 第一步,将G的头结点,赋给rG的头结点
  2. 第二步 将v,w以w,v形式插入到rg中,以分配新的存储空间的形式插入
  3. 第三步,将点的数量,弧的数量赋值给rG
    在这里插入图片描述
//邻接表的插入操作(头插法)
void insert(VertexType v,VertexType w,ALGraph &rG)
{
    ArcNode *p=(ArcNode *)malloc(sizeof(ArcNode));
    p->adjvex=w;
    p->nextArc=rG.vertices[v].firstArc;
    rG.vertices[v].firstArc=p;
}

void reverse(ALGraph &G,ALGraph &rG)
{
    // 第一步,将G的头结点,赋给rG的头结点
    for(int i=1;i<=G.vexnum;i++)
    {
        rG.vertices[i].data=G.vertices[i].data;
        rG.vertices[i].firstArc=NULL;
    }
    //第二步 将v,w以w,v形式插入到rg中,以分配新的存储空间的形式插入
    for(int i=1;i<=G.vexnum;i++)
    {
        ArcNode *p=G.vertices[i].firstArc;
        VertexType v=G.vertices[i].data;
        while(p!=NULL)
        {
            VertexType w=p->adjvex;
            insert(w,v,rG);
            p=p->nextArc;
        }
    }
    //第三步,将点的数量,弧的数量赋值给rG
    rG.vexnum=G.vexnum;
    rG.arcnum=G.arcnum;
   
}

代码结果如下:
在这里插入图片描述

2.5 将邻接表表示的有向网转换为邻接数组表示

在ArcNode类型中加入成员变量 weight
大体思路为:将邻接数组初始化,遍历邻接表,将weight值赋值给二维数组,比如之前v到w有边,设置为1,现在就可以设置为权值66

3.图的遍历

3.1 图的深度优先搜索(DFS)

图的判路专题:

总结一下:图的DFS
(1)从顶点Vi开始出发,只要顶点vi存在的结点,则wi一定会被访问到。
(2)访问一个顶点之后务必进行标记,防止结点重复访问

3.1.1 图的判路

3.1.1 对有向图的DFS遍历

本质上是遍历图上的全部点,按照深搜的策略找点,为什么要将头结点遍历,目的是因为这个图可能是不联通的,可能有孤立的点

在这里插入图片描述

void DFS(ALGraph G,VertexType v)
{
    //首先要设置,当前访问的结点的visit数组为1
    visit[v]=1;
    printf("%d ",v);
    //然后,开始访问每一条链上的结点,对每一个未曾访问过的结点dfs
    ArcNode *p=G.vertices[v].firstArc;
    while(p!=NULL)
    {
        VertexType adjVertex=p->adjvex; //访问邻接点当前是几号元素,对他进行DFS
        if(visit[adjVertex]==0)DFS(G, adjVertex);
        p=p->nextArc;
    }
}
void DFSTraverse(ALGraph G) //对图进行深度优先遍历
{
    //遍历一遍全部的图的所有的头结点,这里是单层循环,只遍历头结点,至于头结点内部每一条单链表的遍历放在DFS函数中处理
    for(int i=1;i<=G.vexnum;i++)
    {
        if(visit[i]==0)  //顶点未被访问过,就访问它
        {
            DFS(G,i);
        }
    }
    
}

在这里插入图片描述

3.1.2 编写算法判断以邻接表方式存储的有向图G是否存在srcVertex到destVertex的路径

srcVertex是起点,destVertex是终点

精辟点睛之句:ret=DFS_is_path(w, destVertex, G);//进入递归,其实所有的dfs()函数,都停留在ret=DFS()这块,直到最后一个dfs结束,开始返回才能打破僵局

int DFS_is_path(VertexType v,VertexType destVertex,ALGraph G)
{
    //首先DFS模板开头,visit数组=1
    visit[v]=1;
    
    if(v==destVertex) return 1;  //从v找到了destVerte,返回找到了
    
    //在该算法实现的过程中,不需要进行头结点的遍历,因为我们的目标是得出是否有A点到B点,A点是确定的,从这个点开始找即可
    ArcNode *p=G.vertices[v].firstArc; //得到邻接点的地址,接下来访问每一个邻接点
    while(p!=NULL)
    {
        VertexType w=p->adjvex;
        int ret=0;
        if(visit[w]==0)  //如果当前的结点未被访问过,就DFS进入
        {
            ret=DFS_is_path(w, destVertex, G);//进入递归,其实所有的dfs()函数,都停留在ret=DFS()这块,直到最后一个dfs结束,开始返回才能打破僵局
        }
        if(ret==1)
        {
            return 1;
        }
        p=p->nextArc;
    }
    return 0;//没找到
}

//判断A点到B点是否存在路径
int isExistedPath(VertexType srcVertex,VertexType destVertex,ALGraph G)
{
    return DFS_is_path(srcVertex,destVertex,G);
}

3.1.3 设计算法,求出以邻接表存储的有向图中所有从顶点v到顶点w的简单路径

与之间的图的dfs遍历相比,该问题需要我们存储多条路径,并输出这些路径,我们需要一个数组list辅助存储,同时记录数组的长度length辅助数组的输出
为了使路径能被保存输出,我们要恢复现场,即在一次DFS调用结束前,重新将visit[]置为0

三个深刻问题的解决:
1.为什么要恢复现场?

以图为例,如果不恢复现场,那么3永远无法,访问到2,再到6。

在这里插入图片描述

2.为什么恢复现场之后,回到上一个结点之后,它没有访问刚才访问过的结点,形成一种无限循环?

因为实际上,邻接表这种存储结构,其实是有顺序的,在访问完1的邻接点2后,会移动一位到3,之后就永远不会走2了,所以不需要担心出现"无限循环"

在这里插入图片描述

3.为什么if(v==destVertex) 输出完路径之后,就不继续递归了,再dfs下去也没有任何意义?

答:因为当找到终点时,所有的路径,都只可能是它前面的不同路径到达,所以用if-else语句,当if找到了就输出,if语句的输出作为当前的最后一条语句结束,再返回之前的层,不用再向深搜。

给出题目如下:遍历出1到6的全部路径
在这里插入图片描述

void printf_path(VertexType list[],int path_length)
{
    for(int i=0;i<path_length;i++)
    {
        printf("%d ",list[i]);
    }
    printf("\n");
}
VertexType list[MAX_NUM]={0};
int path_length=0;
void DFS_allpath(ALGraph G,VertexType v,VertexType destVertex)  //实现输出全部DFS路径
{
    visit[v]=1;
    list[path_length]=v;
    path_length++;
    if(v==destVertex)
    {
        printf_path(list, path_length);
    }else
    {
        ArcNode *p=G.vertices[v].firstArc;
        while (p!=NULL) {
            VertexType w=p->adjvex;
            if(visit[w]==0)
            {
                DFS_allpath(G, w, destVertex);
            }
            p=p->nextArc;
        }
    }
    visit[v]=0;
    path_length--;
    
}

在这里插入图片描述

3.1.4 设计算法,求出以邻接表存储的有向图中所有从顶点v到顶点w长度为d的路径

在上面的代码模版中,加一句&&path_length==d,大功告成

void printf_path(VertexType list[],int path_length)
{
    for(int i=0;i<path_length;i++)
    {
        printf("%d ",list[i]);
    }
    printf("\n");
}
VertexType list[MAX_NUM]={0};
int path_length=0;
void DFS_length_path(ALGraph G,VertexType v,VertexType destVertex,int d)  //实现输出全部DFS路径
{
    visit[v]=1;
    list[path_length]=v;
    path_length++;
    if(v==destVertex&&path_length==d)
    {
        printf_path(list, path_length);
    }else
    {
        ArcNode *p=G.vertices[v].firstArc;
        while (p!=NULL) {
            VertexType w=p->adjvex;
            if(visit[w]==0)
            {
                DFS_length_path(G, w, destVertex,d);
            }
            p=p->nextArc;
        }
    }
    visit[v]=0;
    path_length--;
}

在这里插入图片描述

3.2 图的判环

图的判环专题:

首先,我们要知道在最开始有向图的遍历时,看似我们放回过结点,其实没有结点是重复访问的,因为基于邻接表的结构,不难看出,他是一种移动,并没有实际访问过两次结点。

判断它是不是环,不需要存储路径,也不需要使用list数组,保存路径。
一个简单的想法是,通过在从一个点出发在这一趟中,访问到两次相同的结点,代表着有有环,从v到v,但是这种想法是不全面的,存在其他无环的情况,也满足访问到两次相同的情况,需要我们区分这两种情况
在这里插入图片描述
左图:在以3开始的情况下,3到1访问了一次1,3到2到1又访问了一次1,这存在了两次。和右图相比,左图的递归状态已经结束,而右图是在递归的过程中二次访问到了某点。所以递归状态是解题的关键所在。

在编写算法前的思路梳理:
1.将visit数组改造,vist数组现在有三种状态,0,1,2--------0代表未被访问,1代表递归进行中,2代表递归已结束同时也被访问
2.在一趟访问中,第二次访问到了visit已经置为1的数时,表示有环,否则无环。
3.在该问题中,我们不能确定,这个图是否为一个连通图,它可能包含多个子图,就需要我们遍历结点,又引出了一个思考,以左图为例,2开始,会访问到1,3开始也会访问1,这种情况会不会发生在一个普通的有向图中?循环多个结点,必然会多次访问到某一个结点,但是实际上思路梳理1,这个递归状态判断的思路,已经解决了这种情况。

int DFS_ring(ALGraph G,VertexType v)
{
    int ret=0;
    visit[v]=1; //表示进入递归状态
    
    ArcNode *p=G.vertices[v].firstArc;
    while (p!=NULL) {
        VertexType w=p->adjvex;
        if(visit[w]==0)
        {
            ret=DFS_ring(G, w);
        }else if(visit[w]==1)
        {
            ret=1;
        }
        p=p->nextArc;
    }
    
    visit[v]=2; //表示v点结束递归状态
    return ret;
}
void isExistedRing(ALGraph G)
{
    int ret=0;
    for(int i=1;i<=G.vexnum;i++)
    {
        if(visit[i]==0)
        {
            ret=DFS_ring(G,i);  //ret=1表示存在环,ret=0,表示不存在环
        }
        if(ret==1)
        {
            printf("以%d开始搜索存在环",i);
            return;
        }
    }
    
    printf("不存在环");
    return;
}

在这里插入图片描述

3.3 拓扑排序

如何使用DFS实现拓扑排序?
在通过对一个图实现了DFS和BFS后,我们发现DFS和BFS遍历的结果是相反的,BFS的结果就是拓扑排序的结果,DFS的结果是拓扑排序逆序的结果用一个栈将结果保存并输出就可以
DFS实现拓扑排序,DFS是一个逆向的拓扑排序。

实现的重点是:
在某次的递归结束后,将当前的v入栈

int stack[MAX_NUM]={0};
int stack_length=0;
void top_sort_DFS(ALGraph G,VertexType v)
{
    visit[v]=1; //v点被访问
    
    ArcNode *p=G.vertices[v].firstArc;
    while(p!=NULL)
    {
        VertexType w=p->adjvex;
        if(visit[w]==0)
        {
            top_sort_DFS(G, w);
        }
        p=p->nextArc;
    }
    stack[stack_length++]=v;
}

void print_stack(int stack[],int stack_length)
{
    for(int i=stack_length-1;i>=0;i--)
    {
        printf("%d ",stack[i]);
    }
    printf("\n");
}

void top_sort_travel(ALGraph G)
{
    //这个函数实现遍历每个头结点
    for(int i=1;i<=G.vexnum;i++)
    {
        if(visit[i]==0)
        {
            top_sort_DFS(G,i);
        }
    }
    print_stack(stack, stack_length);   
}

在这里插入图片描述

3.4 图的广度优先遍历(BFS)

图的BFS相比于树的BFS,多了visit数组,即确定是否访问过,避免重复访问,为什么树不用,因为在访问树左右孩子结点的过程中,可以理解为它是有向的,由树(子树)的根节点出发,有向的指向。

遍历思路:这里以无向连通图为例
假如是不连通的,需要遍历一遍全部的表结点
传入参数,图G和点v,这个点v可以是顶点也可以是别的点。
ALGraph G, (vertextpye)也就是int v
首先,是初始化操作:初始化visit数组和队列
将访问数组visit初始化为0,init队列
其次,将v结点加入队列中,并标记visit数组为true,代表已经访问过。
以上操作都是一一对应的


进入到队列不为空的操作
若队列不为空,将队头元素出队,将弹出的队头元素放入v(传入的那个参数)中存储,定义一个边表结点p,将v的邻接点存储到新定义的边表结点中好进行,边表循环,p不为空,假如没有访问过,就把它加入到队列中,并将访问的visit数组置为true


脑海里回响着BFS的过程

队列代码:

//队列的基本操作
//队列的结构体
#define MaxSize 50   // 定义队列中元素的最大个数
typedef struct {
  int data[MaxSize]; // 存放队列元素
  int front, rear;   // 队头和队尾指针
} SqQueue;
// 初始化队列(带头结点)
void Queue_Init(SqQueue &Q) {
  Q.rear = Q.front = 0;
}
// 判断队列是否为空
bool Queue_Empty(SqQueue Q) {
  return Q.front == Q.rear;
}
// 入队
bool Queue_En(SqQueue &Q, int e) {
  if ((Q.rear + 1) % MaxSize == Q.front) return false; // 队满则报错
  Q.data[Q.rear] = e;
  Q.rear = (Q.rear + 1) % MaxSize; // 队尾指针加 1 取模
  return true;
}
// 出队
bool Queue_De(SqQueue &Q, int &e) {
  if (Queue_Empty(Q)) return false; // 队空则报错
  e = Q.data[Q.front];
  Q.front = (Q.front + 1) % MaxSize;
  return true;
}

BFS代码:

void BFS(ALGraph G,VertexType v)
{
    int visit[MASVETEX]={0};  //初始化访问数组
    SqQueue Q;
    Queue_Init(Q);
    Queue_En(Q, v);
    visit[v]=1;
    
    while (!Queue_Empty(Q)) {  //假设队列不为空,开始循环
        
        Queue_De(Q, v); //从队列中弹出表头,此时弹出的元素存放在v中
        printf("%d ",v);   //输出BFS
        
        ArcNode *p=G.vertices[v].firstArc;
        
        while(p!=NULL)
        {
            if(visit[p->adjvex]==0)  //没有被访问过
            {
                visit[p->adjvex]=1;  //更新visit数组
                Queue_En(Q, p->adjvex); //将该元素加入队列中
            }
            p=p->nextArc;
        }
        
    }
    
}

测试结果如下:

在这里插入图片描述
在这里插入图片描述

3.4.1 求出无向连通图中距离V0的最短路径长度为K的所有节点

问题分析,最短路,无容质疑的是图的BFS,在BFS的基础上,我们需要改造visit[]数组,这个数组记录着,v点到其他的点的距离值
初始化visit数组为INF,就是-1,每次更新距离是,它上一个指向的节点的距离+1

#define INF -1    // 表示尚未访问的节点
void BFS_K(ALGraph G,VertexType v,VertexType K)
{
    int visit[MASVETEX];  //初始化访问数组
    for(int i=0;i<MASVETEX;i++)visit[i]=INF;
   
    SqQueue Q;
    Queue_Init(Q);
    Queue_En(Q, v);
    visit[v]=0; //顶点0到顶点0的距离为0
    
    while (!Queue_Empty(Q)) {  //假设队列不为空,开始循环
        
        Queue_De(Q, v); //从队列中弹出表头,此时弹出的元素存放在v中
        if (visit[v] > K) break;  //加快操作,以后的循环是没有意义的
        
        ArcNode *p=G.vertices[v].firstArc;
        
        while(p!=NULL)
        {
            if(visit[p->adjvex]==INF)  //没有被访问过
            {
                visit[p->adjvex]=visit[v]+1;  //更新visit数组
                Queue_En(Q, p->adjvex); //将该元素加入队列中
            }
            p=p->nextArc;
        }
        
    }
    
    for(VertexType i=1;i<=G.vexnum;i++)
    {
        if(visit[i]==K)
        {
            printf("%d ",i);
        }
    }
    
}

在这里插入图片描述

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小徐要考研

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值