【数据结构】图的存储结构—邻接矩阵

简介: 【数据结构】图的存储结构—邻接矩阵

前言


由于图的结构比较复杂,任意两个顶点之间都可能存在关系(边),无法通过存储位置表示这种任意的逻辑关系,所以,图无法采用顺序存储结构。这一点同其他数据结构(如线性表、树)不同。


因为图中的顶点具有相对概念,没有固定的位置,且顶点和顶点之间通过添加和删除边,维持着不同的关系。考虑图的定义,图是由顶点和边组成的。所以,分别考虑如何存储顶点和边。图常用的存储结构有邻接矩阵、邻接表、十字链表和邻接多重表。那么对于一般情况下该怎么存储图的数据结构呢?这里我们主要分两个章节详细介绍两种常用的图存储结构 — 邻接矩阵、邻接表


477a9cd13fea4554812e5de1ab9fd368.gif


图的类型&存储结构的介绍

图的类型主要有4种:无向图、有向图、无向网和有向网。

可以用枚举表示为:

public enum GraphKind{
    UDG,  //无向图(UnDirected Graph)
    DG,   //有向图(Directed Graph)
    UDN,  //无向网(UnDirected Network)
    DN,   //有向网(Directed Network)
}

图有多种存储结构,每种存储结构都能表示上面的4种的类型。图的存储结构除了存储图中各个顶点的信息外,还需要存储与顶点相关的边的信息。


常见图的存储结构:


邻接矩阵


邻接表


邻接多重表


十字链表


ef4a5769a5f741bcb47bb34fbf5819ee.png


邻接矩阵


— 无向图、有向图的邻接矩阵定义


逻辑结构分为两部分:V和E集合,其中,V是顶点,E是边。因此,用一个一维数组存放图中所有顶点数据;用一个二维数组存放顶点间关系(边或弧)的数据,这个二维数组称为邻接矩阵。邻接矩阵又分为有向图邻接矩阵和无向图邻接矩阵。


图的邻接矩阵:用来表示顶点之间相邻关系的矩阵。


图G=(V, E)具有n(n >= 1)个顶点,顶点的顺序依次为{v0,v1,...,vn-1}


设图A=(V,E)有n个顶点,则关于顶点数据的一维数组为:


aa9cc045f7634145914a9af0b76603fd.png


则图G的邻接矩阵A是一个n阶方阵,定义如下:


5025ff8fa6cf4f779e875fb315a97398.png


特点1:无向图 的邻接矩阵一定是对称 的,而 有向图 的邻接矩阵不一定对称。


因此,用 对无向图而言,邻接矩阵一定是对称的,而且主对角线一定为零,副对角线不一定为0,有向图则不一定如此。


邻接矩阵来表示一个具有n个顶点的有向图时需要n^2个单元来存储邻接矩阵;对有n个顶点的无向图则只需存入上(下)三角阵中剔除了左上右下对角线上的0元素后剩余的元素压缩存储,故只需1+2+...+(n-1)=n(n-1)/2个单元。


5075576495de4cb296fecb6c819bbecb.png


特点2:无向图邻接矩阵的 第i行(或第i列)非零元素的个数 正好是第i个顶点的度。


度(Degree):一个顶点的度是指与该顶点相关联的边的条数,顶点v的度记作d(v)”


无向图,没有方向,所有两个顶点连线必定是相互的。因此行或列的非零元素个数必定一样。则找顶点的度时,看行或列都可以。


2a7d83f0822e4671936447372fe89487.png


特点3:


有向图顶点的度【行表示出度,列表示入度】

顶点v的入边数目是该顶点的入度,记为ID(v);

顶点v的出边数目是该顶点的出度,记为OD(v);

顶点v的度等于它的入度和出度之和,即D(v)=ID(v)+OD(v)

有向图邻接矩阵中第i行非零元素的个数为第i个顶点的出度,第i列非零元素的个数为第i个顶点的入度,第i个顶点的度为第i行与第i列非零元素个数之和。


6ec2a0e937804c94826ed4648c998b89.png


特点4:用邻接矩阵表示图,很容易看出确定图中任意两个顶点是否有边相连。


无向图的邻接矩阵是对称的,一般可以采用压缩存储。


— 网的邻接矩阵的定义


网(network):带权的图


60101805428844d6babb1a1321cb84a9.png


图与网的区别:


               1. 权值:网里面对应的边是有权值的,用以表示边的某种属性比如距离等。而图的边是没有权值的


               2. 表示零元素的形式不同:不管是无向图还是有向图表示零元素时,都用0代替表示;而在网中,表示零元素则是用无穷大∞ 表示。


无向网的邻接矩阵:


763cc04a16b64c81bb5d8a8a4c3d7650.png


有向网的邻接矩阵 :


e474f8cefb3b41da98027126440b8d71.png


邻接矩阵:类的描述


public class MGraph implements IGraph {
  public final static int INFINITY = Integer.MAX_VALUE;
  private GraphKind kind;   //图的种类标志
  private int vexNum, arcNum; //图的当前顶点数和边数
  private Object[] vexs;    //顶点集
  private int[][] arcs;   //邻接矩阵(边集)
    public void createGraph(){}   //创建图
    private void createUDG() {}   //创建无向图
    private void createDG() {}    //创建有向图
    private void createUDN() {}   //创建无向网
    private void createDN() {}    //创建有向网
  public int getVexNum() {    //返回定点数
    return vexNum;
  }
  public int getArcNum() {    //返回边数
    return arcNum;
  }
    // 给定顶点的值vex,返回其在图中的位置,如果图中不包含此顶点,则返回-1
    public int locateVex(Object vex) {}
    // 返回v表示结点的值,0 <= v <= vexNum
    public Object getVex(int v) throws Exception {}
    // 返回v的第一个邻接点,若v没有邻接点则返回-1。其中 0 <= v <= vexNum
    public int firstAdjVex(int v) throws Exception {}
    // 返回v相对于w的下一个邻接点,若w是v的最后一个邻接点,则返回-1。其中 0 <= v,w <= vexNum
    public int nextAdjVex(int v, int w) throws Exception {}
}

邻接矩阵:基本操作

1)创建图

// 创建图
  public void createGraph() {
        // 获得用户输入的图的类型
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入图的类型:");
    GraphKind kind = GraphKind.valueOf(sc.next()); //通过字符串获得对应枚举类型
    switch (kind) {   //根据不同的枚举值,选择不同的图或网的创建
    case UDG:
      createUDG();  // 构建无向图
      return;
    case DG:
      createDG();   // 构建有向图
      return;
    case UDN:
      createUDN();  // 构建无向网
      return;
    case DN:
      createDN();   // 构建有向网
      return;
    }
  }

2)创建无向网

  • 输入图的顶点、边及权值构造无向图,步骤:
  1. 输入顶点数或边数
  2. 根据图的顶点数构建邻接矩阵
  3. 根据图的边数,确定输入边的数目
  4. 根据输入每条边的顶点再邻接矩阵相应位置保存每条边的权值。
  • 代码
private void createUDN() {
    //初始化变量
    Scanner sc = new Scanner(System.in);
    System.out.println("请分别输入图的顶点数、图的边数:");
    vexNum = sc.nextInt();//顶点数n
    arcNum = sc.nextInt();//边数e
    //输入图中各顶点
    vexs = new Object[vexNum];      //顶点集对应的数组
    System.out.println("请分别输入图的各个顶点:");
    for (int v = 0; v < vexNum; v++)  //通过循环依次输入各个顶点
        vexs[v] = sc.next();
    //定义邻接矩阵
    arcs = new int[vexNum][vexNum];
    // 初始化邻接矩阵
    for (int v = 0; v < vexNum; v++)
        for (int u = 0; u < vexNum; u++)
            arcs[v][u] = INFINITY;      //初始为无穷大
    //输入边信息
    System.out.println("请输入各个边的两个顶点及其权值:");
    for (int k = 0; k < arcNum; k++) {
        int v = locateVex(sc.next()); //顶点
        int u = locateVex(sc.next()); //顶点
        arcs[v][u] = arcs[u][v] = sc.nextInt();  //权值, 对称
    }
}

10ddb1c6cf9f4b96a00710340c101766.png

3)创建有向网

// 构建有向网
  private void createDN() {
    Scanner sc = new Scanner(System.in);
    System.out.println("请分别输入图的顶点数、图的边数:");
    vexNum = sc.nextInt();
    arcNum = sc.nextInt();
    vexs = new Object[vexNum];
    System.out.println("请分别输入图的各个顶点:");
    for (int v = 0; v < vexNum; v++)
      //构造顶点向量
      vexs[v] = sc.next();
    arcs = new int[vexNum][vexNum];
    for (int v = 0; v < vexNum; v++)
      // 初始化邻接矩阵
      for (int u = 0; u < vexNum; u++)
        arcs[v][u] = INFINITY;
    System.out.println("请输入各个边的两个顶点及其权值:");
    for (int k = 0; k < arcNum; k++) {
      int v = locateVex(sc.next());
      int u = locateVex(sc.next());
      arcs[v][u] = sc.nextInt();  //仅设置顶点v-->顶点u,不是对称的,只需要设置一个
    }
  }

4)顶点定位

  • 根据顶点信息vex,取得其在顶点数组中的位置,若图中无此顶点,则返回-1。
  • 代码:
// 根据顶点信息vex,取得其在顶点数组中的位置,若图中无此顶点,则返回-1。
  public int locateVex(Object vex) {
    // 遍历顶点数组
    for (int v = 0; v < vexNum; v++)
      if (vexs[v].equals(vex))
        return v;
    return -1;
  }
  • 时间复杂度:O(n)

5)查询第一个邻接点

步骤:

  • 判断v的合法性
  • 顶点v的索引号,对应邻接矩阵的第v行,遍历第v行,查找是否有非0、非无穷大值的元素
  • 代码:
// 已知图中的一个顶点v,返回v的第一个邻接点,如果v没有连接点,则返回-1,其中0 <= v < vexNum
public int firstAdjVex(int v) throws Exception {
  if (v < 0 || v >= vexNum)
    throw new Exception("第" + v + "个顶点不存在!");
  for (int j = 0; j < vexNum; j++)
    if (arcs[v][j] != 0 && arcs[v][j] < INFINITY)
      return j;
  return -1;
}
  • 时间复杂度:O(n)

6)查找下一个邻接点

步骤:

  • 判断v的合法性
  • 从邻接矩阵第v行第w+1列开始遍历查找是否有非0、非无穷大值的元素
  • 代码:
public int nextAdjVex(int v, int w) throws Exception {
  if (v < 0 || v >= vexNum)
    throw new Exception("第" + v + "个顶点不存在!");
  for (int j = w + 1; j < vexNum; j++)
    if (arcs[v][j] != 0 && arcs[v][j] < INFINITY)
      return j;
  return -1;
}
  • 时间复杂度:O(n)
相关文章
|
11月前
|
存储 算法 C++
【C++数据结构——图】图的邻接矩阵和邻接表的存储(头歌实践教学平台习题)【合集】
本任务要求编写程序实现图的邻接矩阵和邻接表的存储。需掌握带权有向图、图的邻接矩阵及邻接表的概念。邻接矩阵用于表示顶点间的连接关系,邻接表则通过链表结构存储图信息。测试输入为图的顶点数、边数及邻接矩阵,预期输出为Prim算法求解结果。通关代码提供了完整的C++实现,包括输入、构建和打印邻接矩阵与邻接表的功能。
480 10
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
625 16
java数据结构,线性表链式存储(单链表)的实现
文章讲解了单链表的基本概念和Java实现,包括头指针、尾节点和节点结构。提供了实现代码,包括数据结构、接口定义和具体实现类。通过测试代码演示了单链表的基本操作,如添加、删除、更新和查找元素,并总结了操作的时间复杂度。
java数据结构,线性表链式存储(单链表)的实现
|
存储 安全 数据库
除了 HashMap,还有哪些数据结构可以实现键值对存储?
【10月更文挑战第11天】 除了`HashMap`,其他常见支持键值对存储的数据结构包括:`TreeMap`(基于红黑树,键有序)、`LinkedHashMap`(保留插入顺序)、`HashTable`(线程安全)、`B-Tree`和`B+Tree`(高效存储大量数据)、`SkipList`(通过跳跃指针提高查找效率)及`UnorderedMap`(类似`HashMap`)。选择合适的数据结构需根据排序、并发、存储和查找性能等需求。
|
存储 编译器 C++
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
173 4
探索顺序结构:栈的实现方式
探索顺序结构:栈的实现方式
177 0
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
298 59
|
6月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
131 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。