图的存储方式

news2024/11/18 1:36:32

一、邻接矩阵

图的邻接矩阵存储方式就是用两个数组来表示图。一个一维数组存储图的顶点信息,另一个二维数组存储图中边的信息。

对于无向图来说,我们可以用1表示两顶点相连,用0表示两顶点不相连。任意顶点的度为邻接矩阵中该节点的行或列的元素之和。

对于有向图,任意顶点的入度为其对应列的元素之和,出度为其对应行的元素之和。

对于有权图,可以将邻接矩阵中的元素存储为权值。对于不可达的顶点,可以用一个权值不可能达到的极限值表示。

代码如下

public class GraphByAdjacencyMatrix<T>:IGraph<T>
{
    private T[] _nodes;
    private int[,] _matrix;
    private int _count;

    public GraphByAdjacencyMatrix(int capacity)
    {
        _nodes = new T[capacity];
        _matrix = new int[capacity, capacity];
        // 初始化邻接矩阵
        for (int i = 0; i < capacity; i++)
        {
            for (int j = 0; j < capacity; j++)
            {
                if (i == j) 
                    _matrix[i, j] = 0;
                else
                    _matrix[i, j] = Int32.MaxValue;
            }
        }
    }
	/// <summary>  
	/// 添加节点  
	/// </summary>  
	/// <param name="e"></param>
    public void AddNode(T e)
    {
        _nodes[_count++] = e;
    }

	/// <summary>  
	/// 添加边  
	/// </summary>  
	/// <param name="node1Index"></param>  
	/// <param name="node2Index"></param>  
	/// <param name="weight"></param>
    public void AddEdge(int node1Index, int node2Index, int weight)
    {
        _matrix[node1Index, node2Index] = weight;
    }
}

因为邻接矩阵使用二维数组存储图中边的信息,所以如果图的顶点数量较多而边的数量较少时,会浪费大量的空间。

二、邻接表

为了节省空间,我们可以将邻接矩阵改为链表,即邻接表结构。图的邻接表存储方式采用一个一维数组存储顶点。这些顶点还需要存储第一个邻接点的指针,每个顶点的所有邻接点构成一个链表。

对于带权图,可以给邻接点增加一个“权值”的数据域

代码如下

public class GraphByAdjacencyList<T>:IGraph<T>
{
    /// <summary>
    /// 边结构
    /// </summary>
    class Edge
    {
        public int index;
        public int weight;
        public Edge next;

        public Edge(int index,int weight)
        {
            this.index = index;
            this.weight = weight;
        }
    }
    /// <summary>
    /// 顶点结构
    /// </summary>
    /// <typeparam name="T"></typeparam>
    class Node<T>
    {
        public T data;
        public Edge next;

        public Node(T e)
        {
            data = e;
        }
    }
    
    private Node<T>[] _nodes;
    private int _count;
    
    public GraphByAdjacencyList(int capacity)
    {
        _nodes = new Node<T>[capacity];
    }

    /// <summary>
    /// 添加顶点
    /// </summary>
    /// <param name="e"></param>
    public void AddNode(T e)
    {
        _nodes[_count++] = new Node<T>(e);
    }

    /// <summary>
    /// 添加边
    /// </summary>
    /// <param name="node1Index"></param>
    /// <param name="node2Index"></param>
    /// <param name="weight"></param>
    public void AddEdge(int node1Index, int node2Index, int weight)
    {
        Edge edge = new Edge(node2Index, weight);
        // 头插法
		edge.next = _nodes[node1Index].next;  
		_nodes[node1Index].next = edge;
    }
}

邻接表可以有效地解决邻接矩阵浪费空间的问题,但也引入了新的问题:对于有向图来说,想要了解某个顶点的出度可以遍历顶点后的邻接点链表获得,但想要了解其入度则需要遍历整个图才能获得。

我们当然也可以将邻接表反转,将到达顶点的边作为邻接表,也就是逆邻接表。逆邻接表可以通过遍历链表来得到某个顶点的入度,但是出度则需要遍历整个图

三、十字链表

对于有向图来说,无论是邻接表还是逆邻接表都无法完美地解决入度与出度的问题。但我们可以将它们整合在一起,也就是十字链表。

十字链表给顶点和边的结构都增加了几块区域用来存储额外的指针和下标。顶点结构用两个指针区域分别存储「入顶点的边的链表」与「出顶点的边的链表」指针。

边结构用两个区域分别存储「边起始顶点的下标」与「边终止顶点的下标」,另外两个指针区域分别存储「入该顶点的下一条边」与「出该顶点的下一条边」的指针。

最终形成的结构如下

如果我们想要获取B点的出度,只需要沿着黑色箭头方向遍历;如果要获取B点的入度,只需要沿着蓝色箭头方向遍历。虽然数据结构变得更加复杂,但降低了遍历出边和入边的时间复杂度。

代码如下

public class GraphByOrthogonalList<T> : IGraph<T>
{
    private class Node<T>
    {
        public T data;
        public Edge inPointer;
        public Edge outPointer;

        public Node(T e)
        {
            data = e;
        }
    }
    private class Edge
    {
        public int startIndex;
        public int endIndex;
        public int weight;
        public Edge pre;
        public Edge next;

        public Edge(int startIndex,int endIndex,int weight)
        {
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.weight = weight;
        }
    }

    private readonly Node<T>[] _nodes;
    private int _count;

    public GraphByOrthogonalList(int capacity)
    {
        _nodes = new Node<T>[capacity];
    }
    public void AddNode(T e)
    {
        _nodes[_count++] = new Node<T>(e);
    }

    public void AddEdge(int node1Index, int node2Index, int weight)
    {
        var edge = new Edge(node1Index,node2Index,weight);
        // 先插入出边
        edge.next = _nodes[node1Index].outPointer;
        _nodes[node1Index].outPointer = edge;
        // 再插入入边
        edge.pre = _nodes[node2Index].inPointer;
        _nodes[node2Index].inPointer = edge;
    }
}

四、邻接多重表

邻接表对于无向图来说同样存在某些问题。比如如果我们需要删除下图中B->C的边,就需要删除邻接表中的两个节点。要找到这两个节点,就需要对B的邻接链表和C的邻接链表分别进行遍历。

为了解决这一问题,我们需要对边结构进行如下改造。index1和index2分别存储这条边连接的两个顶点。next1指向依附index1的下一条边,next2指向依附index2的下一条边。

图的邻接多重表结构如下。可能有些凌乱,但其核心思想就是将原本需要两个节点表示的边简化为一个节点表示。为了让一个节点能够表示一条边,理所当然的就要存储起止节点的下标。同时,因为每条边都必定与两个顶点相连,所以就需要两个指针域分别存储对应顶点的指针。
比如对于B顶点来说,有三条从自己出发的边。为了能够在邻接链表中遍历到这三条边,就需要通过自己的指针先找到(1,3)这条边。然后根据1右侧的指针域,找到(1,2)这条边。然后再根据1右侧的指针域找到(0,1)这条边。对于C顶点来说,就需要通过自己的指针找到(1,2)这条边,然后根据2右侧的指针域找到(0,2)这条边。

构造过程的代码如下

public class GraphByAdjacencyMultiList<T> : IGraph<T>
{
    private class Node<T>
    {
        public T data;
        public Edge next;
        public Node(T data)
        {
            this.data = data;
        }
    }
    
    private class Edge
    {
        public int index1;
        public Edge next1;
        public int index2;
        public Edge next2;
        public int weight;

        public Edge(int index1,int index2,int weight)
        {
            this.index1 = index1;
            this.index2 = index2;
            this.weight = weight;
        }
    }

    private Node<T>[] _nodes;
    private int _count;
    
    public GraphByAdjacencyMultiList(int capacity)
    {
        _nodes = new Node<T>[capacity];
    }
    
    public void AddNode(T e)
    {
        _nodes[_count++] = new Node<T>(e);
    }

    public void AddEdge(int node1Index, int node2Index, int weight)
    {
        var edge = new Edge(node1Index, node2Index,weight);
        edge.next1 = _nodes[node1Index].next;
        _nodes[node1Index].next = edge;

        edge.next2 = _nodes[node2Index].next;
        _nodes[node2Index].next = edge;
    }
}

五、边集数组

边集数组就比较简单了,它由两个一维数组组成。一个存储顶点信息,另一个存储边的信息。由于采用了一维数组,所以想要查询一个顶点的度就需要遍历整个数组,效率不高。但它本身就不是为了关注顶点相关的操作,而关注的是对边依次进行处理的操作。

边集数组的边结构设计如下。index1和index2分别存储起止顶点下标,weight存储边的权值。

边集数组结构如下。很容易理解,这里不再赘述。

代码如下

public class GraphByEdgeCollectionArray<T> : IGraph<T>
{
    private class Node<T>
    {
        public T data;
        public Node(T data)
        {
            this.data = data;
        }
    }
    private class Edge
    {
        public int index1;
        public int index2;
        public int weight;

        public Edge(int index1,int index2,int weight)
        {
            this.index1 = index1;
            this.index2 = index2;
            this.weight = weight;
        }
    }

    private Node<T>[] _nodes;
    private List<Edge> _edges;
    private int _count;

    public GraphByEdgeCollectionArray(int capacity)
    {
        _nodes = new Node<T>[capacity];
        _edges = new List<Edge>();
    }
    
    public void AddNode(T e)
    {
        _nodes[_count++] = new Node<T>(e);
    }

    public void AddEdge(int node1Index, int node2Index, int weight)
    {
        var edge = new Edge(node1Index, node2Index, weight);
        _edges.Add(edge);
    }
}

六、参考资料

[1].《大话数据结构》
[2]. https://blog.csdn.net/bible_reader/article/details/71250117

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/30009.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

pytorch案例代码-2

循环神经网络——基础知识 适合前后有联系的连续数据预测&#xff0c;比如天气预测、股市预测、自然语言等&#xff0c;而这些用DNN、CNN来做计算量就太大或者没法做&#xff0c;h0是先验&#xff0c;也可以前面接上CNNFC后面连上RNN&#xff0c;就可以完成图像到文本的转换&am…

AE VAE 代码和结果记录

Auto Encoder 在MNIST 上记录 直接上代码 import os os.chdir(os.path.dirname(__file__)) import torch import torch.nn as nn import torch.nn.functional as F import torchvision from torchvision import transforms from torchvision.utils import save_image from to…

数据结构-学习-01-线性表之顺序表-初始化、销毁、清理、获取长度、判断为空、获取元素等实现

一、测试环境 名称值cpu12th Gen Intel Core™ i7-12700H操作系统CentOS Linux release 7.9.2009 (Core)内存3G逻辑核数2gcc 版本4.8.5 20150623 二、个人理解 数据结构分为逻辑结构和物理结构&#xff08;也称为存储结构&#xff09;。 1、逻辑结构 逻辑结构又可以分为以下…

JS 事件

事件 事件是 JS 和 HTML 交互的桥梁。采用“观察者模式”&#xff0c;使用仅在事件发生时执行的监听器&#xff08;也叫处理程序&#xff09;订阅事件 事件流 事件流描述的是页面接收事件的顺序。分为 3 各阶段&#xff1a; 事件捕获&#xff1a;最先触发&#xff0c;可以做…

致敬经典 睛彩再现——AVS产业联盟和中国移动咪咕公司携手推动AVS3视频、音频标准

2022年11月14日&#xff0c;中国移动咪咕公司首发AVS3移动端规模化商用版本咪咕视频6.0.7.00&#xff0c;该版本下设的“致敬经典 睛彩再现”专区、以及“菁彩视听”双Vivid直播视角&#xff08;Audio Vivid & HDR Vivid&#xff09;&#xff0c;通过国家自主的AVS3、Audio…

回顾复习【矩阵分析】初等因子 和 矩阵的相似 || 由不变因子求初等因子 || 由初等因子和秩求Smith标准形(不变因子)

目录 1. 由不变因子,引出 初等因子的概念2. 【必看】例子:已知 不变因子,求初等因子。3.【必看】 例子:已知 秩和初等因子,求史密斯标准形(不变因子)4. 分块矩阵 初等因子的 求法5. 数字矩阵的相似 与 入-矩阵的等价1. 由不变因子,引出 初等因子的概念 例如,下面两个矩阵…

Kotlin 开发Android app(十):Android控件绑定ViewBinding

上一节中&#xff0c;我们知道了Android的布局&#xff0c;这种把界面和逻辑控制分开&#xff0c;是编程里很好的分离方式&#xff0c;也大大的解耦了界面和逻辑控制&#xff0c;使得编程的逻辑不在和界面挂钩。 有了界面的布局&#xff0c;我们需要把界面和代码部分进行绑定&…

OpenPose训练教程

找遍全网都没有非常完整的OpenPose训练教程 决定自己摸索并且记录下来 openpose作者发布了一份训练代码&#xff0c;下面根据这个来操作 GitHUB地址&#xff1a; openpsoe_train 环境&#xff1a;ubuntu 执行matklab脚本的时候懒得下载新的matlab 就在windows下运行的 感觉没…

品质为先,服务不停,广州流辰信息公司恪守初心,匠心为民!

随着互联网技术的蓬勃发展&#xff0c;越来越多的企业也感受到了日益激烈的竞争&#xff0c;也意识到墨守成规的发展模式必当会让企业停滞不前&#xff0c;只有一步一个脚印&#xff0c;始终跟随市场的脚步创新升级&#xff0c;才有可能在汹涌的市场洪流中站稳脚跟。广州流辰信…

精简 Windows10

下载链接文后评论里找&#xff1a; 旧机福音 极限精简Win10系统Tiny10https://baijiahao.baidu.com/s?id1743901721464184983不想成天折腾操作系统&#xff0c;一直以来都认为跟着微软每月升级就好了。但是现实啪啪的打脸&#xff1a;升级到Windows11 22H2 后&#xff0c; 连…

常见算法设计与分析的简单C++代码实现(排列、二分法搜索、Dijkstra算法、元素换位、单调子序列、硬币问题、运动员最佳匹配问题)

常见算法设计与分析的简单C代码实现&#xff08;排列、二分法搜索、Dijkstra算法、元素换位、单调子序列、硬币问题、运动员最佳匹配问题&#xff09;1 一些简单排列问题2 二分法查找3 前后元素换位4 找最长单调递增子序列&#xff08;O(n2)复杂度&#xff09;5最小硬币问题一、…

c3p0,DBCP,Druid(德鲁伊)数据库连接池

c3p0&#xff0c;DBCP&#xff0c;Druid&#xff08;德鲁伊&#xff09;数据库连接池 每博一文案 佛说&#xff1a;前世 500 次的回眸&#xff0c;才换来今生的一次擦肩而过。 人与人之间的缘分&#xff0c;真的无需强求&#xff0c;并不是所有的感情都能天长地久&#xff0c;…

C#压缩图片

SqlSer数据库设置保存图片字段类型为Image类型 对应保存 方法参数为图片路径&#xff0c;压缩后路径&#xff0c;压缩最大宽度&#xff0c;压缩最大高度 引用类型using System.Data; using System.Drawing; using System.IO; \完整类 /// <summary> /// 按比例缩放&…

七牛qshell 批量上传 mac 本地目录

七牛qshell 批量上传 mac 本地目录下载路径及使用方法(官方)下载到自己指定的文件夹添加环境变量,使qshell在任意地方可以执行添加密钥 生成账户文件下载路径及使用方法(官方) https://developer.qiniu.com/kodo/1302/qshell记录自己部署遇到的问题及操作步骤 下载到自己指定…

音视频开发核心知识点及源码解析,还不赶紧收藏起来

随着基础设施的完善&#xff08;光纤入户、wifi覆盖、5G普及&#xff09;的影响&#xff0c;将短视频、直播、视频会议、在线教育、在线医疗瞬间推到了顶峰&#xff0c;人们对音视频的需求和要求也越来越强烈 音视频开发还具有许多方向&#xff0c;比如&#xff1a; 如果对音视…

C语言:while后加分号与for后加分号的区别

while 后面不能加分号&#xff0c;否则虽然编译可以通过&#xff0c;但是执行程序时会发生死循环#include <stdio.h> int main() { int i1,total0; while(i<100)//不能在 while 后面加分号 { totali; i;//循环…

个人付费专栏上线预热

个人付费专栏上线预热 专栏地址&#xff1a;请点击访问 文章目录一、订阅这个专栏有什么好处&#xff1f;二、实战项目预告1. 活动类站点 &#xff08;已完成前端后端&#xff09;2. 电商项目 &#xff08;筹备中&#xff0c;一比一还原设计图&#xff09;3. 论坛问答系统 &…

每日三题-爬楼梯、买卖股票的最佳时机、正则表达式匹配

&#x1f468;‍&#x1f4bb;个人主页&#xff1a; 才疏学浅的木子 &#x1f647;‍♂️ 本人也在学习阶段如若发现问题&#xff0c;请告知非常感谢 &#x1f647;‍♂️ &#x1f4d2; 本文来自专栏&#xff1a; 算法 &#x1f308; 算法类型&#xff1a;Hot100题 &#x1f3…

IP 摄像机移动应用 SDK 开发入门教程(安卓版)

涂鸦智能安卓版摄像机&#xff08;IP Camera&#xff0c;简称 IPC&#xff09;SDK 是基于智能生活 App SDK 开发而成。 通过移动应用控制物理网设备是常见的使用场景&#xff0c;但由于设备的品类丰富&#xff0c;增大了应用开发难度。因此 智能生活 App SDK 提供了常见的垂直…

支付宝支付内网穿透

支付宝支付&内网穿透一 沙箱环境二 python第三方模块python-alipay-sdk三 python-alipay-sdk二次封装四 支付接口五 内网穿透5.1 cpolar软件5.2 测试支付宝post回调一 沙箱环境 注册认证沙箱环境&#xff1a;https://openhome.alipay.com/platform/appDaily.htm?tabinfo …