您的位置:首页 > 理论基础 > 数据结构算法

数据结构之图的存储结构二及其实现

2018-02-27 21:57 375 查看
上一节我们讲述了邻接矩阵法实现图,本节再来讲述一下邻接链表法实现图。
邻接链表
邻接表是图的一种链式存储结构。在邻接表中,对图中的每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点vi边(对有向图是以顶点vi为尾的弧)。
基本思想:从同一个顶点发出的边链接在同一个链表中,每一个链表结点代表一条边, 结点中保存边的另一顶点的下标和权值。如下图所示:





要想通过邻接链表方法实现图,首先得定义一个邻接链表头结点,如下:

邻接链表法的头结点:
1.    记录顶点个数;
2.     记录与顶点相关的数据描述;
3.     记录描述边集的链表数组。typedef struct _tag_LGraph
{
int count; // 记录顶点个数
LVertex** v; // 记录与顶点相关的数据描述
LinkList** la; // 记录描述边集的链表数组
} TLGraph;下面我们讲解一下实现代码:
1.创建图// 创建图
LGraph* LGraph_Create(LVertex** v, int n) // O(n)
{
// 定义图结构体返回变量
TLGraph* ret = NULL;
int ok = 1;
// 入口参数合法性检查OK
if( (v != NULL ) && (n > 0) )
{
// 申请图链表头结点内存
ret = (TLGraph*)malloc(sizeof(TLGraph));
// 申请内存成功
if( ret != NULL )
{
// 初始化图个数
ret->count = n;
// 申请数据内存空间,并初始化内存空间
ret->v = (LVertex**)calloc(n, sizeof(LVertex*));
// 申请一维链表数组头结点内存空间
ret->la = (LinkList**)calloc(n, sizeof(LinkList*));
// 申请内存检查
ok = (ret->v != NULL) && (ret->la != NULL);
// 申请
4000
成功
if( ok )
{
int i = 0;
// 保存图数据
for(i=0; i<n; i++)
{
ret->v[i] = v[i];
}
// 创建一维链表数组
for(i=0; (i<n) && ok; i++)
{
ok = ok && ((ret->la[i] = LinkList_Create()) != NULL);
}
}
// 申请失败
if( !ok )
{
// 一维链表数组不为空
if( ret->la != NULL )
{
int i = 0;
// 销毁所有一维链表数组
for(i=0; i<n; i++)
{
LinkList_Destroy(ret->la[i]);
}
}
// 释放申请的内存,防止内存泄漏
free(ret->la);
free(ret->v);
free(ret);

ret = NULL;
}
}
}

return ret;
}2.销毁图
// 销毁图
void LGraph_Destroy(LGraph* graph) // O(n*n)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
// 清空图
LGraph_Clear(tGraph);
// 入口参数合法
if( tGraph != NULL )
{
int i = 0;
// 销毁所有一维链表数组
for(i=0; i<tGraph->count; i++)
{
LinkList_Destroy(tGraph->la[i]);
}
// 释放内存
free(tGraph->la);
free(tGraph->v);
free(tGraph);
}
}
销毁是将图的存储内存给清除,销毁后就不存在图了,所以先将图清空,然后将链表数组也销毁,最后释放申请的所有内存。
3.清空图// 清空图
void LGraph_Clear(LGraph* graph) // O(n*n)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
// 入口参数合法
if( tGraph != NULL )
{
int i = 0;
// 遍历一维链表数组,如果链表不为空就将链表结点弹出,删除后释放内存
for(i=0; i<tGraph->count; i++)
{
while( LinkList_Length(tGraph->la[i]) > 0 )
{
free(LinkList_Delete(tGraph->la[i], 0));
}
}
}
}清空图就是将链表数组的所有内存释放,使其成为一个只有头结点的空图。
4.给图中指定位置加上边// 在graph所指图中的v1和v2之间加上边,且边的权为w
int LGraph_AddEdge(LGraph* graph, int v1, int v2, int w) // O(1)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
// 定义链表结点
TListNode* node = NULL;
// 合法性检查
int ret = (tGraph != NULL);

ret = ret && (0 <= v1) && (v1 < tGraph->count);
ret = ret && (0 <= v2) && (v2 < tGraph->count);
ret = ret && (0 < w) && ((node = (TListNode*)malloc(sizeof(TListNode))) != NULL);

if( ret )
{
node->v = v2; // 保存顶点v2,用于插入链表
node->w = w; // 保存权值
// 在v1所在的链表中插入结点
LinkList_Insert(tGraph->la[v1], (LinkListNode*)node, 0);
}

return ret;
}给图图中的v1和v2之间加上边的实质就是在顶点v1所在的链表中插入结点v2。
5.删除图中指定位置的边// 将graph所指图中v1和v2之间的边删除,返回权值
int LGraph_RemoveEdge(LGraph* graph, int v1, int v2) // O(n*n)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
// 合法性检查
int condition = (tGraph != NULL);
int ret = 0;

condition = condition && (0 <= v1) && (v1 < tGraph->count);
condition = condition && (0 <= v2) && (v2 < tGraph->count);
// 合法性检查OK
if( condition )
{
// 定义链表结点变量,用于存放获取的结点
TListNode* node = NULL;
int i = 0;
// 遍历顶点v1所在的链表
for(i=0; i<LinkList_Length(tGraph->la[v1]); i++)
{
node = (TListNode*)LinkList_Get(tGraph->la[v1], i);
// 找到和顶点v2相同的结点
if( node->v == v2)
{
ret = node->w; // 保存权值用于返回

LinkList_Delete(tGraph->la[v1], i); // 将找到的结点从链表中删除

free(node); // 释放内存,防止内存泄漏

break; // 跳出遍历操作
}
}
}

return ret;
}删除图中的边就是将顶点从链表中删除,并且释放申请的内存。
6.获取图中的边// 将graph所指图中v1和v2之间的边的权值返回
int LGraph_GetEdge(LGraph* graph, int v1, int v2) // O(n*n)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
// 合法性检查
int condition = (tGraph != NULL);
int ret = 0;

condition = condition && (0 <= v1) && (v1 < tGraph->count);
condition = condition && (0 <= v2) && (v2 < tGraph->count);

// 合法性检查OK
if( condition )
{
// 定义链表结点变量,用于存放获取的结点
TListNode* node = NULL;
int i = 0;
// 遍历顶点v1所在的链表
for(i=0; i<LinkList_Length(tGraph->la[v1]); i++)
{
node = (TListNode*)LinkList_Get(tGraph->la[v1], i);
// 找到和顶点v2相同的结点
if( node->v == v2)
{
ret = node->w; // 保存权值用于返回

break; // 跳出遍历操作
}
}
}

return ret;
}获取图中的边和删除图中的边操作基本相似,就是少了删除结点和释放内存的操作。
7.求图中指定顶点的度数// 将graph所指图中v顶点的度数
int LGraph_TD(LGraph* graph, int v) // O(n*n*n)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
// 合法性检查
int condition = (tGraph != NULL);
int ret = 0;

condition = condition && (0 <= v) && (v < tGraph->count);

// 合法性检查OK
if( condition )
{
int i = 0;
int j = 0;
// 遍历所有的链表
for(i=0; i<tGraph->count; i++)
{
// 遍历顶点v所在链表中的所有结点
for(j=0; j<LinkList_Length(tGraph->la[i]); j++)
{
TListNode* node = (TListNode*)LinkList_Get(tGraph->la[i], j);

// 找到和顶点v相同的结点,入度数量加1
if( node->v == v )
{
ret++;
}
}
}
// 入度的数量加上出度的数量
ret += LinkList_Length(tGraph->la[v]);
}

return ret;
}这里的度是针对有向图的,对于有向图的顶点度就是该顶点入度和出度的和,很显然出度就是该顶点所在链表的长度,而入度需要遍历整个图来寻找个顶点相同的结点个数,然后将出度和入度个数相加即可。
8.求图中顶点个数// 将graph所指图中的顶点数返回
int LGraph_VertexCount(LGraph* graph) // O(1)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
int ret = 0;
// 入口参数合法,直接返回图的个数
if( tGraph != NULL )
{
ret = tGraph->count;
}

return ret;
}这个没啥好说的,创建图时就确定了顶点的个数了,直接返回就行。
9.求图中边的个数// 将graph所指图中的边数返回
int LGraph_EdgeCount(LGraph* graph) // O(n)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
int ret = 0;
// 入口参数合法
if( tGraph != NULL )
{
int i = 0;
// 将所有链表的长度相加,返回
for(i=0; i<tGraph->count; i++)
{
ret += LinkList_Length(tGraph->la[i]);
}
}

return ret;
}我们前面在讲述图的定义的时提过,有向图的边等于所有顶点出度的和,上面又说过顶点的出度就是该顶点所在链表的长度。那么求图中的边的数目只要将所有顶点所在的链表长度相加即可。
图的基本操作都实现了,但是我们如何知道这些操作的正确性呢?下面我们通过一个显示函数来验证一下。
10.显示图// 显示图
void LGraph_Display(LGraph* graph, LGraph_Printf* pFunc) // O(n*n*n)
{
// 定义图结点结构体变量,并强制转换入口参数
TLGraph* tGraph = (TLGraph*)graph;
// 入口参数合法性检查OK
if( (tGraph != NULL) && (pFunc != NULL) )
{
int i = 0;
int j = 0;
// 打印图中顶点
for(i=0; i<tGraph->count; i++)
{
printf("%d:", i);
pFunc(tGraph->v[i]);
printf(" ");
}

printf("\n");
// 遍历链表数组中的每一个结点,取出结点用于打印
// 链表数组中的每个结点就是一个边
for(i=0; i<tGraph->count; i++)
{
for(j=0; j<LinkList_Length(tGraph->la[i]); j++)
{
TListNode* node = (TListNode*)LinkList_Get(tGraph->la[i], j);

printf("<");
pFunc(tGraph->v[i]); // 打印第一个顶点
printf(", ");
pFunc(tGraph->v[node->v]); // 打印第二个顶点
printf(", %d", node->w); // 打印权值
printf(">");
printf(" ");
}
}

printf("\n");
}
}整体代码的验证函数如下:#include <stdio.h>
#include <stdlib.h>
#include "LGraph.h"

/* run this program using the console pauser or add your own getch, system("pause") or input loop */

void print_data(LVertex* v)
{
printf("%s", (char*)v);
}

int main(int argc, char *argv[])
{
LVertex* v[] = {"A", "B", "C", "D", "E", "F"};
LGraph* graph = LGraph_Create(v, 6);

LGraph_AddEdge(graph, 0, 1, 1);
LGraph_AddEdge(graph, 0, 2, 1);
LGraph_AddEdge(graph, 0, 3, 1);
LGraph_AddEdge(graph, 1, 5, 1);
LGraph_AddEdge(graph, 1, 4, 1);
LGraph_AddEdge(graph, 2, 1, 1);
LGraph_AddEdge(graph, 3, 4, 1);
LGraph_AddEdge(graph, 4, 2, 1);

LGraph_Display(graph, print_data);

LGraph_Destroy(graph);

return 0;
}通过上面的代码分析我们不难发现邻接链表方法实现的图明显比邻接矩阵的方法实现的复杂的多,更耗时间。但是它也有其优点,那就是有效利用空间。所以邻接矩阵法和邻接链表法的选择不是绝对的,需要根据实际情况综合考虑 。对时间要求严格的就使用邻接矩阵法,对空间要求严格的就使用邻接链表法。

至此,邻接链表实现图的操作已经讲解完了,当然图的实现不仅仅就是邻接矩阵法和邻接链表法,还有其他方法这里不再一一赘述。
实现代码:邻接链表法实现图C代码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: