数据结构与算法-图(Graph)-JS
图结构是一种与树结构相似的数据结构。图论是数学的一个分支,在数学的概念上,树是图的一种。以图为研究对象,研究顶点和边组成的图形的数学理论和方法,主要研究目的是事务之间的关系,顶点代表事务,边代表两个事务间的关系
图可以模拟很多现实的数据结构,比如家谱或公司组织架构等
图是使用几叉树都不能模拟的。图的特点:一组顶点(通常用V Vertex表示顶点的集合);一组边(通常用E Edge表示边的集合),边可以是有向的,也可以是无向的
欧拉七桥问题开辟了图论与集合拓扑,欧拉最终解释这个问题无解,并给出了一笔画的充要条件是:奇点(连到一点的边的数目是奇数条)的数目不是0个就是2个。
图的术语:
顶点:图中的一个节点;边:顶点和顶点之间的连线;相邻顶点:有一条边连接在一起的顶点;度:一个顶点的度是相邻顶点的数量;路径:顶点与另一个顶点的一个连续序列;简单路径:路径不包含重复的顶点;回路:第一个顶点和最后一个顶点相同的路径称为回路;
无向图:所有的边都没有方向;有向图:图中的边是有方向的;无权图:边没有携带权重;有权图:边有一定的权重;
程序中如何表示图,需要体现顶点和边,顶点可以使用一个数组来存储起来或者使用链表,边的表示方法有些复杂
图的一种表示方式:邻接矩阵。使用二维数组来表示顶点直接的连接,可以表示方向,也可以用数字表示权重。邻接矩阵的问题:如果图是一个稀疏图,矩阵中存在大量的0,这就意味着浪费了计算机存储空间来表示根本不存在的边。
图的另一种表示方式:邻接表。可以通过数组、链表、字典、哈希表实现。邻接表的问题:计算“出度”(自己指向别人的数量)是比较简单的,计算“入度”就很麻烦了,需要遍历,可以构造一个“逆邻接表”,才能有效地计算“入度”,但是在开发中,“入度”相对用的比较少。
图的遍历思想和树的遍历思想是一样的,图的遍历意味着需要将图中的每个顶点访问一遍,并且不能有重复的访问
有两种算法可以对图进行遍历,广度优先搜索(Breath-First Search,BFS)深度优先搜索(Depth-First-Search,DFS),这两种遍历算法都需要明确指定第一个被访问的顶点。
两种算法的思想:BFS:基于队列,入队列的顶点先被探索;DFS:基于栈或使用递归(递归本质上就是函数栈的调用),通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问。
为了记录顶点是否被访问过,使用三种颜色来反应它们的状态:白色:表示该顶点还没有被访问(没有入队列);灰色:表示该顶点被访问过(已经入队列了),但并未被探索过(还没有获取该顶点的相邻顶点);黑色:表示该顶点被访问过且别完全探索过(获取了该顶点的相邻顶点)
广度优先搜索:
从指定的第一个顶点开始遍历图,新访问其所有的相邻点,当依次访问相邻点时,继续访问相邻点的相邻点。
BFS实现步骤:创建一个队列Q;将白色v入队列Q(访问v),v变为灰色;如果Q非空,执行下面步骤,将灰色v从Q中取出队列,再将v所有的未被访问过的邻接点(白色)加入到队列中(被探索),此时v变为黑色,v可以进行一些其他操作,比如之后的呈现操作handler。
深度优先搜索:
从第一个指定的顶点开始遍历图,沿着路径知道这条路径最后的顶点被访问了,接着原路回退变探索下一条路径。(广度优先搜索相当于从开始顶点逐渐扩散,深度优先搜索相当于先一条路走到黑,然后原路返回找其他岔路)
//先进行一个字典的封装
function Dictionary() {
//字典属性
this.items = {};
//字典操作方法
//在字典中添加键值对
Dictionary.prototype.set = function (key, value) {
this.items[key] = value;
}
//判断字典中是否有某个key
Dictionary.prototype.has = function (key) {
return this.items.hasOwnProperty(key);
}
//从字典中移除元素
Dictionary.prototype.remove = function(key) {
//判断字典中是否有这个key
if (!this.has(key)) return false;
//从字典中删除key
delete this.items[key];
return true;
}
//根据key去获取value
Dictionary.prototype.get = function (key) {
return this.has(key) ? this.items[key] : undefined;
}
//获取所有的keys
Dictionary.prototype.keys = function () {
return Object.keys(this.items);
}
//获取所有的values
Dictionary.prototype.values = function () {
return Object.values(this.items);
}
//size方法
Dictionary.prototype.size = function () {
return this.keys().length;
}
//clear方法
Dictionary.prototype.clear = function () {
this.items = [];
}
}
//封装队列类
function Queue() {
//属性
this.items = [];
//方法
//1.将元素加入到队列中
Queue.prototype.enqueue = function (element) {
this.items.push(element);
}
//2.从队列中删除前端元素
Queue.prototype.dequeue = function () {
return this.items.shift();
}
//3.查看前端的元素
Queue.prototype.front = function () {
return this.items[0];
}
//查看队列是否为空
Queue.prototype.isempty = function () {
return this.items.length == 0;
}
//查看队列中的元素个数
Queue.prototype.size = function () {
return this.items.length;
}
//6.toString方法
Queue.prototype.toString = function () {
var resultStr = '';
for (var i = 0; i < this.items.length; i++) {
resultStr += this.items[i];
}
return resultStr;
}
}
//图结构的封装
function Graph() {
//属性:顶点(数组)/边(字典)
this.vertexes = []; //顶点
this.edges = new Dictionary() //边
//方法
//添加顶点的方法
Graph.prototype.addVertex = function (v) {
this.vertexes.push(v);
this.edges.set(v, []);
}
//添加边的方法
Graph.prototype.addEdge = function (v1, v2) {
//下面两句表示的是无向图,如果是有向图,只需要写其中的句就行
this.edges.get(v1).push(v2);
this.edges.get(v2).push(v1);
}
//实现toString方法
Graph.prototype.toString = function () {
//定义字符串,保存最终结果
var resultString = "";
//遍历所有的顶点,以及顶点对应的边,达到这样效果A->B C D
for (var i = 0; i < this.vertexes.length; i++) {
resultString += this.vertexes[i] + '->';
var vEdges = this.edges.get(this.vertexes[i]);
for (var j = 0; j < vEdges.length; j++) {
resultString += vEdges[j] + ' ';
}
resultString += '\n';
}
return resultString;
}
//初始化状态颜色
Graph.prototype.initializeColor = function () {
var colors = [];
for (var i = 0; i < this.vertexes.length; i++) {
colors[this.vertexes[i]] = 'white';
}
//返回的数组有很多属性,这些属性的名称为遍历的顶点名,这些属性的值都是white
return colors;
}
//实现广度优先搜索(BFS)
//handler为呈现出来的函数
Graph.prototype.bfs = function (initV, handler) {
//初始化颜色,把图的所有顶点都变为白色的,colors是个数组,这个数组的属性值都是white
var colors = this.initializeColor();
//创建队列
var queue = new Queue();
//将顶点加入到队列中
queue.enqueue(initV);
//循环从队列中取出元素
while (!queue.isempty()) {
//将v的颜色设置为灰色
colors[v] = 'grey';
//从队列中取出一个顶点
var v = queue.dequeue();
//获取和顶点相连的另外顶点
var vList = this.edges.get(v);
//遍历所有的顶点并且加入到队列中
for (var i = 0; i < vList.length; i++) {
var e = vList[i];
if (colors[e] == 'white') {
colors[e] = 'grey';
queue.enqueue(e);
}
}
//将顶点设置为黑色
colors[v] = 'black';
//处理顶点
handler(v);
}
}
//深度优先探索(DFS)
Graph.prototype.dfs = function (initV, handler) {
//初始化颜色,白色
var colors = this.initializeColor();
//从某个顶点开始一次递归访问
this.dfsVisit(initV, colors, handler);
}
//
Graph.prototype.dfsVisit = function (v, colors, handler) {
//将颜色变为灰色
colors[v] = 'grey';
//处理v顶点
handler(v);
//访问v相连的顶点
var vList = this.edges.get(v);
for (var i = 0; i < vList.length; i++) {
var e = vList[i];
if (colors[e] == 'white') {
this.dfsVisit(e, colors, handler);
}
}
//将v设置为黑色
colors[v] = 'black';
}
}
//测试代码
//创建图结构
var graph = new Graph();
//添加顶点
var myVertexes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
//不能用下面的代码直接添加顶点,理论上只是添加顶点没问题,但是少了addVertex方法为每个顶点加的边
// graph.vertexes = myVertexes;
for (var i = 0; i < myVertexes.length; i++) {
graph.addVertex(myVertexes[i]);
}
//添加边
graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'G');
graph.addEdge('D', 'G');
graph.addEdge('D', 'H');
graph.addEdge('B', 'E');
graph.addEdge('B', 'F');
graph.addEdge('E', 'I');
//测试结果
alert(graph);
//测试bfs
var result = '';
graph.bfs(graph.vertexes[0], function (v) {
result += v + ' ';
})
alert(result); //A B C D E F G H I
//测试dfs
result = '';
graph.dfs(graph.vertexes[0], function (v) {
result += v + ' ';
})
alert(result); //A B E I F C D G H