基础数据结构(六):图结构

73 阅读5分钟

图的定义

维基百科这样定义图结构:(英语:graph)是一种抽象数据类型,用于实现数学图论无向图有向图的概念。

图的数据结构包含一个有限(可能是可变的)的集合作为节点集合,以及一个无序对(对应无向图)或有序对(对应有向图)的集合作为(有向图中也称作)的集合。节点可以是图结构的一部分,也可以是用整数下标或引用表示的外部实体。

图的数据结构还可能包含和每条边相关联的数值(edge value),例如一个标号或一个数值(即权重,weight;表示花费、容量、长度等)。

image.png
通俗的说,图由一组顶点和一组边组成,边是顶点和顶点之间的连线,边可以是有向的也可以是无向的。

七桥问题

七桥问题是18世纪著名的数学问题之一,问题是:由七座桥连接起来的小河两岸及两个岛,一个人怎么才能不重复、不遗漏地一次走完七座桥,最后回到出发点

image.png 该问题由著名数学家欧拉给出答案:该问题无解。并给出了连通图可以一笔画的充要条件:

  • 奇点的数目不是0个就是2个
  • 连到一点的边的数目如果是奇数条,就称为奇点;如果是偶数条就称为偶点
  • 想要一笔画成,必须中间点均是偶点,奇点只能在两端,因此任何图能一笔画成,奇点要么没有要么在两端

图的表示

在程序中如何表示图结构呢?图是由顶点和边构成,对于顶点我们可以使用数组保存,对于边则有两种表示方式:邻接矩阵和邻接表

邻接矩阵

邻接矩阵让每个节点和一个整数项关联,该整数作为数组的下标值,我们用一个二维数组来表示顶点之间的连接。如下图所示:

image.png 如果两个顶点之间被直接连接,则二维矩阵中对应的值就是1,反之为0。ps:A与B、C、D直接连接,所以arr[0][1]、arr[0][2]、arr[0][3]都是1。通过二维数组,我们可以很快的找到一个顶点和哪些顶点有连线。
邻接矩阵有一个比较严重的问题:当图比较稀疏的时候,二维数组中有着大量的0,非常浪费内存空间。

邻接表

邻接表由图中每个顶点以及和顶点相邻的顶点列表组成,这个顶点列表可以是数组/链表/哈希表等。如下图所示

image.png 邻接表的问题:邻接表计算出度是比较简单的(出度:指向别人的数量,入度:指向自己的数量),但是计算入度是一件非常麻烦的事情。所以一般构造一个“逆邻接表”用来计算入度。

图的封装

我们使用邻接表的方式来封装,邻接表使用Set,目的是可以自动去重。图的定义:

class Graph<T> {
  // 顶点
  vertices: T[] = [];
  // 边 用邻接表存储
  private adjList: Map<T, Set<T>> = new Map();
}

添加顶点和边

添加顶点很简单,只需要往vertices中push的同时,adjList set一个新数组

// 添加顶点
addVertex(v: T) {
  if (!this.vertices.includes(v)) {
    this.vertices.push(v);
    // 初始化邻接表
    this.adjList.set(v, new Set());
  }
}

添加边,入参为两个顶点:

// 添加边 v和w之间添加边
addEdge(v: T, w: T) {
  if (!this.adjList.get(v)) {
    this.addVertex(v);
  }
  if (!this.adjList.get(w)) {
    this.addVertex(w);
  }
  this.adjList.get(v)!.add(w);
  this.adjList.get(w)!.add(v);
}

打印

// 打印
traverse() {
  this.vertices.forEach(v => {
    console.log(`${v} -> ${Array.from(this.adjList.get(v)!).join(' ')}`)
  })
}

图的遍历

图的遍历需要保证每个顶点都要被访问一遍且不能重复访问,有两种遍历方式:广度优先搜索(BFS)和深度优先搜索(DFS),两种方法都要明确执行第一个被访问的顶点。

广度优先搜索

BFS是从根节点开始,沿着树的宽度遍历树的节点。如果所有节点均被访问,则算法中止。这种搜索算法可以使用队列来完成,就像树的层序遍历一样。

// 广度优先遍历
// 广度优先搜索
bfs() {
  // 判断有无顶点
  if (!this.vertices.length) return;
  // 创建一个队列
  const queue: T[] = [];
  // 创建一个Set来保存已经被加入到队列中的顶点
  const pushed = new Set<T>();
  // 将第一个顶点加入到队列
  queue.push(this.vertices[0]);
  // 添加到visited
  pushed.add(this.vertices[0])
  // 遍历队列
  while(queue.length) {
    const vertex = queue.shift()!;
    // 访问
    console.log(vertex)
    // 获取vertex的邻接表
    const adjList = this.adjList.get(vertex)!
    // 将邻接表中没有被访问过的顶点添加到队列
    adjList.forEach(vertexItem => {
      if (!pushed.has(vertexItem)) {
        queue.push(vertexItem)
        pushed.add(vertexItem)
      }
    })
  }
}
深度优先搜索

深度优先搜索会尽可能深地搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
深度优先搜索可以使用栈来完成,思路其实跟广度优先搜索类似,只是把队列换成了栈:

// 深度优先搜索
dfs() {
  // 判断有无顶点
  if (!this.vertices.length) return;
  // 创建一个Set用来记录已经入栈的顶点
  const pushed = new Set<T>();
  // 创建一个栈
  const stack: T[] = [];
  // 先将第一个顶点入栈
  stack.push(this.vertices[0]);
  pushed.add(this.vertices[0]);
  while(stack.length) {
    // 出栈
    const vertex = stack.pop()!;
    console.log(vertex);
    // 获取邻接表
    const adjList = this.adjList.get(vertex)!;
    // 为了确保访问顺序是添加的顺序 需要翻转一下邻接表 保证先建立联系顶点先出栈
    const adjListReverse = Array.from(adjList).reverse()
    adjListReverse.forEach(vertexItem => {
      if (!pushed.has(vertexItem)) {
        stack.push(vertexItem)
        pushed.add(vertexItem)
      }
    })
  }
}

以上就是图结构的简单封装