图论其一:图的存储

图的存储

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

 

邻接矩阵

  邻接矩阵是表示图的数据结构中最简单也是最常用的一种,对于一个有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;
}
链式前向星(无向图)

 

谢谢各位看到最后。

posted @ 2019-03-16 16:05  egoist的翻绳游戏  阅读(468)  评论(0编辑  收藏  举报