算法面试6---递归与回溯

1 递归典型问题

 递归法不仅仅局限于二叉树这些已经明确的数据结构中的使用,在更广泛的问题中也会使用,这类问题通常有一个明显的特征即树形结构。

例1:LeetCode 17,根据题目可以分析出如下的结构图:从图上看这是一个明显的树形结构。

把图换位形式化的表述:

 其代码如下:

class Solution {
    //保存所对应的的字符串
    private String[] letterMap = new String[]{"abc","def","ghi","jkl", "mno","pqrs","tuv","wxyz"};
    //用来保存结果
    List<String> res = new LinkedList<>();

    public List<String> letterCombinations(String digits) {
        //清空,当为空的时候不应该有任何东西
        res.clear();
        if (digits.equals("")){
            return res;
        }
        findCombination(digits,0,"");
    }

    //s中保存了从digits[0...index-1]翻译得到的一个字符串
    //这个方法是寻找和digits[index]匹配的字母,获得digits[0...index]翻译得到的解
    private void findCombination(String digits,int index,String s){
        //递归的终止条件
        if (index == digits.length()){
            res.add(s);
            return;
        }
        char c = digits.charAt(index);
        String letters = letterMap[c-'2'];
        for (int i = 0; i < letters.length(); i++) {
            findCombination(digits,index+1,s+letters.charAt(i));
        }
        return;
    }
}

分析整个代码流程可以看出,递归调用每次都会返回一个结果,根据这个特点递归也常被称为回溯,回溯法也是常用的暴力解决方法。在本题中比较简单,只需要暴力求解即可了,后面类似题目有LeetCode 93、131

2 回溯法的应用

例1:LeetCode 46,根据题目可以分析出如下图结构:同样是一个树形结构

 形式化的表述如下:

 与前面的题目有所不同的是这里的数字会相互影响的,而在上面的题目中数字与数字之间是不会冲突的。

/**
 * 本题和17题的答案很相似,只不过在递归的时候要加入一些限制条件
 */

class Solution {

    //返回的结果
    List<List<Integer>> res = new LinkedList<>();
    //记录元素状态
    boolean[] used;

    public List<List<Integer>> permute(int[] nums) {
        //清空
        res.clear();
        if (nums.length == 0){
            return res;
        }

        //构造一个数组用来记录元素是否被使用过了
        used = new boolean[nums.length];
        Arrays.fill(used,false);
        //用来保存中间的生成结果
        ArrayList<Integer> p = new ArrayList<>();
        generatePermution(nums,0,p);

        return res;
    }

    // p中保存了一个有index个元素的排列
    // 该方法向这个排列的末尾添加第index+1个元素,获得一个有index+1个元素的排列
    private void generatePermution(int[] nums,int index, ArrayList<Integer>  p){
        if (index == nums.length){
            //添加的时候必须要加上new不知道为什么
            //res.add(p)这样是添加进去没有数据的
            res.add(new LinkedList<Integer>(p));
            return;
        }
        //递归部分
        for (int i = 0; i < nums.length; i++) {
            if (!used[i]){
                //如果第i个元素没有被使用,则将第i个元素添加到p中
                p.add(nums[i]);
                //使用过后修改元素状态
                used[i] = true;
                //递归调用方法
                generatePermution(nums,index+1,p);
                /**
                 * 一开始我是想着generatePermution(nums,index+1,p.add(nums[i]));这样调用,但是出错,查询了api后才发现原因
                 * linkedlist的add方法返回的是boolean类型,没有办法转换为list类型的所以必须先添加然后再删除
                 */
                //当递归调用后需要回去,这时应该把所有的状态恢复的
                p.remove(p.size()-1);
                used[i] = false;
            }
        }
        return;
    }
}

这道题目稍微提高的为47需要处理一下。

3 组合问题

例1::LeetCode 77,根据题目可以画出如下的图示:

 对于组合问题是不考虑数字顺序的,因此分支要少很多,之所以选用递归,是因为每次的操作流程都差不多,都是从一个数组中取出一个数字。在优化的时候还要考虑剪枝操作。其代码如下:

class Solution {

    //保存结果
    private List<List<Integer>> res = new LinkedList<>();

    public List<List<Integer>> combine(int n, int k) {
        res.clear();
        if (n <= 0|| k<=0 ||k>n){
            //当不满足条件时直接返回即可
            return res;
        }

        LinkedList<Integer> temp = new LinkedList<>();
        //根据题目要求最开始是从1搜索的
        generateCombinations(n,k,1,temp);
        return res;
    }

    //求解C(n,k),当前已经找到的组合存储在c中,需要从start开始搜索新的元素
    private void generateCombinations(int n,int k, int start, LinkedList<Integer> temp){
        //递归的终止条件
        if (temp.size() == k){
            res.add(new LinkedList<>(temp));
            return;
        }
        /*  这里是没有进行剪枝优化的操作,可在博客的图中看出不管怎样还会遍历到n=4,但实际上对于这种我们并不想遍历
            //在写递归时这里的递归循环可以放在最后写,先把第一次的调用流程写完
            for (int i = start; i <= n ; i++) {
                temp.add(i);
                generateCombinations(n,k,i+1,temp);
                //要返回原来的状态,即回溯
                temp.remove(temp.size()-1);
            }
        */
        // 在没有循环前一共是有k-c.seize()个空位的,所有在[i...n]中要有k-c.seize()个元素
        // 因此i最多为n-(k-c.seize())+1
        for (int i = start; i <= n-(k-temp.size())+1; i++) {
            temp.add(i);
            generateCombinations(n,k,i+1,temp);
            //要返回原来的状态,即回溯
            temp.remove(temp.size()-1);
        }
    }
}

与此类似的题目有39、40、216、78、90、401

 4 在二维平面上使用回溯法

例1:LeetCode 79。本题中使用的二维平面不容易直接思考,最好是画出图形来。左侧是给定的字符数组,右侧是待寻找的字符串。

 

 在寻找的时候从(0,0)位置开始寻找,按照上、右、下、左的顺时针顺序进行递归寻找。

class Solution {
    // 这是定义了四个方向的位移,上,右,下,左
    private int[][] d = new int[][]{{-1,0},{0,1},{1,0},{0,-1}};
    // 定义数组的范围的变量
    private int m,n;
    // 记录元素是否被访问过
    private boolean[][] visited;

    public boolean exist(char[][] board, String word) {
        // 二维平面的长度
        m = board.length;
        n = board[0].length;
        // 初始化全部为false
        visited = new boolean[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                visited[i][j] = false;
            }
        }
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (searchWord(board,word,0,i,j)){
                    return true;
                }
            }
        }
        return false;
    }
    // 从board[startx][starty]开始,寻找word[index...word.size()-1]
    private boolean searchWord(char[][] board,String word,int index,int startx,int starty){
        // 递归的终止条件,搜寻到最后一个元素时直接判断
        if (index == word.length() - 1){
            return board[startx][starty] == word.charAt(index);
        }
        if (board[startx][starty] == word.charAt(index)){
            visited[startx][starty] = true;
            // 从startx、starty出发,向四个方向寻找
            for (int i = 0; i < 4; i++) {
                int newx = startx + d[i][0];
                int newy = starty + d[i][1];
                // 判断是否越界,并且元素以前并没有被访问过
                if (inArea(newx,newy) && !visited[newx][newy]){
                    if (searchWord(board,word,index+1,newx,newy)){
                        return true;
                    }
                }
            }
            visited[startx][starty] = false;
        }
        return false;
    }
    // 判断给定的坐标是否在二维平面中
    private boolean inArea(int x,int y){
        return x>= 0 && x< m && y>=0 && y < n;
    }
}

 例2:floodfill算法,LeetCode 200。floodfill就是在区域内不断的进行深度优先遍历,进行着色,其代码和例1很类似。

class Solution {
    // 这是定义了四个方向的位移,上,右,下,左
    private int[][] d = new int[][]{{-1,0},{0,1},{1,0},{0,-1}};
    // 定义数组的范围的变量
    private int m,n;
    // 记录元素是否被访问过
    private boolean[][] visited;

    public int numIslands(char[][] grid) {
        // 二维平面的长度
        m = grid.length;
        // 在测试用例中有数据为空,必须判断一下
        if (m == 0){
            return 0;
        }
        n = grid[0].length;
        // 初始化全部为false
        visited = new boolean[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                visited[i][j] = false;
            }
        }
        int res = 0;
        // 遍历二维平面
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1' && !visited[i][j]){
                    res ++;
                    // 进行深度优先遍历,进行标记
                    dfs(grid,i,j);
                }
            }
        }
        return res;
    }

    // grid[x][y]的位置开始,进行floodfill算法
    // 在内部的if三个条件中保证(x,y)合法,其grid[x][y]是没有访问过的陆地
    private void dfs(char[][] grid,int x,int y){
        // 访问到当前坐标了标记为true
        visited[x][y] = true;
        for (int i = 0; i < 4; i++) {
            int newx = x + d[i][0];
            int newy = y + d[i][1];
            // 在这个递归中没有定义递归终止条件,其实在这三个判断中已经定义好了递归的终止条件,不满足这三个条件就无法进入递归中
            if (inArea(newx,newy) && !visited[newx][newy] && grid[newx][newy] == '1'){
                dfs(grid,newx,newy);
            }
        }
    }
    // 判断给定的坐标是否在二维平面中
    private boolean inArea(int x,int y){
        return x>= 0 && x< m && y>=0 && y < n;
    }
}

 与此类似题目:LeetCode 130、417

 5 困难问题视频中有讲但还没记录

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

递归常常包含着回溯的思想。

转载于:https://www.cnblogs.com/youngao/p/11457061.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值