宽度优先搜索算法实现8数码_「Javascript算法设计」× 广度优先搜索与深度优先搜索...

目录:

  • 图的遍历
  • 广度优先搜索
  • 深度优先搜索

为了方便我们理解和实现代码,我们先创建一个图类(Graph)

class Graph{
  constructor(isDirected = false){
    this.isDirected = isDirected  // {1}
    this.vertices = []  // {2}
    this.adjList = new Dictionary()  // {3}
  }
}

Graph构造函数可以接收一个参数来表示图是否有向(行{1}),默认情况下,图是无向的。我们使用一个数组来存储图中所有顶点的名字(行{2}),以及一个字典(后面给出定义)来存储邻接表(行{3})。字典将会使用顶点的名字作为键,邻接顶点列表作为值

接着我们实现两个方法:一个用来向图中添加一个新的顶点(因为图实例化后是空的),另外一个方法用来添加顶点之间的边。

addVertex函数:

addVertex(v){
  if(!this.vertices.includes(v)){  // {5}
    this.vertices.push(v)  // {6}
    this.adjList.set(v, []) // {7}
  }
}

这个方法接收顶点v作为参数,只有在这个顶点不存在与图中时(行{5})我们将该顶点添加到顶点列表中(行{6}),并且在邻接表中,设置顶点v作为键对应的字典值为一个空数组(行{7})

addEdge函数

addEdge(v,w){
  if(!this.adjList.get(v)){
    this.addVertex(v) // {8}
  }
  if(!this.adjList.get(w)){
    this.addVertex(w) // {9}
  }
  this.adjList.get(v).push(w) // {10}
  if(!this.isDirected){
    this.adjList.get(w).push(v)  // {11}
  }
}

这个方法接收两个顶点作为参数,也就是我们要建立连接的两个顶点。在连接顶点之前,需要验证顶点是否存在与图中。如果顶点v或w不存在于图中,要将它们加入顶点列表(行{8} 和 行{9})

然后,通过将w加入到v的邻接表中,我们添加一条自顶点v到顶点w的边(行{10}),如果是有向图,则行{10}就足够,但如果是无向图,我们还需要添加一条自w到v的边(行{11})

注意:我们只是往数组中新增元素,因为数组已经在行{7}被初始化

要完成Graph类,我们还要声明两个取值的方法:一个返回顶点列表,一个返回邻接表

getVertices(){
  return this.vertices
}
getAdjList(){
  return this.adjList
}

图的遍历

和树数据结构类似,我们可以访问图的所有节点。有两种算法可以对图进行遍历:广度优先搜索(BFS)深度优先搜索(DFS)。图遍历可以用来寻找特定的顶点或者寻找两个顶点之间的路径,检查图是否连通,检查图是否有环,等等

在实现算法之前,让我们来更好地理解一下图遍历的思想

图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。

完全搜索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中

为了保证算法的效率,务必访问每个顶点至多两次。连通图中每条边和顶点都会被访问到

广度优先搜索算法和深度优先搜索算法基本上是相同的,只有一点不同,那就是待访问顶点列表的数据结构,如下表所示:

7d6803eaba107c62ef3ba6a2200cbaec.png

当要标注已经访问过的顶点时,我们用三种颜色来反映它们的状态

  • 白色:表示该顶点还没有被访问
  • 灰色:表示该顶点被访问过,但并未被探索过
  • 黑色:表示该顶点被访问过,且被完全探索过

为了有助于在广度优先和深度优先算法中标记顶点,我们是要定义一个Colors变量作为一个枚举器,如下:

const Colors = {
  WHITE:0,
  GREY:1,
  BLACK:2
}

两个算法还需要一个辅助对象来帮助存储顶点是否被访问过。在每个算法的开头,所有的顶点会被标记为未访问(白色)。我们要用下面的函数来初始化每个顶点的颜色

const initColor = (vertices) => {
  const color = {}
  for(let i = 0; i < vertices.length; i++){
    color[vertices[i]] = Colors.WHITE
  }

  return color
}

广度优先搜索

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

3f8fce5e795af1f14cdc10932b18b228.png

以下是从顶点v开始的广度优先搜索算法所遵循的步骤

  1. 创建一个队列Q
  2. 标注v为被发现的(灰色),并将v推入队列Q
  3. 如果Q非空,则运行以下步骤
    1. 将u从Q中出队列
    2. 标注u为被发现的(灰色)
    3. 将u所有未被访问过的领点(白色)入队列
    4. 标注u为被探索的(黑色)

让我们来实现广度优先搜索算法

function breadthFirstSearch(graph, startVertex, callback){
  const vertices = graph.getVertices()
  const adjList = graph.getAdjList()
  const color = initColor(vertices)  // {1}

  const queue = new Queue()  // {2}
  queue.enqueue(startVertex)  // {3}
  
  while(!queue.isEmpty()){  // {4}
    const u = queue.dequeue()  // {5}
    const neighbors = adjList.get(u)  // {6}
    color[u] = Color.GREY  // {7}
    for(let i = 0; i < neighbors.length; i++){  // {8}
      const w = neighbors[i]  // {9}
      if(color[w] === Colors.WHITE){  // {10}
        color[w] = Color.GREY  // {11}
        queue.enqueue(w)  // {12}
      }
    }
    color[u] = Color.BLACK  // {13}
    if(callback){  // {14}
      callback(u)
    }
  }
}

让我们深入学习广度优先搜索的实现过程。我们要做的第一件事情是用initColor函数来将color数组初始化为白色(行{1})。我们还需要声明和创建一个Queue实例(行{2}),它将会存储待访问和待探索的顶点

breadthFirstSearch函数接收一个图实例和顶点作为算法的起始点。起始点是必要的,我们将此顶点存入队列(行{3})

如果队列非空(行{4}),我们将通过出队列(行{5})操作从队列中移除一个顶点,并取得一个包含其所有邻点的邻接表(行{6})。该顶点将被标注为灰色(行{7}),表示我们发现了它(但还未完成对其的探索)

对于u(行{8})的每个邻点,我们取得其值(该顶点的名字 行{9}),如果它还未被访问过(颜色为白色 行{10}),则将其标注为我们已经发现了它(灰色 行{11}),并将这个顶点加入队列(行{12})。这样当其从队列中出列时,我们可以完成对其的探索

当完成探索该顶点和其相邻顶点后,我们将该顶点标注为已探索过的(颜色设置为黑色 行{13})

我们实现这个breadthFirstSearch函数也接收一个回调,这个是可选的,如果传递了回调函数(行{14}),就会用到它


深度优先搜索

深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点,如下图所示:

588d74e0b6ec18090dd129ade5c40c8c.png

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

要访问顶点v,照如下步骤:

  1. 标注v为被发现的(灰色)
  2. 对于v的所有未访问(白色)的领点w,访问顶点w
  3. 标注v为已被探索的(黑色)

如你所见,深度优先搜索的步骤是递归的,这意味着深度优先搜索算法使用来存储函数调用(由递归调用所创建的栈)

我们实现一下深度优先搜索算法

function depthFirstSearch(graph, callback){  // {1}
  const vertices = graph.getVertices()
  const adjList = graph.getAdjList()
  const color = initColor(vertices)

  for(let i = 0; i < vertices.length; i++){  // {2}
    if(color[vertices[i]] === Colors.WHITE){  // {3}
      depthFirstSearch(vertices[i], color, adjList, callback)  // {4}
    }
  }
}

function depthFirstSearch(u, color, adjList, callback){
  color[u] = Colors.GREY   // {5}

  const neighbors = adjList.get(u)  // {6}
  for(let i = 0; i < neighbors.length; i++){  // {7}
    const w = neighbors[i]  // {8}
    if(color[w] === Colors.WHITE){  // {9}
      depthFirstSearch(w, color, adjList, callback)  // {10}
    }
  }

  color[u] = Colors.BLACK  // {11}
  if(callback){  // {12}
    callback(u)
  }
}

depthFirstSearch函数接收一个Graph类实例和回调函数作为参数(行{1})。在初始化每个顶点的颜色后,对于图实例中每一个未被访问过的顶点(行{2}和行{3}),我们调用私有的递归函数depthFirstSearch,传递的参数为要访问的顶点u,颜色数组以及回调函数(行{4})

当访问顶点u时,我们标注其为被发现的(灰色 行{5}),接下来的一步是取得包含顶点u所有邻点的邻接表(行{6})。对于顶点u的每一个未被访问过(白色 行{7} 行{9})的邻点w(行{8}),我们将调用depthFirstSearch函数,传递w和其他参数(行{10}),添加顶点w入栈(递归栈),这样接下来就能处理顶点w。最后,在该顶点和邻点按深度访问之后,就会开始回退,意思是该顶点已经完全被探索了,标记其未黑色(行{11}),对这个已经探索完的顶点点执行回调(行{12})

下面我们以上面的图执行深度优先搜索的过程来展示:

2d043dde6f915937342424f64e9776a2.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值