C/C++每日一练:图的邻接矩阵和邻接表表示

一、相关概念

1. 图(graph)

图是一种非线性数据结构,由顶点(vertex)边(edge) 组成。可以将图抽象地表示为一组顶点 V 和一组边 E的集合。

以下示例展示了一个包含 5 个顶点和 7 条边的图。

cpp 复制代码
顶点:V = {1,2,3,4,5}

边:E = {(1,2),(1,3),(1,5),(2,3),(2,4),(2,5),(4,5)}

图:G = {V,E}

将顶点看作节点,边看作连接各个节点的引用(指针),就可以将图看作一种从链表拓展而来的数据结构。

如图所示,相较于线性关系(链表)分治关系(树) ,**网络关系(图)**的自由度更高,因而更为复杂。

1.1 图的常见类型与术语

1.1.1 常见类型
1.1.1.1 无向图(undirected graph)有向图(directed graph)

根据边是否具有方向,可分为无向图和有向图,如图所示:

  • 无向图:边表示两顶点之间的 "双向" 连接关系,例如 QQ 中的 "好友关系"。
  • 有向图 :边具有方向性,即 1→31←3 两个方向的边是相互独立的,例如微博或抖音上的"关注"与"被关注"关系。
1.1.1.2 连通图(connected graph)非连通图(disconnected graph)

根据所有顶点是否连通,可分为连通图和非连通图,如图所示:

  • 连通图:从某个顶点出发,可以到达其余任意顶点。
  • 非连通图:从某个顶点出发,至少有一个顶点无法到达。
1.1.1.3 有权图( weighted graph**)**和 无权图(unweighted graph)

为边添加 "权重" 变量,从而得到如下图所示的有权图。

例如,在手游中,系统会根据游戏玩家之间的 "共同游戏时间" 来计算玩家之间的"亲密度",这种亲密度网络就可以用有权图来表示。

1.1.2 常用术语

图数据结构包含以下常用术语:

  • 邻接(adjacency):当两顶点之间存在边相连时,称这两顶点"邻接"。例如,在有权图中,顶点 1 的邻接顶点为顶点 2、3、5。
  • 路径(path):从顶点 A 到顶点 B 经过的边构成的序列被称为从 A 到 B 的"路径"。例如,在有权图中,边序列 1-5-2-4 是顶点 1 到顶点 4 的一条路径。
  • 度(degree) :一个顶点拥有的边数。对于有向图,入度(in-degree) 表示有多少条边指向该顶点,**出度(out-degree)**表示有多少条边从该顶点指出。

1.2 图的表示

图的常用表示方式包括"邻接矩阵"和"邻接表"。以下使用无向图进行举例。

1.2.1 邻接矩阵(adjacency matrix)

设图的顶点数量为 n,邻接矩阵使用一个 n×n 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 1 或 0 表示两个顶点之间是否存在边。

设邻接矩阵为 Matrix、顶点列表为 V ,那么矩阵元素 Matrix[i][j] = 1 且 Matrix[j][i]=1,表示顶点 V[i] 到顶点 V[j] 之间存在边,反之 M[i,j] = 0 表示两顶点之间无边。

邻接矩阵具有以下特性。

  • 顶点不能与自身相连,因此邻接矩阵主对角线元素没有意义。
  • 对于无向图,两个方向的边等价,此时邻接矩阵关于主对角线对称。
  • 将邻接矩阵的元素从 1 和 0 替换为权重,则可表示有权图。

使用邻接矩阵表示图时,可以直接访问矩阵元素以获取边,因此增删查改操作的效率很高,时间复杂度均为 O(1) 。然而,矩阵的空间复杂度为 O(n^2) ,内存占用较多。

1.2.2 邻接表(adjacency list)

邻接表使用数组加 n 个链表的形式存储来表示图,链表节点表示顶点。第 i 个链表对应顶点 i ,其中存储了该顶点的所有邻接顶点(与该顶点相连的顶点)。图中展示了一个使用邻接表存储的图的示例。

邻接表仅存储实际存在的边,而边的总数通常远小于 n^2 ,因此它更加节省空间。然而,在邻接表中需要通过遍历链表来查找边,因此其时间效率不如邻接矩阵。

二、题目要求

题目:实现一个无向图,分别用邻接矩阵和邻接表表示,支持以下操作:

  1. 添加一条边。
  2. 删除一条边。
  3. 查询两个节点是否直接相连。

三、做题思路

3.1 邻接矩阵

数据结构

  • 使用一个二维数组表示图,其中 matrix[ i ][ j ] 表示节点 i 和节点 j 之间的关系。
  • 如果是无向图,矩阵是对称的;如果是有向图,则矩阵可能是非对称的。
  • 如果是加权图,可以用边的权值代替布尔值(0 和 1)。

基本操作

  • 添加边:将对应位置赋值为 1(或权值)。
  • 删除边:将对应位置赋值为 0。
  • 查询是否相连:检查对应位置的值是否为 1。

适用场景

  • 邻接矩阵适合边密集的图(稠密图),查询是否相连的时间复杂度为 O(1)。
  • 当图的边数较少时,矩阵会浪费大量存储空间。

3.2 邻接表

数据结构

  • 使用一个数组或向量,每个位置存储一个链表或动态数组,表示该节点的所有邻接点。
  • 节点的连接关系存储在链表中,动态扩展存储空间以适应图的规模。

基本操作

  • 添加边:在节点 u 的链表中添加节点 v,同时在节点 v 的链表中添加节点 u(无向图)。
  • 删除边:从节点 u 的链表中删除节点 v,同时从节点 v 的链表中删除节点 u(无向图)。
  • 查询是否相连:遍历节点 u 的链表,查找是否存在节点 v。

适用场景

  • 邻接表适合边稀疏的图(稀疏图),节省存储空间,但查询是否相连的复杂度较高,为 O(degree(u))。

四、过程解析

4.1 邻接矩阵

初始化矩阵

  • 创建一个 n×n 的二维数组,并初始化所有值为 0。
  • 在 C++ 中可以使用 vector ,在 C 中需要动态分配内存。

实现添加边功能

  • 对于无向图,在矩阵中对称设置 matrix[u][v]=1 和 matrix[v][u]=1。
  • 对于有向图,只设置 matrix[u][v]=1。

实现删除边功能

  • 对应的位置值改为 0,表示移除该边。

实现查询功能

  • 直接检查 matrix[u][v] 是否为 1 即可。

内存管理

  • 在 C 中,动态分配内存后需要手动释放,以避免内存泄漏。

4.2 邻接表

初始化邻接表

  • 创建一个大小为 n 的数组,每个元素是一个链表或向量,表示节点的邻接点集合。
  • 初始时,每个链表为空。

实现添加边功能

  • 在节点 u 的链表中插入节点 v,在节点 v 的链表中插入节点 u(无向图)。

实现删除边功能

  • 遍历链表,找到目标节点后将其移除。

实现查询功能

  • 遍历节点 u 的链表,查找是否存在节点 v。

存储空间优化

  • 邻接表在稀疏图中可以显著节省空间,尤其当节点数量大但边数量少时。

五、运用到的知识点

  • 数组的基本操作。
  • 动态数组或链表的实现。
  • 图的基础概念(节点、边)。
  • 时间复杂度和空间复杂度分析。

六、代码示例

1. 邻接矩阵 - C 实现

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 图的邻接矩阵表示
typedef struct {
    int** matrix; // 动态分配的二维数组,表示邻接矩阵
    int n;        // 图中节点的数量
} GraphMatrix;

// 初始化图
GraphMatrix* createGraph(int n) {
    GraphMatrix* graph = (GraphMatrix*)malloc(sizeof(GraphMatrix)); // 分配内存给图结构
    graph->n = n; // 设置节点数量

    // 分配 n*n 的二维数组并初始化为 0
    graph->matrix = (int**)malloc(n * sizeof(int*)); // 分配行指针数组
    for (int i = 0; i < n; i++) {
        graph->matrix[i] = (int*)calloc(n, sizeof(int)); // 每行分配 n 个整型并初始化为 0
    }
    return graph;
}

// 添加边
void addEdge(GraphMatrix* graph, int u, int v) {
    graph->matrix[u][v] = 1; // 设置 u 到 v 的值为 1
    graph->matrix[v][u] = 1; // 无向图对称,设置 v 到 u 的值为 1
}

// 删除边
void removeEdge(GraphMatrix* graph, int u, int v) {
    graph->matrix[u][v] = 0; // 将 u 到 v 的值设置为 0
    graph->matrix[v][u] = 0; // 同时设置 v 到 u 的值为 0
}

// 查询是否相连
int isConnected(GraphMatrix* graph, int u, int v) {
    return graph->matrix[u][v]; // 返回 u 到 v 的值(1 表示相连,0 表示不相连)
}

// 打印邻接矩阵
void printMatrix(GraphMatrix* graph) {
    for (int i = 0; i < graph->n; i++) { // 遍历每一行
        for (int j = 0; j < graph->n; j++) { // 遍历每一列
            printf("%d ", graph->matrix[i][j]); // 输出矩阵的值
        }
        printf("\n"); // 换行表示下一行
    }
}

// 释放内存
void freeGraph(GraphMatrix* graph) {
    for (int i = 0; i < graph->n; i++) {
        free(graph->matrix[i]); // 释放每一行的内存
    }
    free(graph->matrix); // 释放行指针数组
    free(graph);         // 释放图结构
}

int main() 
{
    int n = 5; // 节点数量
    GraphMatrix* graph = createGraph(n); // 初始化图

    // 添加一些边
    addEdge(graph, 0, 1);
    addEdge(graph, 1, 2);
    addEdge(graph, 2, 3);
    addEdge(graph, 3, 4);

    printf("Adjacency Matrix:\n");
    printMatrix(graph); // 打印邻接矩阵

    printf("Is 1 connected to 2? %s\n", isConnected(graph, 1, 2) ? "Yes" : "No"); // 查询是否相连
    removeEdge(graph, 1, 2); // 删除边
    printf("Is 1 connected to 2? %s\n", isConnected(graph, 1, 2) ? "Yes" : "No"); // 再次查询

    freeGraph(graph); // 释放内存
    return 0;
}

2. 邻接表 - C++ 实现

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
using namespace std;

// 图的邻接表表示
class GraphList {
private:
    vector<list<int>> adjList; // 用动态数组(vector)和链表(list)实现邻接表
    int n; // 图中的节点数量

public:
    // 构造函数,初始化邻接表
    GraphList(int n) : n(n) {
        adjList = vector<list<int>>(n); // 创建 n 个空链表
    }

    // 添加边
    void addEdge(int u, int v) {
        adjList[u].push_back(v); // 在节点 u 的链表中添加节点 v
        adjList[v].push_back(u); // 在节点 v 的链表中添加节点 u(无向图)
    }

    // 删除边
    void removeEdge(int u, int v) {
        adjList[u].remove(v); // 从节点 u 的链表中删除节点 v
        adjList[v].remove(u); // 从节点 v 的链表中删除节点 u(无向图)
    }

    // 查询两个节点是否相连
    bool isConnected(int u, int v) {
        for (int neighbor : adjList[u]) { // 遍历节点 u 的链表
            if (neighbor == v) return true; // 如果找到 v,说明相连
        }
        return false; // 否则不相连
    }

    // 打印邻接表
    void printList() {
        for (int i = 0; i < n; i++) { // 遍历每个节点
            cout << i << ": "; // 输出节点编号
            for (int neighbor : adjList[i]) { // 遍历该节点的邻接链表
                cout << neighbor << " "; // 输出相邻节点
            }
            cout << endl; // 换行表示下一节点
        }
    }
};

int main() 
{
    int n = 5; // 节点数量
    GraphList graph(n); // 初始化邻接表

    // 添加一些边
    graph.addEdge(0, 1);
    graph.addEdge(1, 2);
    graph.addEdge(2, 3);
    graph.addEdge(3, 4);

    cout << "Adjacency List:" << endl;
    graph.printList(); // 打印邻接表

    cout << "Is 1 connected to 2? " << (graph.isConnected(1, 2) ? "Yes" : "No") << endl; // 查询是否相连
    graph.removeEdge(1, 2); // 删除边
    cout << "Is 1 connected to 2? " << (graph.isConnected(1, 2) ? "Yes" : "No") << endl; // 再次查询

    return 0;
}
相关推荐
祖坟冒青烟9 分钟前
Qt 的构建系统
c++
小雄abc27 分钟前
决定系数R2 浅谈三 : 决定系数R2与相关系数r的关系、决定系数R2是否等于相关系数r的平方
经验分享·笔记·深度学习·算法·机器学习·学习方法·论文笔记
uyeonashi1 小时前
【C++】刷题强训(day14)--乒乓球匡、组队竞赛、删除相邻数字的最大分数
开发语言·c++·算法·哈希算法
机器学习之心1 小时前
一区正弦余弦算法!SCA-SVM正弦余弦算法优化支持向量机多特征分类预测
算法·支持向量机·分类·sca-svm·正弦余弦算法优化
a栋栋栋2 小时前
刷算法心得
算法
妈妈说名字太长显傻2 小时前
【C++】string类
开发语言·c++
丢丢丢丢丢丢~2 小时前
c++创建每日文件夹,放入每日日志
开发语言·c++
華華3552 小时前
读程序题...
开发语言·c++·算法
m0_547486662 小时前
西安交通大学2001年数据结构试题
数据结构
legendary_1633 小时前
LDR6500:音频双C支持,数字与模拟的完美结合
c语言·开发语言·网络·计算机外设·电脑·音视频