递归与回溯

递归与回溯

一句话讲递归与回溯

递归:自己调用自己。本质就是找到前后的联系,找到递归的公式。

回溯:执行一次深度优先遍历(DFS),一条路走到底,走不通的时候,返回回来,继续执行,一直这样下去,直到回到起点。

递归

一般情况为:

if (终止条件) {        
return;
}
recursion(参数1);

例子:阶乘

  1. 阶乘

    public int recursion(int n) {
    //终止条件
    if (n == 1)
    return 1;
    //调用自己
    return n * recursion(n - 1);
    }

    递归过程:

求f(5)的时候,只需要求出f(4)即可,如果求f(4)我们要求出f(3)……,一层一层的调用,当n=1的时候,我们直接返回1,然后再一层一层的返回,直到返回f(5)为止。

一些较实际的情况:

if (终止条件) {       
return;
}
可能有一些逻辑运算
recursion(参数1);
可能有一些逻辑运算
recursion(参数2);
……
recursion(参数n);
可能有一些逻辑运算

例子:反转链表

  1. 反转链表

    public static void reverseList(ListNode root) {
    //(终止条件)
    if (root == null)
    return ;
    //(递归调用)先打印下一个
    reverseList(root.next);
    //(逻辑处理)把后面的都打印完了在打印当前节点
    System.out.print(root.val + " ");
    }

    结果如下:

回溯

回溯的本质,其实是在递归基础上进行了改进

一般情况为:

if(不满足继续递归查找的条件,通常为界限判断)
return;
if(满足查找条件)
return 这个值/节点;
递归左边
递归右边
递归结果-回溯

例子:矩阵中的路径(剑指Offer12)

判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。

示例:

输入:board = [
["a","b"],
["c","d"]
],
word = "abcd"

输出:false
  • 思路:回溯算法(DFS+剪枝)。遍历矩阵中所以字符,先朝一个方向搜索到底,再回溯至上个节点,再沿另一方向搜索,以此类推。在搜索中,遇到匹配不成功(如索引越界、此元素已访问、此矩阵元素和目标字符不同)的情况就立即返回。

  • 代码实现

    class Solution {
    public boolean exist(char[][] board, String word) {
    char[] words = word.toCharArray();
    for(int i = 0; i < board.length; i++) {
    for(int j = 0; j < board[0].length; j++) {
    if(dfs(board, words, i, j, 0)) return true;
    }
    }
    return false;
    }
    //回溯
    boolean dfs(char[][] board, char[] word, int i, int j, int k) {
    //边界条件(越界、与目标元素不同)
    if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
    //全部匹配完成
    if(k == word.length - 1) return true;

    char temp = board[i][j];
    board[i][j] = '\0';//将当前元素标记为'\0',不可再被访问
    //递归上下左右
    boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);

    board[i][j] = temp;//恢复其本身值
    //递归结果判断-回溯
    return res;//上面4个方向,只要有一个能查找到,就返回true
    }
    }

例子:二叉搜索树的最近公共祖先(剑指Offer68-Ⅱ)

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。

示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
  • 思想:通过递归对二叉树进行后序遍历,当遇到节点 p 或 q 时返回。从底至顶回溯,当节点 p, q 在节点 root 的异侧时,节点root 即为最近公共祖先,则向上返回 root 。

    p,q和root有三种情况: 1). p,q在root左右,返回root 2). p,q都在root左,返回lowestCommonAncestor( root.left, TreeNode p, TreeNode q) 3). p,q都在root右,返回lowestCommonAncestor( root.right, TreeNode p, TreeNode q)

    • 递归终止条件 if(root == null) return null;//越过叶子节点,返回null if(root == p) return p;//找到p,返回p if(root == q) return q;//找到q,返回q

    • 递归体 递归左子节点,返回值为left TreeNode left = lowestCommonAncestor( root.left, TreeNode p, TreeNode q); 递归右子节点,返回值为right TreeNode right = lowestCommonAncestor( root.right, TreeNode p, TreeNode q);

    • 递归结果 1).left == null && right == null 两边都没找到,返回null 2).left == null 右边找到,返回right 3). right == null 右边找到,返回left 4).left != null && right != null 说明p,q在root两侧,返回root

  • 图文过程详解

  • 代码实现

    class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    //终止条件
    if(root == null || root == p || root == q) return root;

    //递归体
    TreeNode left = lowestCommonAncestor( root.left, p, q);
    TreeNode right = lowestCommonAncestor( root.right, p, q);

    //递归结果判断-回溯
    if(left == null && right == null) return null;
    if(left == null) return right;
    if(right == null) return left;
    return root;
    }
    }

与上一题相似的机器人运动范围问题

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:

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

示例 2:

输入:m = 3, n = 1, k = 0
输出:1
  • 方法一:递归回溯(DFS)。与上一题类似。

    • 递归出口: ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过
    • 递归体:将[i,j]状态设为true,代表已走过,在回溯之后继续走的时候需要避开已走过的方格
    • 回溯返回值: 返回 1 + 右方搜索的可达解总数 + 下方搜索的可达解总数
    class Solution {
    public int movingCount(int m, int n, int k) {
    boolean[][] visited = new boolean[m][n];
    return dfs(0,0,m,n,k,visited);

    }
    //计算两个数的位数和
    public int sum(int i, int j){
    int si = 0, sj = 0;
    while(i != 0){
    si += i % 10;
    i/=10;
    }
    while(j != 0){
    sj += j % 10;
    j /= 10;
    }
    return si + sj;
    }
    //DFS+回溯
    public int dfs(int i, int j, int m, int n, int k, boolean[][] visited){
    //递归出口: ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过
    if(i >= m || j >= n || sum(i,j) > k || visited[i][j]) return 0;
    //将【i,j】状态设为true,代表已走过,在回溯之后继续走的时候需要避开已走过的方格
    visited[i][j] = true;
    //回溯返回值: 返回 1 + 右方搜索的可达解总数 + 下方搜索的可达解总数
    return 1 + dfs(i + 1, j, m, n, k, visited) + dfs(i, j + 1, m, n, k, visited);
    }
    }

礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

示例 1:

输入: 
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
  • 方法一:递归回溯。每次只能向右或向下,对每一格的来源只有向左或向上。从右下角开始递归,定义helper方法找到当前位置的礼物最大值。

    • 递归出口: ① 行列索引越界 ,返回 0(礼物最小为1) ② 当前元素已访问过 ,返回当前结果
    • 递归体:比较左边格子与上边格子的返回结果,取最大值,与当前位置的价值加和。将visited[i,j]状态设为true,代表已走过,在回溯之后继续走的时候需要避开已走过的方格。
    • 回溯返回值: 返回 当前位置加和后的结果。
    class Solution {
    int m ;//行数
    int n ;//列数
    public int maxValue(int[][] grid) {
    m = grid.length;
    n = grid[0].length;
    if(grid.length == 0 ) return 0;
    boolean[][] visited = new boolean[m][n];
    return helper(grid, m - 1, n - 1, visited);
    }
    public int helper(int[][] grid, int i, int j, boolean[][] visited){
    //递归出口:边界条件
    if(i < 0 || i >= m || j >= n || j < 0) return 0;
    //如果已访问过,直接返回
    if(visited[i][j] == true) return grid[i][j];
    //递归比较当前位置的左边格子与上边格子的返回结果,取最大值
    grid[i][j] += Math.max(helper(grid, i - 1, j, visited),helper(grid, i, j - 1, visited));
    //并把当前元素状态设为true
    visited[i][j] = true;
    return grid[i][j];
    }
    }
  • 方法二:动态规划。

结束语

算法小白对递归和回溯的理解只到这里,希望随着刷的题越来越多,理解也越来越清晰透彻,到时再来分享!

------ 本文结束感谢您的阅读 ------