剑指Offer-34-把数组排成最小的数

题目

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

解析

此题的目的是如何找到这些数的所有的排列中最小的字典序的那一个排列。所以问题转化为如何找到字典序最小的排列。为了防止组成的数溢出,下面统一转换为字符串来做。

思路一

既然需要找到字典序最小的哪个排列,那么我们干脆一不做二不休,我求出所有的排列组合,然后排序后取最小不就得了。进一步转化问题为全排列问题,只不过参与排列的元素为字符串了,而不在是单纯的单个字符。
求全排列的算法我已经在这篇博客写的很清楚了。以下内容为那篇博客里最优的求全排列的解法,包含如何对于重复元素的处理防止不必要的处理操作。
可以把排列问题分成固定第一个位置,剩余元素全排列问题。之后对剩余元素又进行同样的处理,固定第一个位置,剩余元素全排列。如图:
这里写图片描述
1,2,3的全排列问题,可以看做1与23的全排列,2与13的全排列,3与21的全排列的总和。也就是我们可以把原始排列问题划分为2部分,第一部分固定,第二部分为剩余元素全排列。所以只要让第一部分不断与后面的交换元素,然后继续处理当前新组成第二部分的全排列问题即可。而我们求剩余元素的全排列的时候,又可以按下面划分为2部分继续搞,直到剩余的元素个数为1,那么当前组成一个排列。这时向上回溯即可,开始更新各个子问题的第一部分的元素,继续处理新的排列问题。
能不能优化一下,不产生多余的排列呢。
第一个想法可能就是遇到与第一部分元素相等,就不交换。比如abb, 第一个位置可分别于第二个,第三个位置交换,因为他们都不与a相同,得到 abb, bab, bba。 考察第二位置时,对于bab,第二位置会与第三个位置交换,得到bba。而bba已在与第一位置交换的过程中出现过了。所以单纯的看交换元素是否相等是不行的。
我们发现导致有重复的出现的原因为,已经有b在第一个位置出现过了,不能在有相同的元素交换到此位置上,即不能有重复的元素作为排列问题的第一部分,这肯定会导致重复的子问题产生。所以对于每一个位置遍历,我们添加一个set用于记录以在该位置出现过的元素。
对于得到的全排列进行排序,取第一个就是我们所要的字典序最小的排列。

    public static String PrintMinNumber(int [] numbers) {
        if(numbers == null || numbers.length == 0) {
            return "";
        }
        List<String> result = new ArrayList<>();
        getAllCombine(0, numbers, result);
        Collections.sort(result);
        return result.get(0);
    }

    private static void getAllCombine(int index, int[] numbers, List<String> result) {
        if(index == numbers.length - 1) {
            StringBuilder sb = new StringBuilder();
            for(int num : numbers) {
                sb.append(num);
            }
            result.add(sb.toString());
            return;
        }
        //记录已经出现第一部分的元素
        Set<Integer> ocur = new HashSet<>();
        for(int i = index; i < numbers.length; i++) {
            if(!(i != index && ocur.contains(numbers[i]))) {
                ocur.add(numbers[i]);
                swap(numbers, i, index);
                getAllCombine(index + 1, numbers, result);
                swap(numbers, i, index);
            }
        }
    }

    public static void swap(int[] temp, int i, int j) {
        if(i != j) {
            int str = temp[i];
            temp[i] = temp[j];
            temp[j] = str;
        }
    }
思路二

思路一的复杂度为O(n!),那么我们能不能不进行全排列就能找到那个特定的排列呢?很容易想到就是排序,如果能想到一个很好的排序策略,即可优雅得到最后的结果。
对于给定的2个数,a和b。如何确定两者之间的排序策略呢?我们可以发现这两者的排列为:ab,ba。我们最终目的是找到字典序最小的那个排列,所以我们肯定应该保持这种关系,从而决定是否交换顺序:
1. 当ab < ba, a排在b的左边
2. 当ab > ba, b排在a的左边
3. 当ab = ba, 位置关系随意

不理解的话,可以结合选择排序算法来理解,我们在n个元素选择出最小的作为左边的值,显然它应该位于所有n-1个元素的左边,也就是第一个位置上。接着在n-1个元素找到次最小,反复如此,直到没有剩余元素。
如果你理解了选择排序的做法,又因为一般的排序算法的结果都是一样,所以我们可以采用更快的排序算法来解答此题,为了方便,直接了使用了库函数,当然你可以自行实现快速排序或归并排序,这些都未尝不可。

    public static String PrintMinNumber2(int [] numbers) {
        if(numbers == null || numbers.length == 0) {
            return "";
        }
        String[] strNumbers = new String[numbers.length];
        for(int i = 0; i < numbers.length; i++) {
            strNumbers[i] = String.valueOf(numbers[i]);
        }
        Arrays.sort(strNumbers, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return (o1 + o2).compareTo(o2 + o1);
            }
        });
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < strNumbers.length; i++) {
            sb.append(strNumbers[i]);
        }
        return sb.toString();
    }

总结

对于给定一组数,让求特定的排列时,这时肯定可以用全排列来做,只不过涉及到效率问题。进一步优化则可以往深处深究。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值