Annuaire d'articles
avant-propos
Depth First Search ( DFS ) est un algorithme permettant de parcourir ou de rechercher une structure de données arborescente ou graphique. L'algorithme part du nœud racine (dans le cas d'un graphe, choisit un nœud arbitraire comme nœud racine) et explore autant que possible le long de chaque branche avant de revenir en arrière. Une mémoire supplémentaire, généralement une pile, est nécessaire pour garder une trace des nœuds découverts jusqu'à présent le long d'une branche donnée, ce qui facilite le retour en arrière.
Les caractéristiques de l'algorithme de recherche en profondeur d'abord :
- En partant d'un nœud de départ, continuez à visiter les nœuds adjacents le long d'un chemin jusqu'à ce qu'il n'y ait plus de nœuds adjacents non visités, puis revenez au nœud précédent et continuez à visiter d'autres nœuds adjacents.
- Utilisez la pile ou la récursivité pour y parvenir.
- Une table triée topologiquement correspondante du graphe cible peut être générée.
Avantages de l'algorithme de recherche en profondeur :
- Simple et facile à mettre en œuvre.
- Prend moins de place.
- Un chemin peut être trouvé d'un nœud de départ à n'importe quel nœud accessible.
Inconvénients de l'algorithme de recherche en profondeur :
- Il n'est pas nécessairement possible de trouver le chemin le plus court ou la solution optimale.
- Peut tomber dans une boucle infinie ou une récursivité infinie.
Scénarios d'application de l'algorithme de recherche en profondeur :
- Tri topologique (horaire des cours, avancement du projet, dépendances)
- Jeux de simulation (par exemple, échecs, labyrinthes, etc.)
- Détection de connectivité (telle que juger s'il y a un cycle dans le graphique, etc.)
- Problème de voyageur de commerce (tel que trouver le chemin le plus court, etc.)
- Correspondance entre parenthèses (par exemple, vérifier si les parenthèses de l'expression correspondent, etc.)
- Traversée de structures de données telles que des arbres binaires, des arbres de segments de ligne, des arbres rouge-noir et des graphiques
Dans cet article, nous présenterons les principes de base et la mise en œuvre de l'algorithme de recherche en profondeur d'abord, et démontrerons son application à travers quelques exemples.
1. Réalisation
1.1 Implémentation récursive
À partir d'un nœud de départ, visitez les nœuds adjacents le long d'un chemin jusqu'à ce qu'il n'y ait plus de nœuds adjacents non visités, puis revenez au nœud précédent et continuez à visiter d'autres nœuds adjacents jusqu'à ce que tous les nœuds aient été visités.
L'exemple de code est le suivant :
public void dfs(int start) {
visited[start] = true; //将起始节点标记为已访问
for (int i = 0; i < n; i++) {
//遍历邻接矩阵中start所在行
if (matrix[start][i] == 1 && !visited[i]) {
//如果存在边且未被访问过
dfs(i); //递归调用dfs方法,以该节点为新起点进行遍历
}
}
}
1.2 Implémentation de la pile
Commencez avec un nœud de départ, poussez-le sur la pile et répétez les étapes suivantes : faites apparaître l'élément supérieur de la pile et marquez-le comme visité ; poussez tous les voisins non visités de cet élément sur la pile. jusqu'à ce que la pile soit vide
L'exemple de code est le suivant :
public void dfs(int start) {
Stack<Integer> stack = new Stack<>(); //创建栈对象
stack.push(start); //起始节点入栈
Set<Integer> visited = new HashSet<>(); //创建集合对象
visited.add(start); //起始节点加入集合
while (!stack.isEmpty()) {
//只要栈不为空就继续循环
int cur = stack.peek(); //获取栈顶元素但不出栈
boolean flag = false; //设置标志位,表示是否有未访问过的邻接节点
for (int i = 0; i < n; i++) {
//遍历邻接矩阵中cur所在行
if (matrix[cur][i] == 1 && !visited.contains(i)) {
//如果存在边且未被访问过
stack.push(i); //将该节点入栈
visited.add(i); //将该节点加入集合
System.out.print(i + " "); //打印该节点
flag = true; //修改标志位为true,表示有未访问过的邻接节点
break; //跳出循环,以该节点为新起点进行遍历
}
}
if (!flag) {
//如果没有未访问过的邻接节点,则说明已经到达最深处,需要回溯上一层继续遍历其他分支路径。
stack.pop(); //将栈顶元素出栈
}
}
}
Ce qui suit est une animation de recherche dfs
1.3 La différence entre les deux
- L'implémentation récursive consiste à utiliser la pile système pour enregistrer l'état du nœud actuel. Lorsqu'une impasse est rencontrée, il reviendra automatiquement au nœud précédent pour continuer la recherche. L'implémentation de la pile utilise une pile personnalisée pour enregistrer l'état du nœud actuel. Lorsqu'une impasse est rencontrée, l'élément supérieur de la pile est renvoyé manuellement au nœud précédent pour continuer la recherche.
- L'implémentation récursive est relativement concise et facile à comprendre, mais l'efficacité n'est pas élevée et elle peut provoquer un débordement de pile pour les graphes à grande échelle. L'implémentation de la pile est plus compliquée, mais elle est plus efficace et peut éviter le problème de débordement de la pile.
- L'implémentation récursive et l'implémentation de la pile ont toutes deux besoin d'un tableau d'indicateurs pour enregistrer les nœuds qui ont été visités afin d'éviter des visites ou des boucles répétées.
2. Combat réel avec LeetCode
2.1 Parcours préordonné de l'arbre binaire
94. Traversée pré-commande d'arbres binaires
Étant donné le nœud racine de votre arbre binaire , renvoyez un parcours de préordre
root
de ses valeurs de nœud .
List<Integer> ans = new ArrayList(); //定义一个整数列表,用来存储前序遍历的结果
public List<Integer> preorderTraversal(TreeNode root) {
if (root != null) {
//如果当前节点不为空,才进行以下操作
ans.add(root.val); //把当前节点的值加入列表
preorderTraversal(root.left); //递归地对左子树进行前序遍历
preorderTraversal(root.right); //递归地对右子树进行前序遍历
}
return ans; //返回前序遍历的结果
}
2.2 Nombre d'îles
Étant donné une grille bidimensionnelle composée de
'1'
(terre) et (eau), veuillez compter le nombre d'îles dans la grille.'0'
Les îles sont toujours entourées d'eau, et chaque île ne peut être formée qu'en reliant horizontalement et/ou verticalement des terres adjacentes.
De plus, vous pouvez supposer que le maillage est entouré d'eau sur les quatre côtés.
// 定义一个二维数组pos,表示四个方向
int[][] pos = {
{
0, 1 }, {
1, 0 }, {
0, -1 }, {
-1, 0 } };
// 定义一个变量ans,表示岛屿的数量
int ans = 0;
// 定义一个方法numIslands,接受一个二维字符数组grid作为参数,返回岛屿的数量
public int numIslands(char[][] grid) {
// 获取grid的行数和列数
int m = grid.length, n = grid[0].length;
// 定义一个二维布尔数组visited,表示每个位置是否被访问过
boolean[][] visited = new boolean[m][n];
// 遍历grid中的每个位置
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 如果当前位置是'1'且没有被访问过,则从该位置开始深度优先搜索,并将ans加一
if (grid[i][j] == '1' && !visited[i][j]) {
dfs(grid, visited, i, j);
ans++;
}
}
}
// 返回ans作为结果
return ans;
}
// 定义一个方法dfs,接受一个二维字符数组grid、一个二维布尔数组visited、两个整数i和j作为参数,无返回值
public void dfs(char[][] grid, boolean[][] visited, int i, int j) {
// 如果i或j越界或者当前位置是'0'或者已经被访问过,则直接返回
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0'
|| visited[i][j]) {
return;
}
// 将当前位置标记为已访问
visited[i][j] = true;
// 遍历四个方向,并递归调用dfs方法
for (int[] p : pos) {
dfs(grid, visited, i + p[0], j + p[1]);
}
}
2.3 Compter le nombre d'îlots fermés
1254. Compter le nombre d'îles fermées
La matrice bidimensionnelle se compose
grid
de0
(terre) et1
(eau).0
Une île est un groupe composé des 4 plus grandes directions reliées , et une île fermée est une完全
île entourée de 1 (gauche, haut, droite, bas).Veuillez retourner le nombre d' îles fermées .
// 定义一个二维数组pos来存储上下左右四个方向的偏移量
int[][] pos = {
{
0, 1 }, {
1, 0 }, {
0, -1 }, {
-1, 0 } };
// 定义一个变量ans来记录封闭岛屿的个数
int ans = 0;
public int closedIsland(int[][] grid) {
// 判断矩阵是否为空,如果为空,直接返回0
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
// 获取矩阵的行数和列数
int m = grid.length, n = grid[0].length;
// 遍历矩阵中的每一个元素
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 如果当前元素是岛屿(0),则调用dfs函数来检查它是否被水域(1)完全包围
if (grid[i][j] == 0 && dfs(grid, i, j)) {
// 如果dfs函数返回true,说明当前岛屿是封闭的,ans加一
ans++;
}
}
}
// 返回ans作为最终答案
return ans;
}
public boolean dfs(int [][] grid, int i, int j) {
// 如果当前坐标超出了矩阵的边界,说明当前岛屿不是封闭的,返回false
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) {
return false;
}
// 如果当前元素是水域(1),说明没有遇到边界,返回true
if (grid[i][j] == 1) {
return true;
}
// 将当前元素标记为水域(1),避免重复访问
grid[i][j] = 1;
// 使用一个for循环来遍历上下左右四个方向,并将结果进行逻辑与运算
boolean res = true;
for (int [] p: pos) {
res &= dfs(grid, i + p[0], j + p[1]);
}
// 返回res作为dfs函数的结果
return res;
}
2.4 Restaurer l'arbre binaire à partir de la traversée de préordre
1028. Restaurer l'arbre binaire à partir de la traversée de la précommande
Nous
root
effectuons une recherche en profondeur d'abord à partir du nœud racine de l'arbre binaire.À chaque nœud de la traversée, nous produisons
D
des tirets (oùD
est la profondeur du nœud), suivis de la valeur du nœud. ( Si un nœud a une profondeur deD
, alors ses enfants immédiats ont une profondeur deD + 1
. Le nœud racine a une profondeur de0
).Si le nœud n'a qu'un seul enfant, il est garanti que cet enfant sera l'enfant de gauche.
Compte tenu de la sortie de traversée
S
, restaurez l'arbre et renvoyez son nœud racineroot
.
int index = 0; // 定义全局变量index
public TreeNode recoverFromPreorder(String traversal) {
int[] deep = Arrays.stream(traversal.split("[0-9]{1,10}")).mapToInt(String::length).toArray(); // 将输入字符串按照数字分割成数组deep
int[] number = Arrays.stream(traversal.split("-{1,100}")).mapToInt(Integer::parseInt).toArray(); // 将输入字符串按照连字符分割成数组number
if (deep.length == 0) deep = new int[]{
0}; // 如果deep为空,则赋值为[0]
return dfs(deep, number); // 调用dfs函数并返回结果
}
public TreeNode dfs(int [] deep, int [] number) {
TreeNode treeNode = new TreeNode(number[index]); // 创建新的TreeNode对象并赋值
int curHeight = deep[index]; // 获取当前节点的深度
if (index + 1 < deep.length && curHeight == deep[index + 1] - 1) {
// 判断是否有左子节点
index++; // 将index加1
treeNode.left = dfs(deep, number); // 递归调用dfs并赋值给左子节点
}
if (index + 1 < deep.length && curHeight == deep[index + 1] - 1) {
// 判断是否有右子节点
index++; // 将index加1
treeNode.right = dfs(deep, number); // 递归调用dfs并赋值给右子节点
}
return treeNode; // 返回当前节点
}