图的深度优先遍历
深度优先遍历(Depth_First_Search),也有称为深度优先搜索,简称为DFS。
- 这种遍历方法,从图中的某一个顶点开始遍历搜索,首先访问该顶点V(我们可以将其打印),然后从V开始寻找未被访问过的邻接顶点,将遍历后的顶点的状态设置为已访问,如果某个顶点已经访问过,则返回“上层”其实就是一种递归(它相当于树的前序遍历),直至图中的所有都被访问(或打印)。
(该图来自大话数据结构,所有的遍历路径如上图中的右图所示)首先从顶点A开始遍历访问,将A的状态修改为已访问,然后继续寻找邻接A顶点的相关顶点,B和F,但是遍历的时候一直向右寻找遍历,所有遍历到B,然后继续修改访问状态。当程序寻找到已经访问过的顶点时就要“回退”,直到所有的顶点都被遍历完。
基于邻接矩阵图的存储,代码有:
public void DFS(int i){
visited[i] = true; //首先将visited[]状态数组修改为以访问
System.out.print(this.vertexdata[i]+" "); //打印访问到的顶点
for(int j=0;j<this.vertexs;j++){ //继续向“右”遍历
if(arc[i][j]!=INFINITY&&!visited[j]){ //如果邻接矩阵的值不等于INFINITY(空)
//并且下个顶点也未被访问,则为true,否则继续for循环寻找满足条件的顶点
DFS(j); //这里是向下递归,也就是“深度”递归
//因为j代表的是列,j++之后,就是向二维数组的右边走了一步,但是递归时带入的值是j
//所以就相当于下次遍历时是向列方向遍历
}
}
}
public void DFSPrint(){
for(int i=0;i<this.vertexs;i++){//先向邻接矩阵的列方向遍历
if(!visited[i]){ //如果该顶点的访问状态是false,即未被访问,则为true
DFS(i); //将未被访问的顶点进行递归遍历
}
}
}
代码中的visited布尔型数组在图的存储结构中已经创建。代码解释详看注释。
图的存储结构详见:数据结构与算法之图和图的存储结构Java语言描述
上述解释的是基于邻接矩阵实现的DFS,其实邻接表和邻接矩阵的代码也类似,只要理解了邻接矩阵则邻接表就很简单了。下面解释基于邻接表实现的DFS。
基于邻接表图的存储,代码有:
public void DFSlist(int i){
EdgeNode p = new EdgeNode(); //遍历开始时先设置一个用于循环的结点p
visited[i] = true; //将visited[]状态数组修改为以访问
System.out.print(adjlist[i].data+" ");//打印
p = adjlist[i].firstedge; //因为数组后面的是单链表
//所以要将结点p移动到顶点信息数组后面的边表上面
while(p!=null){ //如果移动到的边表上的结点不为空就执行while循环
if(!visited[p.adjvex]){ //判断下个顶点是否被访问
DFSlist(p.adjvex); //递归遍历(相当于向顶点表的列方向遍历)
}
p = p.next;//遍历完后,将p结点继续向后移动,直到p结点指向为空
//则弹出while循环
}
}
public void DFSPrintlist(){
for(int i=0;i<this.vertexs;i++){
if(!visited[i]){
DFSlist(i);
}
}
}
图的广度优先遍历
图的广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称BFS。
这种方法也需要设置一个记录顶点状态的数组visited,并且这种方法与树的层序遍历相似,我们利用数据结构中的队列进行顶点的遍历。
将代码解释一下,会很直观。
基于邻接矩阵图的存储,代码有:
/**
* 广度优先遍历
*/
public void BFSPrint() {
Queue<Integer> queue = new LinkedList<>();
//遍历顶点数组A就是0
for (int i = 0; i < vertexList.size(); i++) {
//如果没有访问过,那么进行广度的遍历
if (!visited[i]) {
visited[i] = true;
System.out.print(vertexList.get(i) + " ");
//将访问过的入队
queue.add(i);
//只要队列不为空,就对入队的该顶点继续进行“行hang”的遍历
while (!queue.isEmpty()) {
//获取到当前顶点的索引值i
i = queue.remove();
//遍历行
for (int j = 0; j < vertexList.size(); j++) {
//在第i行和第j列进行遍历判断
if (!visited[j] && arc[i][j] != 0) {
visited[j] = true;
System.out.print(vertexList.get(j) + " ");
queue.add(j);
}
}
}
}
}
}
上述的代码中,数据结构中的队列会在以后的更新博客中,总之,利用的是队列数据结构进行的遍历访问。
代码的解释主要看代码中的注释。
而邻接表的存储结构的图的广度优先遍历代码其实和邻接矩阵类似,只不过就是把邻接矩阵改成了一个存储顶点信息的对象数组和多个单链表。
基于邻接表图的存储,代码有:
public void BFS(){
MyQueue<Integer> q = new MyQueue<>();
//因为后面需要遍历单链表,所以必须创建一个关于边的p结点
EdgeNode p = new EdgeNode();
for(int i=0;i<this.vertexs;i++){
if(!visited[i]){
visited[i] = true;
p = adjlist[i].firstedge;//将p结点指向遍历后结点的邻接顶点
//即将p指向顶点信息数组后面的单链表
System.out.print(adjlist[i].data+" ");//打印
q.EnQueue(i);//入队
while(!q.isEmpty()){
q.Dequeue();
while(p!=null){//判断p指向的元素是否为空
if(!visited[p.adjvex]){
visited[p.adjvex] = true;
System.out.print(adjlist[p.adjvex].data+" ");//打印
}
p = p.next;//访问完一个顶点后,将p结点继续向后移动
}
}
}
}
}
两者之间的代码在广度和深度的遍历上都很相似。在这个数据结构上,只要先理解了它的工作原理,我们的代码实现也就不是很难了。
这两种的方法的时间复杂度相同,不同的是在对顶点访问的顺序。
如果图中的顶点和边非常多,深度优先更适合目标比较明确,以找到目标为主要目的的情况;而广度优先遍历更适合在不断扩大遍历范围时找到相对最优解的情况。
在广度优先遍历中的队列数据结构,会持续更新。