js 数据结构之图

143 阅读6分钟

图的基本概念与实现……


一 基本概念

图(Graph)是由顶点和连接顶点的构成的离散结构。在计算机科学中,图是最灵活的数据结构之一,很多问题都可以使用图模型进行建模求解。

图可用公式表示:

// V:一组顶点;
// E: 一组边,用来连接 V 中的顶点
G=(V,E)

1. 普通概念

  • 相邻顶点:由一条边连接在一起的顶点。
  • 路径:依次遍历顶点序列之间的边所形成的轨迹。注意,依次就意味着有序,先 A 后 B 和先 B 后 A 不一样。

2. 无向图

图的边没有方向。

无向图.png

3. 有向图

图的边有方向。

有向图.png

4. 加权图

加权图.png

5. 环

通过某条路径可以返回到起始顶点。

环.png

二 图结构的表示方式

exampleGraph.png

1. 邻接矩阵

如下表,纵向为顶点,横向为相邻顶点,使用二维数组存储,如果两个顶点之间存在连线,则值为 1, 否则为 0。

会有比较多的内存浪费。

ABCDEFGHI
A011000000
B100110000
C100001100
D010000001
E010000000
F001000010
G001000010
H000001100
I000100000

2. 邻接表

邻接表是对邻接矩阵的改进,可以使用数组、链表、字典、散列表等来表示。

顶点相邻顶点
AB-C
BA-D-E
CA-F-G
DB-I
EB
FC-H
GC-H
HF-G
ID

3. 关联矩阵

纵向为顶点,横向为边。适用于顶点远大于边的情况。

e1e2e3e4e5e6e7e8e9
A110000000
B011100000
C100001100
D001010000
E000100000
F000001010
G000000101
H000000011
I000010000

三 图的创建

class Graph {
  constructor() {
    this.vertices = []; // 存放顶点
    // Dictionary类见 https://uninspire.gitee.io/blog/2021/06/28/0023-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8B%E6%95%A3%E5%88%97/
    this.edgeList = new Dictionary(); // 存放边
  }
  /*
  * 添加顶点
  */
  addVertex(v) {
    if (!this.vertices.includes(v)) {
      this.vertices.push(v);
      this.edgeList.push(v, []);
    }
  }
  /*
  * 添加边
  */
  addEdge(a, b) {
    // 如果顶点a不存在,则在图中添加a
    if (!this.vertices.includes(a)) {
      this.vertices.push(a);
    }
    // 如果顶点b不存在,则在图中添加b
    if (!this.vertices.includes(b)) {
      this.vertices.push(b);
    }
    // 在顶点a中添加指向顶点b的边
    this.edgeList.get(a).push(b);
    // 在顶点b中添加指向顶点a的边
    this.edgeList.get(b).push(a);
  }
  /*
  * 获取顶点列表
  */
  getVertices() {
    return this.vertices;
  }
  /*
  * 获取边列表
  */
  getEdgeList() {
    return this.edgeList;
  }
}

四 图的遍历

遍历的辅助功能:在遍历顶点的过程中,给每个节点定义一个状态,用来表示访问次数。每个顶点最多被访问两次。

  • 白:未被访问
  • 灰:被访问但是未被处理
  • 黑:已被处理

1. 广度优先搜索

从第一个顶点开始遍历,访问所有相邻顶点,再访问相邻顶点的相邻顶点。直到所有顶点访问结束。

{% asset_img 广度优先搜索.jpg 广度优先搜索图解 %}

// 定义三种状态 黑白灰
let Colors = {
  WHITE: 0,
  GRAY: 1,
  BLACK: 2
}
// 初始化顶点状态
let initVertices = vertices => {
  let color = {}
	vertices.forEach(v => color[v] = Colors.WHITE)
}

function deepSearch (graph, start, cb) {
  // 获取顶点列表
  let vertices = graph.getVertices()
  // 获取边裂变
  let edgeList = graph.getEdgeList()
  // 将所有顶点初始化状态
  let color = initVertices(vertices)
  // 定义一个空队列
  let queue = new Queue()
  // 将开始顶点推入队列中
  queue.push(start)
  // 循环遍历,直到队列为空
  while(!queue.isEmpty()) {
    // 获取队列第一个元素 队列从头部出队列
    let u = queue.pop()
    // 获取到顶点u所对应的所有相邻顶点,
    edgeList.get(u).forEach(item => {
      // 如果相邻顶点的状态为未访问,则推入到队列中
      if (color[item] = Colors.WHITE) {
        // 将已经访问过的顶点状态置为灰色
        color[item] = Colors.GRAY;
        // 将未访问的顶点推入到队列中
        queue.push(item)
      }
    })
    // 将已经处理过的顶点置为黑色
    color[u] = Color.BLACK
    // 打印输出顶点u
    console.log(u)
  }
}

无权图的最短路径问题

// 计算最短路径,只需要在 deepSearch 方法中添加计数即可
function deepSearch (graph, start, cb) {
  let vertices = graph.getVertices()
  let edgeList = graph.getEdgeList()
  let color = initVertices(vertices)
  let queue = new Queue()
  queue.push(start)
  
  // -------------------新添加-----------------------
  
  let distanceData = {} // 统计顶点之间距离
  let preVertices = {} // 统计顶点的前置顶点
  
  // 初始化顶点之间距离和前置顶点
  vertices.forEach(child => {
    distanceData[child] = 0
    preVertices[child] = null
  })
  
  // ------------------- 结束 -----------------------
  
  while(!queue.isEmpty()) {
    let u = queue.pop()
    edgeList.get(u).forEach(item => {
      if (color[item] = Colors.WHITE) {
        color[item] = Colors.GRAY;
        // 每经过一个新顶点,距离加1
        distanceData[item] = distanceData[item] + 1
        // 前置节点修改为 item
        preVertices[item] = item
        queue.push(item)
      }
    })
    color[u] = Color.BLACK
  }
  // 打印所有节点之间的距离和前直节点
  console.log(distanceData, preVertices)
}

加权图的最短路径(狄克斯特拉算法)

// 取当前数值的最小值
const INF = Number.MAX_SAFE_INTEGER;
// 狄克斯特拉算法主体
const dijkstra = (graph, src) => {
  // 当前图的顶点列表
  const dist = [];
  // 已经遍历过的顶点列表
  const visited = [];
  // 图中的顶点数量
  const {length} = graph;
  // 将所有的权重设置为INF 所有的节点设置为为遍历状态
  for (let i = 0; i < length; i++) {
    dist[i] = INF;
    visited[i] = false;
  }
  // 当前节点的权重设置为0
  dist[src] = 0;
  // 遍历所有的顶点
  for (let i = 0; i < length - 1; i++) {
    // 获取到距离当前顶点权重最低的节点
    const index = minDistance(dist, visited);
    // 将此节点设置为访问状态
    visited[index] = true;
    for (let v = 0; v < length; v++) {
      if (!visited[v] && graph[index][v] !== 0 && dist[index] !== INF && dist[index] + graph[index][v] < dist[v]) {
        dist[v] = dist[index] + graph[index][v];
      }
    }
  }
  return dist;
};
// 获取距离当前顶点最短距离的顶点索引
const minDistance = (dist, visited) => {
  let min = INF;
  let minIndex = -1;
  for (let v = 0; v < dist.length; v++) {
    // 如果当前顶点未被访问过,且两者之间的距离小于最小距离,则将最小距离设置为此顶点,并记录此节点的索引
    if (visited[v] === false && dist[v] <= min) {
      min = dist[v];
      minIndex = v;
    }
  }
  return minIndex;
};

2. 深度优先搜索

沿着第一个顶点的一条路径递归查找到最后一个顶点,然后返回查找未被探索的路径,直到所有路径都被查到。

{% asset_img 深度优先搜索.jpg 深度优先搜索图解 %}

let Colors = {
  WHITE: 0,
  GRAY: 1,
  BLACK: 2
}
// 深度遍历顶点的方法
let depthFirstSearchVisit = (u, color, adjList, callback) => {
    color[u] = Colors.GREY;
    if (callback) callback(u);
		// 遍历顶点
    adjList.get(u).forEach(n => {
      	// 如果顶点的颜色为白色,则将此顶点推入栈中
        if (color[n] === Colors.WHITE) {
            depthFirstSearchVisit(n, color, adjList, callback);
        }
    });
		// 处理过的顶点置为黑色
    color[u] = Colors.BLACK;
};

let depthFirstSearch = (graph, callback) => {
  	// 获取当前的顶点列表
    let vertices = graph.getVertices();
  	// 获取当前的边列表
    let adjList = graph.getAdjList();
  	// 初始化顶点状态
    let color = initializeColor(vertices);
		// 遍历为被访问过的顶点
    vertices.forEach(v => {
        if (color[v] === Colors.WHITE) {
            depthFirstSearchVisit(v, color, adjList, callback);
        }
    });
};

参考:yancy__Jerzyraywenderlich