全组合与全排列

问题

给定一个字符串(或者数组),列举出其所有子序列字符串以及所有元素的全排列,并均按指定的排序规则输出。

排序规则:
指标一:较短的字符串排在较长的字符串之前,即按字符串长度升序排列
指标二:长度相同的字符串按照字典顺序排列


输入样例

bca


输出样例

[a, b, c, ba, bc, ca, bca]
[abc, acb, bac, bca, cab, cba]


分析

求排列组合是笔试中常考的经典问题,都可以用循环和递归来实现。这种问题的关键是理解其算法思想和操作技巧,做到不重复且不遗漏地列举出所有的组合模式。本文对上述两个问题分别采用较易理解的循环和递归方式实现,这里需要用到多指标排序,正好也可以练习下其用法。
P.S. 最近阿里内推面试就问到了相关问题。


全组合

/**
 * 
 * 求一个字符串不同子串的所有组合,要求结果按照字典顺序排列且组合中的字符保持在原字符串中的相对顺序
 * 
 * 算法思想:
 * 字符的组合不存在字符的先后顺序问题,所以"ab"和"ba"属于同一种组合。
 * 一个字符串所有子串的个数为组合数C(n,1)+C(n,2)+...+C(n,n)=2^n-1,其中n为字符总个数。
 * n个二进制位能表示2^n-1种不同的模式,每一个二进制串正好对应字符串的一种组合模式,即
 * 第i个1(0)表示取(不取)字符串中的第i个字符。
 * 
 */
public List<String> combinationOf(String str) {
    Set<String> set = new HashSet<>();

    final char[] word = str.toCharArray();// 字符串转成字符数组
    final int n = (int) Math.pow(2, str.length());// 总的排列种数为2^str.length()-1

    for (int i = 1; i < n; ++i) {
        StringBuffer sb = new StringBuffer();
        // template为word在不同组合模式下对应的二进制串
        for (int j = 0, template = i; template > 0; ++j, template >>= 1) {
            // 如果该二进制位为1,则取出对应的字母组成一个排列
            if ((template & 1) == 1)
                sb.append(word[j]);
        }
        set.add(sb.toString());// 去除所有组合中的重复
    }

    //集合转数组,不支持将Object[]数组直接强转为String[]
    //String[] a = (String[])set.toArray();
    String[] a = set.toArray(new String[set.size()]);//集合转数组

    //多指标排序方案一:给sort方法传递一个comparator,先按长度升序排列,再结合compareTo方法按字典顺序排列
    Arrays.sort(a, new Comparator<String>() {
        public int compare(String o1, String o2) {
            if (o1.length() < o2.length())
                return -1;
            else if (o1.length() > o2.length())
                return 1;
            else
                return o1.compareTo(o2);
        }
    });

    // 多指标排序方案二:使用稳定排序算法,比如冒泡排序、插入排序等
    for (int i = a.length - 1; i > 0; --i) {
        for (int j = 0; j < i; ++j) {
            if (a[j].length() > a[j + 1].length())//先按长度排序
                MyUtil.swap(a, j, j + 1);
            else if (a[j].length() == a[j + 1].length()) {
                if (a[j].compareTo(a[j + 1]) > 0)//再按字典顺序排序
                    MyUtil.swap(a, j, j + 1);
            }
        }
    }

    //数组转集合,注意Arrays.asList()方法返回一个受指定数组支持的固定大小的列表,不支持Add、Remove操作!
    //return Arrays.asList(a);
    return new ArrayList<>(Arrays.asList(a));
}

全排列

/**
 * 
 * 求一个字符串中所有字符的全排列,要求结果按照字典顺序排列
 * 
 * 算法思想:
 * 以求字符串"abc..."的全排列为例,首先固定第一个字符a,求后面所有字符的全排列,
 * 每一轮循环中当字符交换到了字符串尾时,此时的字符串就是一种排列模式,记录之。
 * 然后,把第一个字符a和第二个字符b交换,固定第一个字符b求后面所有字符的全排列。
 * 注意在第一轮循环求a后面所有字符的全排列的过程中,已经把所有字符的顺序打乱了,
 * 在第二轮循环求b后面所有字符的全排列之前需要把字符串的顺序还原。
 * 第三轮循环即是把c交换到字符串首位,重复上述步骤,循环结束之后就得到了所有的排列。
 * 
 */
public List<String> permutationOf(String str) {
    char[] a = str.toCharArray();
    List<String> list = new ArrayList<>();
    handler(a , 0, list);
    Collections.sort(list);//按照字典顺序排列
    return list;
}

//递归法求全排列
public void handler(char[] a , int k, List<String> list) {
    if (k == a.length - 1)
        list.add(new String(a));//记录当前排列得到的字符串
    for (int i = k; i < a.length; i++) {
        if (i != k && (a[k] == a[i]))//如果k和i是相同字符,跳过
            continue;
        MyUtil.swapChar(a, i, k);//交换
        handler(a, k + 1, list);//得到除首位字符之外的所有字符的全排列
        MyUtil.swapChar(a, i, k);//还原
    }
}

工具类

public class MyUtil {

    // 交换数组中的两个对象类型元素
    public static <T> void swap(T[] a, int i, int j) {
        T temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    // 交换数组中的两个整型元素
    public static void swapInt(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    // 交换数组中的两个字符元素
    public static void swapChar(char[] a, int i, int j) {
        char temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

说明

  • 求全组合的代码中有一句关键代码sb.append(word[j]),我们的算法是根据template中的二进制位从word数组中取字符的,所以template中每个二进制位和word中的相应位置元素是一一对应的,而对template中二进制位的遍历方向是从右往左的,那么对应的从word数组中取字符也应该按照从右到左的顺序取,可是代码里j是从0开始递增的,为什么结果却是正确的? 这里可不可以把append方法改成insert方法?什么情况下才可以改?
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值