博主简介:一个爱打游戏的计算机专业学生
- 博主主页:夏驰和徐策
- 所属专栏:夏驰和徐策带你从零开始学习数据结构
1. 邻接表表示法
邻接(Adiacenoy List)是图的一种链式存储结构(前面的章节在分类的时候有所提及,后面的章节都是对前面的每个方向进行研究)。在邻接表中,对图中每个项点建立一个单链表,把与vi相邻接的顶点这个链表中。邻接表中每个单链表的第一个节点存放有关顶点的信息,把这一节点看成链表的表头,其余节点存放有关边的信息,这样邻接表便由两部分组成:表头节点表和边表。
(1)表头节点:由所有表头节点以顺序结构的形式存储,以便可以随机访问任一项点的边链表。表头节点包括数据城(data) 和链域(frstarc)两部分,如图6.11(a)所示。其中,数据域用于存储顶点w的名称或其他有关信息;链域用于指向链表中第一个节点(与顶点u邻接的第一个邻接点)。
(2)边表:由表示图中顶点间关系的2n个边链表组成。边链表中边节点包括邻接点域
(adjvex)、数据域 (into)和链域(nextare)3个部分,如图6.11(b)所示。其中,邻接点域
指示与顶点,邻接的点在图中的位置;数据域存储和边相关的信息,如权值等;链域指示与顶点
,邻接的下一条边的节点。
图6.11 表头节点和边节点
例如,图6.12(日)和图6.12(6)所示分别为图6.1中G和G,的邻接表。在无向图的邻接表中,顶点口的度恰为第个链表中的节点个数;而在有向图中,第个链表中的节点个数只是顶点y的出度,为求人度,必领遍历整个邻接表。在所有链表中,其邻接点域的值为的节点个数是顶点w的人度。有时,为了便于确定顶点的人度,可以建立一个有向图的逝邻接表,即对每个顶点》建立一个链接所有进人以的边的表,例如,图6.12(。)所示为有向图
//- - - - -图的邻接表存储表示- - - - -
#define MVnum 100//最大顶点数
typedef struct ArcNode//边结点
{
int adjvex;//改边所指向的顶点的位置
struct ArcNode * nexttarc;//指向下一条边的指针
OtherInfo info;
} ArcNode;
typedef struct VNode//顶点信息
{
VerTexType data;
ArcNode *firstarc;//指向第一条依附该顶点的边的指针
}VNode,AdjList[MVNum];//AdjList表示邻接表类型
typedef struct
{
AdjList vertices;
int vexnum,arcnum;//图的当前顶点数和边数
}ALGraph;
构建结构体来分别表示,点的信息,邻接表的结构体,图的的结构体,运用了嵌套的思想。我们阅读代码时可以由外入里的看。即先宏观在微观。
2.买用
基于上述
无向图为例来
算法6.2
【算法古
①输人
②依次
NULL.
③创交
号湘之后Status CreateUDG(ALGraph &G) {//采用邻接表表示法,创建无向图G cin>>G.vexnum>>G.arcnum;//输入总顶点数,总边数 for(i=0;i
>G.vertices[i].data;//输入顶点值 G.vertices[i].firstarc=NULL;//初始化表头节点的指针域 } for(k=0;k >G.vertices[i]=data;//输入顶点值 G.vertices[i].firstarc=NULL; //初始化表头结点 } for(k=0;k >v1>>v2;//输入一条边衣服的两个顶点 i=LocateVex(G,v1);j=LocateVex(G,v2); //确定v1和v2在G中的位置,即顶点在G.vertices中的序号 p1=new ArcNode;//生成另一个对称的新的边结点*p1 p1->adjvex=j; p1->nextarc=G.vertices[i].firstarc;G.vertices[i].firstarc=p1; //将新节点*p1插入顶点vi的边表头部 p2=new ArcNode; p2->adjvex=i; p2->nextarc=G.vertices[j],firstarc;G.vertices[j].firstarc=p2; //将新节点*p2插入顶点vj的边表头部 } return OK; }
图6.12
邻接表和逆邻接表
根据上述讨论,要定义一个邻接表,香要先定义其存放顶点的头节点和表示边的边节点
图的邻接表存储结构说明如下:
-图的邻接表存储表示2.采用邻接表表示法创建无向图
基于上述的邻接表表示法,要创建一个图则需要创建其相应的顶点表和边表。下面以一个
无向图为例来说明采用邻接表表示法创建无向图的算法。
算法6.2 采用邻接表表示法创建无向图
【算法步骤】
①输人总项点数和总边数。
② 依次输人点的信息存人顶点表中,使每个表头节点的指针域初始化为NULL。
③ 创建邻接表。依次输人每条边依附的两个顶点,确定这两个项点的序
采用邻接表号;淅之后,将此边节点分别插人,和y对应的两个边链表的头部。算法的时间复杂度是O(n+e)。
建立有向图的邻接表与此类似,只是更加简单,每读人一个项点对<,广,仅需生成一个邻
接点序号为的边表节点,并将其插人»的边链表头部即可。若要创建网的邻接表,可以将边的
权值存储在info域中。
注意
值得注意的是,一个图的邻接矩阵表示是啡一的,但其邻接表表示不唯一,这是因为邻接表装示中,各边表节点的链接次序取决于建立邻接表的算法,以及边的输入次序,邻接矩阵和邻接表是图的两种常用的存储结构,它们各有所长。与邻接矩阵相比,邻接表有其自己的优缺点。
3.邻接表表示法的优缺点(1)优点
①便于增加和删除顶点。
② 便于统计边的数目,按顶点表顺序查找所有边表可得到边的数目,时间复杂度为
O(n+ e).
③空间效率高。对于一个具有八个顶点、e条边的图G,若G是无向图,则在其邻接表表示中
有n个顶点表节点和2e个边表节点;若G是有向图,则在它的邻接表表示或道邻接表表示中均有
n个顶点表节点和e个边表节点。因此,邻接表或逆邻接表表示的空间复杂度为O(G+e,适合表
示稀疏图。对于稠密图,考虑到邻接表中要附加链域,因此常采取邻接矩阵表示法。
(2)缺点
①不便于判断顶点之间是否有边,要判定口和y之间是否有边,就需查找第个边表,最坏
情况下时间复杂度为O(n。
②不便于计算有向图各个顶点的度。对于无向图,在邻接表表示中顶点u的度是第个个边表
中的节点个数。在有向图的邻接表中,第个边表上的节点个数是项点以的出度,但求w的人度较
困难,需遍历各顶点的边表。若有向因采用逆邻接表表示,则与邻接表表示相反,求顶点的人
度较容易,而求顶点的出度较困难。
邻接表是一种表示图的数据结构,它使用链表来存储每个顶点及其相邻顶点。对于一个无向图或有向图,邻接表通过在每个顶点上维护一个链表,将该顶点与所有相邻的顶点连接起来。
具体而言,邻接表由两个部分组成:一个数组和一个链表。数组中的每个元素对应一个点,链表则包含所有从该顶点出发的边以及它们所指向的顶点。
例如,考虑以下无向图的邻接表表示:
可以用如下的邻接表表示:
在这个邻接表中,每个顶点被表示为一个链表,链表的头节点即为该顶点。例如,顶点1的链表包含了它所连接的所有其他顶点2、3、4。
邻接表在表示稀疏图(即顶点数远大于边数)时非常有效,因为它只需要存储与每个顶点相邻接的部分顶点,而不需要为每个顶点都分配一定数量的空间来表示可能不存在的边。
用C语言实现:
#include
#include
// 定义链表节点结构体
typedef struct node {
int vertex;
struct node* next;
} Node;
// 定义图结构体
typedef struct graph {
int num_vertices;
Node** adj_list;
} Graph;
// 创建节点函数
Node* create_node(int v) {
Node* new_node = (Node*) malloc(sizeof(Node));
new_node->vertex = v;
new_node->next = NULL;
return new_node;
}
// 创建图函数
Graph* create_graph(int vertices) {
Graph* graph = (Graph*) malloc(sizeof(Graph));
graph->num_vertices = vertices;
// 创建邻接表数组
graph->adj_list = (Node**) malloc(vertices * sizeof(Node*));
// 将邻接表数组初始化为 NULL
for (int i = 0; i < vertices; ++i) {
graph->adj_list[i] = NULL;
}
return graph;
}
// 添加边函数
void add_edge(Graph* graph, int src, int dest) {
// 添加从 src 到 dest 的边
Node* new_node = create_node(dest);
new_node->next = graph->adj_list[src];
graph->adj_list[src] = new_node;
// 添加从 dest 到 src 的边(因为这是无向图)
new_node = create_node(src);
new_node->next = graph->adj_list[dest];
graph->adj_list[dest] = new_node;
}
// 打印邻接表
void print_graph(Graph* graph) {
for (int i = 0; i < graph->num_vertices; ++i) {
Node* curr = graph->adj_list[i];
printf("顶点 %d 的邻接表:", i);
while (curr) {
printf("%d -> ", curr->vertex);
curr = curr->next;
}
printf("\n");
}
}
// 测试代码
int main() {
Graph* graph = create_graph(5);
add_edge(graph, 0, 1);
add_edge(graph, 0, 4);
add_edge(graph, 1, 2);
add_edge(graph, 1, 3);
add_edge(graph, 1, 4);
add_edge(graph, 2, 3);
add_edge(graph, 3, 4);
print_graph(graph);
return 0;
}
此示例代码创建了一个无向图,使用邻接表来表示图的结构,并打印每个顶点的邻接表。在这个示例中,我们可以看到如何使用链表来实现邻接表,以及如何添加边并打印邻接表。
#include
#include
using namespace std;
// 定义图结构体
class Graph {
int num_vertices;
list* adj_list; // 邻接表链表数组
public:
Graph(int vertices) : num_vertices(vertices) {
// 创建邻接表数组
adj_list = new list[num_vertices];
}
// 添加边函数
void add_edge(int src, int dest) {
// 添加从 src 到 dest 的边
adj_list[src].push_back(dest);
// 添加从 dest 到 src 的边(因为这是无向图)
adj_list[dest].push_back(src);
}
// 打印邻接表
void print_graph() {
for (int i = 0; i < num_vertices; ++i) {
cout << "顶点 " << i << " 的邻接表:";
for (auto it = adj_list[i].begin(); it != adj_list[i].end(); ++it) {
cout << *it << " -> ";
}
cout << "NULL" << endl;
}
}
};
// 测试代码
int main() {
Graph graph(5);
graph.add_edge(0, 1);
graph.add_edge(0, 4);
graph.add_edge(1, 2);
graph.add_edge(1, 3);
graph.add_edge(1, 4);
graph.add_edge(2, 3);
graph.add_edge(3, 4);
graph.print_graph();
return 0;
}
在这个示例中,我们使用了 std::list 来实现邻接表。类 Graph 封装了顶点数(对封装回忆不起来了)以及邻接表数组,使用 add_edge 函数来添加边,并使用 print_graph 函数来打印邻接表(函数的思想)。同时,在这个示例中我们也可以看到如何使用 C++ 的语法来实现此种数据结构。
内存管理:邻接表是一种动态数据结构,需要手动分配和释放内存。在使用邻接表时,需要注意正确地分配和释放内存,避免出现内存泄漏或悬空指针等问题。
变量命名:为了提高代码可读性和可维护性,应当给变量、函数、结构体等起具有意义的名称。特别是在邻接表中,顶点、边、权值等变量名应当清晰明确。
基本操作实现:邻接表的基本操作包括添加顶点、添加边、删除顶点、删除边等。在实现这些操作时,需要注意算法的正确性和效率。
数据结构定义:邻接表的数据结构通常包含顶点数组和邻接表链表。在定义数据结构时,应当考虑如何表示顶点和边,并定义相应的结构体。同时还要注意链表的实现方式(如单向链表、双向链表等)。
算法设计:邻接表可以用于解决诸如最短路径、最小生成树等图论问题。在设计算法时需要深入理解邻接表的数据结构和相关算法,并考虑如何应用到具体问题中。
可扩展性:邻接表是一种动态数据结构,应当考虑如何支持动态添加和删除顶点和边。同时,还要考虑如何扩展邻接表,以支持更复杂的图论算法。
语法不同:C++使用了类、对象、构造函数等概念,而C语言则没有这些。因此,在C++实现中,我们可以将邻接表结构体封装为一个类,并将成员函数作为其方法来操作。
内存分配方式不同:在C语言中,常常使用 malloc() 动态分配内存,而在C++中,我们可以使用 new 和 delete 来分配和释放内存。在C++中,还有 std::list 这样的 STL 容器,可以直接使用链表来实现邻接表。
操作方式相似:无论是C语言还是C++,它们都使用链表来表示邻接表,并且都需要实现添加边和打印邻接表等基本操作。从这个角度来说,它们的思路是一致的。
综上所述,虽然实现方式略有不同,但C语言和C++都能够很好地实现邻接表数据结构,提供了方便、高效的图算法实现方式。
邻接表由两个部分组成:顶点数组和邻接表链表。
顶点数组中每个元素对应一个顶点,在该元素中存储该顶点以及与之相邻接的所有顶点的信息。
邻接表链表存储了从该顶点出发可以到达的所有其他顶点。链表中的每个节点包含两部分:一个指向相邻的顶点的指针和一个指向下一个邻接节点的指针。
邻接表的特点:对于无向图或有向图,在添加边时需要在两个顶点之间都加上边。因为邻接表表示的是一个节点所能到达的其他节点,而不是边。
使用范围:邻接表适用于稀疏图(即顶点数远大于边数),因为它只需要为每个顶点存储其相邻顶点的信息,而不需要为顶点间可能不存在的边分配空间。
在邻接表中查询某个顶点的相邻节点时,只需要遍历该顶点对应的邻接表链表即可。这比邻接矩阵更高效。
邻接表空间复杂度影响因素:邻接表的空间复杂度取决于图的密度。当图非常稀疏时,邻接表的空间复杂度比邻接矩阵低。但是,在稠密图中,邻接表的空间复杂度会更高。
综上所述,邻接表是一种简单而高效的表示图结构的方法,其特点是能够快速查询顶点的所有相邻节点和边,因此广泛用于各种图算法中。