数据结构标准入门-DFS

160 阅读4分钟

图的遍历

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

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

图遍历算法的思想是必须追踪每个第一次访问的结点,并且追踪有哪些结点还没有被完全探索。

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

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

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

  • 深度优先搜索:栈,通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问。

  • 广度优先搜素:队列,通过将顶点存入队列中,最先进入队列的顶点被探索

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

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

这就是之前提到的务必访问每个顶点最多两次的原因。

深度优先搜索

我们常常通过系统的检查每一个顶点和每一条边来获取图的各种性质。要得到图的一些简单性质(比如,计算所有顶点的度数)很容易,只要检查每一条边就好了。但是图的许多其他性质和路径有关,因此一种很简单的想法就是沿着图的边从一个顶点移动到另外一个顶点。尽管存在各种各样的处理策略。但是后面要学习的几乎所有与图有关的算法都使用到了这个简单的抽象模型,其中最简单地的就是下面介绍的这种经典的方法:

走迷宫

思考图的搜索过程的一种有益的方法是,考虑另一个和他等价但是历史悠久的特别问题,在一个由各种通道和路口组成的迷宫中找到出口,有些迷宫的规则很简单。但是大多数迷宫则需要很复杂的策略才行。用迷宫替代图,通道代替边,路口代替顶点。探索迷宫的一种古老的方法,叫做Tremaux搜索。

  • 选择一条没有被标记过的通道,在你走过的路上铺上绳子
  • 标记你一第一次通过的路口和通道
  • 当来到一个标记过的路口时(用绳子回退到上个路口)
  • 当回退到的路口已没有可走的通道时继续回退

绳子可以保证你总能找到一条出路。标记则保证你不会两次通过同一条通道或者入口。要知道是否完全探索了整个迷宫需要证明的更复杂,只有用图搜索才能更好的处理问题。Tremaux搜索很直接,但他与完全搜索一张图任然有不同。下面我们来看怎么实现DFS

DFS代码实现

class DepthFirstSearch{  constructor(G,s){    this.count=0;    this.marked=new Boolean(G.countVer());    this.dfs(G,s)  }  dfs(G,v){    console.log(v)    this.marked[v]=true;    this.count++    for(let item of G.getAdjList(v)){      if(!this.marked[item]) this.dfs(G,item)    }  }}

实现思路:首先,我们要证明这个算法能够标记与起点S联通的所有顶点(并且不会标记其他顶点)。因为算法仅仅通过边来寻找顶点,所以每个被标记过的顶点都与S联通。现在,假设某一个没有被标记过的顶点w与s联通,因为s本身是被标记过的,由s到w的任意一条路径中至少有一条边链接的两个顶点分别是被标记过的和没有被标记过的,列如v-w。更具算法,在标记了v之后必然会发现x。因此这样的边是不存在的。前后矛盾,每个顶点都只会被访问一次保证了时间上限。

寻找路径

单点路径问题在图的处理领域非常的重要,更具标准设计模式,我们将使用如下API

  • Paths(Graph G,let s) //在G图中找出所有起点为s的路径

  • hasPathTo(let v) //是否存在从s到v的路径

  • pathTo(int v) //s到v的路径,如果不存在返回null