js数据结构和算法(8)-图
8-圖(第11章)
8.1 圖的定義
圖是一種非線性結構,由一系列頂點及其連接頂點的邊組成。比如A和B、A和D是相鄰的,而A和E不是相鄰的。一個頂點相鄰頂點的數量叫作度,比如A的度為3、D的度為4。路徑指相鄰頂點的一個連續序列,如ABEI、ACDG;簡單路徑指不包含重復頂點的路徑(除環外),如ADG;環指首尾頂點相同的路徑,如ADCA,環也屬于簡單路徑。如果圖中每兩個頂點之間都有路徑相連,則稱該圖是連通的。
如果圖中的邊具有方向,則成為有向圖。如果圖中的邊是雙向的,則稱圖是強連通的。比如下圖中的C和D是強連通的
8.2 圖的表示
8.2.1 頂點
創建圖類的第一步就是要創建一個 Vertex 類來保存頂點和邊,Vertex 類有兩個數據成員:一個用于標識頂點,另一個是表明這個頂點是否被訪問過的布爾值 。實現這個類的程序如下:
class Vertex {constructor(label, color) {this.label = labelthis.color = color} } 復制代碼(原書上關于頂點的實現跟我上面的程序還不一樣。其實頂點不需要用程序實現的,接下來也用不到)
8.2.2 邊
圖的實際信息都保存在邊上面,因為它們描述了圖的結構。表示圖的方法有好幾種,我們選用最常見的鄰接表法表示邊。這種方法將邊存儲為由頂點的相鄰頂點列表構成的數組,并以此頂點作為索引。使用這種方案,當我們在程序中引用一個頂點時,可以高效地訪問與這個頂點相連的所有頂點的列表。比如,如果頂點 2 與頂點 0、1、 3、 4 相連,我們就可以在map鍵值為2的位置,存儲它的相鄰頂點0、1、3、4
8.2.3 圖
程序如下:這個類會記錄一個圖表示了多少條邊,并使用數組來記錄頂點數。使用一個鍵值為數組的hash表來存儲頂點的相鄰頂點
class Graph {constructor() {// 頂點數量this.vertices = []// 鄰接表this.adj = new Map()}// 添加頂點addVertice(v) {this.vertices.push(v)this.adj.set(v, [])}// 添加邊addEdge(v, w) {// console.log('this.adj.get(v):', this.adj.get(v))// console.log('this.adj.get(w):', this.adj.get(w))this.adj.get(v).push(w)this.adj.get(w).push(v)}// 顯示這個圖showGraph() {let keys = this.adj.keys()for (let k of keys) {console.log(k + '->')for (let i of this.adj.get(k)) {console.log(i + ' ')}}} } 復制代碼測試:
let g = new Graph() let vertices = [1, 2, 3, 4, 5, 6, 7]vertices.forEach(v => {g.addVertice(v) })g.addEdge(1, 2) g.addEdge(1, 3) g.addEdge(1, 6) g.addEdge(2, 7) g.addEdge(3, 5) g.addEdge(3, 4) g.addEdge(3, 6) g.showGraph() // 打印結果 1-> 2 3 6 2-> 1 7 3-> 1 5 4 6 4-> 3 5-> 3 6-> 1 3 7-> 2 復制代碼打印結果正確,說明我的程序沒有問題。
8.3 圖的遍歷
圖的遍歷有兩種常用的方法:深度優先搜索和廣度優先搜索。
8.3.1 深度優先搜索
深度優先搜索包括從一條路徑的起始頂點開始追溯,直到到達最后一個頂點,然后回溯,繼續追溯下一條路徑,直到到達最后的頂點,如此往復,直到沒有路徑為止。
深度優先搜索算法比較簡單:訪問一個沒有訪問過的頂點,將它標記為已訪問,再遞歸地去訪問在初始頂點的鄰接表中其他沒有訪問過的頂點。
8.3.2 廣度優先遍歷
廣度優先搜索從第一個頂點開始,嘗試訪問盡可能靠近它的頂點。本質上,這種搜索在圖上是逐層移動的,首先檢查最靠近第一個頂點的層,再逐漸向下移動到離起始頂點最遠的層。
8.4 程序實現
假設我們有以下一個圖,我們嘗試下實現這個圖,并且分別用深度遍歷和廣度遍歷訪問這個圖
原書中對圖的實現太粗糙了,原書中圖要求每個頂點都是數字,我這邊做了一下優化
// 圖 class Graph {constructor() {// 頂點數量this.vertices = []// 鄰接表this.adj = new Map()}// 添加頂點addVertice(v) {this.vertices.push(v)this.adj.set(v, [])}// 添加邊addEdge(v, w) {// console.log('this.adj.get(v):', this.adj.get(v))// console.log('this.adj.get(w):', this.adj.get(w))this.adj.get(v).push(w)this.adj.get(w).push(v)}// 顯示這個圖showGraph() {let keys = this.adj.keys()for (let k of keys) {console.log(k + '->')for (let i of this.adj.get(k)) {console.log(i + ' ')}}}// 搜索輔助函數,對對頂點進行初始化,頂點沒有被訪問過就是白色,頂點被訪問過但是還沒探索的話是灰色,頂點被探索過就是黑色initialColor() {let color = new Map()this.vertices.forEach(ele => {color.set(ele, 'white')})return color}// 深度遍歷所有頂點dfs(callback) {let color = this.initialColor()this.vertices.forEach(node => {if (color.get(node) === 'white') {this.dfsVisit(node, color, callback)}})}// 遞歸遍歷頂點的鄰接表dfsVisit(node, color, callback) {let neighbors = this.adj.get(node)color.set(node, 'grey')if (callback) {callback(node)}neighbors.forEach(neighNode => {if (color.get(neighNode) === 'white') {this.dfsVisit(neighNode, color, callback)}})color.set(node, 'black')}// 廣度遍歷所有頂點bfs(callback) {let color = this.initialColor()let queue = []if (this.vertices[0]) {queue.push(this.vertices[0])}while (queue.length > 0) {let qNode = queue.shift()let neighbors = this.adj.get(qNode)color.set(qNode, 'grey')neighbors.forEach(ele => {if (color.get(ele) === 'white') {color.set(ele, 'grey')queue.push(ele)}})color.set(qNode, 'black')if (callback) {callback(qNode)}}} } 復制代碼上面的程序實現圖的定義,還實現了深度遍歷和廣度遍歷的方法。我們隊程序測試下
let g = new Graph() let vertices = [1, 2, 3, 4, 5, 6, 7] vertices.forEach(v => {g.addVertice(v) }) g.addEdge(1, 2) g.addEdge(1, 3) g.addEdge(1, 6) g.addEdge(2, 7) g.addEdge(3, 5) g.addEdge(3, 4) g.addEdge(3, 6)// 深度遍歷 g.dfs(v=>console.log(v)) // 1 2 7 3 5 4 6 // 廣度遍歷 g.bfs(v=>console.log(v)) // 1 2 3 6 7 5 4 復制代碼測試結果是正確的,說明我們的程序實現沒有問題
8.5 查找最短路徑
圖的最常見的操作是查找從一個頂點到另一個頂點的最短路徑,就要用到廣度搜索。要查找從頂點 A 到頂點 D 的最短路徑,我們首先會查找從 A 到 D 是否有任何一條單邊路徑,接著查找兩條邊的路徑,以此類推。這正是廣度優先搜索的搜索過程,因此我們可以輕松地修改廣度優先搜索算法,找出最短路徑。
代碼如下:
// 獲取每一個點到頂點的距離和其上一個頂點bfsDistance() {let color = this.initialColor()let queue = []let distance = new Map()let pred = new Map()if (this.vertices[0]) {queue.push(this.vertices[0])}this.vertices.forEach(ele => {distance.set(ele, 0)pred.set(ele, null)});while (queue.length > 0) {let qNode = queue.shift()let neighbors = this.adj.get(qNode)color.set(qNode, 'grey')neighbors.forEach(ele => {if (color.get(ele) === 'white') {color.set(ele, 'grey')queue.push(ele)distance.set(ele, distance.get(qNode) + 1)pred.set(ele, qNode)}})color.set(qNode, 'black')}return {distance: distance,pred: pred}}// 獲取每一個點到頂點的路徑getBfsPath() {if (this.vertices.length > 0) {let shortestPath = this.bfsDistance()let fromVertex = this.vertices[0]let paths = []for (let i = 0; i < this.vertices.length; i++) {let path = []let toVertex = this.vertices[i]for (let v = toVertex; v !== fromVertex; v = shortestPath.pred.get(v)) {path.push(v)}path.push(fromVertex)paths.push(path)}return paths}}// 獲取到頂點的最短路徑getShortestPath(v) {let path = nulllet paths = this.getBfsPath()for (let i = 0; i < paths.length; i++) {if (paths[i][0] === v) {path = paths[i]break}}return path} 復制代碼測試:
我們以獲取頂點到第4個點的最短路徑為例,測試如下
g.getShortestPath(g.vertices[4]) // [5,3,1] 復制代碼案例獲取頂點到某個點的最短路徑,怎么樣獲取非頂點的兩個點之間的最短路徑呢? 其實道理很簡單,你換下頂點就可以了。在圖里,并沒有規定那個必須是頂點。
8.6 拓撲排序
拓撲排序會對有向圖的所有頂點進行排序,使有向圖從前面的頂點指向后面的頂點。如下圖是計算機學科的有向圖模型。
這個圖的拓撲排序結果將會是以下序列:
課程3和課程4可以同時上,課程5和課程6也可以同時上,但是其他課程就要有優先級了。比如c1一定要在cs2之前上。
拓撲排序代碼如下:
// 拓撲排序topSort() {let stack = []let color = this.initialColor()this.vertices.forEach(ele => {if (color.get(ele) === 'white') {this.topSortVisit(ele, stack, color)}})console.log('stack:', stack)}topSortVisit(ele, stack, color) {color.set(ele, 'grey')stack.push(ele)let neighbors = this.adj.get(ele)neighbors.forEach(nNode => {if (color.get(nNode) === 'white') {this.topSortVisit(nNode, stack, color)}})} 復制代碼用已有的案例測試:
g.topSort() // stack: (7)?[1, 2, 7, 3, 5, 4, 6] 復制代碼用其他案例測試下:
let g = new Graph() let vertices = ['cs1', 'cs2', '匯編語言', '數據結構', '操作系統', '算法']vertices.forEach(v => {g.addVertice(v) })g.addEdge('cs1', 'cs2') g.addEdge('cs2', '匯編語言') g.addEdge('cs2', '數據結構') g.addEdge('數據結構', '操作系統') g.addEdge('數據結構', '算法') g.topSort() // stack: (6)?["cs1", "cs2", "匯編語言", "數據結構", "操作系統", "算法"] 復制代碼測試應該是沒問題的。
本章節完整代碼如下:
// 圖 class Graph {constructor() {// 頂點數量this.vertices = []// 鄰接表this.adj = new Map()}// 添加頂點addVertice(v) {this.vertices.push(v)this.adj.set(v, [])}// 添加邊addEdge(v, w) {// console.log('this.adj.get(v):', this.adj.get(v))// console.log('this.adj.get(w):', this.adj.get(w))this.adj.get(v).push(w)this.adj.get(w).push(v)}// 顯示這個圖showGraph() {let keys = this.adj.keys()for (let k of keys) {console.log(k + '->')for (let i of this.adj.get(k)) {console.log(i + ' ')}}}// 搜索輔助函數,對對頂點進行初始化,頂點沒有被訪問過就是白色,頂點被訪問過但是還沒探索的話是灰色,頂點被探索過就是黑色initialColor() {let color = new Map()this.vertices.forEach(ele => {color.set(ele, 'white')})return color}// 深度遍歷所有頂點dfs(callback) {let color = this.initialColor()this.vertices.forEach(node => {if (color.get(node) === 'white') {this.dfsVisit(node, color, callback)}})}// 遞歸遍歷頂點的鄰接表dfsVisit(node, color, callback) {let neighbors = this.adj.get(node)color.set(node, 'grey')if (callback) {callback(node)}neighbors.forEach(neighNode => {if (color.get(neighNode) === 'white') {this.dfsVisit(neighNode, color, callback)}})color.set(node, 'black')}// 廣度遍歷所有頂點bfs(callback) {let color = this.initialColor()let queue = []if (this.vertices[0]) {queue.push(this.vertices[0])}while (queue.length > 0) {let qNode = queue.shift()let neighbors = this.adj.get(qNode)color.set(qNode, 'grey')neighbors.forEach(ele => {if (color.get(ele) === 'white') {color.set(ele, 'grey')queue.push(ele)}})color.set(qNode, 'black')if (callback) {callback(qNode)}}}// 獲取每一個點到頂點的距離和其上一個頂點bfsDistance() {let color = this.initialColor()let queue = []let distance = new Map()let pred = new Map()if (this.vertices[0]) {queue.push(this.vertices[0])}this.vertices.forEach(ele => {distance.set(ele, 0)pred.set(ele, null)});while (queue.length > 0) {let qNode = queue.shift()let neighbors = this.adj.get(qNode)color.set(qNode, 'grey')neighbors.forEach(ele => {if (color.get(ele) === 'white') {color.set(ele, 'grey')queue.push(ele)distance.set(ele, distance.get(qNode) + 1)pred.set(ele, qNode)}})color.set(qNode, 'black')}return {distance: distance,pred: pred}}// 獲取每一個點到頂點的路徑getBfsPath() {if (this.vertices.length > 0) {let shortestPath = this.bfsDistance()let fromVertex = this.vertices[0]let paths = []for (let i = 0; i < this.vertices.length; i++) {let path = []let toVertex = this.vertices[i]for (let v = toVertex; v !== fromVertex; v = shortestPath.pred.get(v)) {path.push(v)}path.push(fromVertex)paths.push(path)}return paths}}// 獲取到頂點的最短路徑getShortestPath(v) {let path = nulllet paths = this.getBfsPath()for (let i = 0; i < paths.length; i++) {if (paths[i][0] === v) {path = paths[i]break}}return path}// 拓撲排序topSort() {let stack = []let color = this.initialColor()this.vertices.forEach(ele => {if (color.get(ele) === 'white') {this.topSortVisit(ele, stack, color)}})console.log('stack:', stack)}topSortVisit(ele, stack, color) {color.set(ele, 'grey')stack.push(ele)let neighbors = this.adj.get(ele)neighbors.forEach(nNode => {if (color.get(nNode) === 'white') {this.topSortVisit(nNode, stack, color)}})} }let g = new Graph() let vertices = [1, 2, 3, 4, 5, 6, 7]vertices.forEach(v => {g.addVertice(v) })g.addEdge(1, 2) g.addEdge(1, 3) g.addEdge(1, 6) g.addEdge(2, 7) g.addEdge(3, 5) g.addEdge(3, 4) g.addEdge(3, 6) g.getBfsPath() 復制代碼個人評價:關于圖的知識常用的大概就這么多,書上有關圖的代碼寫的真是一團糟,很多要自己查資料實現。這作者真是太不負責任了
總結
以上是生活随笔為你收集整理的js数据结构和算法(8)-图的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: layui 表格新增删除一行
- 下一篇: Mozilla 准备让“合格” Linu