剑指Offer_编程题——把数组排成最小的数
题目描述:
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3, 32, 321},则打印出这三个数字能排成的最小数字为321323
具体要求:
时间限制: C/C++ 1秒,其他语言2秒
空间限制: C/C++32M,其他语言64M
具体思路:
背景知识介绍
在本题中会用到全排列的知识,我们可以去*中得到全排列生成算法。全排列生成算法是指将给定的序列中所有可能的全排列无重复无遗漏地枚举出来。此处全排列的定义是:从n个元素中取出m个元素进行排列,当n=m时这个排列被称为全排列。字典序、邻位对换法、循环左移法、循环右移法、递增进位制法、递减进位制法都是常见的全排列生成算法。
字典序法就是将元素按照字典的顺序(a-z, 1-9)进行排列。以字典的顺序作为比较的依据,可以比较出两个串的大小。比如 “1” < “13”<“14”<“153”, 就是按每个数字位逐个比较的结果。对于一个串“123456789”, 可以知道最小的串是“123456789”,而最大的串“987654321”。这样针对这个串以字典序法生成全排列生成全排列,就是依次生成“123456789”->“123456798”->…->“987654312”->"987654321"这样的串。字典序法要求这一个与下一个有尽可能长的共同前缀,也即变化限制在尽可能短的后缀上。
插入法:如果已知n-1个元素的排列,将n插入到排列的不同位置,就得到了n个元素的排列。用这种方法可以产生出任意n个元素的排列。这个方法有一个缺点:为了产生n个元素的排列,我们必须知道并存储所有n-1个元素的排列,然后才能产生出所有n阶排列。
邻位对换法: 该算法由Johnson-Trotter首先提出,是一个能快速生成全排列的算法。它的下一个全排列总是上一个全排列对换某相邻两位得到的。
具体实现一
此题的目的是如何找到这些数的所有的排列中最小的字典序的那一个排列。所以问题转化为如何找到字典序最小的排列。为了防止组成的数溢出,我们可以将数字转换为字符串来做。
根据以上我们介绍全排列的相关知识。既然需要找到字典序最小的哪个排列,我们可以求出所有的排列组合,然后排序后取最小不就得了。进一步转化问题为全排列问题,只不过参与排列的元素为字符串了,而不在是单纯的单个字符。可以把排列问题分成固定第一个位置,剩余元素全排列问题。之后对剩余元素又进行同样的处理,固定第一个位置,剩余元素全排列。具体操作如图所示:
1,2,3的全排列问题,可以看做1与23的全排列,2与13的全排列,3与21的全排列的总和。也就是我们可以把原始排列问题划分为2部分,第一部分固定,第二部分为剩余元素全排列。所以只要让第一部分不断与后面的交换元素,然后继续处理当前新组成第二部分的全排列问题即可。而我们求剩余元素的全排列的时候,又可以按下面划分为2部分继续搞,直到剩余的元素个数为1,那么当前组成一个排列。这时向上回溯即可,开始更新各个子问题的第一部分的元素,继续处理新的排列问题。我们是否可以优化一下,不产生多余的排列呢。
第一个想法可能就是遇到与第一部分元素相等,就不交换。比如abb, 第一个位置可分别于第二个,第三个位置交换,因为他们都不与a相同,得到 abb, bab, bba。 考察第二位置时,对于bab,第二位置会与第三个位置交换,得到bba。而bba已在与第一位置交换的过程中出现过了。所以单纯的看交换元素是否相等是不行的。
我们发现导致有重复的出现的原因为,已经有b在第一个位置出现过了,不能在有相同的元素交换到此位置上,即不能有重复的元素作为排列问题的第一部分,这肯定会导致重复的子问题产生。所以对于每一个位置遍历,我们添加一个set用于记录以在该位置出现过的元素。对于得到的全排列进行排序,取第一个就是我们所要的字典序最小的排列。接下来我们可以用java将其思路实现。
import java.util.*;
public class Solution{
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个元素找到次最小,反复如此,直到没有剩余元素。又因为一般的排序算法的结果都是一样,所以我们可以采用更快的排序算法来解答此题,为了方便,直接了使用了库函数,当然,如果你可以自行实现快速排序或归并排序,这些都未尝不可。具体用java实现该思路。
import java.util.*;
public class Solution{
public static String PrintMinNumber(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();
}
}
代码效果图如图所示:
具体实现二:
数组中每个数的长度可能不一致,这是排序的难点,设想每个数长度都一致,那么排序后直接将小的数放前面,大的数放后面就行了。所以一个想法是将数组中所有数都变成同一长度,例如将题目中数组的每个数都变成三位数。现在的问题是一位数如何变成三位数。比如3变成3xx,后面两位如何确定?我们可以从结果推回来,已知3排在32后面,所以3xx比32x要大,如果数组中有一个35,那么3应该排在35前面,所以3xx比35x要小,这时我们可以大胆的猜想3xx就是333。进一步,我们猜想所有后面补齐的数字就是该数的最后一位。将补齐后的数字和原数字组合为一个集合,存储到列表中,然后对列表排序,最后依序取出原数字即可。具体我们用python将其实现。
class Solution:
def PrintMinNumber(self, numbers):
# write code here
if not numbers:
return ''
lst = []
max_len = len(str(max(numbers)))
for i in range(len(numbers)):
digit = numbers[i] % 10
key = str(numbers[i]) + (max_len-len(str(numbers[i])))*str(digit)
lst.append((key, numbers[i]))
results = ''
lst = sorted(lst, key=lambda x: x[0])
for key, val in lst:
results += str(val)
return int(results)
代码效果图如图所示:
具体实现三
比较两个字符串s1, s2大小的时候,先将它们拼接起来,比较s1+s2,和s2+s1那个大,如果s1+s2大,那说明s2应该放前面,所以按这个规则,s2就应该排在s1前面。在python中可以直接对拼接后的字符串进行大小比较。此处要注意,在py3中要调用该关键字必须要import functools库里面的cmp_to_key。具体实现如下:
from functools import cmp_to_key
class Solution:
def cmp(self, a, b):
if a + b > b + a:
return 1
elif a + b < b + a:
return -1
else:
return 0
def PrintMinNumber(self, numbers):
if not numbers:
return ""
number = list(map(str, numbers))
number.sort(key=cmp_to_key(self.cmp))
return "".join(number).lstrip('0') or '0'
代码效果图如图所示:
具体实现四
其实有一个最简单的做法,把要比较的两个数字进行不同顺序的前后拼接,进行大小比较。按拼接次序进行排序得到的数字串变成str后数字是最小的。具体用python实现如下:
class Solution:
def PrintMinNumber(self, numbers):
return "".join(map(str, sorted(numbers, cmp = lambda a, b : cmp(str(a) + str(b), str(b) + str(a)))))
代码效果图如图所示:
总结
本道题主要是通过数组考察字符串的拼接以及数字与字符串的相互转换,另外,也考排序,可以用任何一种排序。因此,我们用了四种方法来解决此题,其中第一种是用到了全排序,在实现思路之前,我们具体给大家普及了全排序的相关知识,在写完代码之后尽管能通过案例,但是其时间复杂度为O(n!),比较高,因此我们将其全排序改为了其他的排序算法,发现时间复杂度立马就降下来了。接着我们又用到了python中的库函数以及一个最巧妙的以及被证明了的结论用一行实现了该题。因此,我们在做题的时候,应该多次尝试各种方法,扩展自己的思维,写出优质的代码。总之,我们要继续加油,争取早日找到工作,Good Luck!!!
参考文献
[1] 全排列生成算法
[2] Special__Yang
[3] qq_27668313
[4] 负雪明烛
[5] Juanly Jack