1、基础部分
在图中实现最基本的操作之一就是搜索从一个指定顶点可以到达哪些顶点,比如从武汉出发的高铁可以到达哪些城市,一些城市可以直达,一些城市不能直达。现在有一份全国高铁模拟图,要从某个城市(顶点)开始,沿着铁轨(边)移动到其他城市(顶点),有两种方法可以用来搜索图:深度优先搜索(DFS)和广度优先搜索(BFS)。它们最终都会到达所有连通的顶点,深度优先搜索通过栈来实现,而广度优先搜索通过队列来实现,不同的实现机制导致不同的搜索方式。
1.1 深度优先搜索
深度优先搜索算法有如下规则:
规则1:如果可能,访问一个邻接的未访问顶点,标记它,并将它放入栈中。
规则2:当不能执行规则 1 时,如果栈不为空,就从栈中弹出一个顶点。
规则3:如果不能执行规则 1 和规则 2 时,就完成了整个搜索过程。
对于上图,应用深度优先搜索如下:假设选取 A 顶点为起始点,并且按照字母优先顺序进行访问,那么应用规则 1 ,接下来访问顶点 B,然后标记它,并将它放入栈中;再次应用规则 1,接下来访问顶点 F,再次应用规则 1,访问顶点 H。我们这时候发现,没有 H 顶点的邻接点了,这时候应用规则 2,从栈中弹出 H,这时候回到了顶点 F,但是我们发现 F 也除了 H 也没有与之邻接且未访问的顶点了,那么再弹出 F,这时候回到顶点 B,同理规则 1 应用不了,应用规则 2,弹出 B,这时候栈中只有顶点 A了,然后 A 还有未访问的邻接点,所有接下来访问顶点 C,但是 C又是这条线的终点,所以从栈中弹出它,再次回到 A,接着访问 D,G,I,最后也回到了 A,然后访问 E,但是最后又回到了顶点 A,这时候我们发现 A没有未访问的邻接点了,所以也把它弹出栈。现在栈中已无顶点,于是应用规则 3,完成了整个搜索过程。
深度优先搜索在于能够找到与某一顶点邻接且没有访问过的顶点。这里以邻接矩阵为例,找到顶点所在的行,从第一列开始向后寻找值为1的列;列号是邻接顶点的号码,检查这个顶点是否未访问过,如果是这样,那么这就是要访问的下一个顶点,如果该行没有顶点既等于1(邻接)且又是未访问的,那么与指定点相邻接的顶点就全部访问过了(后面会用算法实现)。
1.2 广度优先搜索
深度优先搜索要尽可能的远离起始点,而广度优先搜索则要尽可能的靠近起始点,它首先访问起始顶点的所有邻接点,然后再访问较远的区域,这种搜索不能用栈实现,而是用队列实现。
规则1:访问下一个未访问的邻接点(如果存在),这个顶点必须是当前顶点的邻接点,标记它,并把它插入到队列中。
规则2:如果已经没有未访问的邻接点而不能执行规则 1 时,那么从队列列头取出一个顶点(如果存在),并使其成为当前顶点。
规则3:如果因为队列为空而不能执行规则 2,则搜索结束。
对于上面的图,应用广度优先搜索:以A为起始点,首先访问所有与 A 相邻的顶点,并在访问的同时将其插入队列中,现在已经访问了 A,B,C,D和E。这时队列(从头到尾)包含 BCDE,已经没有未访问的且与顶点 A 邻接的顶点了,所以从队列中取出B,寻找与B邻接的顶点,这时找到F,所以把F插入到队列中。已经没有未访问且与B邻接的顶点了,所以从队列列头取出C,它没有未访问的邻接点。因此取出 D 并访问 G,D也没有未访问的邻接点了,所以取出E,现在队列中有 FG,在取出 F,访问 H,然后取出 G,访问 I,现在队列中有 HI,当取出他们时,发现没有其它为访问的顶点了,这时队列为空,搜索结束。
2、代码实现
实现深度优先搜索的栈 StackX.class:
package testOffer.graphpro; //实现深度优先搜索的栈 public class StackX { private final int SIZE = 20; private int[] st; private int top; public StackX(){ st = new int[SIZE]; top = -1; } public void push(int j){ st[++top] = j; } public int pop(){ return st[top--]; } public int peek(){ return st[top]; } public boolean isEmpty(){ return (top==-1); } }
实现广度优先搜索的队列Queue.class:
package testOffer.graphpro; //实现广度优先搜索的队列 public class QueueX { private final int SIZE = 20; private int[] queArray; private int front; private int rear; public QueueX(){ queArray = new int[SIZE]; front = 0; rear = -1; } public void insert(int j){ if (rear == SIZE-1) rear = -1; queArray[++rear] = j; } public int remove(){ int temp = queArray[front++]; if (front == SIZE){ front = 0; } return temp; } public boolean isEmpty(){ return (rear+1 == front || front+SIZE-1 == rear); } }
图代码 Graph.class:
package testOffer.graphpro; public class Graph { private final int MAX_VERTS = 20;//表示顶点的个数 private Vertex vertexList[];//用来存储顶点的数组 private int adjMat[][];//用邻接矩阵来存储边,数组元素表示没有边界,1表示有边界 private int nVerts;//顶点个数 private StackX theStack;//用栈实现深度优先搜索 private QueueX queue;//用队列实现广度优先搜索 /** * 顶点类 * */ class Vertex{ public char label; public boolean wasVisited; public Vertex(char label){ this.label = label; wasVisited = false; } } public Graph(){ vertexList = new Vertex[MAX_VERTS]; adjMat = new int[MAX_VERTS][MAX_VERTS]; nVerts = 0;//初始化顶点个数为0 //初始化邻接矩阵所有元素都为0,即所有顶点都没有边 for (int i=0;i<MAX_VERTS;i++){ for (int j=0;j<MAX_VERTS;j++){ adjMat[i][j] = 0; } } theStack = new StackX(); queue = new QueueX(); } //将顶点添加到数组中,是否访问标志置为wasVisited=false(未访问)、 public void addVertex(char lab){ vertexList[nVerts++] = new Vertex(lab); } //注意用邻接矩阵表示边,是对称的,两部分都要赋值 public void addEdge(int start,int end){ adjMat[start][end] = 1; adjMat[end][start] = 1; } //打印某个顶点表示的值 public void displayVertex(int v){ System.out.print(vertexList[v].label+" "); } /**深度优先搜索算法 * 1、用peek方法检查栈顶的顶点 * 2、用getAdjUnvisitedVertex方法找到当前栈顶邻接且未被访问的顶点 * 3、第二步返回值不等于-1则找到下一个未访问的邻接顶点,访问这个顶点,并入栈 * 如第二步返回值等于-1,则没有找到,出栈 * */ public void depthFirstSearch(){ //从第一个顶点开始访问 vertexList[0].wasVisited = true;//访问之后标记为true displayVertex(0); theStack.push(0); while (!theStack.isEmpty()){ //找到栈当前顶点邻接且未被访问的顶点 int v = getAdjUnvisitedVertex(theStack.peek()); if (v==-1){//如果当前顶点值为-1,则表示没有邻接且未被访问的顶点,那么出栈顶点 theStack.pop(); }else { //否则访问下一个邻接点 vertexList[v].wasVisited = true; displayVertex(v); theStack.push(v); } } //栈访问完毕,重置所有标记位为false for (int i=0;i<nVerts;i++){ vertexList[i].wasVisited = false; } } //找到与某一顶点邻接且未被访问的顶点 public int getAdjUnvisitedVertex(int v){ for (int i=0;i<nVerts;i++){ //v顶点与i顶点相邻且未被访问 if (adjMat[v][i]==1&&vertexList[i].wasVisited==false) return i; } return -1; } /**广度优先搜索 * 1、用remove方法检查栈顶的栈顶 * 2、试图找到这个顶点还未被访问的邻接点 * 3、如果没有找到,该顶点出列 * 4、如果找到这样的顶点,访问这个顶点,并把它放入队列中 * */ public void breadthFirstSearch(){ vertexList[0].wasVisited = true; displayVertex(0); queue.insert(0); int v2; while(!queue.isEmpty()){ int v1 = queue.remove(); while ((v2 =getAdjUnvisitedVertex(v1))!=-1){ vertexList[v2].wasVisited = true; displayVertex(v2); queue.insert(v2); } } //搜索完毕,初始化,便于下次搜索 for (int i=0;i<nVerts;i++){ vertexList[i].wasVisited = false; } } public static void main(String[] args) { Graph graph = new Graph(); graph.addVertex('A'); graph.addVertex('B'); graph.addVertex('C'); graph.addVertex('D'); graph.addVertex('E'); graph.addEdge(0,1);//AB graph.addEdge(1,2);//BC graph.addEdge(0,3);//AD graph.addEdge(3,4);//DE System.out.println("深度优先搜索算法:"); graph.depthFirstSearch(); System.out.println(); System.out.println("---------------"); System.out.println("广度优先搜索算法:"); graph.breadthFirstSearch(); } }