重温数据结构与算法之深度优先搜索

news2025/7/13 7:39:19

文章目录

  • 前言
  • 一、实现
    • 1.1 递归实现
    • 1.2 栈实现
    • 1.3 两者区别
  • 二、LeetCode 实战
    • 2.1 二叉树的前序遍历
    • 2.2 岛屿数量
    • 2.3 统计封闭岛屿的数目
    • 2.4 从先序遍历还原二叉树
  • 参考

前言

深度优先搜索(Depth First SearchDFS)是一种遍历或搜索树或图数据结构的算法。该算法从根节点开始(在图的情况下,选择一些任意的节点作为根节点),并在回溯之前尽可能地沿着每个分支进行探索。需要额外的内存,通常是一个堆栈,来跟踪到目前为止沿着指定分支发现的节点,这有助于回溯。

深度优先搜索算法的特点:

  • 从一个起始节点开始,沿着一条路径不断访问邻接节点,直到没有未访问的邻接节点为止,然后回溯到上一个节点,继续访问其他邻接节点。
  • 利用栈或递归来实现。
  • 可以产生目标图的相应拓扑排序表。

深度优先搜索算法的优点:

  • 简单易实现。
  • 占用空间少。
  • 可以找到从起始节点到任意可达节点的路径。

深度优先搜索算法的缺点:

  • 不一定能找到最短路径或最优解。
  • 可能会陷入死循环或无限递归。

深度优先搜索算法的应用场景:

  • 拓扑排序 (课程安排、工程进度、依赖关系)
  • 模拟游戏(如象棋、迷宫等)
  • 连通性检测(如判断图中是否有环等)
  • 旅行商问题(如求解最短路径等)
  • 括号匹配(如检查表达式中的括号是否匹配等)
  • 二叉树、线段树、红黑树、图等数据结构的遍历

在本文中,我们将介绍深度优先搜索算法的基本原理和实现方法,并通过一些例题来展示其应用。

一、实现

1.1 递归实现

从一个起始节点开始,沿着一条路径不断访问邻接节点,直到没有未访问的邻接节点为止,然后回溯到上一个节点,继续访问其他邻接节点,直到所有节点都被访问过为止。

示例代码如下:

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 栈实现

从一个起始节点开始,将其压入栈中,然后重复以下步骤:弹出栈顶元素,并将其标记为已访问;将该元素的所有未访问的邻接节点压入栈中。直到栈为空为止

示例代码如下:

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(); //将栈顶元素出栈 
        }
    }
}

下面是一个dfs搜索的动图

1.3 两者区别

  • 递归实现是利用系统栈来保存当前节点的状态,当遇到死路时,自动回溯到上一个节点继续搜索。而栈实现是利用自定义的栈来保存当前节点的状态,当遇到死路时,手动弹出栈顶元素回溯到上一个节点继续搜索。
  • 递归实现比较简洁易懂,但是效率不高,而且对于规模较大的图可能会导致栈溢出。而栈实现比较复杂一些,但是效率更高,而且可以避免栈溢出的问题。
  • 递归实现和栈实现都需要一个标志数组来记录哪些节点已经被访问过,以防止重复访问或者陷入环路。

二、LeetCode 实战

2.1 二叉树的前序遍历

94. 二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

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 岛屿数量

200. 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

// 定义一个二维数组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 统计封闭岛屿的数目

1254. 统计封闭岛屿的数目

二维矩阵 grid0 (土地)和 1 (水)组成。岛是由最大的4个方向连通的 0 组成的群,封闭岛是一个 完全 由1包围(左、上、右、下)的岛。

请返回 封闭岛屿 的数目。

// 定义一个二维数组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 从先序遍历还原二叉树

1028. 从先序遍历还原二叉树

我们从二叉树的根节点 root 开始进行深度优先搜索。

在遍历中的每个节点处,我们输出 D 条短划线(其中 D 是该节点的深度),然后输出该节点的值。(如果节点的深度为 D,则其直接子节点的深度为 D + 1。根节点的深度为 0)。

如果节点只有一个子节点,那么保证该子节点为左子节点。

给出遍历输出 S,还原树并返回其根节点 root

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; // 返回当前节点
}

参考

  1. https://en.wikipedia.org/wiki/Depth-first_search
  2. https://zh.wikipedia.org/wiki/深度优先搜索
  3. 深度优先搜索 —— 新手上路的一道坎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/396118.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构(七)优先级队列——堆

一、优先级队列概念队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该中场景下&#xff0c;使用队列显然不合适&#xff0c;比如&#xff1…

前端秘籍之=>八股文经卷=>(原生Js篇)【持续更新中...】

大家好&#xff0c;最近想了想&#xff0c;打算总结归纳一版前端八股文经卷&#xff0c;给大家提供学习参考&#xff0c;如果帮助到大家&#xff0c;请大家&#xff0c;一键三连支持一下&#xff0c;你们的支持会激励我更加努力的更新更多有用的知识&#xff0c;博主先在这里谢…

ONLYOFFICE中利用chatGPT帮助我们策划一场生日派对

近日&#xff0c;人工智能chatGPT聊天机器人爆火&#xff0c;在去年年底发布后&#xff0c;仅仅两个月就吸引了全球近一亿的用户&#xff0c;成为史上最快的应用消费程序&#xff0c;chatGPT拥有强大的学习和交互能力 可以被学生&#xff0c;教师&#xff0c;上班族各种职业运…

C++复习笔记1

字符串 字符串的输入 面向行输入&#xff1a;getline() 通过回车键来确定输入结尾&#xff0c;调用方法为cin.getline() 面向行的输入&#xff1a;get() 处理换行符方法&#xff1a; cin.get(name,20); cin.get(); cin.get(nn,20); 或者将两个类成员函数拼接起来 混合输入…

超赞,用python实现流媒体服务器功能,寥寥几句搞定。

步骤&#xff1a; 要使用Python将实时摄像机传送流写入H5页面&#xff0c;可以使用以下步骤。 1、安装必要的软件包。您需要安装OpenCV和Flask以及gunicorn 与 gevent 。您可以通过在终端中运行以下命令来执行此操作。 pip install opencv-python pip install Flask pip ins…

buu [HDCTF2019]together 1

题目描述&#xff1a; 给了4个文件 pubkey2.pem:-----BEGIN PUBLIC KEY----- MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQB1qLiqKtKVDprtSNGGN q7jLqDJoXMlPRRczMBAGJIRsz5Dzwtt1ulr0s5yu8RdaufiYeU6sYIKk92b3yygL FvaYCzjdqBF2EyTWGVE7PL5lh3rPUfxwQFqDR8EhIH5xOb8rjlkftI…

SQL的优化思路和使用规范

1、索引优化 1.1 建表或加索引时&#xff0c;保证表里互相不存在冗余索引。 对于MySQL来说&#xff0c;如果表里已经存在key(a,b)&#xff0c;则key(a)为冗余索引&#xff0c;需要删除。 1.2 复合索引 建立索引时&#xff0c;多考虑建立复合索引&#xff0c;并把区分度最高…

基于Django4.1.4的入门学习记录

基于Django4.1.4的入门学习记录Django创建Django项目创建工程工程目录说明运行开发服务器settings.py配置文件应用的创建创建应用模块应用模块文件说明App应用配置注册安装子应用数据模型ORM概述定义模型类生成数据库表查看数据库文件Admin管理工具管理界面本地化创建管理员注册…

STM32CubeMX使用说明

目录1 软件安装1.1 软件&环境下载1.1.1 STM32CubeMX软件下载1.1.2 Java环境下载1.2 安装1.2.1 Java环境安装1.2.2 STM32CubeMX软件安装2 软件启动与安装库文件2.1 软件界面2.2 修改默认库文件路径2.3 在线安装固件库3 新建第一个工程3.1 新建工程3.2 选择MCU型号3.3 设置De…

30min入手正则表达式

限定符a* a出现任意次a a出现次数不为0a&#xff1f;a出现&#xff08;1&#xff09;或不出现a{n}a出现n次a{n,nx}a出现在n——nx次a{2&#xff0c;}a至少出现2次或运算符&#xff08;a|b&#xff09;a或者b中选一个&#xff08;ab&#xff09;|&#xff08;cd&#xff09;ab或…

安卓反编译入门04-对反编译重新打包的APK进行重新签名

重签的前提需要安装java环境&#xff0c;我电脑安装的地址&#xff1a;C:\Program Files\Java\.重签的文件目录地址要在jdk的bin目录.进入命令行&#xff0c;执行 cd/d C:\Program Files\Java\jdk1.8.0_181\bin(注意&#xff0c;由于这个文件C:\Program Files要用管理员权限才能…

HBaseAPI——IDEA操作HBase数据库HBase与Hive的集成

目录 一、IDEA操作HBase数据库 (一)添加依赖 (二)配置log4j (三)IDEA连接HBase并插入数据 1.代码实现 2.查看命名空间的表 (四)java操作HBase数据库——单元测试 1.导包 2.初始化 3.关闭连接 4.创建命名空间 5.创建表 6.删除命名空间下的指定表 7.查看所有的命…

【记录nuxt2项目运行遇到的问题】

背景 前段时间小老弟离职了,之前交给他的nuxt2官网项目又回到了我的手中,然后产品跟我说有几个东西需要优化一下,我说小意思,然后我就clone了最新代码准备露一手,然后…我项目就跑不起来了…理论上来说不应该啊,之前这个项目我开发过,不存在环境上的兼容,然后我就npm,cnpm,yarn…

从0开始自制解释器——实现多位整数的加减法计算器

上一篇我们实现了一个简单的加法计算器&#xff0c;并且了解了基本的词法分析、词法分析器的概念。本篇我们将要对之前实现的加法计算器进行扩展&#xff0c;我们为它添加以下几个功能 计算减法能自动识别并跳过空白字符不再局限于单个整数&#xff0c;而是能计算多位整数 提…

基于轻量级YOLOv5开发构建汉字检测识别分析系统

汉字检测、字母检测、手写数字检测、藏文检测、甲骨文检测在我之前的文章中都有做过了&#xff0c;今天主要是因为实际项目的需要&#xff0c;之前的汉字检测模型较为古老了还使用的yolov3时期的模型&#xff0c;检测精度和推理速度都有不小的滞后了&#xff0c;这里要基于yolo…

rabbitmq集群-普通模式

RabbitMQ的Cluster模式分为两种 普通模式镜像模式 1. 概念解释 1.1 什么是普通模式 普通集群模式&#xff0c;就是将 RabbitMQ 部署到多台服务器上&#xff0c;每个服务器启动一个 RabbitMQ 实例&#xff0c;多个实例之间进行消息通信。 此时我们创建的队列 Queue&#xf…

Android ANR trace日志如何导出

什么是ANR &#xff1f;上网搜索&#xff0c;一搜一大片&#xff0c;我就说个很容易识别的字眼&#xff0c;XXXAPP无响应 ANR trace日志如何导出&#xff1f;使用ADB命令&#xff1a; adb pull data/anr/trace.txt 你要存放的路径。查看ANR报错位置全局搜索你APP的包名&#x…

基于MATLAB的无线信道的传播与衰落(附完整代码与分析)

目录 一. 一般路径损耗模型 1. 1自由环境下路径损耗 1. 2 考虑实际情况 1.3 考虑阴影衰落 二. 代码仿真与理解 &#xff08;1&#xff09;函数文件 &#xff08;2&#xff09;函数文件 &#xff08;3&#xff09;主运行文件 三. 运行结果及理解 3.1 3.2 3.3 一. …

Nacos2.2.0多数据源适配oracle12C-修改Nacos源码

从2.2.0版本开始,可通过SPI机制注入多数据源实现插件,并在引入对应数据源实现后,便可在Nacos启动时通过读取application.properties配置文件中spring.datasource.platform配置项选择加载对应多数据源插件.本文档详细介绍一个多数据源插件如何实现以及如何使其生效。 文章目录一…

机器人运动|浅谈Time Elastic Band算法

前言在自主移动机器人路径规划的学习与开发过程中&#xff0c;我接触到Time Elastic Band算法&#xff0c;并将该算法应用于实际机器人&#xff0c;用于机器人的局部路径规划。在此期间&#xff0c;我也阅读了部分论文、官方文档以及多位大佬的文章&#xff0c;在此对各位大佬的…