《剑指Offer》Java刷题 NO.27 字符串的排列(全排列、去重、字典序)

《剑指Offer》Java刷题 NO.27 字符串的排列(全排列、去重、字典序)

传送门:《剑指Offer刷题总目录》

时间:2020-03-30
题目:
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
输入描述:
输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。


什么是字典序?
1.自然排序vs字典序vs字符串排序
自然排序就是字典序排序,不分大小写。字符串排序要分大小写的,就是大小写也要先后顺序。
2.单个字符vs多个字符(字符串)

'0' < '1' < '2' < ... < '9' < 'a' < 'b' < ... < 'z'

两个字符串比较大小
是按照从左到右的顺序进行比较,如果第1位相等,就比较第2位,直至有一位可以比较出大小来,则不再继续比较。

'ab' < 'ac''abc' < 'ac''abc' < 'abcd'

思路:全排列问题
1.递归算法(得出结果之后还要再进行一次排序,效率不高)
<1>基本思想是用第一个字符和后面的每个字符交换,然后固定第一个字符,用第二个字符和后面的每个字符交换,以此类推的递归;
<2>那么排列次数就是除了第一个字符以外其他字符全排quan列的次数加一;
<3>递归的出口就是子字符串只剩一个字符的时候
(图源:牛客网)
在这里插入图片描述
但是对于字符串中有重复字符的情况就需要特殊处理,比如ABBCD,两个相同的字符和子序列最左字符交换的效果是相同的,都是把同一个字符固定在最左边然后对剩下的做全排列:B1(AB2CD)和B2(AB1CD)是相同的,所以这里采用:
方案1.从第一个数字起,每个数分别与它后面非重复出现的数字交换。

  • 用HashSet保存每一轮递归时的字符,没出现过的才交换字符位置并把字符放进HashSet
  • 该方法得出结果之后需要再用Collections.sort()【很耗时】 排序以得到字典序的全排列
    方案2:先用TreeSet临时保存得到的String,可以保证排序和去重。
    (TreeSet可以理解成数组实现的二叉搜索树的容器,可以直接用addAll添加到ArrayList中)
    注意点: 递归回到上一层时要先把字符串复原(两个字符交换回来,回溯)再进行下一次交换;
    比较: TreeSet的add和addAll方法比较耗时,总体来讲两个方案差别不大
    2.字典序全排列(从算法流程中能看出重复元素不影响排序)【效率较高】
    寻找下一个排列的方法: (eg:当前排列为358754
    复杂度为:O(n)+O(n)+O(n)=O(n)
    总的时间复杂度为O(n*n!)
    若是从小到大的字典序,我们希望下一个(肯定比当前值要大)和当前值的差值尽量小,并且两个之间不可能再存在其他值
  1. 从右向左找左邻值小于当前值的那一位(如果左邻大于当前值,根本不可能在该段内使得数字更大,所以继续往左找),左邻的索引记为a,值记为A;此时A的右边肯定是从左往右依次减小的;如果找到最前面也没找到,就说明当前值是最后一个排列,已经是最大值了
  2. 然后从右往左找第一个比A大的值,记为B
  3. 交换AB378554(还没完)(此时a后面的数依旧是从大到小排列的)
    4.接下来应该把a右边的数字再从小到大排列:374558(其实只需要倒序一下即可)

Java代码

import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

/**
 * @ClassName Permutation
 * @Discription 输入一个字符串, 按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出
 * 由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
 * @Author lemon
 * @Date 2020/3/18 21:03
 **/
public class Permutation {

    /**
     *方法一:递归法(回溯法)
     * 利用Collections.sort排序(效率较低)
     */
    public ArrayList<String> permutationOne(String str) {
        ArrayList<String> result = new ArrayList<>();
        //判断str是否为空
        if (str == null || str.length() == 0) {
            return result;
        }
        permutationOneHelper(str.toCharArray(), 0, result);
        //Collections.sort底层调用Arrays.sort,得到自然排序也就是字典序的list
        Collections.sort(result);
        return result;

    }

    private void permutationOneHelper(char[] chars, int index, ArrayList<String> result) {
        //递归出口,子字符串只剩最后一个字符,直接把字符串加入result
        if (index == chars.length - 1) {
            result.add(String.valueOf(chars));
        } else {
            Set<Character> charSet = new HashSet<>();
            for (int j = index; j < chars.length; j++) {
                //只对从未出现过的(非重复)字符进行交换,比list.contains效率高
                if (!charSet.contains(chars[j])) {
                    charSet.add(chars[j]);
                    swap(chars, index, j);
                    //对剩下的子字符串进行递归
                    permutationOneHelper(chars, index + 1, result);
                    //需要把字符串复原再进行下一次字符交换
                    swap(chars, j, index);
                }
            }
        }
    }

    /**
     *交换两个字符
     */
    private void swap(char[] chars, int i, int j) {
        char temp = chars[i];
        chars[i] = chars[j];
        chars[j] = temp;
    }

    /**
     * 按格式打印ArrayList
     */
    private void printArrayListOfString(ArrayList<String> list) {
        StringBuilder data = new StringBuilder();
        data.append("[");
        for (String str : list) {
            data.append(str);
            data.append(",");
        }
        data.deleteCharAt(data.lastIndexOf(","));
        data.append("]");
        System.out.println(data.toString());
    }

    /**
     *方法一改进版,利用TreeSet暂存
     */
    public ArrayList<String> permutationTwo(String str) {
        ArrayList<String> result = new ArrayList<>();
        TreeSet<String> temp = new TreeSet<>();
        //判断str是否为空
        if (str == null || str.length() == 0) {
            return result;
        }
        permutationTwoHelper(str.toCharArray(),0,temp);
        result.addAll(temp);
        return result;
    }

    private void permutationTwoHelper(char[] chars, int index, TreeSet<String> result) {
        //递归出口,子字符串只剩最后一个字符,直接把字符串加入result
        if (index == chars.length - 1) {
            result.add(String.valueOf(chars));
        } else {
            Set<Character> charSet = new HashSet<>();
            for (int j = index; j < chars.length; j++) {
                //只对从未出现过的(非重复)字符进行交换,比list.contains效率高
                if (!charSet.contains(chars[j])) {
                    charSet.add(chars[j]);
                    swap(chars, index, j);
                    //对剩下的子字符串进行递归
                    permutationTwoHelper(chars, index + 1, result);
                    //需要把字符串复原再进行下一次字符交换
                    swap(chars, j, index);
                }
            }
        }
    }

    /**
     *方法二:字典序全排列
     */
    public ArrayList<String> permutationThree(String str){
        ArrayList<String> result = new ArrayList<>();
        if(str == null || str.length() == 0){
            return result;
        }
        char[] chars = str.toCharArray();
        int length = str.length();
        //Arrays.sort能得到第一个字符串,然后以此为基础往下找
        Arrays.sort(chars);
        String next = new String(chars);
        result.add(next);
        while(true){
             next = findNextString(next,length);
             //如果已经完成了就退出循环
            if(next.equals("done")){break;}
            result.add(next);
        }
        return result;
    }

    /**
     *返回下一个排列的String
     */
    private String findNextString(String str,int length){
        char[] chars = str.toCharArray();
        //从右往左找左邻比本身大的元素,i为该左邻元素的索引值
        int i = length - 2;
        for(;i>=0 && chars[i]>=chars[i+1] ;i--){}
        //找到最前面也没找到,说明当前值为最大的,可以结束了
        if(i == -1){
            return "done";
        }
        //从右往左找第一个比chars[i]大的元素
        int j = length - 1;
        for(;chars[j] <= chars[i];j--){}
        //交换
        swap(chars,i,j);
        //逆序i后面的序列
        for(int left = i+1,right = length - 1;left < right;left++,right--){
            swap(chars,left,right);
        }
        return String.valueOf(chars);
    }


    public static void main(String[] args) {
        Permutation permutation = new Permutation();
        permutation.permutationOne("");
        permutation.permutationTwo("");

        permutation.permutationOne("a");
        permutation.permutationTwo("a");

        permutation.permutationOne("aa");
        permutation.permutationTwo("aa");

        permutation.permutationOne("abac");
        permutation.permutationTwo("abac");

        long s = Clock.systemDefaultZone().millis();
        permutation.permutationOne("futgehatfgw");
        System.out.println("permutationOne耗时: " + (Clock.systemDefaultZone().millis() - s) + " ms");

        s = Clock.systemDefaultZone().millis();
        permutation.permutationTwo("futgehatfgw");
        System.out.println("permutationTwo耗时: " + (Clock.systemDefaultZone().millis() - s) + " ms");
        s = Clock.systemDefaultZone().millis();
        permutation.permutationThree("futgehatfgw");
        System.out.println("permutationThree耗时: " + (Clock.systemDefaultZone().millis() - s) + " ms");
       /* permutationOne耗时: 4312 ms
        其中Collections.sort耗时2500ms左右
        permutationTwo耗时: 3553 ms
        permutationThree耗时: 883 ms*/
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值