1 图的存储
1. 1 邻接矩阵法
1. 1. 1 无向图
假设它包含 n 个顶点,那么它的邻接矩阵是一个 n×n 的矩阵,其中元素 A[i][j] 表示顶点 i 到顶点 j 是否存在边。
#include <iostream>
using namespace std;
const int MAX = 100; // 假设最多有100个顶点
class Graph {
int adjMatrix[MAX][MAX]; // 邻接矩阵
int vertices; // 图的顶点数
public:
Graph(int v) {
vertices = v;
for (int i = 0; i < vertices; i++)
for (int j = 0; j < vertices; j++)
adjMatrix[i][j] = 0; // 初始化所有值为0
}
void addEdge(int src, int dest) {
// 对于无向图,添加边 src -> dest 和 dest -> src
adjMatrix[src][dest] = 1;
adjMatrix[dest][src] = 1;
}
void printGraph() {
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++)
cout << adjMatrix[i][j] << " ";
cout << endl;
}
}
};
int main() {
Graph g(5); // 创建一个5个顶点的图
// 添加边
g.addEdge(0, 1);
g.addEdge(0, 4);
g.addEdge(1, 2);
g.addEdge(1, 3);
g.addEdge(1, 4);
g.addEdge(2, 3);
g.addEdge(3, 4);
// 打印邻接矩阵
g.printGraph();
return 0;
}
这个程序首先定义了一个 Graph 类,其中包含了邻接矩阵的数组和一些基本操作,如添加边和打印图形。 main 函数创建了一个图对象,并添加了一些边,然后输出了邻接矩阵。
需要处理有向图,只需删除 addEdge 函数中的反向边即可。
1. 1. 2 有向带权图(有向网)
对于带权有向图的邻接矩阵表示,矩阵中的每个非零元素将不再只是简单的 1 或 0 ,而是代表从一个顶点到另一个顶点的边的权重。
#include <iostream>
using namespace std;
const int MAX = 100; // 假设最多有100个顶点
class Graph {
int adjMatrix[MAX][MAX]; // 邻接矩阵,存储边的权重
int vertices; // 图的顶点数
public:
Graph(int v) {
vertices = v;
for (int i = 0; i < vertices; i++)
for (int j = 0; j < vertices; j++)
adjMatrix[i][j] = -1; // 初始化所有值为-1,表示没有边
}
void addEdge(int src, int dest, int weight) {
// 添加有向边 src -> dest,权重为weight
adjMatrix[src][dest] = weight;
}
void printGraph() {
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++)
if (adjMatrix[i][j] != -1)
cout << adjMatrix[i][j] << " ";
else
cout << "INF ";
cout << endl;
}
}
};
int main() {
Graph g(5); // 创建一个5个顶点的图
// 添加带权边
g.addEdge(0, 1, 5);
g.addEdge(0, 4, 10);
g.addEdge(1, 2, 3);
g.addEdge(1, 3, 1);
g.addEdge(1, 4, 2);
g.addEdge(2, 3, 4);
g.addEdge(3, 4, 6);
// 打印邻接矩阵
g.printGraph();
return 0;
}
Graph 类的 addEdge 方法接受三个参数:源顶点、目标顶点和权重。初始化时,邻接矩阵中的所有值被设置为 -1 ,这通常表示两个顶点之间没有直接连接的边。在 printGraph 方法中,如果 adjMatrix[i][j] 不等于 -1 ,则输出其值;否则,输出 INF 来表示无限大,即没有边。
这里的 -1 和 INF 的选择仅作为示例,可以根据实际需求选择不同的值,比如使用 INT_MAX 或其他特定值来表示没有边的情况。
1. 2 邻接表法
邻接表是一种用于表示图的数据结构,尤其适用于稀疏图,因为它可以更有效地存储图的信息,而不必为不存在的边分配空间。对于无向图,每条边会在两个顶点的邻接列表中各出现一次。
1. 2. 1 无向图
#include <iostream>
#include <list>
using namespace std;
class Graph {
int vertices; // 图的顶点数
list<int> *adjList; // 动态数组,每个元素是一个指向邻接节点的链表
public:
Graph(int v) {
vertices = v;
adjList = new list<int>[v]; // 创建一个大小为v的邻接表数组
}
void addEdge(int src, int dest) {
// 添加边 src -> dest 和 dest -> src,因为是无向图
adjList[src].push_back(dest);
adjList[dest].push_back(src);
}
void printGraph() {
for (int i = 0; i < vertices; i++) {
cout << "\nAdjacency list of vertex " << i << ": head";
for (auto x : adjList[i])
cout << " -> " << x;
cout << endl;
}
}
};
int main() {
Graph g(5); // 创建一个5个顶点的图
// 添加边
g.addEdge(0, 1);
g.addEdge(0, 4);
g.addEdge(1, 2);
g.addEdge(1, 3);
g.addEdge(1, 4);
g.addEdge(2, 3);
g.addEdge(3, 4);
// 打印邻接表
g.printGraph();
return 0;
}
Graph 类使用一个动态数组 adjList 来存储每个顶点的邻接列表。 addEdge 函数用于添加无向图的边,它会将两个顶点添加到彼此的邻接列表中。 printGraph 函数遍历每个顶点的邻接列表并打印出来。
通过这种方式,邻接表可以节省内存并且提供快速的边查询能力,尤其是在边的数量远小于顶点数量的平方的情况下。
1. 2. 2 有向网
对于有向带权图的邻接表表示,每个顶点的邻接列表不仅包含可以到达的顶点,还包含到达这些顶点的边的权重。这样,邻接列表中的每个元素实际上是一个结构体,存储着目标顶点的索引和对应的权重。
#include <iostream>
#include <list>
using namespace std;
// 边的结构体,包含目标顶点和权重
struct Edge {
int dest;
int weight;
};
class DirectedWeightedGraph {
int vertices; // 图的顶点数
list<Edge> *adjList; // 动态数组,每个元素是一个指向边的链表
public:
DirectedWeightedGraph(int v) {
vertices = v;
adjList = new list<Edge>[v]; // 创建一个大小为v的邻接表数组
}
void addEdge(int src, int dest, int weight) {
// 添加有向带权边 src -> dest,权重为weight
Edge e = {dest, weight};
adjList[src].push_back(e);
}
void printGraph() {
for (int i = 0; i < vertices; i++) {
cout << "\nAdjacency list of vertex " << i << ": head";
for (auto x : adjList[i])
cout << " -> (" << x.dest << ", " << x.weight << ")";
cout << endl;
}
}
};
int main() {
DirectedWeightedGraph g(5); // 创建一个5个顶点的有向带权图
// 添加有向带权边
g.addEdge(0, 1, 5);
g.addEdge(0, 4, 10);
g.addEdge(1, 2, 3);
g.addEdge(1, 3, 1);
g.addEdge(1, 4, 2);
g.addEdge(2, 3, 4);
g.addEdge(3, 4, 6);
// 打印邻接表
g.printGraph();
return 0;
}
这里定义了一个 Edge 结构体,它包含两个成员: dest 用于存储目标顶点的索引, weight 用于存储边的权重。 DirectedWeightedGraph 类使用一个动态数组 adjList 来存储每个顶点的邻接列表,其中每个列表包含 Edge 类型的元素。 addEdge 函数创建一个 Edge 实例并将其添加到源顶点的邻接列表中。
1. 3 十字链表法
十字链表(Crossed Linked List)是一种特殊的数据结构,主要用于表示稀疏矩阵和图。在图的表示中,十字链表特别适合于表示有向图和带权图,它可以有效地存储边的信息,包括边的目标顶点和权重,并且支持高效的边插入和删除操作。
在十字链表中,每个边都由两个节点表示:一个是从源顶点指向目标顶点的节点,另一个是从目标顶点回指源顶点的节点。这两个节点通过指针相互链接,形成一个十字形状。这样,每个顶点都有一个前向链表和一个后向链表,分别表示从该顶点出发的边和到达该顶点的边。
#include <iostream>
using namespace std;
// 边的结构体
struct Edge {
int dest; // 目标顶点
int weight; // 边的权重
Edge *next; // 指向同一顶点的下一个边
Edge *cross; // 指向交叉边的指针
};
// 顶点的结构体
struct Vertex {
int index; // 顶点编号
Edge *outgoing; // 指向从该顶点出发的所有边的头结点
Edge *incoming; // 指向到达该顶点的所有边的头结点
};
class CrossedLinkedListGraph {
int vertices; // 图的顶点数
Vertex *verticesList; // 存储所有顶点的数组
public:
CrossedLinkedListGraph(int v) {
vertices = v;
verticesList = new Vertex[v];
for (int i = 0; i < v; i++) {
verticesList[i].index = i;
verticesList[i].outgoing = nullptr;
verticesList[i].incoming = nullptr;
}
}
~CrossedLinkedListGraph() {
for (int i = 0; i < vertices; i++) {
Edge *e = verticesList[i].outgoing;
while (e != nullptr) {
Edge *temp = e;
e = e->next;
delete temp;
}
}
delete[] verticesList;
}
void addEdge(int src, int dest, int weight) {
// 创建两个边节点
Edge *edge1 = new Edge{dest, weight, nullptr, nullptr};
Edge *edge2 = new Edge{src, weight, nullptr, edge1};
// 将新边添加到源顶点的前向链表
edge1->cross = edge2;
edge1->next = verticesList[src].outgoing;
verticesList[src].outgoing = edge1;
// 将新边添加到目标顶点的后向链表
edge2->cross = edge1;
edge2->next = verticesList[dest].incoming;
verticesList[dest].incoming = edge2;
}
void printGraph() {
for (int i = 0; i < vertices; i++) {
cout << "Vertex " << i << ": ";
for (Edge *e = verticesList[i].outgoing; e != nullptr; e = e->next) {
cout << "(" << e->dest << ", " << e->weight << ") ";
}
cout << endl;
}
}
};
int main() {
CrossedLinkedListGraph g(5); // 创建一个5个顶点的图
// 添加有向带权边
g.addEdge(0, 1, 5);
g.addEdge(0, 4, 10);
g.addEdge(1, 2, 3);
g.addEdge(1, 3, 1);
g.addEdge(1, 4, 2);
g.addEdge(2, 3, 4);
g.addEdge(3, 4, 6);
// 打印邻接表
g.printGraph();
return 0;
}
在上面的代码中, Edge 结构体包含了目标顶点、权重、指向同一顶点下一个边的指针以及指向交叉边的指针。 Vertex 结构体包含了顶点的编号、指向从该顶点出发的所有边的头结点以及指向到达该顶点的所有边的头结点。CrossedLinkedListGraph 类提供了创建图、添加边和打印图的方法。 addEdge 函数用于添加有向带权边,它创建两个 Edge 结构体实例,一个用于表示从源顶点到目标顶点的边,另一个用于表示从目标顶点回指源顶点的边。两个边通过 cross 指针相互链接。 printGraph 函数遍历图的每个顶点,打印出从该顶点出发的所有边的目标顶点和权重。
1. 4 邻接多重表
邻接多重表(Adjacency Multilist)是一种用于表示有向图的数据结构,它改进了传统的邻接表表示方法,特别适用于处理需要频繁修改边的情况,例如在图算法中边的插入和删除操作。邻接多重表对于每条边维护了两个链表,一个在起始顶点的出边列表中,另一个在终止顶点的入边列表中。这使得对边的操作更加高效。
#include <iostream>
using namespace std;
// 边的结构体
struct Edge {
int dest; // 目标顶点
int weight; // 边的权重
Edge *nextOut; // 指向同一顶点的下一个出边
Edge *nextIn; // 指向同一顶点的下一个入边
Edge *sym; // 指向对称边的指针
};
// 顶点的结构体
struct Vertex {
int index; // 顶点编号
Edge *firstOut; // 指向从该顶点出发的第一条边
Edge *firstIn; // 指向到达该顶点的第一条边
};
class AdjacencyMultilistGraph {
int vertices; // 图的顶点数
Vertex *verticesList; // 存储所有顶点的数组
public:
AdjacencyMultilistGraph(int v) {
vertices = v;
verticesList = new Vertex[v];
for (int i = 0; i < v; i++) {
verticesList[i].index = i;
verticesList[i].firstOut = nullptr;
verticesList[i].firstIn = nullptr;
}
}
~AdjacencyMultilistGraph() {
for (int i = 0; i < vertices; i++) {
Edge *e = verticesList[i].firstOut;
while (e != nullptr) {
Edge *temp = e;
e = e->nextOut;
delete temp;
}
}
delete[] verticesList;
}
void addEdge(int src, int dest, int weight) {
// 创建两个边节点
Edge *edge1 = new Edge{dest, weight, nullptr, nullptr, nullptr};
Edge *edge2 = new Edge{src, weight, nullptr, nullptr, edge1};
// 链接到源顶点的出边列表
edge1->nextOut = verticesList[src].firstOut;
verticesList[src].firstOut = edge1;
// 链接到目标顶点的入边列表
edge2->nextIn = verticesList[dest].firstIn;
verticesList[dest].firstIn = edge2;
// 设置对称边
edge1->sym = edge2;
edge2->sym = edge1;
}
void printGraph() {
for (int i = 0; i < vertices; i++) {
cout << "Vertex " << i << ": ";
for (Edge *e = verticesList[i].firstOut; e != nullptr; e = e->nextOut) {
cout << "(" << e->dest << ", " << e->weight << ") ";
}
cout << endl;
}
}
};
int main() {
AdjacencyMultilistGraph g(5); // 创建一个5个顶点的图
// 添加有向带权边
g.addEdge(0, 1, 5);
g.addEdge(0, 4, 10);
g.addEdge(1, 2, 3);
g.addEdge(1, 3, 1);
g.addEdge(1, 4, 2);
g.addEdge(2, 3, 4);
g.addEdge(3, 4, 6);
// 打印邻接多重表
g.printGraph();
return 0;
}
示例中的 AdjacencyMultilistGraph 类提供了创建图、添加边和打印图的方法。 addEdge 函数用于添加有向带权边,它创建两个 Edge 结构体实例,一个用于表示从源顶点到目标顶点的边,另一个用于表示反方向的边。两个边通过 sym 指针相互链接。 printGraph 函数遍历图的每个顶点,打印出从该顶点出发的所有边的目标顶点和权重。
示例仅展示了如何添加边,但在实际应用中,邻接多重表的真正优势在于能够快速地删除边。这是因为每条边都有一个指向其对称边的指针,这样在删除一条边时可以同时更新两条边的链表。如果需要实现删除边的功能,可以在类中添加一个 removeEdge 方法。
void removeEdge(int src, int dest) {
// 寻找从源顶点到目标顶点的边
Edge *e = verticesList[src].firstOut;
Edge *prevOut = nullptr;
while (e!= nullptr && e->dest!= dest) {
prevOut = e;
e = e->nextOut;
}
if (e == nullptr) return; // 边不存在
// 更新出边列表
if (prevOut == nullptr) {
verticesList[src].firstOut = e->nextOut;
} else {
prevOut->nextOut = e->nextOut;
}
// 保存对称边的指针
Edge *sym = e->sym;
// 更新入边列表
Edge *eIn = verticesList[dest].firstIn; // 修改:重新初始化入边指针
Edge *prevIn = nullptr;
while (eIn!= nullptr && eIn->dest!= src) { // 修改:使用新的指针进行比较
prevIn = eIn;
eIn = eIn->nextIn;
}
if (prevIn == nullptr) {
verticesList[dest].firstIn = eIn->nextIn;
} else {
prevIn->nextIn = eIn->nextIn;
}
// 确保对称边的 nextIn 和 nextOut 指针被正确更新
if (sym->nextIn == nullptr && sym->nextOut == nullptr) {
sym->nextIn = sym->nextOut = nullptr; // 修改:明确设置为 nullptr
}
// 删除边
delete e; // 删除从源顶点到目标顶点的边
delete sym; // 删除对称边
}
int main{
......
// 打印邻接多重表
g.printGraph();
// 移除一条边
g.removeEdge(1, 2);
// 再次打印邻接多重表
g.printGraph();
return 0;
}