文章目录
图
要点提炼:
1.图的基本概念
2.邻接矩阵法、图的遍历(深度优先搜索和广度优先搜索)
3.图的基本应用:最小代价生成树
一、图的定义
研究 顶点(vertex) 和 边(edge) 组成的图形的数学理论和方法,事物之间的关系,顶点代表事物,边代表两个事物间的关系
图涉及的基本概念:
无向边用小括号“()”表示,有向边则是尖括号“<>”表示
图(由顶点的有穷非空集合和顶点之间边的集合组成 : G ( V , E ) G(V,E) G(V,E),V是顶点集合,E是边集合)、无向边(边没有方向)
简单图:图中不存在顶点到其自身的边,且同一条边不重复出现
无向完全图:在无向图中,任意两个顶点之间都存在边!含有 n n n 个顶点的无向完全图有 n ∗ ( n − 1 ) 2 {n*(n-1)} \over 2 2n∗(n−1) 边
有向完全图:任意两个顶点之间都存在方向互为相反的两条弧
稀疏图(边的条数E远小于结点V的平方) && 稠密图(边的条数E接近于结点V的平方) : 有很少条边或弧的图–>稀疏图,反之–>稠密图(两者是相对概念)
权:边或弧带数字的
网:带权的图
子图:
路径的长度:路径上的边或弧的数目
回路或环:第一个顶点到最后一个顶点相同的路径
简单回路或环:序列中顶点不重复的路径称为简单路径,除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路
- 连通图:在无向图中,任意两个顶点 v 1 , v 2 ϵ E {v1,v2 }\epsilon E v1,v2ϵE ,v1,v2都是连通的,间接相通也是可以的!
欧拉的解答:连通图一笔画充要条件 1. 奇点的数目不是0个就是2个 2. 连到一点的边的数目如是奇数条--称奇点 3. 如是偶数条--称偶点 4. 一笔画,必须中间点均是偶点 5. 奇点只可能在两端,要么没有奇点,要不就在两端
- 强连通图:有向的叫强连通图
1. 抽象数据类型
2.图存储结构、图的表示
-
邻接矩阵(二维数组)
一般表示稠密图(边的条数E接近于结点V的平方),优点可以快速判断两个结点之间是否有边相连O(1)
不带权的邻接矩阵中,1表示两顶点连通,0表示不连通
-
邻接表(数组+链表)
一般表示稀疏图(边的条数E远小于结点V的平方)
代码实现:
// 采用的是,数组对应dict-key-value的值 function Graph() { // 属性:顶点(数组)/边(字典链表) this.vertexes = [] // 顶点 this.edges = new Dictionay() // 边 // 方法 // 添加方法 // 1.添加顶点的方法 Graph.prototype.addVertexes = function (v) { // 顶点加入数组中 this.vertexes.push(v) // 只设置key顶点,不设置值-边 this.edges.set(v,[]) } // 2. 添加边的方法 Graph.prototype.addEdges = function (v1,v2) { // 取出数组v1,加入 v2 this.edges.get(v1).push(v2) this.edges.get(v2).push(v1) } // 实现toString方法 Graph.prototype.toString = function () { // 1.定义字符串,保存最终结果 var resultString = "" // 2.遍历所有的顶点,以及顶点对应的边 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 } }
// 创建字典的构造函数 function Dictionay() { // 字典属性 this.items = {} // 字典操作方法 // 在字典中添加键值对 Dictionay.prototype.set = function (key,value) { this.items[key] = value } // 判断字典中是否存在某个key Dictionay.prototype.has = function (key) { return this.items.hasOwnProperty(key) } // 从字典中移除元素 Dictionay.prototype.remove = function (key) { // 1. 判断字典中是否有这个key if (!this.has(key)) return false // 2.从字典中删除key delete this.items[key] return true } // 根据key去获取value Dictionay.prototype.get = function (key) { return this.has(key) ? this.items[key] : undefined } // 获取所有的key Dictionay.prototype.keys = function () { return Object.keys(this.items) } // 获取所有的value Dictionay.prototype.values = function () { return Object.values(this.items) } // size方法 Dictionay.prototype.size = function () { return this.keys().length } // clear方法 Dictionay.prototype.clear = function () { this.items = {} } }
// 测试代码 // 1.创建图结构 var graph = new Graph() //2. 添加顶点 var myVertexes = ['A','B','C','D','E','F','G','H','I'] for (var i = 0; i < myVertexes.length; i++) { graph.addVertexes(myVertexes[i]) } // 3.添加边 graph.addEdges('A','B') graph.addEdges('A','C') graph.addEdges('A','D') graph.addEdges('C','D') graph.addEdges('C','G') graph.addEdges('D','G') graph.addEdges('D','H') graph.addEdges('B','E') graph.addEdges('B','F') graph.addEdges('E','I') // 测试toString结果: alert(graph)
-
十字链表
-
邻接多重表
-
边集数组
二、图的遍历
图的遍历和树一样,每个顶点访问一遍,并且不能有重复的访问
以下两者图的遍历方式:都需要明确指定第一个被访问的顶点
BFS:基于队列,入队列的顶点先被探索
DFS:基于栈或使用递归,将顶点存入栈中,顶点沿着路径被探索,存在新的相邻顶点就去访问
颜色标记:顶点的三种状态
白色:未被访问
灰色:被访问过,并未被探索
黑色:被访问过且被完全探索
1. 广度优先遍历(Breadth-First Search简称BFS)
-
思路分析
- 创建一个队列Q
- 将V标注为被发现的(灰色),并将V入队列Q
- 如果Q非空
- 将V从Q中取出队列
- 将V标注为被发现的灰色
- 将V所有的未被访问的邻接点(白色),加入队列中
- 将V标注为黑色
2. 深度优先遍历(Depth-First Search,简称DFS)
一条路走到底,全部原路返回,关键返回,
深度优先搜索的思路:
- 深度优先搜索算法将会从第一个指定的顶点开始遍历图, 沿着路径知道这条路径最后被访问了.
- 接着原路回退并探索吓一条路径.
图解DFS:
深度优先搜索算法的实现:
- 广度优先搜索算法我们使用的是队列, 这里可以使用栈完成, 也可以使用递归.
- 方便代码书写, 我们还是使用递归(递归本质上就是函数栈的调用)
深度优先搜索算法的实现:
// 深度优先搜索Graph.prototype.dfs = function (handler) {
// 1.初始化颜色
var color = this.initializeColor()
// 2.遍历所有的顶点, 开始访问
for (var i = 0; i < this.vertexes.length; i++) {
if (color[this.vertexes[i]] === "white") { this.dfsVisit(this.vertexes[i], color, handler)
}
}}
// dfs的递归调用方法
Graph.prototype.dfsVisit = function (u, color, handler) {
// 1.将u的颜色设置为灰色
color[u] = "gray"u
// 2.处理u顶点
handler(u)
// 3.u的所有邻接顶点的访问
var uAdj = this.adjList.get(u)
for (var i = 0; i < uAdj.length; i++) {
var w = uAdj[i]
if (color[w] === "white") {
this.dfsVisit(w, color, handler)
} }
// 4.将u设置为黑色
color[u] = "black"
}
代码解析:
- 代码序号1: 初始化颜色.和广度优先搜索算法一样.
- 代码序号2: 遍历所有的顶点, 每遍历一个顶点, 让其执行递归函数.
- 递归代码1: 探测了u顶点, 所有u顶点的颜色设置为灰色.
- 递归代码2: 访问u顶点, 通过回调函数传入u.
- 递归代码3: 访问u顶点的相连的顶点, 在访问的过程中判断该顶点如果为白色, 说明未探测, 调用递归方法.
- 递归代码4: u被探测过, 也被访问过, 将u的颜色设置为黑色.
递归的代码较难理解一些, 我们这里给出一副图来帮助大家理解过程:
三、最小生成树
一个连通图的生成树是一个极小的连通子图,含有全部的顶点(但只足以构成一颗树的 n − 1 n-1 n−1 边)
连通图的最小代价生成树—> 称最小生成树
1. 普里姆算法
已经确定的顶点看作一个顶点:
1.选择初始点把图中的顶点分成两个不同的顶点集S(最小生成树的顶点集)、V-S(剩下的顶点)
2. 在横跨两个不同顶点集的边中选择一条权值最小的边加入到生成树中
3. 将该边的另一个点加入到顶点集 S 中,并从 V-S 中删除
4. 重复步骤 2,3,直到 V-S 为空集
-
由图构建邻接矩阵
正确结果:
现实代码:
待完成
2. 克鲁斯卡尔算法
已经确定的顶点看作一个顶点:基本思想:
1.把图中的顶点看成 n 个 不同的顶点集,每个顶点集都只含有1个顶点
2.每次在横跨不同顶点集的边中,选择一条权值最小的边加入最小生成树中
3.直到图中所有的顶点都在同一个顶点集为止
- 由图构建–>边集数组:
最终结果
四、最短路径
某个源点,到各各目标顶点的带权最短路径
1. 迪杰斯特拉算法
2. 非洛伊德算法
五、拓扑排序
处理没有回路的图的情况,
拓扑排序就是:有向图构造拓扑序列的过程
1.拓扑排序介绍
2.拓扑排序算法
六、关键路径
相当于,这条路径是最重要的