题目描述
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
这道题最直观的方法就是先求出这个数组中所有数字的排列,然后把每个排列拼起来,最后求出拼起来的数字的最小值。求数组的排列和前面发布的面试题38“字符串的排列”非常类似,但是根据前面提到的只是,N个数总共有N!个排列,因此不得不寻找更快的方法来解决问题。
这道题其实很简单,就是一个找排序规律的题,当数组根据这个规则排序之后能排成一个最小数字。要确定排序规则,就要比较两个数字,也就是给出两个数字m和n,接下来则需要确定一个规则判断m和n哪个应该排在前面,而不是仅仅比较这两个数字的值哪个大。
根据题目的要求,两个数字m和n能拼接成数字mn和nm,如果mn < nm,那么应该打印mn,也就是m应该排在n的前面,我们定义此时m小于n;如果mn > nm,那么应该打印nm,我们定义n小于m;如果mn = nm,则n等于m。在下文中,符号“>”、“<”、“=”表示常规定义的数值的大小关系,而文字“大于”、“小于”、“等于”表示我们新定义的大小关系。
接下来就是拼接数字,即给出数字m和n,怎么得到数字nm和mn并比较他们的大小。直接用数值去计算不难办到,但需要考虑的一个潜规则就是m、n可能都是int类型的,但是连接起来之后,可能就会变成一个大数,所以这还是一个隐藏的大数问题。
所以在此问题中,把数字转化为字符串。另外,由于把数字拼接起来后的nm和mn都是一样长的,所以可以直接按照字符串的比较规则来比较大小即可。
然后基于上述思路,有以下代码:
using System;
using System.Text;
namespace 把数组排成最小的数
{
class Program
{
static void Main(string[] args)
{
int[] numbers = new int[] { 3, 32, 111, 321, 5218, 1147 };
Solution s = new Solution();
Console.WriteLine(s.PrintMinNumber(numbers));
}
}
class Solution
{
public string PrintMinNumber(int[] numbers)
{
string result = string.Empty;
if (numbers == null || numbers.Length <= 0)
return result;
int length = numbers.Length;
string[] strNumbers = new string[length];
//把所有的数字转化为字符串
for (int i = 0; i < length; i++)
strNumbers[i] = numbers[i].ToString();
//按我们规定的规则进行排序
Array.Sort(strNumbers,Compare);
StringBuilder sb = new StringBuilder();
//把排好序的数组添加拼接起来
for (int i = 0; i < length; i++)
sb.Append(strNumbers[i]);
return sb.ToString();
}
/// <summary>
/// 定义的字符串比较的规则,把传入的两个字符串m、n拼接成两个字符串nm和mn,然后比较两个字符串大小,如果m>n返回1,n>m返回-1,相等返回0
/// </summary>
/// <param name="num1">传入的字符串1</param>
/// <param name="num2">传入的字符串2</param>
/// <returns>字符串1对于字符串2是大是小</returns>
private int Compare(string num1, string num2)
{
//字符串的拼接
num1=System.String.Concat(num1, num2);
num2= System.String.Concat(num2, num1);
return String.Compare(num1, num2);
}
}
}
在上述代码中,我们先把数组中的整数转换成字符串,然后在函数Compare中定义比较规则,并根据该规则调用库函数Sort进行排序,最后把排好的数字依次拼接起来,就得到了最小数字。这种思路的时间复杂度和Sort的时间复杂度相同,都是O(nlogn),这比最开始提及的时间n!的算法好得多。
在上述思路中,定义了一个新的比较两个数字大小的规则,这种规则是否有效 ?且我们只是定义了两个数字大小,但是用它来排列多个数字的数组是否合适?最终拼接数组中的所有数字得到的结果是不是真的为最小数字?如果需要严格证明,确保方案的正确性,那么请看下面:
首先,需要证明之前定义的两个数字大小的规则是否有效,一个有效的比较规则需要3个条件:
- 自反性
- 对称性
- 传递性
接下来分别予以证明:
- 自反性:
显然有aa=aa,所以a等于a。 - 对称性:
如果a小于b,则ab < ba,所以ba > ab,因此b大于a - 传递性:
如果a小于b,则ab < ba。假设a和b用十进制表示时分别有n位和m位,于是有ab= a*b^{10}+b,ba=b*10^n+a。
ab < ba –>
a * 10^m + b < b * 10^n + a –>
a * 10^m - a < b * 10^n - b –>
a * (10^m - 1) < b * (10^n - 1) –>
a / (10^n - 1) < b / ( 10^m - 1)
同理,如果b小于c,则bc < cb。假设c用十进制表示时有l位,和前面的证明过程一样,可以得到b / (10^m - 1) < c / ( 10^l - 1)。
a / (10^n - 1) < b / ( 10^m - 1) 并且b / (10^m - 1) < c / ( 10^l - 1)
–> a / (10^n - 1) < c / ( 10^l - 1)
–> a * (10^l - 1) < c * (10^n - 1)
–> a * 10^l + b < c * 10^n + a
–> ac < ca
–> a小于c
于是综上我们就证明了这种比较规则满足自反性,对称性,传递性,是一种有效的比较规则。接下来我们证明根据这种规则吧数组排序之后,把数组中所有的数字拼接起来得到的数字的确是最小的。直接证明不是很容易,不妨用反证法来做。
我们把n个数按照前面的规则排序之后,表示为A_1,A_2,A_3…….,A_n,假设这样拼接出来的数字并不是最小的,即至少存在两个x和y(0 < x < y < n),交换第x个数和第y个数后,A_1A_2A_3…A_y…A_x…A_n < A_1A_2A_3…A_x…A_y…A_n。
由于A_1A_2A_3…A_x…A_y…A_n是按照前面的规则拍好的序列,所以有A_x小于A_{x+1}小于A_{x+2}小于A_{x+3}……小于A_{y-1}小于A_y。
由于A_{y-1}小于A_y。在序列A_1A_2A_3…A_x…A_{y-1}A_y…A_n < A_1A_2A_3…A_x…A_yA_{y-1}…A_n 。就这样一直交换把A_y和前面的数字交换,知道和A_x交换为止。于是就有
A_1 A_2 A_3…A_x…A_{y-2} A_{y-1} A_y…A_n <
A_1 A_2 A_3…A_x…A_{y-2} A_y A_{y-1}…A_n <
A_1 A_2 A_3…A_x…A_y A_{y-2} A_{y-1}…A_n <
…… <
A_1 A_2 A_3…A_y A_x…A_{y-2} A_{y-1}…A_n
同理可得,由于A_x小于A_{x+1},因此A_x A_{x+1} < A_{x+1} A_x 。我们在序列A_1 A_2 A_3…A_y A_x A_{x+1} …A_{y-2} A_{y-1}…A_n 中只交换A_x和A_{x+1} ,有A_1 A_2 A_3…A_y A_x A_{x+1} …A_{y-2} A_{y-1}…A_n < A_1 A_2 A_3…A_y A_{x+1} A_x…A_{y-2} A_{y-1}…A_n 。接下来一直拿A_x和它后面的数字交换,知道和A_{y-1}交换为止。于是有:
A_1 A_2 A_3…A_y A_x A_{x+1}…A_{y-2} A_{y-1}…A_n <
A_1 A_2 A_3…A_y A_{x+1} A_x…A_{y-2} A_{y-1}…A_n <
A_1 A_2 A_3…A_y A_{x+1} A_{x+2}…A_{y-2} A_{y-1}A_x…A_n 。
所以A_1A_2A_3…A_x…A_y…A_n < A_1A_2A_3…A_y…A_x…A_n ,这和我们的假设A_1A_2A_3…A_y…A_x…A_n < A_1A_2A_3…A_x…A_y…A_n相矛盾。
所以假设不成立,我们的算法是正确的。
题外知识
做题时想到了字符串的优化,所以找了一点资料,对于字符串的拼接,其效率性能的优化:
1、使用string.Empty给一个空字符串变量赋初始值
String.Empty是一个指代,而””是具体的实现
string filter=“”;//不建议
string filter=string.Empty; //建议
2、使用str.Length == 0做空串比较
- 最快的方法:if (str.Length == 0)
- 其次:if (str == String.Empty)或 if (str == “”)
3、巧用StringBuilder进行字符串拼接操作
如果要构造一个较长的字符串,尤其是拼接超过10次时(经验值),应使用StringBuilder做字符串拼接操作。
//不建议:
string s = null;
for (int i = 0; i < 10000; i++)
{
s += i;
}
//建议:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append(i);
}
string t = sb.ToString();
4、创建StringBuilder应指定初始大小
默认的初始大小为16,一旦超过即需要Resize一次并增加GC压力。建议根据经验值为其指定初始大小。但是如果不能确定大小的情况下,还是使用第一种情况更好一点。
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++)
{
sb.Append(i);
}
string s = sb.ToString();
//建议修改为
StringBuilder sb = new StringBuilder(256);
for (int i = 0; i < 10; i++)
{
sb.Append(i);
}
string s = sb.ToString();
5、避免滥用StringBuilder
类似str1+str2+str3+str4的字符串拼接操作会被编译为 String.Concat(str1,str2,str3, str4),效率反而高于StringBuilder。String.Concat会一次性确定字符串长度, StringBuilder需要做Resize,适用于多次生成string对象的情况。
在上述代码中就如此,因为每次传入的num1和num2都会在传入时于Compare的函数空间中生成一个字符串,这个时候因为拼接的较少,使用String.Concat来拼接更好一点。
注意:当在网页上进行提交,显示无法使用String或其他封装好的类的时候,可以于前加上系统类,这样就能把这些类导入进去了,比如:System.String.Concat()或System.Array.Sort() 。
更多优化知识可参考https://blog.csdn.net/mss359681091/article/details/54891118
我觉得楼上这位博客大神的博客写得特好