6-递归与回溯

目录

1 递归典型问题

LeetCode 17-电话号码的字母组合

LeetCode 93-复原IP地址

LeetCode 131-分割回文串

2 回溯法的应用

LeetCode 46-全排列

LeetCode 47-全排列 II

3 组合问题

LeetCode 77-组合

LeetCode 39-组合总和

LeetCode 40-组合总和 II

LeetCode 216-组合总和 III

LeetCode 78-子集

LeetCode 90-子集 II

LeetCode 401

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

LeetCode 79-单词搜索

LeetCode 200-岛屿数量

LeetCode 130-被围绕的区域

LeetCode 417

 

 

 

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,根据题目可以分析出如下图结构:同样是一个树形结构

 形式化的表述如下:

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

通过直接暴力递归便可进行回溯

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>>res = new LinkedList<List<Integer>>();
        backTrack(nums,new LinkedList<Integer>(),res);
        return res;
    }

    private void backTrack(int[] nums, LinkedList<Integer> path, List<List<Integer>> res) {
        if (path.size() == nums.length){
            res.add(new LinkedList<Integer>(path));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            //当已经存放过元素时,直接跳过
            if (path.contains(nums[i])) continue;
            //把元素添加进路径中
            path.add(nums[i]);
            backTrack(nums,path,res);
            //回溯时返回之前状态
            path.removeLast();
        }
    }
}

这个代码是优化后的,添加了记忆化搜索

/**
 * 本题和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需要处理一下。

//这道题目虽然跑的很慢,但是自己做出来的,仿照46,虽然元素会重复但是下标不会重复,因此保存的是下标,最后结果输出的时候把转成相应的数字
class Solution {

    public List<List<Integer>> permuteUnique(int[] nums) {
        Set<List<Integer>> set = new HashSet<List<Integer>>();
        backtrack(nums,new LinkedList<Integer>(),set);
        return new LinkedList<List<Integer>>(set);
    }

    private void backtrack(int[] nums, LinkedList<Integer> path, Set<List<Integer>> set) {
        if (path.size() == nums.length) {
            LinkedList temp = new LinkedList();
            for (int i = 0; i < path.size(); i++) {
                temp.add(nums[path.get(i)]);
            }
            set.add(new LinkedList<Integer>(temp));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (path.contains(i)) continue;
            path.add(i);
            backtrack(nums,path,set);
            path.removeLast();
        }
    }
}

 

3 组合问题

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

这道题目也可以按照总结的回溯模板写出代码,虽然很占时间,但是编写的速度快,也能通过

public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> res = new LinkedList<List<Integer>>();
    backTrack(n,k,1,new LinkedList<Integer>(),res);
    return res;
}

private void backTrack(int n,int k,int start, LinkedList<Integer> path, List<List<Integer>> res) {
    if (path.size() == k){
        res.add(new LinkedList<Integer>(path));
        return;
    }

    for (int i = start; i <= n; i++) {
        //当已经存放过元素时,直接跳过
        if (path.contains(i)) continue;
        //把元素添加进路径中
        path.add(i);
        backTrack(n,k,i+1,path,res);
        //回溯时返回之前状态
        path.removeLast();
    }
}

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

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

代码实现:

LeetCode-39

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> res = new LinkedList<List<Integer>>();
    //Arrays.sort(candidates); //先对无序的数组排序
    backtrack(candidates,target,res,0,new LinkedList<>());
    return res;
}

private void backtrack(int[] candidates, int target, List<List<Integer>> res, int start, LinkedList<Integer> temp) {
    if (target < 0) return;
    if (target == 0){
        res.add(new LinkedList<Integer>(temp));
        return;
    }
    for (int i = start; i < candidates.length; i++) {
        if (target < 0) break;
        temp.add(candidates[i]);
        //这里与前面不同,不是i+1
        backtrack(candidates,target-candidates[i],res,i,temp);
        temp.removeLast();
    }
}

LeetCode-40

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Set<List<Integer>> set = new HashSet<>();
    //先排序再用set集合,就解决了[1,7][7,1]这样的问题
    Arrays.sort(candidates);
    backtrack(candidates,target,0,new LinkedList<>(),set);
    return new LinkedList<>(set);
}

private void backtrack(int[] candidates, int target, int start, LinkedList<Integer> temp, Set<List<Integer>> set) {
    if (target < 0) return;
    if (target == 0){
        set.add(new LinkedList<>(temp));
        return;
    }

    for (int i = start; i < candidates.length; i++) {
        if (target < 0) return;
        temp.add(candidates[i]);
        backtrack(candidates,target-candidates[i],i+1,temp,set);
        temp.removeLast();
    }
}

 

 

 

 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 困难问题视频中有讲但还没记录

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

posted @ 2019-09-04 09:34  windy杨树  阅读(315)  评论(0编辑  收藏  举报