通俗易懂的理解递归 回溯 DFS

news2024/12/28 2:35:20

文章目录

  • 递归
    • 概念
    • 递归例子1:递归打印链表
    • 递归例子2:求n数之和
  • 回溯
    • 概念
    • 回溯例子1:组合问题
  • DFS
    • 概念
    • DFS例子1:不同路径
    • DFS例子2:岛屿数量
    • 总结

递归

概念

“方法自己调用自己,每一次调用都会更加接近递归的终止条件,用于解决可以分解为相似子问题的问题。”

看不懂没关系,下面通过两个例子来证明这句话。

递归例子1:递归打印链表

举个例子🌰:递归打印链表,执行下面这段代码

public static void printListReverse(ListNode head) {
    if (head == null) {
        return;
    }
    System.out.print(head.val);  // 正序打印
    printListReverse(head.next);
    System.out.print(head.val);	 // 逆序打印
}

public static void main(String[] args) {
    ListNode listNode1 = new ListNode(1);
    ListNode listNode2 = new ListNode(2);
    ListNode listNode3 = new ListNode(3);
    listNode1.next = listNode2;
    listNode2.next = listNode3;
    ListNode.printListReverse(listNode1); // 输出 123321
}

递的过程:就是每次方法都调用 printListReverse,那么它会一直输出 1 2 3 直接走到 return[终止条件] ,就开始归的过程了,会从后向前走。

如下图的伪代码,调用方法传入1【打印1】,1去递归调用2【打印2】,2去递归调用3【打印3】,3则是递归的出口,接下来开始归的过程,此时执行3的最后一行的print【打印3】,再执行2的【打印2】,最后执行1的【打印1】,就完成了正序输出和逆序输出。

在这里插入图片描述

也就是说,println("after:" + node.value) 这行代码,就是触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。

现在再来回到一开始对递归的解释:方法自己调用自己,每一次调用都会更加接近递归的出口,用于解决可以分解为相似子问题的方法。

printListReverse 每一次递归调用,都会更加靠近 return,每个节点都需要打印则为相似的问题(这个例子中没有拆分子问题)

递归例子2:求n数之和

🌰再举个有拆分子问题的例子:求 1+2+3+…n 的和:

/* 递归 */
int recur(int n) {
    // 终止条件
    if (n == 1)
        return 1;
    // 递:递归调用
    int res = recur(n - 1);
    // 归:返回结果
    return n + res;
}

recur(3); // 输出 6  	-> 3+2+1
recur(4); // 输出 10  -> 4+3+2+1

这个例子中,就将大问题分解了为相似子问题,比如说我要求 recur(4) 是不是先得求 4+(3+2+1)、3+(2+1)、2+(1),递归流程如下图所示。

在这里插入图片描述

所以就将每一次子问题归的结果进行相加,得到了大问题的结果。

回溯

概念

它是一种通过尝试所有可能的选项,并在发现某个选项不可行时撤销上一步重新选择的方法。

那么哪里可以产生回溯呢?就是归的过程,递归函数之后。

与递归不同的是,回退并不仅仅包括函数返回,还包括了其它的的操作,还有点类似于穷举,但是和穷举不同的是回溯会“剪枝”。

回溯都有一个通用的操作,就是尝试,撤销,剪枝,下面是解决回溯问题的框架:

/* 回溯算法框架 */
void backtrack(State state, List<Choice> choices, List<State> res) { // 确定参数,这个步骤在最后
    // 判断是否为解
    if (isSolution(state)) {
        // 记录解
        recordSolution(state, res);
        // 不再继续搜索
        return;
    }
    // 遍历所有选择
    for (Choice choice : choices) {
        // 剪枝:判断选择是否合法
        if (isValid(state, choice)) {
            // 尝试:做出选择,更新状态
            makeChoice(state, choice);
            backtrack(state, choices, res);
            // 回退:撤销选择,恢复到之前的状态
            undoChoice(state, choice);
        }
    }
}

回溯例子1:组合问题

77. 组合 - 力扣(LeetCode)

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。可以按 任何顺序 返回答案。

示例:

输入: n = 4, k = 2 
输出: [[2,4], [3,4], [2,3], [1,2], [1,3], [1,4]]

思路

从 1 开始逐个数字尝试,看是否能作为组合的第一个数。然后,在剩余的数字中递归地寻找剩余的 k-1 个数的组合。重复这个过程,直到找到所有组合。

参考上面的回溯框架,代码如下:

class Solution {

    // 用于收集路径
    List<Integer> path = new ArrayList<>();
    // 用于收集结果集
    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> combine(int n, int k) {
        backtrack(n, k, 1);
        return result;
    }

    public void backtrack(int n, int k, int startIndex){
        if(k == path.size()) {
            // 收集结果
            result.add(new ArrayList(path));
            return;
        }
        // 让 startIndex 成为起始节点,进行去重
        for(int i = startIndex; i <= n; i++) {
            path.add(i);				  // 处理节点
            backtrack(n, k, i + 1);
            path.remove(path.size() - 1); // 撤销本次处理的结果。
        }
    }

}

解释

回溯法解决的问题都可以抽象为 N 叉树,可以使用树形结构来理解回溯就容易多了。

如下图

  • 每一个方框都是一次 for 循环,红色箭头代表 startIndex,代表下一次 for 循环的起始位置
  • 一开始集合是 [1,2,3,4] 到 [3, 4] 到 [4] 最后到 [],从左向右取数,取过的数,不再重复取,每一层都是同样的道理。
  • 图中每次搜索到了叶子节点,就找到了一个结果。

在这里插入图片描述

从某个分支理解回溯,如图中的path.remove和 path 的变化过程,将之前添加过的元素移除,让下一个元素加入进来,符合条件则添加到结果集中。

在这里插入图片描述

从伪代码理解回溯

void backtrack(n = 4, k = 2, startIndex = 1) {
    // 第一次循环
    for (int i = 1; i <= n; i++) {
        path.add(1);	// 添加元素
        backtracking(n = 4, k = 2, startIndex = i+1 = 2) {
            for (int i = 2; i <= n; i++) {
                path.add(2);
				// 【收集元素(1,2)】
                path.remove(2);
            }
            for (int i = 3; i <= n; i++) {
                path.add(3);
				// 【收集元素(1,3)】
                path.remove(3);
            }
            for (int i = 4; i <= n; i++) {
                path.add(4);
				// 【收集元素(1,4)】
                path.remove(4);
            }
        }
        path.remove(1); // 回溯
    }
    // 第二次循环
    for (int i = 2; i <= n; i++) {
        path.add(2);
        backtracking(n = 4, k = 2, startIndex = i+1 = 3) {
            for (int i = 3; i <= n; i++) {
                path.add(3);
				// 【收集元素(2,3)】
                path.remove(3);
            }
            for (int i = 4; i <= n; i++) {
                path.add(4);
				// 【收集元素(2,4)】
                path.remove(4);
            }
        }
        path.remove(2);
    }
    // 第三次循环
    for (int i = 3; i <= n; i++) {
        path.add(3);
        backtracking(n = 4, k = 2, startIndex = i+1 = 4) {
            for (int i = 4; i <= n; i++) {
                path.add(4);
				// 【收集元素(3,4)】
                path.remove(4);
            }
        }
        path.remove(3);
    }
}

DFS

概念

DFS(深度优先搜索)是一种用于遍历或搜索树或图的算法,它沿着每个分支尽可能深地搜索,直到达到目标或叶子节点,然后回溯并继续搜索其他未访问的分支。

DFS例子1:不同路径

62. 不同路径 - 力扣(LeetCode)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:
在这里插入图片描述

输入:m = 3, n = 2
输出:3

解释:

从左上角开始,总共有 3 条路径可以到达右下角。

在这里插入图片描述

  1. 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向下

思路:

因为只有两种走法,向右走和向下走,所以可以使用 DFS 让它一直向下走,走不通之后向右走,如果当前位置是右下角,那么路径数为 1。

  1. 定义状态:使用DFS时,我们需要记录当前的位置(行和列)。
  2. 递归条件:
    • 如果当前位置是右下角(即目标位置),那么路径数为 1。
    • 否则,路径数为从当前位置向右走一步的路径数和向下走一步的路径数之和。
  3. 边界条件:
    • 如果当前位置超出了网格范围,那么路径数为 0。

代码:

public int dfs(int i, int j, int m, int n) {
    if (i > m || j > n) return 0;
    if (i == m && j == n) return 1;
    int down = dfs(i + 1, j, m, n);
    int right = dfs(i, j + 1, m, n);
    return down + right;
}

怎么代码很简短吧,一开始看确实有难度,将上面的代码转换成伪代码结合着图看,就简单多了。

注意点:

  • return 0 代表向右走或向左走越界此路不通所以结果为 0
  • return 1 代表走到了右下角。
  • 递归计算向右走一步和向下走一步的路径数,并将它们相加。

在这里插入图片描述

dfs(int i = 1, int j = 1) { // 向下走
    int down = dfs(int i = 2, int j = 1) { // 向下走
        int down = dfs(int i = 3, int j = 1) { // 向下走
            int down = dfs(int i = 4, int j = 1) { // 向下走
                return 0; // 走不同了返回零
            };
            int right = dfs(int i = 3, int j = 2) { // 向右走
                return 1; // 走到了右下角返回1 -> 也就是情况1【向下 -> 向下 -> 向右】
            };
            return 0+1; // 将这次的结果收集
        };
        int right = dfs(int i = 2, int j = 2) { // 向右走
            int down = dfs(int i = 3, int j = 2) { // 向下走
                return 1; // 走到了右下角返回1 -> 也就是情况3【向下 -> 向右 -> 向下】
            };
            int right = dfs(int i = 3, int j = 3) {
                return 0;
            };
            return 1+0;
        };
        return 1+1;
    };
    int right = dfs(int i = 1, int j = 2) { // 向右走
        int down = dfs(int i = 2, int j = 2) { // 向下走
            int down = dfs(int i = 3, int j = 2) { // 向下走
                return 1; // 走到了右下角返回1 -> 也就是情况2【向右 -> 向下 -> 向下】
            };
            int right = dfs(int i = 2, int j = 3) {
                return 0;
            };
            return 1+0;
        };
        int right = dfs(int i = 2, int j = 3) {
            return 0;
        };
        return 1+0;
    };
    return 2+1;
}

DFS例子2:岛屿数量

200. 岛屿数量 - 力扣(LeetCode)

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

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

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

示例 1:

在这里插入图片描述

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

思路:

遍历二维数组,如果有一个位置为“1”,就使用 DFS 上下左右找,把它相邻的所有一个标识为“0”,相当于把整个岛屿给“沉”了,当 DFS 结束时,岛屿数量++;

把岛屿沉了,是因为避免了之后重复搜索相同岛屿。

在 DFS 过程中,最重要的是不能重复访问之前访问过的格子

在这里插入图片描述

知道把所有元素遍历完,此过程如下:

在这里插入图片描述

代码

class Solution {
    public int numIslands(char[][] grid) {
        int count = 0;
        for(int i = 0; i < grid.length; i++) {
            for(int j = 0; j < grid[0].length; j++) {
                if(grid[i][j] == '1'){	// 找到岛或者岛屿中的某一部分
                    dfs(grid, i, j); 	// 将这个岛整个给“沉”了
                    count++;			// 统计结果
                }
            }
        }
        return count;
    }
    private void dfs(char[][] grid, int i, int j){
        // 判断是否越界 && 位置元素“1”
        if(i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == '0') return;
        // 找到了就将这位置给“沉”了
        grid[i][j] = '0';
        dfs(grid, i + 1, j); // 向上找
        dfs(grid, i, j + 1); // 向右找
        dfs(grid, i - 1, j); // 向下找
        dfs(grid, i, j - 1); // 向左找
    }
}

总结

  • 递归:方法自己调用自己,每一次调用都会更加接近递归的终止条件,用于解决可以分解为相似子问题的问题。
  • 回溯:通过尝试所有可能的选项,并在发现某个选项不可行时撤销上一步重新选择的方法。
  • DFS:用于遍历或搜索树或图的算法,它沿着每个分支尽可能深地搜索,直到达到目标或叶子节点,然后回溯并继续搜索其他未访问的分支。

算法书籍推荐:https://www.hello-algo.com/
刷题路径推荐:https://www.programmercarl.com/

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

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

相关文章

【AD】1-7 AD24软件扩展插件的设置与安装

1.如图所示打开扩展 2.点击齿轮后&#xff0c;确保离线安装位置关联了软件安装包的路径位置后&#xff0c;进行勾选选择后&#xff0c;点击应用即可安装。 注意&#xff1a;如果位置关联错误&#xff0c;则显示如图

Window on ARM解锁所有的TTS语音包供python调用

Window on ARM解锁所有的TTS语音包供python调用 可用的语音包查看查看TTS可用的语音包解锁语音包设置升级系统打开注册表导出注册表修改注册表导入新的注册表可用的语音包查看 微软的Windows 10操作系统为设备上安装的每种语言提供了一套语音。但只有部分已安装的语音能在整个…

pandas数据处理高级系列003---什么是交叉表(Cross Tabulation)以及pandas如何生成

做ab测试的时候遇到了一个新的知识点&#xff0c;交叉表以及如何用pandas生成交叉表 交叉表&#xff08;Cross Tabulation&#xff09;&#xff0c;也称为列联表&#xff08;Contingency Table&#xff09;&#xff0c;是一种用于统计分析的表格&#xff0c;用于显示两个或多个…

MySQL数据库之存储过程的创建与应用

存储过程 procedure 一.存储过程 作用&#xff1a;将经常使用的功能写成存储过程&#xff0c;方便后续重复使用。 二.创建存储过程 三.调用存储过程 call在计算机中是调用的意思 案例1&#xff1a;查看MySQL用户数 如上图所示&#xff0c;这是查看MySQL数据库中的user个数…

手搓简易shell

1.打印命令行 &#xff0c;接受命令行输入 命令行就是&#xff0c;“[用户名主机名 当前目录]$"获取这些信息都存储在Linux内核中的环境变量中&#xff0c;用getenv()函数取出 #include <stdio.h>2 #include <stdlib.h>3 #include <string.h>4 #include…

多个JDK版本之间的切换

首先电脑上可以同时安装多个版本的 JDK&#xff08;Java Development Kit),因为不同的应用程序可能需要不同 Java 版本的支持,安装多个 JDK 版本并不会导致冲突&#xff0c;只要设置好即可,在不同的情况下切换不同的jdk版本保证程序正常工作 很多程序jdk8 已经不支持,所以下载…

鸿蒙生态下开发挑战-鸿蒙低代码开发工具展望及优势

鸿蒙生态下开发挑战 在鸿蒙生态下开发时&#xff0c;开发者可能会遇到多方面的挑战&#xff0c;这些挑战主要涉及开发工具、技术难度、生态竞争以及市场定位等方面。以下是对这些挑战的详细分析&#xff1a; 一、开发工具不完善 尽管鸿蒙系统的开发工具DevEco Studio在逐步完…

celery在django项目中实现并发任务和定时任务

创建一个django项目 django-admin startproject celeryDemo进入项目目录 cd celeryDemo在你的 Django 项目中&#xff0c;创建一个 celery_.py 文件&#xff0c;通常放在项目的根目录&#xff08;与 settings.py 同级&#xff09;&#xff1a; from __future__ import absol…

ST算法解RMQ问题

题目 代码 #include <bits/stdc.h> using namespace std; const int N 2e510, M 20; int st[N][M]; int n, m; int main() {ios::sync_with_stdio(0);cin.tie(0);cin >> n;for(int i 1; i < n; i)cin >> st[i][0];for(int i 1; (1 << i) < …

国内版Sketchfab平台 - CG美术之家(3D编辑发布篇)

CG美术之家为您提供了一个简便的模型上传流程&#xff0c;让发布您的3D模型变得轻而易举。只需准备好通用的3D模型格式&#xff0c;如OBJ、FBX或STL&#xff0c;您就可以轻松上传并分享您的创作。我们的平台支持在线3D渲染&#xff0c;您只需花费不到一分钟的时间&#xff0c;就…

Rocky Linux 9安装后无法远程ssh密码登录解决

在Rocky Linux 9版本中&#xff0c;为了增加安全性&#xff0c;默认情况下禁用SSH root密码登录。这是系统默认设定的规则&#xff0c;我们同样也可以更改它。   允许Rocky Linux 9 root用户通过ssh登录方法&#xff1a; 1.编辑SSH配置文件 2.找到以下内容 PermitRootLogin …

C语言教程——操作符详解(1)

目录 前言 1.操作符的分类&#xff1a; 2.算数操作符 2.1除法 2.2取模 3.移位操作符 3.1二进制相关知识 3.2左移操作符 3.2.1正数 3.2.2负数 3.2.3结论 3.3右移操作符 4.位操作符 4.1 按位与 4.2按位或 4.3按位异或 ​编辑 5.赋值操作符 6.复合赋值符 7.单目操…

mfc140u.dll丢失怎么办? mfc140u.dll文件缺失的修复技巧

mfc140u.dll 是 Microsoft Foundation Classes (MFC) 库的一部分&#xff0c;它是 Visual Studio 2015 的组件之一&#xff0c;主要服务于使用 C 编写的 Windows 应用程序。这个动态链接库文件包含了 MFC 14.0 Unicode 版本的实现代码&#xff0c;为应用程序提供运行时支持。当…

Golang | Leetcode Golang题解之第520题检测大写字母

题目&#xff1a; 题解&#xff1a; func detectCapitalUse(word string) bool {// 若第 1 个字母为小写&#xff0c;则需额外判断第 2 个字母是否为小写if len(word) > 2 && unicode.IsLower(rune(word[0])) && unicode.IsUpper(rune(word[1])) {return f…

专题九——哈希表

目录 0简介 1两数之和 2判定是否互为字符重排 3存在重复元素 4存在重复元素 II 5字母异位词分组 0简介 1两数之和 oj链接&#xff1a;两数之和 解法1 class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {int nnums.size()…

unet中的attn_processor的修改(用于设计新的注意力模块)

参考资料 文章目录 unet中的一些变量的数据情况attn_processorunet.configunet_sd 自己定义自己的attn Processor &#xff0c;对原始的attn Processor进行修改 IP-adapter中设置attn的方法 参考的代码&#xff1a; 腾讯ailabipadapter 的官方训练代码 unet中的一些变量的数据…

客户端时间 与 服务器时间

对客户端时间和服务器有概念&#xff0c;但从来没有这么直观地观察过。直到有一天打开了长久未使用的mac&#xff0c;第一次对时间有了直观的概念&#xff1a; 打开之后就有了上面这样的提示“您的时钟慢了”… 我看了下电脑的时间&#xff0c;然后打开F12获取了下时间&#x…

VLAN高级特性:VLAN聚合

一、VLAN聚合的概述 在一般的三层交换机中&#xff0c;通常是采用一个VLAN对应一个VLANIF接口实现广播域之间的互通&#xff0c;这导致了在一些情况下造成了IP地址的浪费。 因为一个VLAN对应的子网中&#xff0c;子网号&#xff0c;子网广播地址、子网网关地址不能用作VLAN内…

Rust 力扣 - 2653. 滑动子数组的美丽值

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们遍历长度为k的的窗口 因为数据范围比较小&#xff0c;所以我们可以通过计数排序找到窗口中第k小的数 如果小于0&#xff0c;则该窗口的美丽值为第k小的数如果大于等于0&#xff0c;则该窗口的美丽值为0 题…