尽管深度优先搜索具有许多重要用途,但该策略也具有不适合某些应用程序的缺点。 深度优先方法的最大问题在于它从其中一个邻居出发,在它返回该节点或者是访问其他邻居之前,它必须访问完从出发节点开始的整个路径。 如果我们尝试在一个大的图中发现两个节点之间的最短路径,则使用深度优先搜索会将我们一直带到图的远端,即使我们的的目的地离我们的出发点仅有一步之遥。
广度优先搜索
广度优先搜索(Breadth-first search BFS)算法通过按照与起始节点的接近程度来确定该访问哪个节点 来解决该问题,该节点根据沿最短可能路径的弧的数量来测量。 通过计算弧来测量距离时,每个弧构成一跳(hop)。 因此,广度优先搜索的本质是首先访问起始节点,然后是一跳之外的节点,然后是节点两跳,依此类推。
BFS过程分析
我们还是用上次的图来说明一下我们的BFS算法,并且与DFS做一个比较。
- 第一步是一样的,我们得选一个起始节点,所以我们选择与BFS算法一致的起点
- 下一阶段访问与该节点距离一跳的节点(即距离为1),图中我们容易看出是Dallas,Denver还有Portland三个节点:
- 至此我们继续探索跳数为2的节点,如下:
- 重复上述步骤,最终,我们就这样遍历完了图中所有的节点
相对于DFS算法采用栈这种数据结构来实现,实现广度优先算法的最简单方法是使用队列用来存放未处理的节点。 在该过程的每个步骤中,我们将当前节点的邻居排入队列。 因为队列是按顺序处理的,所以距离起始节点一跳的所有节点将比两跳的节点在队列中更早出现。
这种遍历方式,就像《计算机网络》课程中的洪泛法。
实例分析
BFS的具体过程如下:
/*
* 函数: breadthFirstSearch
* 用法: breadthFirstSearch(node);
* --------------------------------
* 从指定的节点处进行广度优先搜索.
*/
void breadthFirstSearch(Node *node) {
Set<Node *> visited; //标记已访问的节点
Queue<Node *> queue;//建立存储未被处理的节点
queue.enqueue(node); //将要处理的节点放入空队列中
while (!queue.isEmpty()) {//当队列中的节点数不为空时
node = queue.dequeue(); //将队列中的节点从队列中移出
if (!visited.contains(node)) {//如果该节点未被标记
visit(node);//对节点进行相应处理
visited.add(node);//处理完毕后标记该节点
//遍历该节点指向的下一条弧
foreach (Arc *arc in node->arcs) {
//将该弧所指的节点放入队列中
queue.enqueue(arc->finish);
}
}
}
}
接下来我们使用BFS来遍历下图:
虽然我们知道根据DFS算法我们可以找到所有的,由起始节点到目标节点的所有路径,但并不代表那条路是最短的或者是最佳的。就像我们上篇文章所说的一样,对于同一幅图,非递归算法找到的路径就明显比递归算法找的要短。
回顾我们之前提到的BFS的基本思想:从起始顶点开始,首先探索邻居节点,然后再移动到下一级邻居。假设我们从a走到i,按照BFS算法,应该是
a -> d-> h ->i (并且这是一条最短的路径)
其BFS伪代码如下:
bfs from v1 to v2:
//建立一个队列q来存储走过的路径path(可以用vector来存储路径)
create a queue of paths (a vector), q
//将节点v1的路径入队
q.enqueue(v1 path)
//当q不为空,并且节点V2未被访问时
while q is not empty and v2 is not yet visited:
//将q中的元素移出队列,并且存入path中
path = q.dequeue()
//将路径中的最后元素赋值给节点型变量v
v = last element in path
//如果v未被访问
if v is not visited:
//标记节点V
mark v as visited
//如果节点是目标节点,停止执行
if v is the end vertex, we can stop.
//遍历V中所有未被标记的邻居
for each unvisited neighbor of v:
//将v节点的邻居作为最后的元素构成新的路径
make new path with v's neighbor as last element
//将新的路径排入队列中
enqueue new path onto q
没错 这是一个很复杂的步骤。理解都有点困难更别提记住了,但是我们可以注意到,这个实现过程跟DFS的非递归实现是非常类似的。下面我们也是同样的分析一下实现过程:(还是从a到i,front指针指向队头)
- 先建立一个vector,用来存储路径,建立一个队列将路径装入:
Vector<Vertex *> startPath
startPath.add(a)
q.enqueue(startPath)
- 接下来到达循环语句:
in while loop:
//将队列中的元素取出,放到当前路径的变量中
curPath = q.dequeue() (path is a)
//由于队列中只有一个元素a,所以它的最后元素也是它本身
v = last element in curPath (v is a)
//标记V
mark v as visited
//将所有的未标记的路径入队q中
enqueue all unvisited neighbor paths onto q
这里注意,这里最后一句入队的是路径而不是节点,标记的才是节点。执行完毕后,队列跟访问集的情况如下:
- 此时队列不为空,直接从while循环开始。
in while loop:
curPath = q.dequeue() (path is ab)
v = last element in curPath (v is b)
mark v as visited
enqueue all unvisited neighbor paths onto q
根据队列的性质,FILO,所以最先出队列的是ab,ab路径中最后的元素是b,所以,标记b,并将b的所有邻居放入队列中去:
5. 然后ad出队列,ad路径中最后的元素是d,所以,标记d,并将d的所有邻居放入队列中去:
6. 重复上述步骤,直到路径中的最后的元素为目标元素 i 时,停止搜索。此时情况如下:
in while loop:
curPath = q.dequeue() (path is adhi)
v = last element in curPath (v is i)
found!
此时的路径 a -> d-> h ->i 也是最短的路径。
总结
在对于需要找到最短路径的图来说,BFS是绝佳的选择。广度优先搜索将查找从起始节点可到达的所有节点。
- 它将按距离递增的顺序访问它们。
- 在n节点m边图中,需要时间O(m + n)并使用空间O(n)。
- 但在实践中,空间使用率远高于DFS。
这里有个有趣的问题,如果我们将BFS思想运用在树的遍历中会得到什么结论?得到一种我们熟悉的遍历方式——层次遍历。树中可是没有环的。