LeetCode部分习题解答记录-递归与回溯

46.全排列

方法1:回溯

  • 代码1
/*
状态值:
	used:值的状态,当为false时,则说明当前值没有被使用,为true则使用。
	path:表示当前结果,遍历过程中不断压入新数值组成一个结果
	res:结果集,当处理的次数等于数组的长度时,则压入当前结果。最终会得到一个结果集。
*/
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        int len = nums.length;
        if(len == 0){
            return res;
        }
        boolean[] used = new boolean[len];
        dfs(nums,len,0,used);
        return res;
    }

    public void dfs(int[] nums, int len, int depth, boolean[] used){
        if(depth == len){
        	//必须使用new ArrayList<>(path);在 Java 中,参数传递是值传递,对象类型变量在传参的过程中,复制的是变量的地址。
        	//这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此直接使用res.add(path)我们会看到结果为一个空的列表对象。
            res.add(new ArrayList<>(path));
            return;
        }

        for(int i = 0 ; i < len ; i++){
            if(used[i]){
                continue;
            }
            path.addLast(nums[i]);
            used[i] = true;
            dfs(nums,len,depth+1,used);
            path.removeLast();
            used[i] = false;
        }
    }
}

47.全排列 II

方法1:回溯

  • 思路:本题在46题的基础上做的改进。初始数组包含重复元素,而结果集不需要包含重复元素。因此,本题重点在于如何取消重复元素,可使用剪枝。
    在这里插入图片描述
    在这里插入图片描述
  • 在图中 ② 处,搜索的数也和上一次一样,但是上一次的 1 还在使用中;
  • 在图中 ① 处,搜索的数也和上一次一样,但是上一次的 1 刚刚被撤销,正是因为刚被撤销,下面的搜索中还会使用到,因此会产生重复,剪掉的就应该是这样的分支。
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new LinkedList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        int len = nums.length;
        if(len == 0){
            return res;
        }
        // 排序(升序或者降序都可以),排序是剪枝的前提
        Arrays.sort(nums);

        boolean[] used = new boolean[len];
        dfs(nums,len,0,used);
        return res;
    }
    public void dfs(int[] nums, int len, int depth, boolean[] used){
        if(depth == len){
            res.add(new ArrayList<>(path));
        }

        for(int i = 0 ; i < len ;i++){
            if(used[i]){
                continue;
            }
            //剪枝
            if(i > 0 && nums[i] == nums[i-1] && !used[i-1]){
                continue;
            }

            path.addLast(nums[i]);
            used[i] = true;
            dfs(nums,len,depth+1,used);
            path.removeLast();
            used[i] = false;
        }
    }
}

39.组合总和

方法1:回溯

  • 思路:在搜索时候去重。如图所示,第一次找到-2后所有满足要求的数字,若其中包含-3。则在第二次从-3开始时,找出的数字必然又会包含-2。因此,在求解过程中需要去重。
    在这里插入图片描述
  • 代码演示:
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if(candidates.length == 0){
            return res;
        }

        dfs(candidates,target,0);
        return res;
    }
	//去重操作,在到达当前数字所代表的递归层是,应从当前数字开始遍历,不包含之前的数字。例如,2、3、6,处理3时从3开始遍历,不包含2。结合图来看。
    public void dfs(int[] candidates, int curTarget,int begin){
        if(curTarget < 0){
            return;
        }

        if(curTarget == 0){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = begin ;i < candidates.length; i++){
            path.addLast(candidates[i]);
            dfs(candidates,curTarget-candidates[i],i);
            path.removeLast();
        }   
    }
}

40.组合总和II

方法1:回溯+剪枝

  • 思路:与39类似,结合47题的剪枝的思想
  • 代码1:
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        int len = candidates.length;
        if(len == 0){
            return res;
        }
        Arrays.sort(candidates);
        dfs(candidates,target,0);
        return res;
    }

    public void dfs(int[] candidates, int curTarget, int begin){
        if(curTarget == 0){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = begin; i < candidates.length; i++){
            //剪枝
            if(candidates[i] > curTarget){
                break;
            }
			//剪枝
            if(i > begin && candidates[i] == candidates[i -1]){
                continue;
            }
            path.addLast(candidates[i]);
            dfs(candidates,curTarget-candidates[i],i+1);
            path.removeLast();
        }
    }
}

77.组合总和 III

方法1:回溯

  • 代码:
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        
        dfs(k,n,1,0,0);
        return res;
    }

    public void dfs(int k , int n, int begin,int curSum,int depth){
        if(depth == k){
            if(curSum == n){
                res.add(new ArrayList<>(path));
                return;
            }
        }
        for(int i = begin; i <= 9 ;i++){
			//若当前i已经大于n-curSum,则break。因为后面i都不会满足要求
            if(i > n - curSum){
                break;
            }
            curSum+=i;
            path.addLast(i);
            dfs(k,n,i+1,curSum,depth+1);
            path.removeLast();
            curSum-=i;
        }
    }
}

方法1:回溯

  • 代码1:
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        if(n == 0){
            return res;
        }
        dfs(n,k,0,1);
        return res;
    }

    public void dfs(int n ,int k ,int depth,int begin){
        if(depth == k){
            res.add(new ArrayList<>(path));
        }
        for(int i = begin; i <= n;i++){
            path.addLast(i);
            dfs(n,k,depth+1,i+1);
            path.removeLast();
        }

    }
}
  • 存在问题:耗时较长,有很多无用的搜索操作

方法2:回溯+剪枝

  • 思路:如下图所示,当n=5,k=3时,从4开始取值已经没有意义了。因此,搜索有上界,可以进行剪枝来优化时间效率。通过分析,上界为n-(k-path.size())+1。
    在这里插入图片描述
  • 代码2:
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    Deque<Integer> path = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        if(n == 0){
            return res;
        }
        dfs(n,k,0,1);
        return res;
    }

    public void dfs(int n ,int k ,int depth,int begin){
        if(depth == k){
            res.add(new ArrayList<>(path));
            return;
        }
        //剪枝操作
        for(int i = begin; i <= n - (k-path.size()) + 1;i++){
            path.addLast(i);
            dfs(n,k,depth+1,i+1);
            path.removeLast();
        }
    }
}

60.排列序列

方法1:回溯

  • 思路:如下图所示。
    • 所求排列 一定在叶子结点处得到,进入每一个分支,可以根据已经选定的数的个数,进而计算还未选定的数的个数,然后计算阶乘,就知道这一个分支的叶子结点的个数(叶子节点为排列的组合):
    • 如果 k 大于这一个分支将要产生的叶子结点数(说明满足输出的排列组合不在此分支),直接跳过这个分支,这个操作叫「剪枝」;
    • 如果 k 小于等于这一个分支将要产生的叶子结点数,那说明所求的全排列一定在这一个分支将要产生的叶子结点里,需要递归求解。

在这里插入图片描述

  • 代码1:
class Solution {
    StringBuilder sb = new StringBuilder();
    public String getPermutation(int n, int k) {
        boolean[] used = new boolean[n+1];
        int[] factorial = new int[n+1];
        calculateFactorial(n,factorial);
        dfs(n,k,0,used,factorial);
        return sb.toString();
    }

    public void dfs(int n,int k,int index, boolean[] used,int[] factorial){
         if(index == n){
            return;
        }
        //固定选择一个数,确认其它数组成的全排列的个数。
        int count = factorial[n- index -1];
        for(int i = 1 ; i <= n ; i++){
            //跳过当前已固定选择的数
            if(used[i]){
                continue;
            }
            //如果当前分支全排列的个数小于k,则跳过并计算下一个分支。直至找到叶子节点。
            if(count < k){
                k -= count;
                continue;
            }
            sb.append(i);
            used[i] = true;
            dfs(n,k,index+1,used,factorial);
            return;
        }
    }
    
    //计算阶乘
    public void calculateFactorial(int n,int[] factorial){
        factorial[0] = 1;
        for(int i = 1; i <= n ; i++){
            factorial[i] = factorial[i-1] * i;
        }
    }
}

17.电话号码的字母组合

方法1:递归

  • 思路:形似树形结构,某个数字代表的字符个数相当于当前层的分支数。处理下一层时,将当前节点所代表的字符串传入。直至寻找到层底,保存整体字符串。
    在这里插入图片描述

  • 代码1:

class Solution {

    List<String> res = new ArrayList<>();
    String[] letterMap = new String[]{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};        

    public List<String> letterCombinations(String digits) {
        if(digits == "" || digits.length() == 0){
            return res;
        }
        findCombinations(digits,0,"");
        return res;
    }

    /*
        subStr:存储了[0,index-1]所转换的一个字符串
        index:当前遍历的索引
     	digits:给定的数字字符串
    */
    public void findCombinations(String digits,int index,String substr){
        if(index == digits.length()){
            res.add(substr);
            return;
        }

        char c = digits.charAt(index);
        String str = letterMap[c-'0'];
        for(int i = 0; i < str.length();i++){
            findCombinations(digits,index + 1,substr +  str.charAt(i));
        }
        return;
    }
}
  • 代码2:在代码1中,有相当多字符串的拼接操作,比较费时。因此,我们通过StringBuilder消除此操作,在当前层处理完返回上一次层时,删除StringBuilder的末尾字符。以免影响上一层其余节点的操作。(对StringBuilder的操作是全局的)。
class Solution {

    List<String> res = new ArrayList<>();
    String[] letterMap = new String[]{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};        

    public List<String> letterCombinations(String digits) {
        if(digits == "" || digits.length() == 0){
            return res;
        }
        findCombinations(digits,0,new StringBuilder());
        return res;
    }
  
    public void findCombinations(String digits,int index,StringBuilder substr){
        if(index == digits.length()){
            res.add(substr.toString());
            return;
        }

        char c = digits.charAt(index);
        String str = letterMap[c-'0'];
        for(int i = 0; i < str.length();i++){
            findCombinations(digits,index + 1,substr.append(str.charAt(i)));
            substr.deleteCharAt(substr.length()-1);  //必须删除末尾字符
        }
        return;
    }
}

93.复原 IP 地址

方法1:递归(回溯)

  • 代码1;
class Solution {
    List<String> res = new ArrayList<>();
    Deque<String> deque = new LinkedList<>();
    public List<String> restoreIpAddresses(String s) {
        if(s.length() < 4 || s.length() > 12){
            return res;
        }
        findIpAddress(s,s.length(),4,0);
        return res;
    }
	
	// s:给定字符串,len:字符串长度,noSolitTimes:字符串未分割次数,begin:分割位(起始为0)	
    public void findIpAddress(String s,int len ,int noSplitTimes, int begin){
        if(begin == len){
            if(noSplitTimes == 0){
                res.add(String.join(".", deque));
            }
            return;
        }

        for(int i = begin ;i < begin + 3 ;i++){
            if(i >= len){
                break;
            }
            //判断[begin,i]当前段是否可以成为一个ip;
            if (judgeIpSegment(s, begin, i)) {
                //若可以成为一个ip,则截取当前字符串添加至队列中。从后面的字符串进行递归判断
                String currentIpSegment = s.substring(begin, i + 1);
                deque.addLast(currentIpSegment);
                findIpAddress(s, len, noSplitTimes - 1, i + 1);
                deque.removeLast();
            }
        }
    }

     private boolean judgeIpSegment(String s, int left, int right) {
        int len = right - left + 1;
        //判断首位是否为0
        if (len > 1 && s.charAt(left) == '0') {
            return false;
        }

        //判断结果是否大于255
        int res = 0;
        while (left <= right) {
            res = res * 10 + s.charAt(left) - '0';
            left++;
        }
        return res >= 0 && res <= 255;
    }
}

131.分割回文串

方法1:回溯

  • 思路:类似于复原IP地址
class Solution {
    List<List<String>> res = new ArrayList<>();
    Deque<String> path = new LinkedList<>();
    public List<List<String>> partition(String s) {
        int len = s.length();
        if(len == 0){
            return res;
        }
        dfs(s,0,len);
        return res;
    }

    public void dfs(String s, int begin, int len){
        if(begin == len){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = begin; i < len ; i++){
            if(isHuiWen(s,begin,i)){
                String substr = s.substring(begin,i + 1);
                path.addLast(substr);
                dfs(s,i+1,len);
                path.removeLast();
            }
        }    
    }
    
	//判断是否回文
    public boolean isHuiWen(String s, int begin ,int end){
        while(begin <= end){
            if(s.charAt(begin) != s.charAt(end)){
                return false;
            }
            begin++;
            end--;
        }
        return true;
    }
}

方法2:回溯优化,通过DP等方式预先判断是否回文。待做

401.二进制手边

方法1:回溯

  • 思路:回溯的思想,但是在遍历时,需要注意小时与分钟的处理。
  • 代码:
class Solution {
    List<String> res = new ArrayList<>();
    int[] times = new int[]{1,2,4,8,1,2,4,8,16,32};
    public List<String> readBinaryWatch(int num) {

        dfs(num,4,0,0,0,0);
        return res;
    }

    public void dfs(int num,int hoursLen, int begin, int depth,int hour, int minute){
        if(depth == num){
            if(hour < 12 && minute < 60){
                res.add(hour + ":" + (minute <= 9 ? "0"+minute : minute));
                return;
            }
            return;
        }
        for(int i = begin; i < times.length;i++){
            if(i < hoursLen){
                hour += times[i];
                dfs(num,hoursLen,i+1,depth+1,hour,minute);
                hour -= times[i];
            }else{
                minute += times[i];
                dfs(num,hoursLen,i+1,depth+1,hour,minute);
                minute -= times[i];
            }
          
        }
    }
}

79.单词搜索

方法1:回溯。

  • 思路:与一维回溯有所区别。二维搜索有一个方向向量,也就是上下左右四个方向。
  • 代码:
class Solution {
    int[][] DIRECTS = new int[][]{{-1,0},{0,-1},{1,0},{0,1}};
    int rows;
    int cols;
    int wordLen;
    boolean[][] visited;
    char[] wordCharArray;
    char[][] board;
    String word;
    public boolean exist(char[][] board, String word) {
        this.rows = board.length;
        if(rows == 0){
            return false;
        }
        this.cols = board[0].length;
        this.visited = new boolean[rows][cols];
        this.wordCharArray = word.toCharArray();
        this.wordLen = word.length();
        this.board = board;
        this.word = word;

        //从各个起点开始进行回溯,找到则返回true,都没有找到则返回false
        for(int i = 0 ; i < rows ; i ++){
            for(int j = 0 ; j < cols ; j++){
                if(dfs(i, j, 0)){
                    return true;
                }
            }
        }
        return false;
    }


    public boolean dfs(int x, int y ,int begin){
        //begin:当前寻找的长度。若已经搜索到最末尾,判断返回
        if(begin == wordLen - 1){
            return board[x][y] == wordCharArray[begin];
        }
		//当前搜索成功,则从下一个位置开始。否则直接false。
        if(board[x][y] == wordCharArray[begin]){
            visited[x][y] = true;
            for(int[] direct : DIRECTS){
                int newX = x + direct[0];
                int newY = y + direct[1];
                //若newx与newy符合要求且当前新位置未被搜索,则从该位置继续开始搜索。
                if(judgeXandY(newX,newY) && !visited[newX][newY]){
                   if(dfs(newX, newY, begin+1)){
                       return true;
                   }
                }
            }
            visited[x][y] = false;
        }        
        return false;
    }

    public boolean judgeXandY(int x, int y){
        return x >= 0 && x < rows && y >= 0 && y < cols; 
    }
}

200.岛屿数量

方法1:回溯,DFS

  • 思路:从未访问且值为1的位置开始, 进行DFS遍历,将路过的节点设置访问标记。
  • 代码
class Solution {
    int[][] DIRECTIONS = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
    char[][] grid;
    int rows;
    int cols;
    boolean[][] visited;
    int count;
    public int numIslands(char[][] grid) {
        this.rows = grid.length;
        if(rows == 0){
            return 0;
        }
        this.cols = grid[0].length;
        this.grid = grid;
        this.visited = new boolean[rows][cols];
        this.count = 0;

        for(int i = 0 ; i < rows ; i ++){
            for(int j = 0 ; j < cols ; j++){
            	//每一次满足条件,则视为一个新的起点,count++。dfs主要是求与此位置可连接的所有位置,其可以组成一个岛屿
                if(!visited[i][j] && grid[i][j] == '1'){
                    dfs(i,j);
                    count++;
                }
            }
        }
        return count;
    }

    public void dfs(int x, int y){
        visited[x][y] = true;
        for(int[] dierction : DIRECTIONS){
            int newX = x + dierction[0];
            int newY = y + dierction[1];
            if(judgeXandY(newX,newY) && !visited[newX][newY] && grid[newX][newY] == '1'){
                dfs(newX,newY);
            }
        }
    }

     public boolean judgeXandY(int x, int y){
        return x >= 0 && x < rows && y >= 0 && y < cols; 
    }
}

130.被围绕的区域

方法1:DFS

  • 思路:与200题,岛屿数量相类似。
  • 不同点:
    • 此题没有用visited数组来判断该位置是否已经访问过。原因:因为在第一次访问时,已经将边界的’O’和与之相连的’O’全部转换为’-’。这个操作类似与visited数组,在dfs中通过判断即可知道该位置是否已访问。
    • DFS有所区别。本题DFS主要是寻找’O’,因此到达一个位置后首先判断其是否为’O’。若是,则继续进行递归寻找。
class Solution {
    int[][] DIRECTIONS = {{-1,0},{0,-1},{1,0},{0,1}};
    int rows;
    int cols;
    char[][] board;
    boolean[][] visited;
    public void solve(char[][] board) {
        this.rows = board.length;
        this.cols = board[0].length;
        this.board = board;

        //首先将四周有'O'和与之相连的'O'都换为'-'
        //处理行
        for(int i = 0 ; i < cols;i++){
            if(board[0][i] == 'O'){
                dfs(0,i);
            }
            if(board[rows-1][i] == 'O'){
                dfs(rows-1,i);
            }
        }

        //处理列
        for(int i = 0 ; i < rows;i++){
            if(board[i][0] == 'O'){
                dfs(i,0);
            }
            if(board[i][cols - 1] == 'O'){
                dfs(i,cols-1);
            }
        }

		//随后遍历整个数组
        for(int i = 0 ; i < rows; i ++){
            for(int j = 0 ; j < cols; j ++){
                if(board[i][j] == '-'){
                    board[i][j] = 'O';
                }else if(board[i][j] == 'O'){
                    board[i][j] = 'X';
                }
            }
        }
    }

    public void dfs(int x, int y){
        if(judge(x,y) && board[x][y] == 'O'){
            board[x][y] = '-';
            for(int[] direct : DIRECTIONS){
                int newx = x + direct[0];
                int newy = y + direct[1];
                dfs(newx,newy);
            }
        }
    }

    public boolean judge(int x,int y ){
        return x >= 0 && x < rows && y >= 0 && y < cols; 
    }
}

733.图像渲染

方法1:DFS

  • 思路:与上一题130基本类似。只是稍微简单
class Solution {
    int[][] DIRECTIONS = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
    int rows;
    int cols;
    int[][] image;
    public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
        this.rows = image.length;
        this.cols = image[0].length;
        this.image = image;
        int oldColor = image[sr][sc];
        if(newColor == oldColor){
            return image;
        }
        dfs(sr,sc,newColor,oldColor);
        return image;
    }

    public void dfs(int x, int y, int newColor,int oldColor){
        if(judgeXandY(x,y) && image[x][y] == oldColor){
            image[x][y] = newColor;
            for(int[] dierction : DIRECTIONS){
                int newX = x + dierction[0];
                int newY = y + dierction[1];
                dfs(newX,newY,newColor,oldColor);   
            }
        }
    }

    public boolean judgeXandY(int x, int y){
        return x >= 0 && x < rows && y >= 0 && y < cols; 
    }
}

784.字母大小写全排列

方法1:DFS

  • 代码:
class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder sb = new StringBuilder();
    public List<String> letterCasePermutation(String S) {
        int len = S.length();
        if(len == 0){
            return res;
        }

        dfs(S,len,0,sb);
        return res;
    }
    public void dfs(String s,int len, int beign, StringBuilder sb){
        if(beign == len){
            res.add(sb.toString());
            return;
        }
        char ch = s.charAt(beign);
        sb.append(ch);
        dfs(s,len,beign+1,new StringBuilder(sb));
        sb.deleteCharAt(sb.length()-1);
        if(!Character.isDigit(ch)){
            sb.append(ch ^= 1<<5);
            dfs(s,len,beign+1,new StringBuilder(sb));
            sb.deleteCharAt(sb.length()-1);
        }
    }
}
  • 注意问题:可以将S直接转换为字符数组String.toCharArray()。对字符数组进行处理,最终通过new String(char)再将其转换为字符串。 这样,在递归过程中,不需要不断的对字符串进行拼接处理。

22.括号生成

方法1:回溯

在这里插入图片描述

  • 代码:
class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder sb = new StringBuilder();
    public List<String> generateParenthesis(int n) {
        if(n == 0){
            return res;
        }

        dfs(n,n,sb);
        return res;
    }
	
	//遍历为二叉树,因此分别判断左右括号数值是否>0。若大于,则可继续添加括号。
    public void dfs(int left,int right,StringBuilder sb){
        if(right == 0 && left == 0){
            res.add(sb.toString());
            return;
        }
		
		//剪枝:当left>rigth,说明已写入的左括号少,不满足条件直接跳过。
        if(left >right){
            return;
        }

        if(left > 0){
            dfs(left - 1,right,new StringBuilder(sb.append("(")));
            sb.deleteCharAt(sb.length()-1);
        }
		
		//当到达右分支,进入递归后可以不恢复字符串。因为后面再没有其余分支。去掉这步时间缩短。
        if(right > 0){
            dfs(left,right-1,new StringBuilder(sb.append(")")));
            // sb.deleteCharAt(sb.length()-1);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值