38. 字符串的排列

剑指 Offer 38. 字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入: s = “abc”
输出: [“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]

限制:
1 <= s 的长度 <= 8


解题思路

深度优先遍历和去重技巧的结合。

根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过字符摆放,先固定第 1 位字符( n 种情况)、再固定第 2 位字符( n-1种情况)、… 、最后固定第n 位字符( 1 种情况)。

说的通俗一点就是把n个数字放到n个框里,有多少种放法,对于每个框,我们都有n个数字,即n个选择,但我们要判断手中的数字是否已经放入其他框了,故需要用到一个标记数组
在这里插入图片描述
上图是没有重复字符的情况,如果出现了重复字符怎么办,这就涉及到去重了,有重复字符的情况如下图。

在这里插入图片描述

我们会发现abb’ab’bbab'b’abbb'ab’ba是重复的,为什么会出现这种情况?

我们可以先回忆一下初中就学过的掷骰子,掷两个骰子,出现(4,2)(2,4)是视为两种不同的情况的,但是出现(3,3)(3,3)则只视为一种情况,因为“第一个骰子点数是3,第二个骰子点数也是3”与“第二个骰子点数是3,第一个骰子点数也是3”是完全相同的结果。

引申到排列中,为了举例方便,这里用数字11'2345

  1. 第一个框放1,然后让1'2345五个数字全排列放在其余位置
  2. 第一个框放1',然后让12345五个数字全排列放在其余位置

不难发现,情况一和二得到的排列结果将一模一样。道理和掷骰子一样,不管是把1还是1'放第一个框,它们终归都是1,然后后面的框放的也同样都是12345的全排列,那么最终产生的所有排列结果的集合肯定是一样的。

由此为了去重,我们可以定义一个规则,即对于相同数,我们人为定序,如上例子中11'2345,我们指定重复数字首次要放入某一个框位时,只能放重复数字的第一个,如这里只能放1,放1'的情况直接过滤,其他框位同理。

Java代码

class Solution {

    private String[] res;
    //最开始并不知道res最终的长度会是多少,故先把排列结果都放到list中
    //之后转到res中即可
    private List<String> list = new ArrayList<>();

    public String[] permutation(String s) {
        if(s == null || s.length() == 0) return new String[0];
        boolean[] isVisited = new boolean[s.length()];

        //由于要去重,故得先排序,因此将String转为char数组再排序
        char[] chs = s.toCharArray();
        Arrays.sort(chs);

        dfs(chs,new StringBuilder(),isVisited);

        //将结果放到数组中,因为返回结果需要的是String数组
        res = new String[list.size()];
        for(int i = 0;i < list.size();i++){
            res[i] = list.get(i);
        }
        return res;

    }

    public void dfs(char[] chs,StringBuilder sb,boolean[] isVisited){
        if(sb.length() == chs.length){
            list.add(new StringBuilder(sb.toString()).toString());
            return ;
        }
        for(int i = 0;i < chs.length;i++){
            //全排列算法去重条件模板
            //如果这个字符和前一个字符相同且前一个字符还没有用过,continue
            //即我们规定重复字符首次要放入某一个框位时,只能放重复字符的第一个
            //比如a,a',b是可以的,但是a',a,b和其重复,应该去重,故当要达到产生a',a,b那一层递归栈时
            //由于a'与前一个字符a相同,且a还没有用过,所以continue,这刚好去重,是我们想要的结果
            if(i > 0 && chs[i] == chs[i - 1] && !isVisited[i - 1]) continue;
            if(!isVisited[i]){
                sb.append(chs[i]);
                isVisited[i] = true;
                dfs(chs,sb,isVisited);
                //恢复现场
                sb.deleteCharAt(sb.length() - 1);
                isVisited[i] = false;
            }
        }
    }
}

在这里插入图片描述


这里的去重对于刚接触的同学来说确实不太好理解(我刚开始学的时候也绕了一天,绕明白后就感觉是常识了。。。),为了更好的理解,下面来两道扩展题,尤其是第二道题,实在没想明白去重时,可以跟着第二题的代码模拟一下代码执行过程,说不定突然理解了,我当时貌似就是这样绕过来的。

扩展题一:LeetCode 46. 全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

解题思路

深度优先即可,可以理解成把n个数字放到n个框里,有多少种放法,对于每个框,我们都有n个数字,即n个选择,但我们要判断手中的数字是否已经放入其他框了,故用到一个标记数组。

Java代码

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if(nums == null) return res;
        boolean[] isVisited = new boolean[nums.length];
        dfs(res,new ArrayList<>(),nums,isVisited);
        return res;
    }
    
   
    private void dfs(List<List<Integer>> res,List<Integer> list,int[] nums,boolean[] isVisited){
        //总共就nums.length个框,当list.size() = nums.length时,说明手中数字都放完了
        if(list.size() == nums.length){
            res.add(new ArrayList<>(list));//添加的是list副本
            return;
        }
        for(int i = 0;i<nums.length;i++){
            if(!isVisited[i]){
                list.add(nums[i]);
                isVisited[i] = true;
                dfs(res,list,nums,isVisited);
                //回溯时,将list,isVisited还原为上一级栈的状态
                list.remove(list.size()-1);
                isVisited[i] = false;
            }
        }   
    }
}

在这里插入图片描述

Go 代码

46. 全排列


扩展题二:LeetCode 47. 全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入: nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]

示例 2:

输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

解题思路

与上题一样,但多个去重,去重方式看代码注释处注意这里去重得先排序(我之前自己有时也忘记了,然后在那里检查哪里出了错,0.0)。

Java代码

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if(nums == null) return res;
        
        Arrays.sort(nums);//要去重,故先排序
        
        boolean[] isVisited = new boolean[nums.length];
        dfs(res,new ArrayList<>(),nums,isVisited);
        return res;
    }
    
  
    private void dfs(List<List<Integer>> res,List<Integer> list,int[] nums,boolean[] isVisited){
        //总共就nums.length个框,当list.size() = nums.length时,说明手中数字都排完了
        if(list.size() == nums.length){
            res.add(new ArrayList<>(list));//添加的是list副本
            return;
        }
        for(int i = 0;i<nums.length;i++){
            //如果这个数和前一个数相同且前一个数还没有用过,continue
            //即我们规定重复数字首次要放入某一个框位时,只能放重复数字的第一个
            //比如1,1',2是可以的,但是1',1,2和其重复,应该去重,故当要达到产生1',1,2那一层递归栈时
            //由于1'与前一个数1相同,且1还没有用过,所以continue,这刚好去重,是我们想要的结果
            if(i>0 && nums[i] == nums[i-1] && !isVisited[i-1]) continue;
            if(!isVisited[i]){
                list.add(nums[i]);
                isVisited[i] = true;
                dfs(res,list,nums,isVisited);
                //回溯时,将list,isVisited还原为上一级栈的状态
                list.remove(list.size()-1);
                isVisited[i] = false;
            }
        }   
    }
}

在这里插入图片描述

Go代码

47. 全排列 II

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值