zoukankan      html  css  js  c++  java
  • 图论其一:图的存储

    图的存储

      图的存储大致分为三类:邻接矩阵,前向星,邻接表。

    邻接矩阵

      邻接矩阵是表示图的数据结构中最简单也是最常用的一种,对于一个有N个点的图,建立一个N* N的矩阵,这个矩阵的第i 行第j 列的值表示Vi 到Vj 的距离。

      邻接矩阵需要初始化,map[i ][i ]= 0, map[i ][j ]= inf。对每组输入的Vi ,Vj 和 Wij ,赋值为map[i ][j ]= Wij 。

       

    【图源维基百科】

      上图中两点间有边连接则为1 ,否则为0 。无向图中不用考虑边的方向。

      对于一个N个点,M条边的图,邻接矩阵的初始化需要O(N2)的时间,建图需要O(M)的时间;总的时间复杂度是O(N2),空间复杂度也是O(N2)。

      邻接矩阵的优点是实现简单,可以直接查询两点之间是否有边和边的权值;缺点是遍历效率极低,并且不能储存重边;初始化效率低;当N比较大时(>=1E+5),建一个邻接矩阵的思路是不现实的;且对于稀疏图(E< N*logN),邻接矩阵的空间利用效率不高,会造成大量浪费。

    前向星

      前向星的构造方式非常简单,读入每条边的信息,将边存放在数组中,把数组中的边按照起点顺序排序,前向星就构造完成了。为了查询方便,常用一个数组存储起点为Vi 的第一条边的位置。

      ①存储结构和比较函数

    const int maxn= 1E+5;
    
    struct Node
    {
        int from;       //起点;
        int to;         //终点;
        int w;          //权;
    };
    bool cmpx (Node a, Node b)
    {
        if (a.from== b.from&& a.to== b.to) return a.w- b.w< 0;
        if (a.from== b.from) return a.to- b.to< 0;
        return a.from- b.from< 0;
    }
    Node ENode[maxn];   //边集;
    int Head[maxn];     //记录每个起点的第一条边的位置;

    Node类型用来保存边;所有的边都存放进ENode[ ]中;Head[ ]存储从Vi 出发的第一条边的地址;cmpx按照 from> to> w 的优先级, 以从小到大的顺序对ENode[ ]里的边排序;

      ②数据输入

        int n, m;
        cin >>n >>m;
        int a, b, w;
        //有向图建图;
        for (int i = 0; i < m; i ++)
        {
            cin >>a >>b >>w;
            ENode[i].from= a;
            ENode[i].to= b;
            ENode[i].w= w;
        }
        sort(ENode, ENode+ m, cmpx);
        memset(Head, -1, sizeof(Head));//如果一个点出发没有边的话,Head[]= -1;
        Head[ENode[0].from ]= 0;
        for (int i = 1; i < m; i ++)
        {
            if (ENode[i].from!= ENode[i- 1].from ) Head[ENode[i].from ]= i;
        }

    要注意的是,无向图时,一条边需要保存两边;

        int n, m;
        cin >>n >>m;
        int a, b, w;
        //无向图建图;
        for (int i = 0; i < m* 2; i ++)
        {
            cin >>a >>b >>w;
            ENode[i].from= a;
            ENode[i].to= b;
            ENode[i].w= w;
            ++ i;       //无向图需要存两次边,各自从一个起点出发;
            ENode[i].from= b;
            ENode[i].to= a;
            ENode[i].w= w;
        }
        sort(ENode, ENode+ m*2, cmpx);
        memset(Head, -1, sizeof(Head));//如果一个点出发没有边的话,Head[]= -1;
        Head[ENode[0].from ]= 0;
        for (int i = 1; i < m* 2; i ++)
        {
            if (ENode[i].from!= ENode[i- 1].from ) Head[ENode[i].from ]= i;
        }

      ③遍历

        for (int j= 1; j <= n; j ++) //从编号为1 的点开始;
        {
            for (int k= Head[j]; ENode[k].from== j&& k< m; k ++)
            {
                cout <<k <<":" <<ENode[k].from <<" " <<ENode[k].to <<" " <<ENode[k].w <<endl;
            }
        }

    注意无向图的边数是m* 2;

        //遍历;
        for (int j= 1; j <= n; j ++) //从编号为1 的点开始;
        {
            for (int k= Head[j]; ENode[k].from== j&& k< m* 2; k ++)
            {
                cout <<k <<":" <<ENode[k].from <<" " <<ENode[k].to <<" " <<ENode[k].w <<endl;
            }
        }

    然后我们来举个例子,以下面的数据为例:

    5 5
    1 2 4
    1 3 5
    1 4 6
    1 5 7
    2 3 8

    我们来做一些修改,将无向图遍历代码改为:

        //遍历;
        for (int j= 1; j <= n; j ++) //从编号为1 的点开始;
        {
            cout <<j <<": ";
            for (int k= Head[j]; ENode[k].from== j&& k< m* 2; k ++)
            {
                cout <<"-->" <<ENode[k].to <<" ";
            }
            cout << endl;
        }

    我们会看到:

    我们能看到,对于从1 这个点出发的每一条边,都按照终点的大小从小到大排序了;

     由于涉及排序,所以前向星的构造时间复杂度与使用的排序算法有关,一般情况下时间复杂度为O(M*logM),空间复杂度为O(M+N);

    前向星的优点在于可以应对非常多的情况,可以存储重边;但缺点在于不能直接判断任意两点间是否有边,而且排序需要浪费一些时间。

     ps:对于判断两点间是否存在边的问题,个人认为,由于前向星对从Vi 出发的每一条边都按照终点进行了排序,故可以使用二分进行查找;

    #include<iostream>
    #include<algorithm>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    const int maxn= 1E+5;
    
    struct Node
    {
        int from;       //起点;
        int to;         //终点;
        int w;          //权;
    };
    bool cmpx (Node a, Node b)
    {
        if (a.from== b.from&& a.to== b.to) return a.w- b.w< 0;
        if (a.from== b.from) return a.to- b.to< 0;
        return a.from- b.from< 0;
    }
    Node ENode[maxn];   //边集;
    int Head[maxn];     //记录每个起点的第一条边的位置;
    int main()
    {
        int n, m;
        cin >>n >>m;
        int a, b, w;
        //无向图建图;
        for (int i = 0; i < m* 2; i ++)
        {
            cin >>a >>b >>w;
            ENode[i].from= a;
            ENode[i].to= b;
            ENode[i].w= w;
            ++ i;       //无向图需要存两次边,各自从一个起点出发;
            ENode[i].from= b;
            ENode[i].to= a;
            ENode[i].w= w;
        }
        sort(ENode, ENode+ m*2, cmpx);
        memset(Head, -1, sizeof(Head));//如果一个点出发没有边的话,Head[]= -1;
        Head[ENode[0].from ]= 0;
        for (int i = 1; i < m* 2; i ++)
        {
            if (ENode[i].from!= ENode[i- 1].from ) Head[ENode[i].from ]= i;
        }
        //遍历;
        for (int j= 1; j <= n; j ++) //从编号为1 的点开始;
        {
            for (int k= Head[j]; ENode[k].from== j&& k< m* 2; k ++)
            {
                cout <<k <<":" <<ENode[k].from <<" " <<ENode[k].to <<" " <<ENode[k].w <<endl;
            }
        }
        return 0;
    }
    前向星(无向图)

    邻接表

      邻接表是图的一种链式存储结构。对于图中的每个顶点Vi ,把所有邻接于Vi 的顶点Vj 链成一个单链表,这个单链表称为顶点Vi 的邻接表。

      邻接表有三种实现方式:动态建表实现,vector模拟链表实现和静态链表实现(链式前向星);

    动态建表

      ①储存结构(以无向图为例)

    const int maxn= 1E+5;
    
    struct ENode           //邻接表节点,边节点;
    {
        int to;            //终点,边指向的点;
        int w;             //权;
        ENode * next;      //指向下一条边的指针;
    };
    struct VNode
    {
        int from;          //起点表节点,点节点;
        ENode * first= NULL;     //表头指针,指向此点邻接表的第一个边节点;
    };
    VNode Adj[maxn];       //存储所有的点节点;

    ENode是边类型,保存一条边的属性,是单链表的节点;next指针指向下一条边;

    VNode是点类型,保存邻接表的表头,是单链表的起点;first指针指向单链表(Vi的邻接表)的第一个边节点。

      ②数据输入

        int n, m;
        cin >>n >>m;
        int a, b, w;
        //无向图建图;
        for (int i = 0; i < m; i ++)
        {
            cin >>a >>b >>w;
            ENode * p1= new ENode();
            p1->to= b;
            p1->w= w;
            p1->next= Adj[a].first;
            Adj[a].first= p1;
            ENode * p2= new ENode();
            p2->to= a;
            p2->w= w;
            p2->next= Adj[b].first;
            Adj[b].first= p2;
        }

    同样,无向图要记得存两次边。

      ③遍历

        for (int i = 1;i <= n; i ++)
        {
            cout << i << ": ";
            for (ENode * k = Adj[i].first; k != NULL; k= k->next )
            {
                cout <<"-->" << k->to << " ";
            }
            cout << endl;
        }

    以上面的例子测试程序:

    可以看到,以1 出发,-->5 -->4 -->3 -->2; 这个顺序刚好与我们输入时的顺序相反,这是链式存储的特点;

      对于无向图的邻接表,顶点Vi的入度恰好是第i个链表中的节点数;而在有向图中第i个链表的节点数只是Vi的出度。为了求入度,必须遍历整个邻接表或者建立一个逆邻接表(以Vi 为边终点的邻接表)。

      对于动态建立的邻接表,他的时间效率是O(M),空间效率也是O(M)。动态建表,不会浪费多余的空间,需要多少申请多少,但这些内存的释放就是个问题了(why?)。另外,一次申请内存和随机申请相比,再申请内存总量相同的情况下,前者明显消耗的时间更少,这也是邻接表的缺点之一。判断两个顶点Vi 和Vj 之间是否连接,需要搜索两条链表。

    #include<iostream>
    #include<algorithm>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    const int maxn= 1E+5;
    
    struct ENode           //邻接表节点,边节点;
    {
        int to;            //终点,边指向的点;
        int w;             //权;
        ENode * next;      //指向下一条边的指针;
    };
    struct VNode
    {
        int from;          //起点表节点,点节点;
        ENode * first= NULL;     //表头指针,指向此点邻接表的第一个边节点;
    };
    VNode Adj[maxn];       //存储所有的点节点;
    int main()
    {
        int n, m;
        cin >>n >>m;
        int a, b, w;
        //无向图建图;
        for (int i = 0; i < m; i ++)
        {
            cin >>a >>b >>w;
            ENode * p1= new ENode();
            p1->to= b;
            p1->w= w;
            p1->next= Adj[a].first;
            Adj[a].first= p1;
            ENode * p2= new ENode();
            p2->to= a;
            p2->w= w;
            p2->next= Adj[b].first;
            Adj[b].first= p2;
        }
        //遍历;
        for (int i = 1;i <= n; i ++)
        {
            cout << i << ": ";
            for (ENode * k = Adj[i].first; k != NULL; k= k->next )
            {
                cout <<"-->" << k->to << " ";
            }
            cout << endl;
        }
        return 0;
    }
    邻接表(动态建表)

    vector模拟链表

      指用vector来模拟(代替)链表而实现邻接表。

      ①存储结构

    const int maxn= 1E+5;
    
    struct ENode           //邻接表节点,边节点;
    {
        int to;            //终点,边指向的点;
        int w;             //权;
    };
    vector<ENode> Adj[maxn];

      ②数据输入

        int n, m;
        cin >>n >>m;
        int a, b, w;
        //无向图建图;
        for (int i = 0; i < m; i ++)
        {
            cin >>a >>b >>w;
            ENode e1, e2;
            e1.to= b;
            e1.w= w;
            Adj[a].push_back(e1);
            e2.to= a;
            e2.w= w;
            Adj[b].push_back(e2);
        }

      ③遍历

        for (int i = 1;i <= n; i ++)
        {
            cout << i << ": ";
            for (vector<ENode>::iterator it= Adj[i].begin(); it!= Adj[i].end(); it ++ )
            {
                ENode e= *it;
                cout <<"-->" <<e.to <<" ";
            }
            cout << endl;
        }

    以上面的例子测试程序:

    以1 出发,-->2 -->3 -->4 -->5; 这个顺序刚好与我们输入时的顺序相同,这是vector模拟与链式存储的不同;

    和前一种实际的区别不大,可能优势是好写吧。

    #include<iostream>
    #include<algorithm>
    #include<vector>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    const int maxn= 1E+5;
    
    struct ENode           //邻接表节点,边节点;
    {
        int to;            //终点,边指向的点;
        int w;             //权;
    };
    vector<ENode> Adj[maxn];
    int main()
    {
        int n, m;
        cin >>n >>m;
        int a, b, w;
        //无向图建图;
        for (int i = 0; i < m; i ++)
        {
            cin >>a >>b >>w;
            ENode e1, e2;
            e1.to= b;
            e1.w= w;
            Adj[a].push_back(e1);
            e2.to= a;
            e2.w= w;
            Adj[b].push_back(e2);
        }
        //遍历;
        for (int i = 1;i <= n; i ++)
        {
            cout << i << ": ";
            for (vector<ENode>::iterator it= Adj[i].begin(); it!= Adj[i].end(); it ++ )
            {
                ENode e= *it;
                cout <<"-->" <<e.to <<" ";
            }
            cout << endl;
        }
        return 0;
    }
    vector模拟链表

    静态链表(链式前向星)

      邻接表的静态建表存储图的方式,也称链式前向星。链式前向星最开始是基于前向星,以提高其构造效率为目的设计的存储方式,最终形成的数据却是一个变形的邻接表。

      链式前向星采用数组模拟链表的方式实现邻接表的功能,并且使用很少的额外空间,可以是说目前建图和遍历效率最高的存储方式

      ①存储结构

    const int maxn= 1E+5;
    
    struct ENode           //邻接表节点,边节点;
    {
        int to;            //终点,边指向的点;
        int w;             //权;
        int next;          //记录下一条边在ENode[]数组中的位置;
    };
    ENode Edegs[maxn];     //存放所有的边;
    int Head[maxn];        //记录点Vi的第一条边在ENode中的位置;

    ENode与之前在邻接表中的相似,都是保存边的属性,唯一不同的应该是"next指针"不再是指针类型,而是换成了int类型,储存的是下一条边在Edegs[ ]中的地址;

    Head[ ]与在前向星中的功能相同,保存Vi 的第一条边在Edegs[ ]中的地址;

      ②数据输入

        int n, m;
        cin >>n >>m;
        int a, b, w;
        //无向图建图;
        memset(Head, -1, sizeof(Head));
        for (int i = 0; i < m* 2; i ++)
        {
            cin >>a >>b >>w;
            Edegs[i].to= b;
            Edegs[i].w= w;
            Edegs[i].next= Head[a];
            Head[a]= i;
            ++ i;          //无向图边存第二次;
            Edegs[i].to= a;
            Edegs[i].w= w;
            Edegs[i].next= Head[b];
            Head[b]= i;
        }

    没什么特别的,记得每次更新Head[ ]即可;

      ③遍历

        //遍历;
        for (int i = 1;i <= n; i ++)
        {
            cout << i << ": ";
            for (int j= Head[i]; j!= -1; j= Edegs[j].next )
            {
                cout <<"-->" <<Edegs[j].to <<" ";
            }
            cout << endl;
        }

    用上面的例子检验:

    可以看出:

    a. 以1 出发,-->5 -->4 -->3 -->2; 顺序与我们输入时的顺序相反,这是链式存储的特点,所以链式前向星对边的储存继承了邻接表(链式结构);

    b.同样由上面看出,前向星原本对边排序的特性并没有保留下来(是否可以尝试继承呢?算了吧,少了排序省了好多时间呢 /笑)。

    c.影响是?缺失了排序的原因是,使用链式的方式对同源的边进行连接,使之前对同一起点的边连续存放的必要性丢失,当然要找特定的边也更不好找了qaq。

    #include<iostream>
    #include<algorithm>
    #include<vector>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    const int maxn= 1E+5;
    
    struct ENode           //邻接表节点,边节点;
    {
        int to;            //终点,边指向的点;
        int w;             //权;
        int next;          //记录下一条边在ENode[]数组中的位置;
    };
    ENode Edegs[maxn];     //存放所有的边;
    int Head[maxn];        //记录点Vi的第一条边在ENode中的位置;
    int main()
    {
        int n, m;
        cin >>n >>m;
        int a, b, w;
        //无向图建图;
        memset(Head, -1, sizeof(Head));
        for (int i = 0; i < m* 2; i ++)
        {
            cin >>a >>b >>w;
            Edegs[i].to= b;
            Edegs[i].w= w;
            Edegs[i].next= Head[a];
            Head[a]= i;
            ++ i;          //无向图边存第二次;
            Edegs[i].to= a;
            Edegs[i].w= w;
            Edegs[i].next= Head[b];
            Head[b]= i;
        }
        //遍历;
        for (int i = 1;i <= n; i ++)
        {
            cout << i << ": ";
            for (int j= Head[i]; j!= -1; j= Edegs[j].next )
            {
                cout <<"-->" <<Edegs[j].to <<" ";
            }
            cout << endl;
        }
        return 0;
    }
    链式前向星(无向图)

    谢谢各位看到最后。

  • 相关阅读:
    简而言之C语言:“char类型省空间”只是一个传说
    原来曾经有人支持过我,感动!
    编程的“武林秘籍”
    没有一种语言解决所有问题
    简而言之C语言:const声明
    关于代码的些许感想
    树莓派上搭建arduino命令行开发环境
    IOT设备的7大安全问题
    OsmocomBB软件实现栈概况
    Win7以上 32/64位系统隐藏托盘图标
  • 原文地址:https://www.cnblogs.com/Amaris-diana/p/10541756.html
Copyright © 2011-2022 走看看