JS实现广度优先搜索与深度优先搜索

2019-05-16  本文已影响0人  小小的开发人员

图在数学及技术上的概念:
一个图G = (V, E),由以下元素组成。
 V: 一组顶点
 E: 一组边,连接V中的顶点

  在着手实现算法之前,让我们先了解一下图的一些术语。

  由一条边连接在一起的顶点称为相邻顶点。比如,A和B是相邻的,A和D是相邻的,A和C是相邻的,A和E不是相邻的。

   一个顶点的度是其相邻顶点的数量。比如,A和其他三个顶点相连接,因此,A的度为3; E和其他两个顶点相连,因此,E的度为2。

  路径是顶点v1, v2, ..., vk的一个连续序列,其中vi和vi+1是相邻的。以上一示意图中的图为例,其中包含路径A B E I和A C D G。

  简单路径要求不包含重复的顶点。举个例子,A D G是一条简单路径。除去最后一个顶点(因为它和第一个顶点是同一个顶点),环也是一个简单路径,比如A D C A(最后一个顶点重新回到A)。

  如果图中不存在环,则称该图是无环的。如果图中每两个顶点间都存在路径,则该图是联通的。

  我们可以使用图来解决计算机科学世界中的很多问题,比如搜索图中的一个特定顶点或搜索一条特定边,寻找图中的一条路径(从一个顶点到另一个顶点),寻找两个顶点之间的最短路径, 以及环检测。

图的表示

邻接矩阵


邻接表


关联矩阵


  我们用JS来实现图:

function Dictionary() {
    let items = {}
    this.set = function (key, value) {
        items[key] = value
    }
    this.has = function (key) {
        return key in items
    }
    this.remove = function (key) {
        if (this.has(key)) {
            delete items[key]
            return true
        }
        return false
    }
    this.get = function (key) {
        return this.has(key) ? items[key] : undefined
    }
    this.values = function () {
        let values = []
        for (let k in items) {
            if (this.has(k)) {
                values.push(items[k])
            }
        }
        return values
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return Object.keys(items).length
    }
    this.keys = function () {
        return Object.keys(items)
    }
    this.getItems = function () {
        return items
    }
}

function Graph() {

    let vertices = [] // 存储图中所有顶点的名字
    let adjList = new Dictionary() // 字典储存邻接表,使用顶点的名字作为键,邻接顶点列表作为值

    // 向图中添加一个新的顶点
    this.addVertex = function (v) {
        vertices.push(v)
        adjList.set(v, [])
    }

    // 添加顶点之间的边
    this.addEdge = function (v, w) {
        adjList.get(v).push(w)
        adjList.get(w).push(v)
    }

    this.toString = function() {
        let s = ''
        for (let i = 0; i < vertices.length; i++){
            s += vertices[i] + ' -> '
            let neighbors = adjList.get(vertices[i])
            for (let j = 0; j < neighbors.length; j++){
                s += neighbors[j] + ' '
            }
            s += '\n'
        }
        return s;
    };
}

let graph = new Graph()
let myVertices = ['A','B','C','D','E','F','G','H','I']
for (let i = 0; i < myVertices.length; i++){
    graph.addVertex(myVertices[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')

console.log(graph.toString())
// A -> B C D
// B -> A E F
// C -> A D G
// D -> A C G H
// E -> B I
// F -> B
// G -> C D
// H -> D
// I -> E 

图的遍历
  和树数据结构类似,我们可以访问图的所有节点。有两种算法可以对图进行遍历:广度优先搜索(Breadth-First Search,BFS)和深度优先搜索(Depth-First Search,DFS)。
  图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环等。
  图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。
  完全探索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。
  为了保证算法的效率,务必访问每个顶点至多两次。连通图中每条边和顶点都会被访问到。
  广度优先搜索算法和深度优先搜索算法基本上是相同的,只有一点不同,那就是待访问顶点列表的数据结构。

广度优先搜索
  广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访问图的一层。换句话说,就是先宽后深地访问顶点,如下图所示:

当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态。
 白色: 表示该顶点还没有被访问。
 灰色: 表示该顶点被访问过,但并未被探索过。
 黑色: 表示该顶点被访问过且被完全探索过。
这就是之前提到的务必访问每个顶点最多两次的原因。

function Queue() {
    let items = []
    this.enqueue = function (element) {
        items.push(element)
    }
    this.dequeue = function () {
        return items.shift()
    }
    this.front = function () {
        return items[0]
    }
    this.isEmpty = function () {
        return items.length == 0
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return items.length
    }
    this.print = function () {
        console.log(items.toString())
    }
}

function Dictionary() {
    let items = {}
    this.set = function (key, value) {
        items[key] = value
    }
    this.has = function (key) {
        return key in items
    }
    this.remove = function (key) {
        if (this.has(key)) {
            delete items[key]
            return true
        }
        return false
    }
    this.get = function (key) {
        return this.has(key) ? items[key] : undefined
    }
    this.values = function () {
        let values = []
        for (let k in items) {
            if (this.has(k)) {
                values.push(items[k])
            }
        }
        return values
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return Object.keys(items).length
    }
    this.keys = function () {
        return Object.keys(items)
    }
    this.getItems = function () {
        return items
    }
}

function Graph() {

    let vertices = [] // 存储图中所有顶点的名字
    let adjList = new Dictionary() // 字典储存邻接表,使用顶点的名字作为键,邻接顶点列表作为值

    // 向图中添加一个新的顶点
    this.addVertex = function (v) {
        vertices.push(v)
        adjList.set(v, [])
    }

    // 添加顶点之间的边
    this.addEdge = function (v, w) {
        adjList.get(v).push(w)
        adjList.get(w).push(v)
    }

    // 在控制台输出图
    this.toString = function(){
        let s = ''
        for (let i = 0; i < vertices.length; i++){
            s += vertices[i] + ' -> '
            let neighbors = adjList.get(vertices[i])
            for (let j = 0; j < neighbors.length; j++){
                s += neighbors[j] + ' '
            }
            s += '\n'
        }
        return s;
    }

    // 广度优先搜索算法
    let initializeColor = function () {
        let color = []
        for (let i = 0; i < vertices.length; i++) {
            color[vertices[i]] = 'white' // 顶点还没有被访问过
        }
        return color
    }
    this.bfs = function (v, callback) {
        let color = initializeColor(),
            queue = new Queue()
        queue.enqueue(v)
        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u)
            color[u] = 'grey'
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i]
                if (color[w] === 'white') {
                    color[w] = 'grey'
                    queue.enqueue(w)
                }
            }
            color[u] = 'black'
            if (callback) {
                callback(u)
            }
        }
    }

}
function printNode (value) {
    console.log('Visited vertex: ' + value)
}


let graph = new Graph()
let myVertices = ['A','B','C','D','E','F','G','H','I']
for (let i = 0; i < myVertices.length; i++){
    graph.addVertex(myVertices[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')

graph.bfs(myVertices[0], printNode)
// Visited vertex: A
// Visited vertex: B
// Visited vertex: C
// Visited vertex: D
// Visited vertex: E
// Visited vertex: F
// Visited vertex: G
// Visited vertex: H
// Visited vertex: I

使用BFS寻找最短路径
  给定一个图G和源顶点v,找出对每个顶点u,u和v之间最短路径的距离(以边的数量计)。
  对于给定顶点v,广度优先算法会访问所有与其距离为1的顶点,接着是距离为2的顶点, 以此类推。所以,可以用广度优先算法来解这个问题。
 从v到u的距离d[u];
 前溯点pred[u],用来推导出从v到其他每个顶点u的最短路径。

// 改进过的广度优先搜索算法
    this.BFS = function (v) {
        let color = initializeColor(),
            queue = new Queue(),
            d = [], // v到u的距离
            pred = [] // 前溯点p
        queue.enqueue(v)

        for (let i = 0; i < vertices.length; i++) {
            d[vertices[i]] = 0
            pred[vertices[i]] = null
        }

        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u)
            color[u] = 'grey'
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i]
                if (color[w] === 'white') {
                    color[w] = 'grey'
                    d[w] = d[u] + 1
                    pred[w] = u
                    queue.enqueue(w)
                }
            }
            color[u] = 'black'
        }

        return {
            distances: d,
            predecessors: pred
        }
    }

  通过前溯点数组,我们可以用下面这段代码来构建从顶点A到其他顶点的路径:

// 对predecessors进行追溯直到A点,然后展示回溯路径
let fromVertex = myVertices[0]
for (let i = 1; i < myVertices.length; i++){
    let toVertex = myVertices[i],
        path = new Stack()
    for (let v = toVertex; v !== fromVertex; v = shortestPathA.predecessors[v]) {
        path.push(v)
    }
    path.push(fromVertex)
    let s = path.pop()
    while (!path.isEmpty()) {
        s += ' - ' + path.pop()
    }
    console.log(s)
}
// A - B
// A - C
// A - D
// A - B - E
// A - B - F
// A - C - G
// A - D - H
// A - B - E - I

深度优先搜索
  深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。

  深度优先搜索算法不需要一个源顶点。在深度优先搜索算法中,若图中顶点v未访问,则访问该顶点v。

要访问顶点v,照如下步骤做。
(1) 标注v为被发现的(灰色)。
(2) 对于v的所有未访问的邻点w,访问顶点w,标注v为已被探索的(黑色)。

 // 深度优先算法
    let dfsVisit = function(u, color, callback){
        color[u] = 'grey'
        if (callback) {
            callback(u)
        }
        let neighbors = adjList.get(u);
        for (let i = 0; i < neighbors.length; i++){
            let w = neighbors[i]
            if (color[w] === 'white'){
                dfsVisit(w, color, callback)
            }
        }
        color[u] = 'black'
    }
    this.dfs = function(callback){
        let color = initializeColor()
        for (let i = 0; i < vertices.length; i++){
            if (color[vertices[i]] === 'white'){
                dfsVisit(vertices[i], color, callback)
            }
        }
    }
console.log(graph.dfs(printNode))
// Visited vertex: A
// Visited vertex: B
// Visited vertex: E
// Visited vertex: I
// Visited vertex: F
// Visited vertex: C
// Visited vertex: D
// Visited vertex: G
// Visited vertex: H

完整代码:

function Stack() {
    let items = []
    this.push = function (element) {  // 添加一个(或几个)新元素到栈顶
        items.push(element)
    }
    this.pop = function (element) { // 移除栈顶的元素,同时返回被移除的元素
        return items.pop()
    }
    this.peek = function (element) { // 返回栈顶的元素,不对栈做任何修改
        return items[items.length - 1]
    }
    this.isEmpty = function (element) {// 如果栈里没有任何元素就返回true
        return items.length === 0
    }
    this.size = function () { // 返回栈里的元素个数
        return items.length
    }
    this.clear = function () { // 移除栈里的所有元素
        items = []
    }
    this.print = function () { // 字符串化
        console.log(items.toString())
    }
}

function Queue() {
    let items = []
    this.enqueue = function (element) {
        items.push(element)
    }
    this.dequeue = function () {
        return items.shift()
    }
    this.front = function () {
        return items[0]
    }
    this.isEmpty = function () {
        return items.length == 0
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return items.length
    }
    this.print = function () {
        console.log(items.toString())
    }
}

function Dictionary() {
    let items = {}
    this.set = function (key, value) {
        items[key] = value
    }
    this.has = function (key) {
        return key in items
    }
    this.remove = function (key) {
        if (this.has(key)) {
            delete items[key]
            return true
        }
        return false
    }
    this.get = function (key) {
        return this.has(key) ? items[key] : undefined
    }
    this.values = function () {
        let values = []
        for (let k in items) {
            if (this.has(k)) {
                values.push(items[k])
            }
        }
        return values
    }
    this.clear = function () {
        items = []
    }
    this.size = function () {
        return Object.keys(items).length
    }
    this.keys = function () {
        return Object.keys(items)
    }
    this.getItems = function () {
        return items
    }
}

function Graph() {

    let vertices = [] // 存储图中所有顶点的名字
    let adjList = new Dictionary() // 字典储存邻接表,使用顶点的名字作为键,邻接顶点列表作为值

    // 向图中添加一个新的顶点
    this.addVertex = function (v) {
        vertices.push(v)
        adjList.set(v, [])
    }

    // 添加顶点之间的边
    this.addEdge = function (v, w) {
        adjList.get(v).push(w)
        adjList.get(w).push(v)
    }

    // 在控制台输出图
    this.toString = function(){
        let s = ''
        for (let i = 0; i < vertices.length; i++){
            s += vertices[i] + ' -> '
            let neighbors = adjList.get(vertices[i])
            for (let j = 0; j < neighbors.length; j++){
                s += neighbors[j] + ' '
            }
            s += '\n'
        }
        return s;
    }

    // 广度优先搜索算法
    let initializeColor = function () {
        let color = []
        for (let i = 0; i < vertices.length; i++) {
            color[vertices[i]] = 'white' // 顶点还没有被访问过
        }
        return color
    }
    this.bfs = function (v, callback) {
        let color = initializeColor(),
            queue = new Queue()
        queue.enqueue(v)
        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u)
            color[u] = 'grey'
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i]
                if (color[w] === 'white') {
                    color[w] = 'grey'
                    queue.enqueue(w)
                }
            }
            color[u] = 'black'
            if (callback) {
                callback(u)
            }
        }
    }

    // 改进过的广度优先搜索算法
    this.BFS = function (v) {
        let color = initializeColor(),
            queue = new Queue(),
            d = [], // v到u的距离
            pred = [] // 前溯点p
        queue.enqueue(v)

        for (let i = 0; i < vertices.length; i++) {
            d[vertices[i]] = 0
            pred[vertices[i]] = null
        }

        while (!queue.isEmpty()) {
            let u = queue.dequeue(),
                neighbors = adjList.get(u)
            color[u] = 'grey'
            for (let i = 0; i < neighbors.length; i++) {
                let w = neighbors[i]
                if (color[w] === 'white') {
                    color[w] = 'grey'
                    d[w] = d[u] + 1
                    pred[w] = u
                    queue.enqueue(w)
                }
            }
            color[u] = 'black'
        }

        return {
            distances: d,
            predecessors: pred
        }
    }

    // 深度优先算法
    let dfsVisit = function(u, color, callback){
        color[u] = 'grey'
        if (callback) {
            callback(u)
        }
        let neighbors = adjList.get(u);
        for (let i = 0; i < neighbors.length; i++){
            let w = neighbors[i]
            if (color[w] === 'white'){
                dfsVisit(w, color, callback)
            }
        }
        color[u] = 'black'
    }
    this.dfs = function(callback){
        let color = initializeColor()
        for (let i = 0; i < vertices.length; i++){
            if (color[vertices[i]] === 'white'){
                dfsVisit(vertices[i], color, callback)
            }
        }
    }

    // 探索深度优先算法
    let time = 0;
    this.DFS = function(){
        let color = initializeColor(),
            d = [], // 顶点u的发现时间
            f = [], // 当顶点u被标注为黑色时,u的完成探索时间
            p = [] // 顶点u的前溯点
        time = 0;
        for (let i = 0; i < vertices.length; i++){
            f[vertices[i]] = 0;
            d[vertices[i]] = 0;
            p[vertices[i]] = null;
        }
        for (let i = 0; i < vertices.length; i++){
            if (color[vertices[i]] === 'white'){
                DFSVisit(vertices[i], color, d, f, p);
            }
        }
        return {
            discovery: d,
            finished: f,
            predecessors: p
        }
    }
    let DFSVisit = function(u, color, d, f, p){
        console.log('discovered ' + u)
        color[u] = 'grey'
        d[u] = ++time
        let neighbors = adjList.get(u)
        for (let i = 0; i < neighbors.length; i++){
            let w = neighbors[i]
            if (color[w] === 'white'){
                p[w] = u
                DFSVisit(w,color, d, f, p)
            }
        }
        color[u] = 'black'
        f[u] = ++time
        console.log('explored ' + u)
    }
}

function printNode (value) {
    console.log('Visited vertex: ' + value)
}

let graph = new Graph()
let myVertices = ['A','B','C','D','E','F','G','H','I']
for (let i = 0; i < myVertices.length; i++){
    graph.addVertex(myVertices[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')

graph.bfs(myVertices[0], printNode)
// Visited vertex: A
// Visited vertex: B
// Visited vertex: C
// Visited vertex: D
// Visited vertex: E
// Visited vertex: F
// Visited vertex: G
// Visited vertex: H
// Visited vertex: I

console.log(graph.BFS(myVertices[0]))
// { distances: [ A: 0, B: 1, C: 1, D: 1, E: 2, F: 2, G: 2, H: 2, I: 3 ],
//     predecessors:
//         [ A: null,
//     B: 'A',
//     C: 'A',
//     D: 'A',
//     E: 'B',
//     F: 'B',
//     G: 'C',
//     H: 'D',
//     I: 'E' ] }

// 对predecessors进行追溯直到A点,然后展示回溯路径
let shortestPathA = graph.BFS(myVertices[0])
let fromVertex = myVertices[0]
for (let i = 1; i < myVertices.length; i++){
    let toVertex = myVertices[i],
        path = new Stack()
    for (let v = toVertex; v !== fromVertex; v = shortestPathA.predecessors[v]) {
        path.push(v)
    }
    path.push(fromVertex)
    let s = path.pop()
    while (!path.isEmpty()) {
        s += ' - ' + path.pop()
    }
    console.log(s)
}
// A - B
// A - C
// A - D
// A - B - E
// A - B - F
// A - C - G
// A - D - H
// A - B - E - I


console.log(graph.dfs(printNode))
// Visited vertex: A
// Visited vertex: B
// Visited vertex: E
// Visited vertex: I
// Visited vertex: F
// Visited vertex: C
// Visited vertex: D
// Visited vertex: G
// Visited vertex: H

console.log(graph.DFS())
// discovered A
// discovered B
// discovered E
// discovered I
// explored I
// explored E
// discovered F
// explored F
// explored B
// discovered C
// discovered D
// discovered G
// explored G
// discovered H
// explored H
// explored D
// explored C
// explored A
// { discovery: [ A: 1, B: 2, C: 10, D: 11, E: 3, F: 7, G: 12, H: 14, I: 4 ],
//     finished:
//         [ A: 18, B: 9, C: 17, D: 16, E: 6, F: 8, G: 13, H: 15, I: 5 ],
//     predecessors:
//         [ A: null,
//     B: 'A',
//     C: 'A',
//     D: 'C',
//     E: 'B',
//     F: 'B',
//     G: 'D',
//     H: 'D',
//     I: 'E' ] }
上一篇 下一篇

猜你喜欢

热点阅读