JavaScript 数据结构与算法(四):图

人与人之间的关系非常复杂。比如你认识的朋友,可能他们之间也互相认识。这就不是简单的一对一、一对多,而是多对多的关系,这便是图。

(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成, 通常表示为:G(V,E),其中,G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。

关于图的定义,需要注意的是:

  • 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,则称之为顶点(Vertex)。
  • 在图结构中,不允许没有顶点。
  • 在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
一些图定义
  • 无向边:若顶点 vi 到 vj 之间的边没有方向,则称这条边为无向边 (Edge),用无序偶对 (vi,vj) 来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图 (Undirected graphs)。

    由于是无方向的,连接顶点 A 与 D 的边,可以表示成无序对 (A,D),也可以写成 (D,A)

  • 有向边:若从顶点 vi 到 vj 的边有方向,则称这条边为有向边,也称为弧 (Arc)。用有序偶对 <vi,vj> 来表示,vi 称为弧尾 (Tail),vj 称为弧头 (Head)。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图

    连接顶点 A 到 D 的有向边就是弧,A 是弧尾,D 是弧头,<A,D> 表示弧,而不能写成 <D,A>。

  • 在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图

  • 在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图

    含有 n 个顶点的无向完全图有 n(n-1)/2 条边。

  • 在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图

    含有 n 个顶点的有向完全图有 n(n-1) 条边。

  • 有很少条边或弧的图称为稀疏图,反之称为稠密图

    不过稀疏和稠密是模糊的概念,都是相对而言的。

  • 有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做 (Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。带权的图通常称为 (Network)。

  • 假设有两个图 G=(V,{E}) 和 G’=(V’,{E’}),如果 V’⊆V 且 E’⊆E,则称 G’ 为 G 的子图

  • 图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为,当中不重复的叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量


应用

可以用图对现实中的很多 系统建模

比如对交通流量建模顶点可以表示街道的十字路 口,可以表示街道。加权的边可以表示限速或者车道的数量。建模人员可以用这个系统来判断最佳路线及最有可能堵车的街道。

任何运输系统都可以用图来建模。比如,航空公司可以用图来为其飞行系统建模。将每个机场看成顶点,将经过两个顶点的每条航线看作一条。加权的边可以表示从一个机场到另一个机场的航班成本,或两个机场间的距离,这取决于建模的对象是什么。

包含局域网和广域网(如互联网)在内的计算机网络,同样经常用图来建模。

消费市场也可以用图来建模,顶点可以用来表示供应商和消费者。


图的存储结构:
  • 邻接矩阵
  • 邻接表
  • 十字链表
  • 邻接多重表
  • 边集数组

图的实现

🍊 表示顶点

创建图类的第一步就是要创建一个 Vertex 类来保存顶点。

Vertex 类有两个数据成员:一个用于标识顶点,另一个是表明这 个顶点是否被访问过的布尔值。它们分别被命名为 labelwasVisited。这个类只需要一个函数,那就是为顶点的数据成员设定值的构造函数。

Vertex 类的代码如下所示:

function Vertex(label) { 
  this.label = label;
}

我们将所有顶点保存到数组中,在图类里,可以通过它们在数组中的位置引用它们。

🍊 表示边

我们将表示图的边的方法称为邻接表或者邻接表数组。这种方法将边存储为由顶点的相邻顶点列表构成的数组,并以此顶点作为索引。

使用这种方法,当在程序中引用一个顶点时,可以高效地访问与这个顶点相连的所有顶点的列表。

比如,如果顶点 2 与顶点 0、 1、3、4 相连,并且它存储在数组中索引为 2 的位置,那么,访问这个元素,我们可以访问到索引为 2 的位置处由顶点 0、1、3、4 组成的数组。

另一种表示图边的方法被称为邻接矩阵。它是一个二维数组,其中的元素表示两个顶点之间是否有一条边。

构建图
🐱 Graph 类
function Graph(v) {
  this.vertices = v;
  this.edges = 0;
  this.adj = [];
  for (var i = 0; i < this.vertices; ++i) { // 通过 `for` 循环为数组中的每个元素添加一个子数组来存储所有的相邻顶点,并将所有元素初始化为空字符串。
    this.adj[i] = [];
    this.adj[i].push("");
  }
  this.addEdge = addEdge;
  this.showGraph = showGraph;
}
// 👆这个类会记录一个图表示了多少条边,并使用一个长度与图的顶点数相同的数组来记录顶点的数量。


function addEdge(v, w) {
  this.adj[v].push(w);
  this.adj[w].push(v);
  this.edges++;
}
// 👆当调用这个函数并传入顶点 A 和 B 时,函数会先查找顶点 A 的邻接表,将顶点 B 添加到列表中,然后再查找顶点 B 的邻接表,将顶点 A 加入列表。最后,这个函数会将边数加 1。


function showGraph() {
  for (var i = 0; i < this.vertices; ++i) {
    console.log(i + " -> ");
    for (var j = 0; j < this.vertices; ++j) {
      if (this.adj[i][j] != undefined) {
        console.log(this.adj[i][j] + ' ');
      }
    }
  }
}
// showGraph() 函数会通过打印所有顶点及其相邻顶点列表的方式来显示图
🐱 测试 Graph 类的实现
g = new Graph(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.showGraph();

输出结果如下:

0 -> 
 
1 
2 
1 -> 
 
0 
3 
2 -> 
 
0 
4 
3 -> 
 
1 
4 -> 
 
2 

搜索图

在图上可以执行两种基础搜索:深度优先搜索和广度优先搜索。

🍇 深度优先搜索

深度优先搜索 包括从一条路径的起始顶点开始追溯,直到到达最后一个顶点,然后回溯, 继续追溯下一条路径,直到到达最后的顶点,如此往复,直到没有路径为止。

深度优先搜索的搜索过程如下:

深度优先搜索算法:访问一个没有访问过的顶点,将它标记为已访问,再递归地
去访问在初始顶点的邻接表中其他没有访问过的顶点。

function Graph(v) {
  this.vertices = v;
  this.edges = 0;
  this.adj = [];
  for (var i = 0; i < this.vertices; ++i) {
    this.adj[i] = [];
    this.adj[i].push("");
  }
  this.addEdge = addEdge;
  this.showGraph = showGraph;
  this.dfs = dfs;
  this.marked = []; // 为 Graph 类添加一个数组,用来存储已访问过的顶点
  for (var i = 0; i < this.vertices; ++i) { // 将它所有元素的值全部初始化为 false
    this.marked[i] = false;
  }
}


function addEdge(v, w) {
  this.adj[v].push(w);
  this.adj[w].push(v);
  this.edges++;
}

function showGraph() {
  for (var i = 0; i < this.vertices; ++i) {
    console.log(i + " -> ");
    for (var j = 0; j <
      this.vertices; ++j) {
      if (this.adj[i][j] != undefined)
        console.log(this.adj[i][j] + ' ');
    }
  }
}

function dfs(v) {
  this.marked[v] = true;
  if (this.adj[v] != undefined) {
    console.log("Visited vertex: " + v);
  }
  if (this.adj[v] !== undefined) {
    this.adj[v].forEach(element => {
      if (!this.marked[element]) {
        this.dfs(element);
      }
    });
  }
}

// 测试 dfs()
g = new Graph(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.showGraph();
g.dfs(0);

输出结果为:

0 -> 
 
1 
2 
1 -> 
 
0 
3 
2 -> 
 
0 
4 
3 -> 
 
1 
4 -> 
 
2 
Visited vertex: 0
Visited vertex: 1
Visited vertex: 3
Visited vertex: 2
Visited vertex: 4
🍇 广度优先搜索

广度优先搜索 从第一个顶点开始,尝试访问尽可能靠近它的顶点。本质上,这种搜索在图上是逐层移动的,首先检查最靠近第一个顶点的层,再逐渐向下移动到离起始顶点最远的层。

广度优先搜索的搜索过程:

广度优先搜索算法 使用了抽象的队列来对已访问过的顶点进行排序。其算法的工作原理如下:
(1) 查找与当前顶点相邻的未访问顶点,将其添加到已访问顶点列表及队列中;
(2) 从图中取出下一个顶点 v,添加到已访问的顶点列表;
(3) 将所有与 v 相邻的未访问顶点添加到队列。

function bfs(s) {
  var queue = [];
  this.marked[s] = true;
  queue.push(s); // 添加到队尾 
  while (queue.length > 0) {
    var v = queue.shift(); // 从队首移除 
    if (this.adj[v] !== undefined) {
      console.log("Visisted vertex: " + v);
    }
    if (this.adj[v] !== undefined) {
      this.adj[v].forEach(element => {
        if (!this.marked[element]) {
          this.marked[element] = true;
          queue.push(element);
        }
      });
    }
  }
}

// 测试
g = new Graph(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.showGraph();
g.bfs(0);

输出结果如下:

0 -> 
 
1 
2 
1 -> 
 
0 
3 
2 -> 
 
0 
4 
3 -> 
 
1 
4 -> 
 
2 
Visisted vertex: 0
Visisted vertex: 1
Visisted vertex: 2
Visisted vertex: 3
Visisted vertex: 4

查找最短路径

广度优先搜索对应的最短路径。

在执行广度优先搜索时,会自动查找从一个顶点到另一个相连顶点的最短路径。

例如,要查找从顶点 A 到顶点 D 的最短路径,首先会查找从 A 到 D 是否有任何一条单边路径, 接着查找两条边的路径,以此类推。这正是广度优先搜索的搜索过程,因此我们可以修改广度优先搜索算法,找出最短路径。

// Graph 类中需要添加
this.edgeTo = [];
this.pathTo = pathTo;
this.hasPathTo = hashPathTo;

// bfs 函数
function bfs(s) {
  var queue = [];
  this.marked[s] = true;
  queue.push(s); // 添加到队尾 
  while (queue.length > 0) {
    var v = queue.shift(); // 从队首移除 
    if (v == undefined) {
      console.log("Visisted vertex: " + v);
    }
    if (this.adj[v] !== undefined) {
      this.adj[v].forEach(w => {
        if (!this.marked[w]) {
          this.edgeTo[w] = v;
          this.marked[w] = true;
          queue.push(w);
        }
      });
    }
  }
}

// 函数 pathTo() 创建了一个栈,用来存储与指定顶点有共同边的所有顶点
function pathTo(v) {
  var source = 0;
  if (!this.hasPathTo(v)) {
    return undefined;
  }
  var path = [];
  for (var i = v; i != source; i = this.edgeTo[i]) {
    path.push(i);
  }
  path.push(source);
  return path;
}

function hasPathTo(v) {
  return this.marked[v];
}


// 测试
g = new Graph(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.bfs(0);
var vertex = 4;
var paths = g.pathTo(vertex);
var str = "";
while (paths.length > 0) {
  if (paths.length > 1) {
    str += paths.pop() + '-';
  } else {
    str += paths.pop();
  }
}
console.log(str);

输出结果为:

0-2-4

拓扑排序

拓扑排序 会对有向图的所有顶点进行排序,使有向边从前面的顶点指向后面的顶点。

拓扑排序算法 与深度优先搜索类似。不同的是,拓扑排序算法不会立即输出已访问的顶点,而是访问当前顶点邻接表中的所有相邻顶点,直到这个列表穷尽时,才将当前顶点压入栈中。



🔗:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值