打印一个字符串s的所有字符的全排列,而且要不重复的字面值

打印一个字符串s的所有字符的全排列,而且要不重复的字面值

提示:暴力递归


题目

打印一个字符串s的所有字符的全排列
打印一个字符串s的所有字符的全排列,同时要求排列的字面值不重复


一、审题

比如:s=abc
其全排列如下:
abc,acb,
bac,bca,
cab,cba

每一个字符都有可能作为派头
后续剩下的字符串,每一个字符也可能做一次派头


不考虑字面值重复的全排列

s中每一个位置i的字符,都有可能作为派头,——这就是排列的定义

(1)当一个i位置的字符做了派头,那么从第i+1那个位置开始,实际上又是一个新的子串的递归打印排列问题,
他们依然要考虑谁做排头的问题,然后打印全排列。
比如:0位置做了排头,则剩下的bc又是一个字符串,让你打印他们的全排列回到(1)
在这里插入图片描述
这就是暴力递归的本质了,不妨设f(s,i)就是打印s所有可能的i–N-1上的全排列
随着i增大,f永远都在解决同一个类型的问题,只不过规模更小罢了。
在这里插入图片描述

细节上来讲:
本题中,咱们为了方便起见,把s转化为字符数组str
比如:s=abc,str=a b c

如果i位置做派头,让它跟0位置交换一下位置,最后我们打印的时候,直接打印str,只不过,派头不同而已
比如,让b做排头,那就是先交换0和1位置,得到bac,算一种排列
也就是说:如果i–N-1上有谁j位置,想做排头的话,j可以来跟i位置先交换一下,再去递归打印i+1–N-1位置。
在这里插入图片描述

深度理解这个方案——我们用s=abc做例子:
打印s的全排列
str=a b c
调用f(str,i=0)

现在i=0位置,j可以为1和2
j其实可以等于i–N-1:0,1,2
j谁想来做排头,就让j跟i交换
1)j=0时,换不换没卵用,剩下bc字符串需要决定谁去做排头?
2)但是j=1时,可以让b做排头,此时剩下ac字符串需要决定谁去做排头?
3)当j=2时,可以让c做排头,此时剩下ab字符串需要决定谁去做排头?
在这里插入图片描述
4)在1)中,b做排头还是c做排头?实际上啊,又开启了同样的递归调用f(bc,i=1)
在这里插入图片描述

5)在2)中,a做排头还是c做排头?实际上啊,又开启了同样的递归调用f(ac,i=1)
在这里插入图片描述
6)在3)中,a做排头还是b做排头?实际上啊,又开启了同样的递归调用f(ab,i=1)
在这里插入图片描述
尤其要小心!!!注意恢复现场
尤其要小心!!!注意恢复现场
尤其要小心!!!注意恢复现场

为什么?
因为在这个递归中,要我们一直用的是同一个str,当你把i和j交换了,比如str中的b和c交换为cb,得到了acb算一种
接下来,你要去生成以b开头的全排列
由于你操作的是同一个str,你需要把刚刚交换的bc,又换回来,成abc,这样你才能换a和b,去探索b做排头可能性
【看下面粉色那条路径】
这个换回来,探索别的可能性的过程,就叫做恢复现场,除非你操作的不是同一个str!
在这里插入图片描述

否则你会出现啥情况呢?
acb你没换回去成为abc的话
现在你交换acb的0 1位置,得到cab,这不是我们要探索的b做排头的情况啊!!
可见恢复现场有多重要了吧!!

好,手撕本题的代码:

//复习:打印全排列
    //s转str,一直都是用的同一个str,所以要注意恢复现场
    //j=i--N-1,谁想来替代i做排头,那交换就行
    public static void f(char[] str, int i){
        //从str的i--N-1决定谁做排头,搞出全排列,打印
        if (i == str.length){
            //越界,说明可以打印str了
            System.out.print(String.valueOf(str) +" ");
            return;
        }

        //i在排头位置,问后面i--N-1的任意位置j,想要做排头吗?
        //以此做一下排头,然后递归决定排列情况
        for (int j = i; j < str.length; j++) {
            swap(str, i, j);//j来做
            f(str, i + 1);//j来做排头之后,再去看i+1--N-1剩下的排列情况--别写错了哦
            //恢复下场,探索新的别的位置做排头可能性--因为str的i位置刚刚被上一种j换了
            swap(str, j, i);
        }
    }
    //调用:
    public static void printAllPermutation(String s){
        if (s == null || s.length() == 0) return;

        //从0位置开始决定,谁做排头,打印
        char[] str = s.toCharArray();
        f(str, 0);
    }

    public static void test(){
        String s = "abc";
        ArrayList<String> res = printPermutation(s);
        for (String s1:res){
            System.out.print(s1 +" ");
        }

        System.out.println("\n复习:打印全排列:");
        printAllPermutation(s);
    }

考虑不打印重复字面值的全排列

如果不要字面字重复的全排列

笔试AC解,用哈希集收集答案,自动去重,耗费空间,时间

哈希集,本身就只能保存不重复的元素
所以弄一个哈希集保存所有的全排列,自动去重
但是这样就会耗费很多时间!

//如果想不要输出重复的,那就需要AC在笔试中,用HashSet转移一下
    public static HashSet<String > printPerNoRepeat(String s){
        if (s == null || s.length() == 0) return null;
        char[] str = s.toCharArray();
        ArrayList<String> res = new ArrayList<>();
        process(str, 0, res);//从0位置开始操作

        HashSet<String> set = new HashSet<>();//自动去重
        for (String s1:res){
            set.add(s1);
        }
        return set;//这样耗时间,不过笔试是没问题的
    }
    
//操作一个字符串数组str,目前在哪个位置i操作,i之后的都可以和后面的字符交换,之前的不行,因为用过了
    //结果都放在res动态数组中
    //i之前的情况订好了,i之后的同样的递归,只不过规模小了,这就是递归的核心
    public static void process(char[] str, int i, ArrayList<String> res){
        if (i == str.length){
            res.add(String.valueOf(str));//将目前的字符数组,转化为字符串
            return;
        }

        //只要还在操作i,i之后的就能交换,不过process是一个递归函数,用同一个str,回来需要恢复现场
        //每个i都做一次排头
        for (int j = i; j < str.length; j++){
            swap(str, i, j);//i==j的话,没换,自己就是排头,j>i的话,j在递增
            process(str, i + 1, res);
            //上面这个process出来后,递归的ij还是刚刚交换过的ij,需要还原
            //才能继续下一种j的情况
            swap(str, i, j);//恢复现场,要把刚刚i和j交换的这种情况还原,考虑别的情况
        }
    }

    public static void test2(){
        String s = "aac";
        HashSet<String> res = printPerNoRepeat(s);
        for (String s1:res){
            System.out.println(s1);
        }
    }

看结果没问题:

aac
caa
aca

不会出现
aac【0位置a在前面】
aac【1位置a在前面】
两个a位置不一样

面试最优解:不交换相同的字符,提前剪枝,节省时间

**剪枝策略:**这是算法中很重要的一个技巧

遇到i和j位置,字符相同,咱就别交换了,跳过这种尝试
在这里插入图片描述

咱们每次递归,用visit数组来记录i位置是哪个字符?a–z中哪个,是a就在a那个位置放true,否则就是false
visit数组从0–25位置,分别代表a–z
来了一个j位置的字符,怎么索引j呢?用str[j]-'a’就能转化到0–25了,visit[0]就是a,通过visit[str[j] - ‘a’]访问j字符是不是和i相同。
在这里插入图片描述
这是一个很重要的
看a–z分支
的方法,频繁在大厂考题中出现的技巧,比如前面咱们见过的前缀树,就是这么干的。
前缀树,查找树,trie数据结构:建前缀树,查字符串,查前缀,删除前缀树中的字符串

下图中i位置是a
j从i–N-1尝试,要不要交换去做排头呢?
(1)j=i时,visit[str[j] - ‘a’]=visit[str[i] - ‘a’]=visit[‘a’- ‘a’]=visit[0]=true;标记好此时i位置就是a字符,visit的0位置就代表a字符哦——这是全排每次都要做的事情,先让i和i交换,尝试i做排头的可能性,也方便标记visit在i位置的字符是啥?
(2)j=i+1,即图中j时,看visit[str[j] - ‘a’]=visit[‘a’- ‘a’]=visit[0],它已经是true了,说明,i和j本就是相同的字符,咱不需要交换了!!逃过这个尝试
(3)j=i+2,即图中c时,看visit[str[j] - ‘a’]=visit[‘c’- ‘a’]=visit[2],是false,代表i位置不是c字符,他们俩可以尝试交换,让c做排头
在这里插入图片描述
这样的话,避免i做排头的aac
和j做排头的acc重复出现
从而达到了剪枝的目的!

这就是提前剪枝的技巧
手撕代码:

//复习:打印全排列:不重复,提前剪枝!!!
    //s转str,一直都是用的同一个str,所以要注意恢复现场
    //j=i--N-1,谁想来替代i做排头,那交换就行
    public static void fNoRepeat(char[] str, int i){
        //从str的i--N-1决定谁做排头,搞出全排列,打印
        if (i == str.length){
            //越界,说明可以打印str了
            System.out.print(String.valueOf(str) +" ");
            return;
        }

        //i在排头位置,问后面i--N-1的任意位置j,想要做排头吗?
        //以此做一下排头,然后递归决定排列情况
        //准备好剪枝
        boolean[] visit = new boolean[26];//0--25代表a--z
        for (int j = i; j < str.length; j++) {
            if (!visit[str[j] - 'a']){
                visit[str[j] - 'a'] = true;//标记str[i]字符在i出现了,待会j不能再出现同样的字符

                swap(str, i, j);//j来做
                fNoRepeat(str, i + 1);//j来做排头之后,再去看i+1--N-1剩下的排列情况--别写错了哦
                //恢复下场,探索新的别的位置做排头可能性--因为str的i位置刚刚被上一种j换了
                swap(str, j, i);
            }
        }
    }
    //调用:
    public static void noRepeatprintAllPermutation(String s){
        if (s == null || s.length() == 0) return;

        //从0位置开始决定,谁做排头,打印
        char[] str = s.toCharArray();
        fNoRepeat(str, 0);
    }

    public static void test3(){
        String s = "aac";
        ArrayList<String> res = printPerNoRepeat2(s);
        for (String s1:res){
            System.out.print(s1 +" ");
        }

        System.out.println("\n复习打印不重复排列");
        noRepeatprintAllPermutation(s);
    }

    public static void main(String[] args) {
//        test();
//        test2();
        test3();
    }

结果:

aac aca caa 
复习打印不重复排列
aac aca caa 

这种剪枝,提前就把不用交换的可能性排除了,后续的情况,也都阻止了,所以加速了算法。


总结

提示:重要经验:

1)全排列,是每次公用一个str,交换不同j=i–N-1的字符做排头,每次做完排头要恢复现场,才能保证其他的可能性出现。
2)如果要字面不重复的全排列,就要提前剪枝,这样保证出现过的排头字符,有j相同时,不要交换了,提前扼杀这种字面重复的可能性
4)如果只需要控制a–z字符的分支,那就可以通过数组arr的0–25来表示a–z,用str[j]-'a’来索引,这个技巧经常用的。
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值