数据结构之图

本文介绍了图的定义,包括有向图和无向图,并详细阐述了图的表示方法——邻接矩阵和邻接链表,讨论了它们的优缺点。此外,文章还讲解了图的度、权、连通图等概念,以及深度优先搜索(DFS)和广度优先搜索(BFS)的算法,并提到了拓扑排序和最小生成树的概念,包括Prim和Kruskal算法。

1.图的定义

图(graph)是由一些点(vertex)和这些点之间的连线(edge)所组成的;其中,点通常称为顶点(vertex),而点到点之间的连线通常称之为边或者弧(edge)。通常记为G=(V,E)。

 

2.图的分类

图通常分为有向图和无向图,而其表示表示方式分为邻接矩阵邻接链表。具体表示如下图。

对于无向图,其所有的边都不区分方向。G=(V,E)。其中,

<1>V={1,2,3,4,5}。V表示有”1,2,3,4,5”几个顶点组成的集合。

<2>E={(1,2),(1,5),(2,1),(2,5),(2,4),(2,3),(3,2),(3,4),(4,3),(4,2),(4,5),(5,1),(5,2),(5,4)}。E就是表示所有边组成的集合,如(1,2)表示由顶点1到顶点2连接成的边。

对于有向图,其所有的边都是有方向的。G=(V,E)。其中,

<1>V={1,2,3,4,5}。V表示有”1,2,3,4,5”几个顶点组成的集合。

<2>E={<1,2>,<2,5><5,4>,<4,2><3,5>,<3,6>,<6,6>}。E就是表示所有边组成的集合,如<1,2>表示由顶点1到顶点2连接成的边。注意又有向图边和无向图边表示方法的不同,有向图的边是矢量,而无向图只是普通的括号。

针对邻接矩阵和邻接链表两种不同的表示方式,有如下优缺点:

<1>邻接矩阵由于没有相连的边也占据空间,相对于邻接链表,存在空间浪费的问题;

<2>但是在查找的时候,邻接链表会比较耗时,对于邻接矩阵来说,它的查找复杂度就是O(1)。

 

用邻接表表示图的代码

#define MAX 100

typedef struct ENode   //邻接表中表对应的链表的顶点
{
    int ivex;          //该边所指向的顶点的位置
    int weight;       //该边的权值
    struct ENode *next_edge;   //指向下一条边的指针
}ENode,*PENode;

typedef struct VNode   //邻接表中表的顶点
{
    char data;       //顶点的数据
    struct VNode *first_edge;  //指向第一条依附该顶点的边
}VNode;

typedef struct LGraph  //邻接表
{
    int vexnum;    //图的顶点数
    int edgenum;   //图的边数
    VNode vexs[MAX];
}LGraph;
  1. 3.度、权、连通图等概念

对于无向图来说,它的顶点的就是指关联于该顶点的边的数目;而对于有向图来说,分为入度和出度,所谓入度就是进入该顶点边的数目,出度就是离开这个顶点边的数目,有向图的度就是入度加出度。

如下图有向图(a)所示,对于顶点3,它的入度为2,出度为1,度为3。

图还被分为有权图和无权图,所谓有权图就是每条边都具有一定的权重,通常就是边上的那个数字;而无权图就是每条边没有权重,也可以理解为权重为。如下图所示即为有权图,(A,B)的权就是13。

如果一个无向图中每个顶点从所有其他顶点都是可达的,则称该图是连通的;如果一个有向图中任意两个顶点互相可达,则该有向图是强连通的。

如图(b)中有3个连通分量:{1,2,5},{3,6},{4}。若一个无向图只有一个连通分量,则该无向图连通。

而图(a)中有3个强连通分量:{1,2,4,5},{3},{6}。{1,2,4,5}中所有顶点对互相可达。而顶点2与6不能相互可达,所以不能构成一个强连通分量。

4.深度优先搜索(Depth First Search,DFS)

图的深度优先算法有点类似于树的前序遍历,首先图中的顶点均未被访问,确定某一顶点,从该顶点出发,依次访问未被访问的邻接点,直到某个邻接点没有未被访问邻接点时,则回溯父节点(此处我们将先被访问的节点当做后被访问节点的父节点,例如对于节点A、B,访问顺序是A ->B,则称A为B的父节点),找到父节点未被访问的子节点;如此类推,直到所有的顶点都被访问到。

注意,深度优先的存储方式一般是以栈的方式存储。

<1>无向图的深度优先搜索

<2>有向图的深度优先搜索

<3>深度优先搜索代码

static void DFS(LGraph G, int i,int *visited)
{
    Enode *node;E

    printf(“%c”,G.vexs[i].data);
    node = G.vexs[i].first_edge;
    while(node != NULL)
    {
        if(!visited[node->ivex])
            DFS(G, node->ivex, visited);  //递归调用DFS
        node = node->next_edge;
    }
}
  1. 5.广度优先搜索(Breadth First Search,BFS)

从图中的某个顶点出发,访问它所有的未被访问邻接点,然后分别从这些邻接点出发访问它的邻接点。说白了就是一层一层的访问,“浅尝辄止”!

注意,广度优先搜索的存储方式一般是以队列的方式存储。

<1>无向图的广度优先搜索

<2>有向图的广度优先搜索

<3>广度优先搜索代码

void BFS(LGraph G)
{
    int head = 0;
    int rear = 0;
    int queue[MAX];   //辅助队列
    int visited[MAX];   //顶点访问标记
    eNode *node;

    for(int i = 0; i < G.vexnum; i++)
        visited[i] = 0;  //初始化所有顶点,标记为未访问

    printf(“BFS:”);
    for(int i = 0; i < G.vexnum; i++)
    {
        if(!visited[i])
        {
            visited[i] = 1;
            printf(“%c”,G.vexs[i].data);
            queue[rear++] = i;    //入队
        }
        while(head != rear)
        {
            int j = queue[head++];  //出队
            node = G.vexs[j].first_edge;
            while(node != NULL)
            {
                int k = node->ivex;
                if(!visited[k])
                {
                    visited[k] = 1;
                    printf(“%c”,G.vesx[k].data);
                    queue[rear++] = k;
                }
                node = node->next_edge;
            }
        }
    }
    printf(“\n”);
}
  1. 6.拓扑排序

拓扑排序(Topological Order)是指讲一个有向无环图(Directed Acyclic Graph,DAG)进行排序而得到一个有序的线性序列。

取个栗子,例如我们早上起床的穿衣顺序,如下图所示。穿衣的顺序也是有个优先级的,有些衣服就必须优先穿上,例如领带依赖于衬衣,所以领带最终排在衬衣之后;对图a中的元素进行合理的排序,就得到了图b的次序图。注意,该次序图不是唯一的。

int topological_sort(LGraph G)
{
    int num = G.vexnum;
    ENode *node;
    int head = 0;     //辅助队列的头
    int rear = 0;     // 辅助队列的尾

    int *ins = (int *)malloc(num * sizeof(int));       //入度数组
    char *tops = (char *)malloc(num * sizeof(char));  //拓扑排序结果数组,记录每个节点排序后的序号
    int *queue = (int *)malloc(num * sizeof(int));     //辅助队列
    assert(ins != NULL && tops != NULL && queue != NULL)
    memset(ins, 0, num * sizeof(int));
    memset(tops, 0, num * sizeof(char));
    memset(queue, 0, num * sizeof(int));

    for(int i = 0; i < num; i ++)   //统计每个顶点的入度数
    {
        node = G.vexs[i].first_edge;
        while(node != NULL)
        {
            ins[node->ivex]++;
            node = node->next_edge;
        }
    }

    for(int i = 0; i < num; i++)   //将所有入度为0的顶点装入队列
        if(ins[i] == 0)
            queue[rear++] = i;

    while(head != rear)     //队列非空
    {
        int j = queue[head++];           //出队列,j为顶点的序号
        tops[index++] = G.vexs[j].data;    //将该顶点添加到tops中,tops是排序结果
        node = G.vexs[j].first_edge;       //获取以该顶点为起点的出边队列

        while(node != NULL)
        {
            ins[node->ivex]--;    //将节点node关联的节点的入度减1
            if(ins[node->ivex] == 0)   //若节点的入度为0,则将其添加到队列中
                queue[rear++] = node->ivex;

            node = node->next_edge;
        }
    }

    if(index != G.vexnum)
    {
        printf(“Graph has a cycle!\n”);
        free(queue);
        free(ins);
        free(tops);
        return 1;  //1表示失败,该有向图是有环的
    }

    printf(“== TopSort: ”);   //打印拓扑排序结果
    for(int i = 0; i < num; i++)
        printf(“%c”,top[i]);
    printf(“\n”);

    free(queue);
    free(ins);
    free(tops);
    return 0;
}
  1. 7.最小生成树

所谓最小生成树就是将图中的顶点全部连接起来,此时这个边的权重最小,并且连接起来的是一个无环的树。很容易知道,若此时的顶点是n,则边的数量为n-1。所以在一个图中找最小生成树就是找最小权值的边,让这些边连成一棵树。常用的算法有Prim算法和Kruskal算法。

<1>Prim算法

该算法就是每次迭代选择权值最小的边对应的点,加入到最小生成树中。具体实现如下所示。

第一步:选取顶点A,此时U={A},V-U={B,C,D,E,F,G}。

第二步:选取与A点连接的权值最小的边,此时就会选择到B,U={A,B},V-U={C,D,E,F,G}。

以上面的步骤类推,得到如上图所示的结果,此时U={A,B,C,D,E,F},V-U={G}。注意到C是此次加入的点,而G没有加入,此时G点的边应该如何选择?

最终,得到如图所示的最小生成树,此时U={A,B,C,D,E,F,G},V-U={}。

 

实现代码如下:

#define INF (~(0x1<<31))  //最大值(即0X7FFFFFFF)

//返回ch在邻接表中的位置
static int get_position(LGraph G, char ch)
{
    for(int i = 0; i < G.vexnum; i++)
        if(G.vexnum[i].data == ch)
            return i;
    return -1;
    }

    //获取G中边<start,end>的权值;若start到end不连通,则返回无穷大
    int getWeight(LGraph G, int start, int end)
    {
        ENode *node;

        if(start == end)
            return 0;

        node = G.vexs[start].first_edge;

        while(node != NULL)
        {
            if(end == node->ivex)
                return node->weight;
        node = node->next_edge;
        }
        return INF;
    }

    void Prim(LGraph G,int start)  //从图中的第start个元素开始,生成最小树
    {
        int index = 0;    //prim最小树的索引,即prims数组的索引
        char prims[MAX];  //prim最小树的结果数组
        int wights[MAX];   //顶点间边的权重

        //prim最小生成树中第一个数,即图中的第start个数
        prims[index++] = G.vexs[start].data;

        for(int i = 0; i < G.vexnum; i++)  //初始化顶点的权值数组
            weights[i] = getWeight(G, start, i); //将每个顶点的权值初始化为“第start个顶点”到“该顶点”的权值

        for(int i = 0; i < G.vexnum; i++)
        {
            if(start == i)   //由于从start开始,因此不需要再对第start个顶点进行处理
                continue;
            int j = 0;
            int k = 0;
            int min = INF;
            //在未被加入到最小生成树的顶点中,找出权值最小的顶点
            while(j < G.vexnum)
            {
                //若weights[j]=0,则说明该节点已经加入了最小生成树
                if(weights[j] != 0 && weights[j] < min)
                {
                    min = weights[j];
                    k = j;
                }
                j++;
            }
            //经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。
            //将第k个顶点加入最小生成树的结果数组中
            prims[index++] = G.cexs[k].data;
            //将第k个顶点的权值标记为0,表示该顶点已经加入了最小生成树
            weights[k] = 0;
            //当第k个顶点被加入到最小生成树的结果数组后,更新其他顶点的权值
            for(int j = 0; j < G.vexnum; j++)
            {
                //获取第k个顶点到第j个顶点的权值
                int temp = getWeight(G, k, j);
                //当第j个节点没有被处理,并且需要更新的时候才会更新
                if(weights[j] != 0 && temp < weights[j])
                    weights[j] = temp;
            }
    }
    //计算最小生成树的权值
    int sum = 0;
    for(int i = 0; i < index; i++)
    {
        min = INF;
        //获取prims[i]在G中的位置
        int n = get_position(G, prims[i]);
        //在vexs[0...i]中,找出到j的权值最小的顶点。
        for(int j = 0; j < i; j++)
        {
           int m = get_position(G,prims[j]);
           int temp = getWeight(G, m, n);
           if(temp < min)
               min = temp;
        }
        sum += min;
    }
    //打印最小生成树
    printf(“Prim(%c) = %d : ”, G.vexs[start].data, sum);
    for(int i = 0; i < index; i++)
        printf(“%c ”,prim[i]);
    printf(“\n”);
}

<2>Kruskal算法

该算法的核心就是对权值进行排序,然后从最小的权值开始,不断增大权值,如何该权值的所在边的两个顶点没有存在的路径连在一起,则加入这条边,否则,则舍弃这条边,知道所有的点都在这颗树中。

如下所示的一个图,我们从中找出最小生成树。

对于左边所示的的图,对各个边的权值排序之后,我们最先找到权值最小的边,即AD。然后我们发现还有一个CE,于是CE也会被标记起来。

对于左边所示的的图,对各个边的权值排序之后,我们最先找到权值最小的边,即AD。然后我们发现还有一个CE,于是CE也会被标记起来。

代码如下:

typedef struct edata  //边的结构体
{
    char start;  //边的起点
    char end;   //边的终点
    int weight;  //边的权重
}EData;

EData *get_edges(LGraph G)
{
    int index = 0;
    ENode *node;
    EData *edges;

    edges = (EData *)malloc(G.edgnum * sizeof(EData));

    for(int i = 0; i < G.vexnum; i++)
    {
        node = G.vexs[i].first_edge;
        while(node != NULL)
        {
            if(node->ivex > i)
            {
                edges[index].start = G.vexs[i].data;
                edges[index].end = G.vexs[node->ivex].data;
                edges[index].weight = node->weight;
                index++;
            }
            node = node->next_edge;
        }
    }
    return edges;
}

void Kruskal(LGraph G)
{
    int index = 0;     //rets数组的索引
    int vends[MAX] = {0};    //用于保存“已有最小生成树”中每个顶点在该最小树中的终点
    EData rets[MAX];    //结果数组,保存kruskal最小生成树的边
    EData *edges;     //图对应的所有边

    edges = get_edges(G);   //获取图中所有的边
    Sorted_edges(edges, G.edgenum);   //对边按照权值进行排序

    for(int i = 0; i < G.edgenum; i++)
    {
        int numOfStart = get_position(G, edges[i].start);  //获取第i条边的起点的序号
        int numOfEnd = get_position(G, edges[i].end);    //获取第i条边的终点的序号

        int m = get_end(vends, numOfStart);  //获取numOfStart在“已有的最小生成树”中的终点
        int n = get_end(vends, numOfEnd);    //获取numOfEnd在“已有的最小生成树”中的终点
        //如果m!=n,表示边i与已经添加到最小生成树中的顶点没有形成环路
        if(m != n)
        {
            vends[m] = n;    //设置m在已有的最小生成树中的终点为n
            rets[index++] = edges[i];   //保存结果
        } 
    }
    free(edges);
    //统计并打印最小生成树的信息
    int length = 0;
    for(int i = 0; i < index; i++)
        length += rets[i].weight;
    printf(“Kruskal = %d : ”,length);
    for(int i = 0; i < index; i++)
        printf(“(%c,%c)”, rets[i].start, rets[i].end);
    printf(“\n”);
}

参考文档

www.cnblogs.com/skywang12345/p/3603935.html

https://zhuanlan.zhihu.com/p/25498681

《算法导论》

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值