图的很多运用基础都是根据广(宽)度或是深度优先来实现的,这两个算法可以说是图最基础的算法,是很多图算法的原型。因此关于图的运用一定会使用广度优先和深度优先搜索。下面就说说这两个算法的基本思想和性质,并使用java实现,其中深度优先分别使用递归和栈来实现。
1、广度优先
思想:广度优先搜索从n顶点的图G一个源节点s出发,优先访问节点深度相同的节点。即是初始情况下访问s邻接节点,然后再以访问的节点为基础访问它们的邻接节点,直到所有s可以到达的节点都被访问完。整个过程可以看成是一个深度不断加深的访问过程,这个算法可以使用在有向图也可以使用在无向图。例如下图无向图:
若选择0为源节点s,同一个邻接链表以数字的小到大的顺序访问,各节点访问过程是:0;1,2;4,3;5;注意5是通过节点4访问的。
实现:广度优先搜索的实现是通过一个维护一个队列,并且通过一个color[n]的数组控制每个节点是否已经被访问来实现的。首先初始化color[n],并将color的每一个元素设为WHITE表示这个节点还未被访问;然后初始化一个空队列并将源节点s加入队列尾,然后将源节点的color属性改为GREY,表示已经发现该节点且通过该节点可能发现未访问的节点;然后扫描队列头元素u的邻接链表,若扫描到的元素v的color[v]属性为WHITE,则将color[v]设为GREY,并将该元素加入队列尾,当扫描完u的邻接链表,将color[u]设为BLANK表示该节点访问结束并且不会通过这个节点访问到未访问的节点,同时将u从队列头删除。取出队列头的元素,重复以上扫描邻接链表的动作,直到队列为空。下面是广度优先搜索的伪代码:为了便于理解和说明性质增加了d属性表示该节点发现时的时间,pi属性表示该节点的前驱结点。
BFS(G,s) for each vertex u属于G,V-{s} u.color=WHITE u.d=-1 //表示无穷大 u.pi=NIL //表示没有前驱结点 s.color=GREY s.d=0 s.pi=NIL Q //初始化空队列 ENQUEUE(Q,s) //将s加入队列 while(Q!=null) u=DEQUEUE(Q) //从队列获得并删除头元素 for each v属于u的邻接链表 if v.color=WHITE v.color=GREY v.d=u.d+1 v.pi=u ENQUEUE(Q,v) //将v加入队列尾 u.color=BLANK //u已经完全访问
性质:
通过广度优先搜索从源节点s可达的节点都被访问一次。
队列中的元素是已经访问过的节点和未访问节点的界限,即灰色节点将图分成两部分,一部分为白色,一部分为黑色。
广度优先搜索向前搜索路径构成一棵以s为根的广度优先树。
注意:在实际应用并未区分GREY和BLANK的区别,因此在代码实现中可以将color声明为boolean或int类型都可以,只要能区别某一个节点是否已经被访问。d属性和pi属性仅仅为了展示算法的性质和算法的运行过程,在实际应用中大多情况中是不需要的。
2、深度优先
思想:从n顶点的图G一个源节点s出发,优先访问深度较大的节点。即发现并访问一个节点后立刻访问该节点的邻接节点,而不是兄弟节点,当深度不能再加深则沿着原路返回并再次扫描返回的节点的邻接节点,直到s可达的所有节点都被访问一次。例如以下无向图:
若选择0为源节点s,同一个邻接链表以数字的小到大的顺序访问,各节点访问过程是:0,1,2,3,4,5。
实现:深度优先搜索优先搜索深度更大的节点,当没有节点访问节点就原路返回继续扫描该节点。整个过程就是一个递归调用和返回的过程,因此深度优先搜索的实现可以使用递归来实现。深度优先也是每一个节点使用一个color属性标记该节点是否已经被访问。首先初始化color[n],并将color的每一个元素设为WHITE表示这个节点还未被访问;然后将源节点s的color属性改为GREY,表示已经发现该节点且通过该节点可能发现未访问的节点;然后扫描s的邻接链表,若扫描到的节点v的color[v]属性为WHITE,则将color[v]设为GREY,然后调用自己扫描当前节点v的邻接链表,当扫描v的邻接链表没有新发现的节点,则将color[u]设为BLANK表示该节点访问结束并且不会通过这个节点访问到未访问的节点,函数调用会自动返回继续扫描动作。下面是深度优先搜索的伪代码:为了便于理解和说明性质增加了d属性表示该节点发现时的时间,pi属性表示该节点的前驱结点。
DFS(G,u) for each vertex u属于G.V u.color=WHITE u.pi=NIL time=0; DFS_VISIT(G,s) //调用递归函数 DFS_VISIT(G,s) time=time+1 u.d=time u.color=GREY for each v属于u的邻接链表 if v.color=WHITE v.pi=u DFS_VISIT(G,v) //递归调用 u.color=BLANK time=time+1 u.f=time //节点u的结束时间 性质:
深度优先搜索向前搜索路径构成一棵以s为根的深度优先树。
在深度优先树中节点v是节点u的后代当且仅当存在一条从节点u到节点v的全部由白色节点所构成的路径。
两个节点的发现时间和结束时间要么互相嵌套,要么互不相交,不存在相交的情况。
注意:注意:在实际应用并未区分GREY和BLANK的区别,因此在代码实现中可以将color声明为boolean或int类型都可以,只要能区别某一个节点是否已经被访问。d属性和pi属性仅仅为了展示算法的性质和算法的运行过程,在实际应用中大多情况中是不需要的。
3、Java实现
以下使用java实现的LinkList类和Node类以及图的生成及存储在 数据结构 图的存储邻接矩阵与邻接链表描述,如需使用可以点击浏览查看。在LinkList类中添加了一些方法,可以使用这些方法模拟出栈和队列。插入在链表尾进行,删除在链表头进行就形成了一个队列;插入在链表头进行,删除也在链表头进行,就形成了栈。下面广度优先搜索使用的队列和栈实现的深度优先搜索使用的栈就是这样的。
宽度优先:
package Algorithm; /* * @vamesary * 4/2017 * 图的宽度优先搜索 */ import Structure.Graph; import Structure.LinkList; import Structure.Node; public class BFS { private LinkList [] list;//邻接链表 private int vertex=0;//图的顶点个数 private int [] color;//是否已经经过 private int [] pi;//前驱结点 private int [] d;//发现时间 //自己随机生成图 public BFS(int theVertex,int theEdge){ Graph graph=new Graph(theVertex,theEdge,false); list=graph.getList(); vertex=graph.getVertex(); System.out.println(graph.toString());; } //使用现有图构造函数 public BFS(Graph graph){ list=graph.getList(); vertex=graph.getVertex(); System.out.println(graph.toString());; } public void BFS(int s){ color=new int[vertex];//颜色,代表该节点是否已被发现 System.out.println("完成初始化color"); pi=new int [vertex];//节点u的前驱结点 System.out.println("完成初始化pi前驱结点"); d=new int [vertex];//源节点到u节点的距离 System.out.println("完成初始化d发现时间"); for(int i=0;i<vertex;i++){ color[i]=0;//0代表White;1代表Gray;2代表Blank d[i]=-1;//初值距离无穷大 pi[i]=-1;//没有前驱结点 } color[s]=1; System.out.println("设置源节点的s.color=1,更改颜色为灰色"); d[s]=0; System.out.println("设置源节点的s.d=0,更改发现时间为0"); pi[s]=-1; System.out.println("设置源节点的s.pi=0,更改前驱结点为没有"); LinkList queue=new LinkList();//宽度优先的队列 System.out.println("完成初始化队列queue"); queue.insertOnEnd(s, 0); System.out.println("将源节点 "+s+" 插入队列"); Node u=queue.removeFromStart(); System.out.print("从队列中获得节点 "+u.id); while(u!=null){ Node v=list[u.id].getRoot(); System.out.print(", 扫描 "+u.id+" 的邻接链表 "); while(v!=null){ if(color[v.id]==0){ System.out.print(", 发现节点 "+v.id); color[v.id]=1; System.out.print(", 更改 "+v.id+"的颜色为灰色 "); d[v.id]=d[u.id]+1; System.out.print(", "+v.id+"的发现时间"+d[v.id]); pi[v.id]=u.id; System.out.print(", "+v.id+"的前驱结点"+pi[v.id]); queue.insertOnEnd(v.id, 0); System.out.print(", 将 "+v.id+" 插入队列 ; "); } v=v.next; } color[u.id]=2; System.out.print(", 更改 "+u.id+"的颜色为黑色 "); System.out.println(); u=queue.removeFromStart(); if(u!=null) System.out.print("从队列中获得节点 "+u.id); } } }
测试:
package Test; /* * @vamesary * 4/2017 * 测试图的宽度优先搜索 */ import Algorithm.BFS; import Structure.Graph; public class BFSTest { public static void main(String[] args) { int [][]array=new int[9][9]; for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ array[i][j]=-1; } } array[0][1]=4;array[0][2]=8; array[1][0]=4;array[1][2]=11;array[1][3]=8; array[2][0]=8;array[2][1]=11;array[2][4]=7;array[2][5]=1; array[3][1]=8;array[3][4]=2;array[3][6]=7;array[3][7]=4; array[4][2]=7;array[4][3]=2;array[4][5]=6; array[5][2]=1;array[5][4]=6;array[5][7]=2; array[6][3]=7;array[6][7]=14;array[6][8]=9; array[7][3]=4;array[7][5]=2;array[7][6]=14;array[7][8]=10; array[8][6]=9;array[8][7]=10; Graph graph =new Graph(array,9); BFS bfs=new BFS(graph); bfs.BFS(0); } }
输出:
顶点 0链表的内容为:1 2 顶点 1链表的内容为:0 2 3 顶点 2链表的内容为:0 1 4 5 顶点 3链表的内容为:1 4 6 7 顶点 4链表的内容为:2 3 5 顶点 5链表的内容为:2 4 7 顶点 6链表的内容为:3 7 8 顶点 7链表的内容为:3 5 6 8 顶点 8链表的内容为:6 7 完成初始化color 完成初始化pi前驱结点 完成初始化d发现时间 设置源节点的s.color=1,更改颜色为灰色 设置源节点的s.d=0,更改发现时间为0 设置源节点的s.pi=0,更改前驱结点为没有 完成初始化队列queue 将源节点 0 插入队列 从队列中获得节点 0, 扫描 0 的邻接链表 , 发现节点 1, 更改 1的颜色为灰色 , 1的发现时间1, 1的前驱结点0, 将 1 插入队列 ; , 发现节点 2, 更改 2的颜色为灰色 , 2的发现时间1, 2的前驱结点0, 将 2 插入队列 ; , 更改 0的颜色为黑色 从队列中获得节点 1, 扫描 1 的邻接链表 , 发现节点 3, 更改 3的颜色为灰色 , 3的发现时间2, 3的前驱结点1, 将 3 插入队列 ; , 更改 1的颜色为黑色 从队列中获得节点 2, 扫描 2 的邻接链表 , 发现节点 4, 更改 4的颜色为灰色 , 4的发现时间2, 4的前驱结点2, 将 4 插入队列 ; , 发现节点 5, 更改 5的颜色为灰色 , 5的发现时间2, 5的前驱结点2, 将 5 插入队列 ; , 更改 2的颜色为黑色 从队列中获得节点 3, 扫描 3 的邻接链表 , 发现节点 6, 更改 6的颜色为灰色 , 6的发现时间3, 6的前驱结点3, 将 6 插入队列 ; , 发现节点 7, 更改 7的颜色为灰色 , 7的发现时间3, 7的前驱结点3, 将 7 插入队列 ; , 更改 3的颜色为黑色 从队列中获得节点 4, 扫描 4 的邻接链表 , 更改 4的颜色为黑色 从队列中获得节点 5, 扫描 5 的邻接链表 , 更改 5的颜色为黑色 从队列中获得节点 6, 扫描 6 的邻接链表 , 发现节点 8, 更改 8的颜色为灰色 , 8的发现时间4, 8的前驱结点6, 将 8 插入队列 ; , 更改 6的颜色为黑色 从队列中获得节点 7, 扫描 7 的邻接链表 , 更改 7的颜色为黑色 从队列中获得节点 8, 扫描 8 的邻接链表 , 更改 8的颜色为黑色
深度优先:包含递归和栈实现
package Algorithm; /* * @vamesary * 4/2017 * 深度优先搜索,包括递归和栈实现 */ import Structure.Graph; import Structure.LinkList; import Structure.Node; public class DFS { private LinkList[] list;// 邻接链表 private int vertex = 0;// 图的顶点个数 private int[] color;// 是否已经经过 private int[] pi;// 前驱结点 private int[] d;// 发现时间 private int[] f;// 结束时间 private int time = 0;// 发现时间 // 自己随机生成图 public DFS(int theVertex, int theEdge) { Graph graph = new Graph(theVertex, theEdge, false); list = graph.getList(); vertex = graph.getVertex(); System.out.println(graph.toString()); } // 使用现有图构造函数 public DFS(Graph graph) { list = graph.getList(); vertex = graph.getVertex(); System.out.println(graph.toString()); ; } public void DFS(int s) { color = new int[vertex];// 颜色,代表该节点是否已被发现 System.out.println("完成初始化color"); pi = new int[vertex];// 节点u的前驱结点 System.out.println("完成初始化pi前驱结点"); d = new int[vertex];// 源节点到u节点的距离 System.out.println("完成初始化d发现时间"); f = new int[vertex]; System.out.println("完成初始化f结束时间"); for (int i = 0; i < vertex; i++) { color[i] = 0;// 0代表White;1代表Gray;2代表Blank pi[i] = -1;// 没有前驱结点 } time = 0; pi[s] = -1; System.out.println("设置源节点的s.pi=0,更改前驱结点为没有"); DFS_VISIT(s); } //递归实现深度优先搜索的递归函数 public void DFS_VISIT(int id) { System.out.println(); time = time + 1; d[id] = time; System.out.print("" + id + "的发现时间" + d[id]); color[id] = 1; System.out.print(", 更改 " + id + "的颜色为灰色 "); System.out.print(", 扫描 " + id + " 的邻接链表 "); Node v = list[id].getRoot(); while (v != null) { if (color[v.id] == 0) { System.out.print(", 发现节点 " + v.id); pi[v.id] = id; System.out.print(", " + v.id + "的前驱结点" + pi[v.id]); DFS_VISIT(v.id); } v = v.next; } time = time + 1; f[id] = time; System.out.print("" + id + "的结束时间" + f[id]); color[id] = 2; System.out.print(", 更改 " + id + "的颜色为黑色 "); System.out.println(); } //使用栈实现深度优先搜索 public void DFSSTACK(int s) { color = new int[vertex];// 颜色,代表该节点是否已被发现 System.out.println("完成初始化color"); pi = new int[vertex];// 节点u的前驱结点 System.out.println("完成初始化pi前驱结点"); d = new int[vertex];// 源节点到u节点的距离 System.out.println("完成初始化d发现时间"); f = new int[vertex]; System.out.println("完成初始化f结束时间"); for (int i = 0; i < vertex; i++) { color[i] = 0;// 0代表White;1代表Gray;2代表Blank d[i] = -1;// 初值距离无穷大 pi[i] = -1;// 没有前驱结点 } color[s] = 1; System.out.println("设置源节点的s.color=1,更改颜色为灰色"); d[s] = 1; System.out.println("设置源节点的s.d=0,更改发现时间为0"); pi[s] = -1; System.out.println("设置源节点的s.pi=0,更改前驱结点为没有"); time = 1; LinkList stack = new LinkList(); stack.insertOnStart(s, 0); Node u = stack.getIndex(0); while (u != null) { System.out.println(); System.out.print("扫描 " + u.id + " 的邻接链表 "); Node v = list[u.id].getRoot(); boolean end = true; while (v != null) { if (color[v.id] == 0) { System.out.print(", 发现节点 " + v.id); time = time + 1; d[v.id] = time; System.out.print("" + v.id + "的发现时间" + d[v.id]); color[v.id] = 1; System.out.print(", 更改 " + v.id + "的颜色为灰色 "); pi[v.id] = u.id; System.out.print(", " + v.id + "的前驱结点" + pi[v.id]); stack.insertOnStart(v.id, 0); end = false; break; } v = v.next; } int lastid = u.id; if (end) { time = time + 1; f[u.id] = time; System.out.print("" + u.id + "的结束时间" + f[u.id]); color[lastid] = 2; System.out.print(", 更改 " + lastid + "的颜色为黑色 "); stack.removeFromStart(); } u = stack.getIndex(0); } } } 测试:
package Test; /* * @vamesary * 4/2017 * 测试图的深度优先搜索,包括递归实现和栈实现 */ import Algorithm.DFS; import Structure.Graph; public class DFSTest { public static void main(String[] args) { int [][]array=new int[9][9]; for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ array[i][j]=-1; } } array[0][1]=4;array[0][2]=8; array[1][0]=4;array[1][2]=11;array[1][3]=8; array[2][0]=8;array[2][1]=11;array[2][4]=7;array[2][5]=1; array[3][1]=8;array[3][4]=2;array[3][6]=7;array[3][7]=4; array[4][2]=7;array[4][3]=2;array[4][5]=6; array[5][2]=1;array[5][4]=6;array[5][7]=2; array[6][3]=7;array[6][7]=14;array[6][8]=9; array[7][3]=4;array[7][5]=2;array[7][6]=14;array[7][8]=10; array[8][6]=9;array[8][7]=10; Graph graph =new Graph(array,9); DFS bfs=new DFS(graph); //递归实现的深度优先搜索,以0为源节点 bfs.DFS(0); System.out.println("===================分隔符========================="); //栈实现的深度优先搜索,以0为源节点 bfs.DFSSTACK(0); } }
输出:
顶点 0链表的内容为:1 2 顶点 1链表的内容为:0 2 3 顶点 2链表的内容为:0 1 4 5 顶点 3链表的内容为:1 4 6 7 顶点 4链表的内容为:2 3 5 顶点 5链表的内容为:2 4 7 顶点 6链表的内容为:3 7 8 顶点 7链表的内容为:3 5 6 8 顶点 8链表的内容为:6 7 完成初始化color 完成初始化pi前驱结点 完成初始化d发现时间 完成初始化f结束时间 设置源节点的s.pi=0,更改前驱结点为没有 0的发现时间1, 更改 0的颜色为灰色 , 扫描 0 的邻接链表 , 发现节点 1, 1的前驱结点0 1的发现时间2, 更改 1的颜色为灰色 , 扫描 1 的邻接链表 , 发现节点 2, 2的前驱结点1 2的发现时间3, 更改 2的颜色为灰色 , 扫描 2 的邻接链表 , 发现节点 4, 4的前驱结点2 4的发现时间4, 更改 4的颜色为灰色 , 扫描 4 的邻接链表 , 发现节点 3, 3的前驱结点4 3的发现时间5, 更改 3的颜色为灰色 , 扫描 3 的邻接链表 , 发现节点 6, 6的前驱结点3 6的发现时间6, 更改 6的颜色为灰色 , 扫描 6 的邻接链表 , 发现节点 7, 7的前驱结点6 7的发现时间7, 更改 7的颜色为灰色 , 扫描 7 的邻接链表 , 发现节点 5, 5的前驱结点7 5的发现时间8, 更改 5的颜色为灰色 , 扫描 5 的邻接链表 5的结束时间9, 更改 5的颜色为黑色 , 发现节点 8, 8的前驱结点7 8的发现时间10, 更改 8的颜色为灰色 , 扫描 8 的邻接链表 8的结束时间11, 更改 8的颜色为黑色 7的结束时间12, 更改 7的颜色为黑色 6的结束时间13, 更改 6的颜色为黑色 3的结束时间14, 更改 3的颜色为黑色 4的结束时间15, 更改 4的颜色为黑色 2的结束时间16, 更改 2的颜色为黑色 1的结束时间17, 更改 1的颜色为黑色 0的结束时间18, 更改 0的颜色为黑色 ===================分隔符========================= 完成初始化color 完成初始化pi前驱结点 完成初始化d发现时间 完成初始化f结束时间 设置源节点的s.color=1,更改颜色为灰色 设置源节点的s.d=0,更改发现时间为0 设置源节点的s.pi=0,更改前驱结点为没有 扫描 0 的邻接链表 , 发现节点 11的发现时间2, 更改 1的颜色为灰色 , 1的前驱结点0 扫描 1 的邻接链表 , 发现节点 22的发现时间3, 更改 2的颜色为灰色 , 2的前驱结点1 扫描 2 的邻接链表 , 发现节点 44的发现时间4, 更改 4的颜色为灰色 , 4的前驱结点2 扫描 4 的邻接链表 , 发现节点 33的发现时间5, 更改 3的颜色为灰色 , 3的前驱结点4 扫描 3 的邻接链表 , 发现节点 66的发现时间6, 更改 6的颜色为灰色 , 6的前驱结点3 扫描 6 的邻接链表 , 发现节点 77的发现时间7, 更改 7的颜色为灰色 , 7的前驱结点6 扫描 7 的邻接链表 , 发现节点 55的发现时间8, 更改 5的颜色为灰色 , 5的前驱结点7 扫描 5 的邻接链表 5的结束时间9, 更改 5的颜色为黑色 扫描 7 的邻接链表 , 发现节点 88的发现时间10, 更改 8的颜色为灰色 , 8的前驱结点7 扫描 8 的邻接链表 8的结束时间11, 更改 8的颜色为黑色 扫描 7 的邻接链表 7的结束时间12, 更改 7的颜色为黑色 扫描 6 的邻接链表 6的结束时间13, 更改 6的颜色为黑色 扫描 3 的邻接链表 3的结束时间14, 更改 3的颜色为黑色 扫描 4 的邻接链表 4的结束时间15, 更改 4的颜色为黑色 扫描 2 的邻接链表 2的结束时间16, 更改 2的颜色为黑色 扫描 1 的邻接链表 1的结束时间17, 更改 1的颜色为黑色 扫描 0 的邻接链表 0的结束时间18, 更改 0的颜色为黑色