图的建立(邻接矩阵,邻接链表,链式前向星)

建图,就是将一个图放进内存的算法。

常用的建图方式有三种:邻接矩阵,邻接链表(vector建表),链式前向星。做题时我个人常用是邻接矩阵和链式前向星,链式前向星是最灵巧和节约空间的,深受ACMER喜爱。但同时也是三种算法里最难理解的。一旦理解,就是如鱼得水,所以我会着重讲链式前向星。后续图论的题,也推荐大家尽量熟悉和使用链式前向星。

邻接矩阵

用二维数组模拟建图,作为最经典和最简单的方式,这种算法最为直观,也深受新手喜爱。它最大的缺点是占用空间太大!在稀疏图中,浪费空间极其严重。(稀疏图即点与点之间联系不多的图)

现有以下无向加权图:

图的建立(邻接矩阵,邻接链表,链式前向星)_第1张图片

(注:以上图像非原创,仅用于个人复习和教学,如有侵权立删。那个水印它自己加的)

通过邻接矩阵存储:

先看看结果:

图的建立(邻接矩阵,邻接链表,链式前向星)_第2张图片

对照着上图自己看一看。

*图中第一个值的地址为map[1] [1] 。

简单说明一下邻接矩阵建加权图的特点:

1.一个顶点到它自己的距离为0,所以对角线上的值都是0.

2.如果两个顶点之间没有连线,则用-1或者INF(无限大,通常用0x3f3f3f3f配合memset使用,图中为1061109567)表示

3.如果两个顶点之间有连线,则用权值表示,如上图所示。map[1] [2]表示顶点1到顶点2的距离是30.

代码,详细说明都在代码里:

#include
#include
#include
#include
using namespace std;
const int MAX_N = 200;//最大节点个数
const int INF = 0x3f3f3f3f;//无限大标志,必须配合memset使用才能表示无限大!
//邻接矩阵
int map[200][200];//这里的map只是一个变量名字,不是C++stl里那个Map!!


//初始化函数
void init(){
    memset(map,INF,sizeof(map));//这句话的意思是把map二维数组里的值全置为无限大
}

//建图
void get_map(int n,int m)
{
    //每个顶点到它自己的距离是0
    for(int i = 1;i<=n;i++)
    {
        map[i][i] = 0;
    }

    printf("请输入起点、终点,边权:\n");
    printf("顶点编号不得大于顶点个数\n");
    for(int i = 0; i

无权图建图方式和有权图大同小异,无权图就是没有边权的图,那么有边值为1,没有边值为0就好。

代码里每一个注释都看看,都是细节。C++和C区别不大,这里尽量都用的C的格式,能看懂。


邻接链表(vector建表)  

实际上vector建图不是严格意义上的邻接链表建图,因为真正的邻接链表建图要向链表那样写,用vector是在模拟链表。说他是邻接链表其实不够准确。这种建图方式我个人用的少,看个人喜好吧。

什么是vector?

vector是C++ STL里一个容器,可变数组,当现数组满了的时候,它会重新分配更大的数组,然后将原数组的值一次拷贝到新数组(实际上是一种比较蠢的动态方式)。现阶段只需要会用它建图就好。

什么是邻接链表建图?

简单来说,一个顶点连接的终点连成一个链表,然后有几个顶点就有几个链表。

图示:

图的建立(邻接矩阵,邻接链表,链式前向星)_第3张图片

邻接链表的形式:

图的建立(邻接矩阵,邻接链表,链式前向星)_第4张图片

 (终于还是动手画图了呜呜,总用别人的也不太好)

 我们发现这样的话只能保存连接到哪个顶点,但是不能保存边权值。所以我们需要一个结构体,既保存终点的编号,同时保存边权值

typedef struct Node{
    int v;//终点编号
    int w;//起点到终点的边权
}Node;

所以:

图的建立(邻接矩阵,邻接链表,链式前向星)_第5张图片

vector建图的优点:

1.写起来方便,动态分配。比起邻接矩阵,节省了很多无效空间。

缺点:不太了解,我觉得重分配数组很蠢。(个人看法)

图的建立(邻接矩阵,邻接链表,链式前向星)_第6张图片

采用vector模拟邻接链表建图:

还是一样,先上结果。

图的建立(邻接矩阵,邻接链表,链式前向星)_第7张图片

对照着上图应该很好看懂。

代码:

关于vector的用法可以自己去学学。

#include
#include
#include
#include
#include
using namespace std;
const int MAX_N = 200;//最大节点个数

typedef struct Node{
    int v;//终点编号
    int w;//起点到终点的边权
}Node;
vector map[MAX_N];//MAX_N为最大顶点个数
//建图
void get_map(int m)
{
    printf("请输入边的起点 终点 权值:\n");
    for(int i = 1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        Node tmp;
        tmp.v = v;tmp.w = w;
        map[u].push_back(tmp);//将终点节点放进链表末尾
        tmp.v = u;tmp.w = w;//如果是有向图,这句和下面一句不用加
        map[v].push_back(tmp);
    }
}

//输出图
void print_map(int n){      
    for(int i = 1;i<=n;i++){
        printf("起点%d->",i);
        for(int j = 0;j");
        }
        printf("\n");
    }
}

int main(){
    int n,m;
    printf("请输入顶点个数,边个数:\n");
    scanf("%d %d",&n,&m);
    //建图
    get_map(m);
    //输出图
    print_map(n);
    return 0;
}

链式前向星 

稍微有点难讲,我尽量用自己的理解说清楚。如果没学懂,有时间一定要去参考各种视频和文章弄懂这个,真的超级有用。现在图论的题解很多都是用的链式前向星,不学题解都看不明白。

 上述邻接矩阵和邻接链表的形式,我们都是存的点与点的信息,在邻接矩阵中,如果两个点i,j之间有边,则map[i][j]有不为0且不为无穷大INF的值。在邻接链表中,我们每个链表的表头存的是起点,链表中每个节点存与起点直接连接的终点以及到终点的边权的信息。而在链式前向星中,我们将换成存边的形式来存图。

现有以下图:

图的建立(邻接矩阵,邻接链表,链式前向星)_第8张图片

 链式前向星所用结构体和相关变量、数组:

const int MAX_N = 200; //最大顶点数
const int MAX_M = 2e5; //最大边数
//___________链式前向星___________
typedef struct E{
    int v;//这条边指向的终点
    int nxt;//跳转到相关的下一条边(下面会说)
    int w;//这条边的权值
}E;//存边的结构体
E edge[MAX_M];//存边数组
int head[MAX_N];//下面会说
int cnt = 0;//边的编号,下面会说

还是根据结果来倒推链式前向星到底是什么东西。

上述图以链式前向星形式存放的最终形式:

 图的建立(邻接矩阵,邻接链表,链式前向星)_第9张图片

v和w就不多说了,应该很好看出来是什么,这个nxt,它的规律其实是:某一条边的起点连接的上一个编号比它小的边。(这也是为什么它叫前向星的原因),比如我们看顶点A,A有四条边,分别是1,2,6,8.表格中8的nxt是6,6的nxt是2,2的nxt是1,1没有下一条边了,所以1的nxt是-1.

这样我们就有规律遍历出同一个起点的所有边了。但是还有一个问题,我们怎么知道一个顶点的第一条边是什么呢?所以我们需要一个head数组来储存每一个顶点的第一条边

 图的建立(邻接矩阵,邻接链表,链式前向星)_第10张图片

每个下标为顶点编号,对应的值为以该顶点为起点的编号最大的一条边。

注意这是个有向图,待会儿的代码里会说在哪一步存无向图。

上述最终存放表格看懂了就理解了链式前向星的原理。接下来一步一步放代码并解析。

首先我们应该初始化数组和相关变量:

//初始化函数
void init()
{
    memset(head,-1,sizeof(head));//将head数组的值全置为-1.
    cnt = 0;//初始化边的编号
}

 加一条边的函数:

void add(int u,int v,int w)//u起点,v终点,w权值
{
    edge[++cnt].v = v;
    edge[cnt].w = w;
    edge[cnt].nxt = head[u];
    head[u] = cnt; 
}

这个代码看不懂最好自己调试时看看egde数组和head数组是怎么变化的。

关于怎么遍历会在代码里讲。在输出图那个函数里,一定要看看,不然你只会存图不会遍历。

#include
#include
#include
#include
#include
using namespace std;
const int MAX_N = 200;//最大节点个数
const int MAX_M = 2e5;//完全图的边数为n*(n-1)/2,即n^2级别
typedef struct Node{
    int v;//终点编号
    int w;//起点到终点的边权
}Node;
//___________链式前向星___________
typedef struct TT{int v,nxt,w;}E;
E edge[MAX_M];
int head[MAX_N];
int cnt = 0;

//初始化函数
void init(){
    memset(head,-1,sizeof(head));
    cnt = 0;
    memset(edge,0,sizeof(edge));//这一句写不写都无所谓
}

//加边
inline void add(int u,int v,int w)
{
    edge[++cnt].v = v;//注意这里是++cnt
    edge[cnt].w = w;
    edge[cnt].nxt = head[u];
    head[u] = cnt; 
}


//建图
void get_map(int m)
{
    printf("请输入边的起点 终点 权值:\n");
    for(int i = 1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w);
        //add(v,u,w); //如果是无向图,加上这一句,即反过去加一条边
    }
}

//输出图
void print_map(int n){      
    //以顶点形式遍历边
    for(int i = 1;i<=n;i++){
        printf("起点%c:",i+'A'-1);
        //以下是遍历某一个顶点的所有边,e代表边的编号,~e等同于e!=-1
        for(int e = head[i];~e;e = edge[e].nxt){
            int v = edge[e].v;
            int w = edge[e].w;
            printf("\t边编号:%d,终点:%c,权值:%d",e,v+'A'-1,w);
        }
        printf("\n");
    }
}

int main(){
    int n,m;
    init();//记得初始化
    printf("请输入顶点个数,边个数:\n");
    scanf("%d %d",&n,&m);
    //建图
    get_map(m);
    //输出图
    print_map(n);
    return 0;
}

//测试数据
/*
5 9
1 2 10
1 3 20
3 5 30
4 5 90
2 4 80
1 5 40
2 3 50
1 4 60
2 5 70
*/

运行结果:

图的建立(邻接矩阵,邻接链表,链式前向星)_第11张图片 这个结果就是之前那个图,加边的时候顶点A即1,B即2...E即5.


三种算法总结:

邻接矩阵优点:

直接,对于新手较为友好,上手简单。但是限制太大,最多只能开20000*20000左右,就是最多20000个顶点。大于这个数就会爆栈。而且有较多内存浪费。

邻接链表(vector)建图:

个人没怎么用过,这种建图步骤较为简单,也是推荐建图方式吧。只是说vector采用可变数组的方式,动态变化时都会耗费额外的时间复制数组,再加上本身就是STL,速度较慢。

链式前向星:

优点:太多,直接说缺点吧,新手不易上手。其他我觉得没什么缺点了。

你可能感兴趣的:(图论,ACM学习,图论,链表,数据结构)