用数组模拟邻接表

用图的邻接矩阵存储法,它的空间和时间复杂度都是N2,现在我来介绍另外一种存储图的方法:邻接表,这样空间和时间复杂度就都是M。对于稀疏图来说,M要远远小于N2。先上数据,如下

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

091650e0f00oqrcjcfnq93.png

第一行两个整数n m。n表示顶点个数(顶点编号为1~n),m表示边的条数。接下来m行表示,每行有3个数x y z,表示顶点x到顶点y的边的权值为z。下图就是一种使用链表来实现邻接表的方法。

091650gyll6hbqbjyxls8s.png

上面这种实现方法为图中的每一个顶点(左边部分)都建立了一个单链表(右边部分)。这样我们就可以通过遍历每个顶点的链表,从而得到该顶点所有的边了。使用链表来实现邻接表对于痛恨指针的的朋友来说,这简直就是噩梦。这里我将为大家介绍另一种使用数组来实现的邻接表,这是一种在实际应用中非常容易实现的方法。这种方法为每个顶点i(i从1~n)也都保存了一个类似“链表”的东西,里面保存的是从顶点i出发的所有的边,具体如下。

首先我们按照读入的顺序为每一条边进行编号(1~m)。比如第一条边“1 4 9”的编号就是1,“1 3 7”这条边的编号是5。

这里用u、v和w三个数组用来记录每条边的具体信息,即u[i]、v[i]和w[i]表示第i条边是从第u[i]号顶点到v[i]号顶点(u[i]àv[i]),且权值为w[i]。

091650h35zq3wgx30x3oe3.png

再用一个first数组来存储每个顶点其中一条边的编号。以便待会我们来枚举每个顶点所有的边(你可能会问:存储其中一条边的编号就可以了?不可能吧,每个顶点都需要存储其所有边的编号才行吧!甭着急,继续往下看)。比如1号顶点有一条边是 “1 4 9”(该条边的编号是1),那么就将first[1]的值设为1。如果某个顶点i没有以该顶点为起始点的边,则将first[i]的值设为-1。现在我们来看看具体如何操作,初始状态如下。

091650zw3988qpj5iljj8g.png

咦?上图中怎么多了一个next数组,有什么作用呢?不着急,待会再解释,现在先读入第一条边“1 4 9”。

读入第1条边(1 4 9),将这条边的信息存储到u[1]、v[1]和w[1]中。同时为这条边赋予一个编号,因为这条边是最先读入的,存储在u、v和w数组下标为1的单元格中,因此编号就是1。这条边的起始点是1号顶点,因此将first[1]的值设为1。

另外这条“编号为1的边”是以1号顶点(即u[1])为起始点的第一条边,所以要将next[1]的值设为-1。也就是说,如果当前这条“编号为i的边”,是我们发现的以u[i]为起始点的第一条边,就将next[i]的值设为-1(貌似的这个next数组很神秘啊⊙_⊙)。

091651kwo5g0aycy07wfwd.png

读入第2条边(4 3 8),将这条边的信息存储到u[2]、v[2]和w[2]中,这条边的编号为2。这条边的起始顶点是4号顶点,因此将first[4]的值设为2。另外这条“编号为2的边”是我们发现以4号顶点为起始点的第一条边,所以将next[2]的值设为-1。

091651fkswcj34c05k8w4k.png

读入第3条边(1 2 5),将这条边的信息存储到u[3]、v[3]和w[3]中,这条边的编号为3,起始顶点是1号顶点。我们发现1号顶点已经有一条“编号为1 的边”了,如果此时将first[1]的值设为3,那“编号为1的边”岂不是就丢失了?我有办法,此时只需将next[3]的值设为1即可。现在你知道next数组是用来做什么的吧。next[i]存储的是“编号为i的边”的“前一条边”的编号。

091651df28foy9ct7fl7qf.png

读入第4条边(2 4 6),将这条边的信息存储到u[4]、v[4]和w[4]中,这条边的编号为4,起始顶点是2号顶点,因此将first[2]的值设为4。另外这条“编号为4的边”是我们发现以2号顶点为起始点的第一条边,所以将next[4]的值设为-1。

091652vf4eg69f5zfsese9.png

读入第5条边(1 3 7),将这条边的信息存储到u[5]、v[5]和w[5]中,这条边的编号为5,起始顶点又是1号顶点。此时需要将first[1]的值设为5,并将next[5]的值改为3。

091652li6mzammza242tmp.png

此时,如果我们想遍历1号顶点的每一条边就很简单了。1号顶点的其中一条边的编号存储在first[1]中。其余的边则可以通过next数组寻找到。请看下图。

091652rtjh5qe2211eee58.png

细心的同学会发现,此时遍历边某个顶点边的时候的遍历顺序正好与读入时候的顺序相反。因为在为每个顶点插入边的时候都直接插入“链表”的首部而不是尾部。不过这并不会产生任何问题,这正是这种方法的其妙之处。

创建邻接表的代码如下。

int n,m,i;   
//u、v和w的数组大小要根据实际情况来设置,要比m的最大值要大1   
int u[6],v[6],w[6];   
//first和next的数组大小要根据实际情况来设置,要比n的最大值要大1   
int first[5],next[5];   
scanf("%d %d",&n,&m);   
//初始化first数组下标1~n的值为-1,表示1~n顶点暂时都没有边   
for(i=1;i<=n;i++)   
    first[i]=-1;   
for(i=1;i<=m;i++)   
{   
    scanf("%d %d %d",&u[i],&v[i],&w[i]);//读入每一条边   
    //下面两句是关键啦   
    next[i]=first[u[i]];   
    first[u[i]]=i;   
}

first[顶点号]=边号  next[边号]=-1(前一条边的边号)


接下来如何遍历每一条边呢?我们之前说过其实first数组存储的就是每个顶点i(i从1~n)的第一条边。比如1号顶点的第一条边是编号为5的边(1 3 7),2号顶点的第一条边是编号为4的边(2 4 6),3号顶点没有出向边,4号顶点的第一条边是编号为2的边(2 4 6)。那么如何遍历1号顶点的每一条边呢?也很简单。请看下图:

遍历1号顶点所有边的代码如下。

    k=first[1];// 1号顶点其中的一条边的编号(其实也是最后读入的边)   
    while(k!=-1) //其余的边都可以在next数组中依次找到   
    {   
        printf("%d %d %d\n",u[k],v[k],w[k]);   
        k=next[k];   
    }

遍历每个顶点的所有边的代码如下。

    for(i=1;i<=n;i++)   
    {   
        k=first[i];   
        while(k!=-1)   
        {   
            printf("%d %d %d\n",u[k],v[k],w[k]);   
            k=next[k];   
        }   
    }

可以发现使用邻接表来存储图的时间空间复杂度是O(M),遍历每一条边的时间复杂度是也是O(M)。如果一个图是稀疏图的话,M要远小于N2。因此稀疏图选用邻接表来存储要比邻接矩阵来存储要好很多。


  • 16
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
好的,以下是使用C语言实现的代码: ``` #include <stdio.h> #include <stdlib.h> // 邻接表中的边结构体 typedef struct EdgeNode { int adjvex; // 邻接顶点的下标 struct EdgeNode *next; // 指向下一条边的指针 } EdgeNode; // 邻接表中的顶点结构体 typedef struct VertexNode { char data; // 顶点信息 EdgeNode *firstedge; // 指向第一条依附该顶点的边的指针 } VertexNode, AdjList[100]; // 邻接表结构体 typedef struct { AdjList adjList; int numVertexes, numEdges; // 顶点数和边数 } GraphAdjList; // 初始化邻接表 void InitGraph(GraphAdjList *G) { int i, j, k; EdgeNode *e; printf("请输入顶点数和边数:\n"); scanf("%d%d", &G->numVertexes, &G->numEdges); printf("请输入顶点信息:\n"); for (i = 0; i < G->numVertexes; i++) { scanf(" %c", &G->adjList[i].data); G->adjList[i].firstedge = NULL; // 初始化指针为空 } printf("请输入边的信息:\n"); for (k = 0; k < G->numEdges; k++) { printf("请输入边(vi, vj)中的顶点序号:\n"); scanf("%d%d", &i, &j); e = (EdgeNode *)malloc(sizeof(EdgeNode)); // 生成新边结点 e->adjvex = j; // 邻接点序号为j e->next = G->adjList[i].firstedge; // 将e指针指向当前顶点指向的结构体 G->adjList[i].firstedge = e; // 将当前顶点的指针指向e } } // 深度优先遍历 void DFS(GraphAdjList G, int i, int *visited) { EdgeNode *p; visited[i] = 1; // 标记该节点已被访问 printf("%c ", G.adjList[i].data); // 打印节点信息 p = G.adjList[i].firstedge; // 指向当前节点的第一条边 while (p) { if (!visited[p->adjvex]) { // 如果该节点未被访问 DFS(G, p->adjvex, visited); // 递归访问该节点 } p = p->next; // 继续访问当前节点的下一条边 } } // 非递归深度优先遍历 void DFS_NonRecursive(GraphAdjList G) { int visited[100] = {0}; // 初始化所有节点未被访问 EdgeNode *stack[100]; // 用数组模拟栈 int top = -1, i; for (i = 0; i < G.numVertexes; i++) { if (!visited[i]) { // 如果该节点未被访问 visited[i] = 1; // 标记该节点已被访问 printf("%c ", G.adjList[i].data); // 打印节点信息 stack[++top] = G.adjList[i].firstedge; // 将第一条边入栈 while (top != -1) { EdgeNode *p = stack[top--]; // 取出栈顶元素 while (p) { if (!visited[p->adjvex]) { // 如果该节点未被访问 visited[p->adjvex] = 1; // 标记该节点已被访问 printf("%c ", G.adjList[p->adjvex].data); // 打印节点信息 stack[++top] = p->next; // 将下一条边入栈 p = G.adjList[p->adjvex].firstedge; // 继续访问该节点的第一条边 } else { p = p->next; // 继续访问该节点的下一条边 } } } } } } // 广度优先遍历 void BFS(GraphAdjList G) { int visited[100] = {0}; // 初始化所有节点未被访问 EdgeNode *queue[100]; // 用数组模拟队列 int front = 0, rear = 0, i; EdgeNode *p; for (i = 0; i < G.numVertexes; i++) { if (!visited[i]) { // 如果该节点未被访问 visited[i] = 1; // 标记该节点已被访问 printf("%c ", G.adjList[i].data); // 打印节点信息 queue[rear++] = G.adjList[i].firstedge; // 将第一条边入队 while (front != rear) { p = queue[front++]; // 取出队头元素 while (p) { if (!visited[p->adjvex]) { // 如果该节点未被访问 visited[p->adjvex] = 1; // 标记该节点已被访问 printf("%c ", G.adjList[p->adjvex].data); // 打印节点信息 queue[rear++] = G.adjList[p->adjvex].firstedge; // 将下一条边入队 } p = p->next; // 继续访问该节点的下一条边 } } } } } int main() { GraphAdjList G; InitGraph(&G); printf("图的邻接表存储结构如下:\n"); int i; for (i = 0; i < G.numVertexes; i++) { printf("%c -> ", G.adjList[i].data); EdgeNode *p = G.adjList[i].firstedge; while (p) { printf("%c -> ", G.adjList[p->adjvex].data); p = p->next; } printf("NULL\n"); } printf("使用非递归算法对图进行深度优先搜索遍历,顶点序列为:\n"); DFS_NonRecursive(G); printf("\n使用递归算法对图进行深度优先搜索遍历,顶点序列为:\n"); int visited[100] = {0}; for (i = 0; i < G.numVertexes; i++) { if (!visited[i]) { DFS(G, i, visited); } } printf("\n使用广度优先搜索遍历,顶点序列为:\n"); BFS(G); printf("\n"); return 0; } ``` 注意:代码中顶点数和边数的上限为100,如果需要更多的节点和边,请根据实际情况修改代码。另外,程序中输入的顶点下标从0开始,如果需要从1开始,请在输入和输出节点信息时将下标加1即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值