1. 說明
本文所有的算法嚴格按照《算法導論》,本文將詳細的對BFS和DFS進行分析,并提供算法的 js 實現,同時會對創建鏈表的方式進行優化
2. 圖的表示圖的表示分為對頂點集 V 的表示和對邊集 E 的表示,這里的重點是如何表示邊,邊的表示分為鄰接矩陣和鄰接鏈表這兩種表示方法,鄰接矩陣適合表示邊稠密的圖,其消耗空間為|V|*|V|,如果是無向圖,則可以用上三角矩陣或者下三角矩陣來表示,是空間消耗變為|V|*|V|/2,鄰接鏈表適合表示邊稀疏的圖,其消耗的空間為 O(|V|+|E|),用鄰接鏈表表示圖很緊湊,沒有空間浪費,用《算法導論》中的原話就是,鄰接鏈表表示圖,魯棒性很高。本文涉及的圖,全部用鄰接鏈表表示。
2.1. 本文的算法都是對該圖的操作
2.2. 對上圖進行鄰接鏈表的轉化
從上圖可以看到我們將圖的分為兩部分,頂點和邊,我們分別對這兩部分進行表示,我們用數組去存放頂點,用鏈表去描述邊。A-E 做為節點的標識。數字表示頂點在數組中的位置。由這幅圖可以看到從節點 A 發出的邊有兩條,分別是 ,和
3. BFS 廣度優先搜索廣度優先搜索的思想是,對于圖G和給定的節點s,廣度優先搜索需要一個輔助的先進先出的隊列 Q
將s加入到Q中
將s從Q總移出,用臨時變量接受s,如果s沒有被訪問過,從s出發,發現s的所有鄰接節點并放入Q中
訪問s
將Q隊列的第一個元素移除隊列作為新的s執行2-4過程直到隊列Q為空
3.1 表示頂點的數據結構
function Vertex() { if (!(this instanceof Vertex)) return new Vertex(); this.color = this.WHITE; //初始為 白色 this.pi = null; //初始為 無前驅 this.d = this.INFINITY; //初始為 無窮大 this.edges = null; //由頂點發出的所有邊 this.value = null; //節點的值 默認為空 } Vertex.prototype = { constructor: Vertex, WHITE: "white", //白色 GRAY: "gray", //灰色 BLACK: "black", //黑色 INFINITY: null, //d 為 null 時表示無窮大 }
為了跟蹤算法的進展,我們對圖進行搜索的時候會對圖中的頂點進行涂色,圖初始化是頂點全部為白色,當第一次發現某個節點時,我們將他涂為灰色,當對某個節點訪問完成后,我們將它涂為黑色。在這里我們看到每個節點都有 五個 屬性,color表示節點的顏色,pi 表示前驅結點,d 表示廣度優先搜索中從源節點到當前節點的距離,edges 表示從當前節點發出的所有邊,value 表示節點存放的數據
3.2 表示邊的數據結構
function Edge() { if (!(this instanceof Edge)) return new Edge(); this.index = null; //邊所依附的節點的位置 this.sibling = null; }
可以看到,邊包含兩個兩個屬性,index,和sibling,index表示這條邊連接的節點在頂點數組中的位置,sibling只想下一個連接兄弟節點的邊。
3.3 表示圖的數據結構
function Graph() { if (!(this instanceof Graph)) return new Graph(); this.graph = []; //存放頂點的數組 } Graph.prototype = { constructor: Graph, addNode: function (node) { this.graph.push(node); }, getNode: function (index) { return this.graph[index]; } }
3.4 構建圖
//創建 頂點 var vA = Vertex(); var vB = Vertex(); var vC = Vertex(); var vD = Vertex(); var vE = Vertex(); var vF = Vertex(); vA.value = "A"; vB.value = "B"; vC.value = "C"; vD.value = "D"; vE.value = "E"; vF.value = "F"; //構建由 A 節點發出的邊集 var eA1 = Edge(); var eA2 = Edge(); eA1.index = 1; eA2.index = 3; eA1.sibling = eA2; vA.edges = eA1; //構建有 B 節點發出的邊集 var eB1 = Edge(); var eB2 = Edge(); var eB3 = Edge(); eB1.index = 0; eB2.index = 4; eB3.index = 2; eB1.sibling = eB2; eB2.sibling = eB3; vB.edges = eB1; //構建由 C 節點發出的邊 var eC1 = Edge(); var eC2 = Edge(); var eC3 = Edge(); eC1.index = 1; eC2.index = 4; eC3.index = 5; eC1.sibling = eC2; eC2.sibling = eC3; vC.edges = eC1; //構建由 D 節點發出的邊 var eD1 = Edge(); eD1.index = 0; vD.edges = eD1; //構建由 E 節點發出的邊 var eE1 = Edge(); var eE2 = Edge(); var eE3 = Edge(); eE1.index = 1; eE2.index = 2; eE3.index = 5; eE1.sibling = eE2; eE2.sibling = eE3; vE.edges = eE1; //構建由 F 節點發出的邊 var eF1 = Edge(); var eF2 = Edge(); eF1.index = 2; eF2.index = 4; eF1.sibling = eF2; vF.edges = eF1; //構建圖 var g = Graph(); g.addNode(vA); g.addNode(vB); g.addNode(vC); g.addNode(vD); g.addNode(vE); g.addNode(vF);
3.5 BFS算法
//廣度優先搜索 function BFS(g, s) { let queue = []; //輔助隊列 Q s.color = s.GRAY; //首次發現s涂為灰色 s.d = 0; //距離為0 queue.push(s); //將s放入隊列 Q while (queue.length > 0) { //當隊列Q中有頂點時執行搜索 let u = queue.shift(); //將Q中的第一個元素移出 if (u.edges == null) continue; //如果從當前頂點沒有發出邊 let sibling = u.edges; //獲取表示鄰接邊的鏈表的頭節點 while (sibling != null) { //當鏈表不為空 let index = sibling.index; //當前邊所連接的頂點在隊列中的位置 let n = g.getNode(index); //獲取頂點 if (n.color == n.WHITE) { //如果沒有被訪問過 n.color = n.GRAY; //涂為灰色 n.d = u.d + 1; //距離加1 n.pi = u; //設置前驅節點 queue.push(n); //將 n 放入隊列 Q } sibling = sibling.sibling; //下一條邊 } u.color = u.BLACK; //當前頂點訪問結束 涂為黑色 } }
3.6 完整代碼可粘貼到瀏覽器控制臺運行
//數據結構 鄰接鏈表-頂點 function Vertex() { if (!(this instanceof Vertex)) return new Vertex(); this.color = this.WHITE; //初始為 白色 this.pi = null; //初始為 無前驅 this.d = this.INFINITY; //初始為 無窮大 this.edges = null; //由頂點發出的所有邊 this.value = null; //節點的值 默認為空 } Vertex.prototype = { constructor: Vertex, WHITE: "white", //白色 GRAY: "gray", //灰色 BLACK: "black", //黑色 INFINITY: null, //d 為 null 時表示無窮大 } //數據結構 鄰接鏈表-邊 function Edge() { if (!(this instanceof Edge)) return new Edge(); this.index = null; //邊所依附的節點的位置 this.sibling = null; } //數據結構 圖-G function Graph() { if (!(this instanceof Graph)) return new Graph(); this.graph = []; } Graph.prototype = { constructor: Graph, //這里加進來的已經具備了邊的關系 addNode: function (node) { this.graph.push(node); }, getNode: function (index) { return this.graph[index]; } } //廣度優先搜索 function BFS(g, s) { let queue = []; s.color = s.GRAY; s.d = 0; queue.push(s); while (queue.length > 0) { let u = queue.shift(); if (u.edges == null) continue; let sibling = u.edges; while (sibling != null) { let index = sibling.index; let n = g.getNode(index); if (n.color == n.WHITE) { n.color = n.GRAY; n.d = u.d + 1; n.pi = u; queue.push(n); } sibling = sibling.sibling; } u.color = u.BLACK; console.log(u); } } //創建 頂點 var vA = Vertex(); var vB = Vertex(); var vC = Vertex(); var vD = Vertex(); var vE = Vertex(); var vF = Vertex(); vA.value = "A"; vB.value = "B"; vC.value = "C"; vD.value = "D"; vE.value = "E"; vF.value = "F"; //構建由 A 節點發出的邊集 var eA1 = Edge(); var eA2 = Edge(); eA1.index = 1; eA2.index = 3; eA1.sibling = eA2; vA.edges = eA1; //構建有 B 節點發出的邊集 var eB1 = Edge(); var eB2 = Edge(); var eB3 = Edge(); eB1.index = 0; eB2.index = 4; eB3.index = 2; eB1.sibling = eB2; eB2.sibling = eB3; vB.edges = eB1; //構建由 C 節點發出的邊 var eC1 = Edge(); var eC2 = Edge(); var eC3 = Edge(); eC1.index = 1; eC2.index = 4; eC3.index = 5; eC1.sibling = eC2; eC2.sibling = eC3; vC.edges = eC1; //構建由 D 節點發出的邊 var eD1 = Edge(); eD1.index = 0; vD.edges = eD1; //構建由 E 節點發出的邊 var eE1 = Edge(); var eE2 = Edge(); var eE3 = Edge(); eE1.index = 1; eE2.index = 2; eE3.index = 5; eE1.sibling = eE2; eE2.sibling = eE3; vE.edges = eE1; //構建由 F 節點發出的邊 var eF1 = Edge(); var eF2 = Edge(); eF1.index = 2; eF2.index = 4; eF1.sibling = eF2; vF.edges = eF1; //構建圖 var g = Graph(); g.addNode(vA); g.addNode(vB); g.addNode(vC); g.addNode(vD); g.addNode(vE); g.addNode(vF); BFS(g, vB);
頂點的訪問順序為 B->A->E->C->D->F
4. DFS 深度優先搜索
特點
深度優先搜索一般默認的源點有多個,搜索時的前驅子圖會構成一個深度優先森林,這是依據深度優先搜索的搜索結果的使用深度優先搜索算法常常作為另一個算法的一個子程序被使用深度優先搜索在節點中增加了一個發現的時間戳,一個訪問的時間戳,通常能幫助我們推斷算法的行為,在d-f之間是灰色,在f之后是黑色,時間戳為1到2*|v|之間的整數
算法思想
只要有可能,就在圖中盡量“深入”,總是對最近才發現的節點v的出發邊進行探索,知道該節點的所有出發邊都被發現為止。一旦v的所有發出的邊都被發現,搜索則“回溯”到v的前驅節點,該過程一直持續到源節點可達的所有節點都被發現為止,如果還有未發現的節點,則深度優先搜索將從這些未被發現的節點中任選一個作為新的源節點,并重復同樣的搜索過程
4.1 算法數據結構
深度優先搜索的數據結構只有在表示頂點時稍有不同,其它的都相同,這里給出表示頂點的數據結構
function Vertex() { if (!(this instanceof Vertex)) return new Vertex(); this.color = this.WHITE; //初始為 白色 this.pi = null; //初始為 無前驅 this.d = null; //時間戳 發現時 this.f = null; //時間戳 鄰接鏈表掃描完成時 this.edges = null; //由頂點發出的所有邊 this.value = null; //節點的值 默認為空 } Vertex.prototype = { constructor: Vertex, WHITE: "white", //白色 GRAY: "gray", //灰色 BLACK: "black", //黑色 }
可以看到頂點數據結構中的多了一個f,同時d的含義也發生了變化d和f作為發現和訪問完成的時間戳,取值為從1到2*|v|
4.2 DFS算法
function DFS(g) { let t = 0; //時間戳 for (let v of g.vertexs) { //讓每個節點都作為一次源節點 if (v.color == v.WHITE) DFSVisit(g, v); } function DFSVisit(g, v) { t = t + 1; //時間戳加一 v.d = t; v.color = v.GRAY; let sibling = v.edges; while (sibling != null) { let index = sibling.index; let n = g.getNode(index); if (n.color == n.WHITE) { n.pi = v; DFSVisit(g, n); //先縱向找 } sibling = sibling.sibling; //利用遞歸的特性來回溯 } v.color = v.BLACK; t = t + 1; //時間戳加一 v.f = t; } }
4.3 DFS完整代碼
function Vertex() { if (!(this instanceof Vertex)) return new Vertex(); this.color = this.WHITE; //初始為 白色 this.pi = null; //初始為 無前驅 this.d = null; //時間戳 發現時 this.f = null; //時間戳 鄰接鏈表掃描完成 this.edges = null; //由頂點發出的所有邊 this.value = null; //節點的值 默認為空 } Vertex.prototype = { constructor: Vertex, WHITE: "white", //白色 GRAY: "gray", //灰色 BLACK: "black", //黑色 } //數據結構 圖-G function Graph() { if (!(this instanceof Graph)) return new Graph(); this.vertexs = []; } Graph.prototype = { constructor: Graph, addNode: function (node) { this.vertexs.push(node); }, getNode: function (index) { return this.vertexs[index]; } } //這里 t 作為全局變量和參數時結果不一樣 因為 js 對于基本類型的參數采用的是值捕獲,對于對象類型的參數采用的是引用捕獲 function DFS(g) { let t = 0; for (let v of g.vertexs) { if (v.color == v.WHITE) DFSVisit(g, v); } function DFSVisit(g, v) { t = t + 1; v.d = t; v.color = v.GRAY; let sibling = v.edges; while (sibling != null) { let index = sibling.index; let n = g.getNode(index); if (n.color == n.WHITE) { n.pi = v; DFSVisit(g, n); //先縱向找 } sibling = sibling.sibling; //利用遞歸的特性來回溯 } v.color = v.BLACK; t = t + 1; v.f = t; console.log(v); } } //數據結構 鄰接鏈表-邊 function Edge() { if (!(this instanceof Edge)) return new Edge(); this.index = null; //邊所依附的節點的位置 this.sibling = null; } //創建 頂點 var vA = Vertex(); var vB = Vertex(); var vC = Vertex(); var vD = Vertex(); var vE = Vertex(); var vF = Vertex(); vA.value = "A"; vB.value = "B"; vC.value = "C"; vD.value = "D"; vE.value = "E"; vF.value = "F"; //構建由 A 節點發出的邊集 var eA1 = Edge(); var eA2 = Edge(); eA1.index = 1; eA2.index = 3; eA1.sibling = eA2; vA.edges = eA1; //構建有 B 節點發出的邊集 var eB1 = Edge(); var eB2 = Edge(); var eB3 = Edge(); eB1.index = 0; eB2.index = 4; eB3.index = 2; eB1.sibling = eB2; eB2.sibling = eB3; vB.edges = eB1; //構建由 C 節點發出的邊 var eC1 = Edge(); var eC2 = Edge(); var eC3 = Edge(); eC1.index = 1; eC2.index = 4; eC3.index = 5; eC1.sibling = eC2; eC2.sibling = eC3; vC.edges = eC1; //構建由 D 節點發出的邊 var eD1 = Edge(); eD1.index = 0; vD.edges = eD1; //構建由 E 節點發出的邊 var eE1 = Edge(); var eE2 = Edge(); var eE3 = Edge(); eE1.index = 1; eE2.index = 2; eE3.index = 5; eE1.sibling = eE2; eE2.sibling = eE3; vE.edges = eE1; //構建由 F 節點發出的邊 var eF1 = Edge(); var eF2 = Edge(); eF1.index = 2; eF2.index = 4; eF1.sibling = eF2; vF.edges = eF1; //構建圖 var g = Graph(); g.addNode(vA); g.addNode(vB); g.addNode(vC); g.addNode(vD); g.addNode(vE); g.addNode(vF); DFS(g);
節點訪問順序為 F->C->E->B->D->A
5. 對構建鏈表的方式進行優化我們發現構建圖的操作過于繁瑣,于是想簡化圖的構建方式,簡化后如下
var vertexs = ["A", "B", "C", "D", "E", "F"]; var edges = { A: [{ id: "B", w: 1 }, { id: "D", w: 2 }], B: [{ id: "A", w: 3 }, { id: "E", w: 3 }, { id: "C", w: 7 }], C: [{ id: "B", w: 5 }, { id: "E", w: 3 }, { id: "F", w: 4 }], D: [{ id: "A", w: 2 }], E: [{ id: "B", w: 3 }, { id: "C", w: 7 }, { id: "F", w: 3 }], F: [{ id: "C", w: 6 }, { id: "E", w: 9 }] } var g = Graph(); g.initVertex(vertexs); g.initEdge(edges);
我們想用這種方式初始化一個圖,w為邊的權值
這里的改進只是針對圖的構建,所有無論時BFS,還是DFS,表示頂點和邊的數據結構都沒有變,只有對表示圖的數據結構 Graph進行改進
5.1 改進之后的Graph
//數據結構 圖-G
//數據結構 圖-G function Graph() { if (!(this instanceof Graph)) return new Graph(); this.graph = []; this.refer = new Map(); //字典 用來映射標節點的識符和數組中的位置 } Graph.prototype = { constructor: Graph, //這里加進來的已經具備了邊的關系 addNode: function(node) { this.graph.push(node); }, getNode: function(index) { return this.graph[index]; }, //創建圖的 節點 initVertex: function(vertexs) { //創建節點并初始化節點屬性 value for (let value of vertexs) { let vertex = Vertex(); vertex.value = value; this.graph.push(vertex); } //初始化 字典 for (let i in this.graph) { this.refer.set(this.graph[i].value,i); } }, //建立圖中 邊 的關系 initEdge: (function(){ //創建鏈表,返回鏈表的第一個節點 function createLink(index, len, edges, refer) { if (index >= len) return null; let edgeNode = Edge(); edgeNode.index = refer.get(edges[index].id); //邊連接的節點 用在數組中的位置表示 參照字典 edgeNode.w = edges[index].w; //邊的權值 edgeNode.sibling = createLink(++index, len, edges, refer); //通過遞歸實現 回溯 return edgeNode; } return function(edges) { for (let field in edges) { let index = this.refer.get(field); //從字典表中找出節點在 graph 中的位置 let vertex = this.graph[index]; //獲取節點 vertex.edges = createLink(0, edges[field].length, edges[field], this.refer); } } }()) }
5.2 改進之后的BFS完整代碼
DFS相同
function Vertex() { if (!(this instanceof Vertex)) return new Vertex(); this.color = this.WHITE; //初始為 白色 this.pi = null; //初始為 無前驅 this.d = this.INFINITY; //初始為 無窮大 this.edges = null; //由頂點發出的所有邊 this.value = null; //節點的值 默認為空 } Vertex.prototype = { constructor: Vertex, WHITE: "white", //白色 GRAY: "gray", //灰色 BLACK: "black", //黑色 INFINITY: null, //d 為 null 時表示無窮大 } //數據結構 鄰接鏈表-邊 function Edge() { if (!(this instanceof Edge)) return new Edge(); this.index = null; //邊所依附的節點的位置 this.sibling = null; this.w = null; //保存邊的權值 } //數據結構 圖-G function Graph() { if (!(this instanceof Graph)) return new Graph(); this.graph = []; this.refer = new Map(); //字典 用來映射標節點的識符和數組中的位置 } Graph.prototype = { constructor: Graph, //這里加進來的已經具備了邊的關系 addNode: function(node) { this.graph.push(node); }, getNode: function(index) { return this.graph[index]; }, //創建圖的 節點 initVertex: function(vertexs) { //創建節點并初始化節點屬性 value for (let value of vertexs) { let vertex = Vertex(); vertex.value = value; this.graph.push(vertex); } //初始化 字典 for (let i in this.graph) { this.refer.set(this.graph[i].value,i); } }, //建立圖中 邊 的關系 initEdge: (function(){ //創建鏈表,返回鏈表的第一個節點 function createLink(index, len, edges, refer) { if (index >= len) return null; let edgeNode = Edge(); edgeNode.index = refer.get(edges[index].id); //邊連接的節點 用在數組中的位置表示 參照字典 edgeNode.w = edges[index].w; //邊的權值 edgeNode.sibling = createLink(++index, len, edges, refer); //通過遞歸實現 回溯 return edgeNode; } return function(edges) { for (let field in edges) { let index = this.refer.get(field); //從字典表中找出節點在 graph 中的位置 let vertex = this.graph[index]; //獲取節點 vertex.edges = createLink(0, edges[field].length, edges[field], this.refer); } } }()) } //廣度優先搜索 function BFS(g, s) { let queue = []; s.color = s.GRAY; s.d = 0; queue.push(s); while (queue.length > 0) { let u = queue.shift(); if (u.edges == null) continue; let sibling = u.edges; while (sibling != null) { let index = sibling.index; let n = g.getNode(index); if (n.color == n.WHITE) { n.color = n.GRAY; n.d = u.d + 1; n.pi = u; queue.push(n); } sibling = sibling.sibling; } u.color = u.BLACK; console.log(u) } } var vertexs = ["A", "B", "C", "D", "E", "F"]; var edges = { A: [{ id: "B", w: 1 }, { id: "D", w: 2 }], B: [{ id: "A", w: 3 }, { id: "E", w: 3 }, { id: "C", w: 7 }], C: [{ id: "B", w: 5 }, { id: "E", w: 3 }, { id: "F", w: 4 }], D: [{ id: "A", w: 2 }], E: [{ id: "B", w: 3 }, { id: "C", w: 7 }, { id: "F", w: 3 }], F: [{ id: "C", w: 6 }, { id: "E", w: 9 }] } //構建圖 var g = Graph(); g.initVertex(vertexs); g.initEdge(edges); //調用BFS BFS(g, g.graph[1]);6. 總結
著重體會
1 如何用鄰接鏈表表示圖的邊
2 如何用遞歸的特性實現回溯
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92176.html
摘要:算法之深度優先遍歷和廣度優先遍歷背景在開發頁面的時候,我們有時候會遇到這種需求在頁面某個節點中遍歷,找到目標節點,我們正常做法是利用選擇器,或者,但在本文,我們從算法的角度去查找節點,同時理解一下深度優先遍歷和廣度優先遍歷的原理。 JS算法之深度優先遍歷(DFS)和廣度優先遍歷(BFS) 背景 在開發頁面的時候,我們有時候會遇到這種需求:在頁面某個dom節點中遍歷,找到目標dom節點,...
摘要:樹中結點的最大層次稱為樹的深度或高度。二叉樹有深度遍歷和廣度遍歷,深度遍歷有前序中序和后序三種遍歷方法。二叉樹的前序遍歷可以用來顯示目錄結構等中序遍歷可以實現表達式樹,在編譯器底層很有用后序遍歷可以用來實現計算目錄內的文件及其信息等。 樹的簡介 棧、隊列、鏈表等數據結構,都是順序數據結構。而樹是非順序數據結構。樹型結構是一類非常重要的非線性結構。直觀地,樹型結構是以分支關系定義的層次結...
摘要:隊列棧隊列是先進先出,后進后出,常用的操作是取第一個元素尾部加入一個元素。棧是后進先出,就像一個垃圾桶,后入的垃圾先被倒出來。遍歷中間過程,每一個節點入棧的時候是灰色的,出棧的時候是黑色的。 0. 前言 廣度優先搜索(BFS)和深度優先搜索(DFS),大家可能在oj上見過,各種求路徑、最短路徑、最優方法、組合等等。于是,我們不妨動手試一下js版本怎么玩。 1.隊列、棧 隊列是先進先出,...
摘要:但是一個偏序關系,如果我們默認,先出現的排在前面,那么我們就能比較,的關系了。對于算法的每個節點,我們都有一個發現時間,一個訪問時間,當運行完成時,對于圖中的任意一條邊,都有所以拓撲排序的次序與頂點的完成時間恰好相反。 1. 偏序和全序的概念 1.1. 偏序 設R是集合A上的一個二元關系,若R滿足下列三個性質則稱R為A上的偏序關系自反性:對任意x∈A,有∈R反對稱性:對任意的x,y∈A...
摘要:我們現在來看二分搜索算法的兩種變形插值搜索和指數搜索。插值搜索是對二分搜索算法的改進,插值搜索可以基于搜索的值選擇到達不同的位置。 預警 在本篇文章中,將為各位老鐵介紹不同的搜索算法以及它們的復雜度。因為力求通俗易懂,所以篇幅可能較長,大伙可以先Mark下來,每天抽時間看一點理解一點。本文配套的Github Repo,歡迎各位老鐵star,會一直更新的。 開篇 和排序類似,搜索或者叫做...
閱讀 1240·2021-11-15 11:37
閱讀 2252·2021-09-30 09:55
閱讀 4516·2021-09-22 15:51
閱讀 3749·2021-09-22 15:46
閱讀 2772·2019-08-30 15:52
閱讀 428·2019-08-29 16:20
閱讀 2895·2019-08-29 15:12
閱讀 1134·2019-08-26 18:27