剑指OFFER第45题——把数组排成最小的数qq_33575542

题目描述

        输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{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个条件:

  • 自反性
  • 对称性
  • 传递性

        接下来分别予以证明:

  1. 自反性:
    显然有aa=aa,所以a等于a。
  2. 对称性:
    如果a小于b,则ab < ba,所以ba > ab,因此b大于a
  3. 传递性:
    如果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
我觉得楼上这位博客大神的博客写得特好

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值