一.DFS概述
1.Depth First Search 是遍历图的常用方法之一,它类似于与数的先根遍历,是数的先根遍历的推广。
DFS的基本实现思想是从图中的某个顶点v出发,访问此顶点,然后依次从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v路径相通的顶点都被访问到。若此时图中尚有顶点未被访问(说明该图不是连通图),则另选图中一个未曾被访问的顶点作为起始点,重复上诉过程,直至图中所有的顶点都被访问到为止。
eg:如下图DFS的遍历的详细过程,选择起始点为v0。下图中给出了其中的一种遍历方式
2.实现DFS的关键要点
(1)结合上文,在实现深度优先搜索时,我们要选择一个未被访问的点作为起始点。那么我们如何知道改点是否被访问过?我们可以引入一个数据结构来标记结点是否被访问,比如一个创建一个boolean数组,对应于图上的各个结点,false表示未被访问,true表示已被访问。
3.java代码实现上图的深度优先搜索
import java.util.Scanner;
import java.util.Stack;
/*
深度优先搜索遍历图(无向图)
8 9
0 1
0 2
1 3
1 4
2 5
2 6
5 6
3 7
4 7
*/
public class Basic_DFS {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int numberOfNode = sc.nextInt(); // 输入顶点的个数
int numberOfEdge = sc.nextInt(); // 输入边的条数
//使用领接表存储该图
Vode[] date = new Vode[numberOfNode]; // 创建一个顶点的数组
for (int i = 0; i < date.length; i++) {
date[i] = new Vode(i, null);
}
for (int i = 0; i < numberOfEdge; i++) { // 录入数据
int borderOne = sc.nextInt();
int borderTwo = sc.nextInt();
ArcNode one = new ArcNode(borderOne, null);
ArcNode two = new ArcNode(borderTwo, null);
if (date[borderOne].fistArc == null) {
date[borderOne].fistArc = two;
} else {
two.next = date[borderOne].fistArc;
date[borderOne].fistArc = two;
}
if (date[borderTwo].fistArc == null) {
date[borderTwo].fistArc = one;
} else {
one.next = date[borderTwo].fistArc;
date[borderTwo].fistArc = one;
}
}
sc.close();
// 创建一个boolean类型的数组
boolean[] bool = new boolean[numberOfNode];
for (int i = 0; i < bool.length; i++) {
bool[i] = false;
}
display(date);
System.out.println();
dfs(date, 0, bool);
}
// 打印图,检查图形的存储是否正确
public static void display(Vode[] date) {
for (int i = 0; i < date.length; i++) {
System.out.print(i + " ");
ArcNode temp = date[i].fistArc;
while (temp != null) {
System.out.print(temp.nextVode + " ");
temp = temp.next;
}
System.out.println();
}
}
// 深度优先搜索,非递归写法
public static void dfs(Vode[] date, int start, boolean[] bool) {
Stack<Vode> stack = new Stack<>();
stack.push(date[start]); // 将起始点压入栈中
bool[start] = true; // 修改访问数组的标记
System.out.print(start + " ");
while (!stack.isEmpty()) {
Vode temp = stack.peek(); // 查看栈顶元素
ArcNode arc = temp.fistArc;
while (arc != null) { //在这里也可以使用for循环
if (bool[arc.nextVode] == false) {
bool[arc.nextVode] = true;
stack.push(date[arc.nextVode]);
System.out.print(arc.nextVode + " ");
temp = stack.peek();
arc = temp.fistArc;
} else {
arc = arc.next;
}
}
stack.pop();
}
}
}
class Vode {
int name; // 该结点的编号,加入此成员变量也是为了防止空指针异常
ArcNode fistArc; // 该顶点指向的第一条弧
public Vode() {
super();
}
public Vode(int name, ArcNode fistArc) {
super();
this.name = name;
this.fistArc = fistArc;
}
}
class ArcNode {
int nextVode; // 该条弧的另一个结点
ArcNode next; // 下一条弧
public ArcNode() {
super();
}
public ArcNode(int nextVode, ArcNode next) {
super();
this.nextVode = nextVode;
this.next = next;
}
}
4.结果展示以及代码分析
(1)非递归DFS代码分析
对于非递归进行DFS深度优先搜索时,我们往往需要配合栈的使用。我们从某个顶点出发,首先将该顶点入栈,然后访问与其相邻的某一个未被访问的邻接点,然后将该点入栈并将其作为起始点继续访问与该点相邻的未被访问的领接点,直到当以某个顶点为起点出发时,其所有的领接点都被访问过了。这时候我们就需要回溯到当前结点的上一结点(在数据结构上就可以表示为出栈),判断其是否还有未被访问的结点,直到图中所有的结点都被访问过了(在数据结构上表现为栈为空)。
(2)递归写法代码分析
同样是选取某个点作为起始点,访问该点未被访问过的领接点,然后以该领接点作为起始点,递归访问以该点作为起始点的未被访问的领接点。直到当以某一个点作为起始点时,其所有的领接点都已被访问,这时就会递归返回。
//DFS的递归写法
public static void dfsD(Vode[] date, int start, boolean[] bool) {
if (bool[start] == false) {
System.out.print(start + " "); // 输出每次的起始点
}
bool[start] = true; //将起始点标记为已访问
ArcNode temp = date[start].fistArc;
while (temp != null) {
if (bool[temp.nextVode] == false) {
dfsD(date, temp.nextVode, bool); //以该点的领接点作为起始点,继续DFS
} else {
temp = temp.next;
}
}
}
二.全排列问题中的DFS
全排列问题:输入一段没有重复的字符串,请你对其进行全排列。
eg:A B C 其全排列有A B C ,A C B,B A C,B C A,C A B,C B A
对于上述问题,有如下三种解决方案:
1.暴力破解
该方法的时间复杂度与输入字符串的长度有关。假设输入为ABC,我们按照组合数学的思想,当对该串进行全排列时,我们开始首先有三个空位,第一个空位有3种选择,第二个空位不能与第一空位相同有2种选择,第三个空位不能与第一个空位和第二个空位相同,有一种选择。对应代码就是三重for循环和条件判断的逻辑表达式。
需要特别注意的是,全排列是有顺序的,只有当地i个空位排好了,才可以排第i+1个空位。
import java.util.Arrays;
import java.util.Scanner;
//全排列的暴力破解
public class Vol {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
sc.close();
char[] c = new char[str.length()]; //该数组表完成排列所需的空位
for(int i=0;i<str.length();i++) {
c[0] = str.charAt(i);
for(int j=0;j<str.length();j++) {
if(i != j) { //注意只有当第二个空位排好了才能进入第三个空位
c[1] = str.charAt(j);
for(int k=0;k<str.length();k++) {
if(k!=i && k!=j) {
c[2] = str.charAt(k);
System.out.println(Arrays.toString(c));
}
}
}
}
}
}
}
2.递归
对于此类全排列,我们不难发现,其可以分为以A开头的全排列,以B开头的全排列和以C开头的全列排列。
import java.util.Arrays;
public class Recursion {
public static void main(String[] args) {
int[] date = new int[] {1,2,3};
perm(date,0,date.length-1);
}
//进行全排列的递归函数,p表示需要全排列的数据的第一个元素,q表示最后一个元素
public static void perm(int[] date,int p,int q) {
//递归出口
if(p==q) {
System.out.println(Arrays.toString(date));
}else {
for(int i=p;i<=q;i++) {
swap(date,i,p);
perm(date,p+1,q);
swap(date,i,p); //由于每个节点都是平等的,需要回溯还原
}
}
}
//交换函数
public static void swap(int[] date,int a,int b) {
int temp = date[a];
date[a] = date[b];
date[b] = temp;
}
}
3.DFS结合递归
两个要点:DFS的截止条件即递归的截止条件,候选节点的选择。将全排列转化为如下的图,然后进行深度优先搜索,即可得到正确答案。
import java.util.Stack;
public class DFS {
public static void main(String[] args) {
char[] date = new char[] {'A','B','C'};
boolean[] bool = new boolean[] {false,false,false};
Stack<Character> stack = new Stack<>();
dfs(date,bool,stack);
}
public static void dfs(char[] date,boolean[] bool,Stack<Character> stack) {
//dfs的截止条件
if(stack.size() == date.length) { //当栈的深度与要求全排列的字符的个数相同时,这里为3(3 = date.length),递归结束
System.out.println(stack.toString());
return;
}
//遍历候选结点
for(int i=0;i<date.length;i++) {
char c = date[i];
if(bool[i] == false) { //说明该结点没有被访问过
bool[i] = true;
stack.push(c);
dfs(date,bool,stack);
bool[i] = false;
stack.pop();
}
}
}
}