跳至主要內容

38-字符串的排列

daipeng大约 4 分钟

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

全排列是组合数学中的很重要的知识,很多问题的解决都有赖于组合排列。

全排列可以有两种解法。

递归解法

​ 递归的解法很好解释,代码也很简短,但不好理解。和《盗梦空间》里的多层嵌套梦境类似,递归计算时是一层一层的,计算时需要从最高层往最底层逐层推进,但合并结果时往往需要从最底层往最高层逐层合并,往往我的内心是崩溃和拒绝的,这都是什么乱七八糟的玩意!

不过仔细研究透了一个递归解法后,会被递归的简洁和美所震撼,大道至简啊大兄弟!

字符串
字符串

递归解法说起来很简单,固定第一个字母,然后对剩余字母进行全排列,这样会得到所以以第一个字母为首字母的全排列。然后将第一个字母和第二个字母交换,这样以第二个字母为首字母,对剩余字母进行全排列,这样会得到以第二个字母为首字母的全排列,然后依次这般这般....

我们来看看代码。

   private  ArrayList<String> result = new ArrayList<>(); 
// a b c
    public ArrayList<String> Permutation(String str) {
        if (str == null || str.isEmpty()) {
            return result;
        }
        char[] chs = str.toCharArray();
        permutation(chs, 0);
        return result;
    }
    private void permutation(char[] chs, int start) {
        if (start >= chs.length ) {
            result.add(String.valueOf(chs));
        }
        for (int i = start; i < chs.length; i++) {
            //将第一个字符(start)与第i个交换,
            Utils.swap(chs,start,i);
            permutation(chs, start+1);
            //将第一个字符(start)与第i个再交换回来 否则会错乱
            Utils.swap(chs, i, start);
        }
    }

是不是很简洁?

字典序解法

字典序的解法理解上会简单些。

同位数的字符串的先后是指从左到右依次比较对应位上字符的先后。

比如 a b c < b a c。

对于一个字符串,我们可以依次找到下一个字符串,下下一个字符串...... 一直到最大的字符串。

下一个串是指刚刚好大于当前的串。

比如给定

1 2 5 4 3

如何找到下一个串呢?

对于一个字符串,顺位排列是最小的,逆序排列是最大的。比如 1 2 3 是最小的,3 2 1是最大的。

因为低位的权重小,所以我们先从低位看起,5 4 3 是[3 4 5]这三个数字排列的最大值,所以要得到下一个串,需要往高位找一位,我们找到2,2需要和5 4 3里的最小的3换个位置,这样我们得到了 1 3 5 4 2,然后我们需要将5 4 2变成最小的,也就是顺位 2 4 5。

好了,这样我们就得到了1 2 5 4 3的下一个串了。

我们总结下:

  1. 找到分割点,这个点是指从右到左遍历时,第一个大于它左边的元素。比如 1 2 3 4 5,第一个大于左边的就是5。有些博客上写的是逆序区。
  2. 将分割点左一个元素p和分割点右侧的刚好大于p的元素进行交换
  3. 将分割点以及其右边的元素排序为顺序。

举例来说。

1 2 3 4 5
  1. 从右向左遍历,5>4 分割点为5
  2. 将分割点左侧的第一个元素4与分割点及其右侧的元素中刚好大于4的元素交换,1 2 3 5 4
  3. 将分割点及其右侧元素排成顺序 1 2 3 5 4
1 2 5 4 3
  1. 分割点为 5
  2. 将2 与 5 4 3中刚好大于2的元素3交换 1 3 5 4 2
  3. 将 5 4 2顺序排列 2 4 5

过程知道了,代码就很简单了。三段论就可以了。

public ArrayList<String> Permutation2(String str) {
        if (str == null || str.isEmpty()) {
            return result1;
        }
        char[] chs = str.toCharArray();
        int border;
        result1.add(String.valueOf(chs));
        while ((border = findBorder(chs)) != -1) {
            int exchangePoint = findExchangeElement(chs, border - 1);
            swap(chs, exchangePoint, border - 1);
            swap(chs, border);
            result1.add(String.valueOf(chs));
        }
        return result1;

    }
	//将分割点及其右侧元素排成顺序 因为其已经是逆序的了所以两两交换即可。
    private void swap(char[] chs, int border) {
        int hi = chs.length - 1;
        while (border <= hi) {
            swap(chs, border++, hi--);
        }

    }
	//找到交换元素位置 即第一个刚好大于分割点左一元素的元素
    private int findExchangeElement(char[] chs, int p) {
        int hi = chs.length - 1;
        for (int i = hi; i > 0; i--) {
            if (chs[p] < chs[i]) {
                return i;
            }
        }
        return -1;
    }
	//找到分割点 	
    private int findBorder(char[] chs) {
        int hi = chs.length - 1;
        for (int i = hi; i > 0; i--) {
            //从右往左 第一个chs[i] > chs[i-1]的元素
            if (chs[i] > chs[i - 1]) {
                return i;
            }
        }
        return -1;
    }
 	public static void swap(char[] arr, int i, int j){
        char tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }