《剑指offer》递归+回溯

目录

BM55 没有重复项数字的全排列

BM56 有重复项数字的全排列

BM58 字符串的排列

BM60 括号生成

BM55 没有重复项数字的全排列

链接:没有重复项数字的全排列     

相似题目:BM56. 有重复项数字的全排列BM58. 字符串的排列BM60. 括号生成

题目描述:给出一组数字,返回该组数字的所有排列,例如:

[1,2,3]的所有排列如下
[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2], [3,2,1].
(以数字在数组中的位置靠前为优先级,按字典序排列输出。)

数据范围:数字个数 0 < n \le 60<n≤6 要求:空间复杂度 O(n!)O(n!) ,时间复杂度 O(n!)O(n!)

解答:全排列就是对数组元素交换位置,使每一种排列都可能出现。因为题目要求按照字典序排列输出,那毫无疑问第一个排列就是数组的升序排列,它的字典序最小,后续每个元素与它后面的元素交换一次位置就是一种排列情况,但是如果要保持原来的位置不变,那就不应该从它后面的元素开始交换而是从自己开始交换才能保证原来的位置不变,不会漏情况。如何保证每个元素能和从自己开始后的每个元素都交换位置,这种时候我们可以考虑递归。为什么可以使用递归?我们可以看数组[1,2,3,4],如果遍历经过一个元素2以后,那就相当于我们确定了数组到该元素为止的前半部分,前半部分1和2的位置都不用变了,只需要对3,4进行排列,这对于后半部分而言同样是一个全排列,同样要对从每个元素开始往后交换位置,因此后面部分就是一个子问题。那我们考虑递归的几个条件:

  • 终止条件: 要交换位置的下标到了数组末尾,没有可交换的了,那这就构成了一个排列情况,可以加入输出数组。
  • 返回值: 每一级的子问题应该把什么东西传递给父问题呢,这个题中我们是交换数组元素位置,前面已经确定好位置的元素就是我们返还给父问题的结果,后续递归下去会逐渐把整个数组位置都确定,形成一种排列情况。
  • 本级任务: 每一级需要做的就是遍历从它开始的后续元素,每一级就与它交换一次位置。

如果只是使用递归,我们会发现,上例中的1与3交换位置以后,如果2再与4交换位置的时候,我们只能得到3412这种排列,无法得到1432这种情况。这是因为遍历的时候1与3交换位置在2与4交换位置前面,递归过程中就默认将后者看成了前者的子问题,但是其实我们1与3没有交换位置的情况都没有结束,相当于上述图示中只进行了第一个分支。因此我们用到了回溯。处理完1与3交换位置的子问题以后,我们再将其交换回原来的情况,相当于上述图示回到了父节点,那后续完整的情况交换我们也能得到。

import java.util.*;

public class Solution {
    //交换函数
    public void swap(ArrayList<Integer>nums,int i,int j){
        int temp=nums.get(i);
        nums.set(i,nums.get(j));
        nums.set(j,temp);
    }
    public void recursion(ArrayList<ArrayList<Integer>>in,ArrayList<Integer>nums,int begin){
        if(begin==nums.size()-1)
        {
            in.add(nums);
        }
        for(int i=begin;i<nums.size();i++){
            swap(nums,i,begin);
            recursion(in,nums,begin+1);
            swap(nums,i,begin);
        }
    }
    public ArrayList<ArrayList<Integer>> permute(int[] num) {
        //先按字典序排序
        Arrays.sort(num); 
        ArrayList<Integer>input=new ArrayList<Integer>();
        for(int i=0;i<num.length;i++){
            input.add(num[i]);
        }
        ArrayList<ArrayList<Integer>>res=new ArrayList<ArrayList<Integer>>();
        recursion(res,input,0);
        return res;
    }
}

BM56 有重复项数字的全排列

链接:有重复项数字的全排列

题目描述:给出一组可能包含重复项的数字,返回该组数字的所有排列。结果以字典序升序排列。

数据范围: 0 < n \le 80<n≤8 ,数组中的值满足 -1 \le val \le 5−1≤val≤5

要求:空间复杂度 O(n!)O(n!),时间复杂度 O(n!)O(n!)

具体做法:

  • step 1:先对数组按照字典序排序,获取第一个排列情况。
  • step 2:准备一个数组暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的数字被加入了。
  • step 3:每次递归从头遍历数组,获取数字加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素num[i]与同一层的前一个元素num[i-1]相同且num[i-1]已经用,也不需要将其纳入。
  • step 4:进入下一层递归前将vis数组当前位置标记为使用过。
  • step 5:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入数组的元素,
  • step 6:临时数组长度到达原数组长度就是一种排列情况。

import java.util.*;

public class Solution {
    public void recursion(ArrayList<ArrayList<Integer>>res,int[] num,Boolean[] vis,ArrayList<Integer>temp){
        if(temp.size()==num.length)
        {
            res.add(new ArrayList<Integer>(temp));
            return;
        }
        for(int i=0;i<num.length;i++){
            //剪枝
            if(vis[i])
                continue;
            if(i>0 && num[i-1]==num[i] && !vis[i-1])
                continue;
            vis[i]=true;
            temp.add(num[i]);
            recursion(res,num,vis,temp);
            //回溯
            vis[i]=false;
            temp.remove(temp.size()-1);
        }
    }
    public ArrayList<ArrayList<Integer>> permuteUnique(int[] num) {
        Arrays.sort(num);
        Boolean[] visited=new Boolean[num.length];
        Arrays.fill(visited,false);
        ArrayList<ArrayList<Integer>>res=new ArrayList<ArrayList<Integer>>();
        ArrayList<Integer>temp=new ArrayList<Integer>();
        recursion(res,num,visited,temp);
        return res;
    }
}

BM58 字符串的排列

链接:字符串的排列

相似题目:矩阵中的路径 、有重复项数字的全排列

题目描述:

输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。

例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。

数据范围:n < 10n<10
要求:空间复杂度 O(n!)O(n!),时间复杂度 O(n!)O(n!)

具体做法:

  • step 1:先对字符串按照字典序排序,获取第一个排列情况。
  • step 2:准备一个空串暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的字符被加入了。
  • step 3:每次递归从头遍历字符串,获取字符加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用,也不需要将其纳入。
  • step 4:进入下一层递归前将vis数组当前位置标记为使用过。
  • step 5:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入字符串的元素,
  • step 6:临时字符串长度到达原串长度就是一种排列情况。

图示:

import java.util.ArrayList;
import java.util.*;
public class Solution {
    public void recursion(ArrayList<String>res,char[] str,StringBuffer temp,boolean[] vis){
        if(temp.length()==str.length)
        {
            res.add(new String(temp));
            return;
        }
        for(int i=0;i<str.length;i++){
            if(vis[i])
                continue;
            if(i>0 && str[i]==str[i-1] && !vis[i-1])
                continue;
            vis[i]=true;
            temp.append(str[i]);
            recursion(res,str,temp,vis);
            vis[i]=false;
            temp.deleteCharAt(temp.length()-1);
        }
    }
    public ArrayList<String> Permutation(String str) {
        ArrayList<String>res=new ArrayList<String>();
        if(str==null|| str.length()==0)
            return res;
        //转字符数组
        char[] input=str.toCharArray();
        Arrays.sort(input);
        boolean[] vis=new boolean[str.length()];
        Arrays.fill(vis,false);
        StringBuffer temp=new StringBuffer();
        //递归获取
        recursion(res,input,temp,vis);
        return res;
    }
}

BM60 括号生成

链接:括号生成

相似题目:BM55. 没有重复项数字的全排列 、BM56. 有重复项数字的全排列、 BM58. 字符串的排列

题目描述:给出n对括号,请编写一个函数来生成所有的由n对括号组成的合法组合。

例如,给出n=3,解集为:

"((()))", "(()())", "(())()", "()()()", "()(())"

数据范围:1 \le n \le 81≤n≤8

思路:相当于一共nnn个左括号和nnn个右括号,可以给我们使用,我们需要依次组装这些括号。每当我们使用一个左括号之后,就剩下n−1n-1n−1个左括号和nnn个右括号给我们使用,结果拼在使用的左括号之后就行了,因此后者就是一个子问题,可以使用递归:

  • 终止条件: 左右括号都使用了n个,将结果加入数组。
  • 返回值: 每一级向上一级返回后续组装后的字符串,即子问题中搭配出来的括号序列。
  • 本级任务: 每一级就是保证左括号还有剩余的情况下,使用一次左括号进入子问题,或者右括号还有剩余且右括号使用次数少于左括号的情况下使用一次右括号进入子问题。

但是这样递归不能保证括号一定合法,我们需要保证左括号出现的次数比右括号多时我们再使用右括号就一定能保证括号合法了,因此每次需要检查左括号和右括号的使用次数。具体做法:

  • step 1:将空串与左右括号各自使用了0个送入递归。
  • step 2:若是左右括号都使用了nnn个,此时就是一种结果。
  • step 3:若是左括号数没有到达nnn个,可以考虑增加左括号,或者右括号数没有到达nnn个且左括号的使用次数多于右括号就可以增加右括号。
import java.util.*;


public class Solution {
    //递归
    public void recursion(int left,int right,ArrayList<String>res,String tmp,int n){
        //如果左括号和右括号都用尽了
        if(left==n&&right==n){
            res.add(tmp);
            return;
        }
        //使用一次左括号
        if(left<n)
            recursion(left+1,right,res,tmp+"(",n);
        //使用一次右括号
        if(right<n && right <left)
            recursion(left,right+1,res,tmp+")",n);
    }
    public ArrayList<String> generateParenthesis (int n) {
        // write code here
        ArrayList<String>res=new ArrayList<String>();
        recursion(0,0,res,"",n);
        return res;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值