1~n整数中1出现的次数

题目描述
        求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数。

        最直观的方法肯定是,累加1~n中每个整数1出现的次数,我们可以通过对10求余数判断整数的个位数字是不是1 。如果这个数字大于10,则除以10之后再判断个位数字是不是1 。基于这种思想,我们可以得出以下代码:

class Solution1
{
    /// <summary>
    /// 通过循环,从一到n遍历所有数字,并对每个数字取余判断,有1就加一次,知道最后所有的数字检测完毕
    /// </summary>
    /// <param name="n">从1到n的n,表示需要遍历个数的上限</param>
    /// <returns>返回1出现的个数</returns>
    public int NumberOf1Between1AndN_Solution(int n)
    {
        int sum = 0;

        //遍历1到n,把该数字拥有的1的个数加到sum里面
        for(int i = 1; i <= n; i++)
        {
            sum += NumberOf1(i);
        }

        return sum;
    }

    /// <summary>
    /// 用于获取数字Number中拥有的1的个数,每次取其余数,判断是否为1,然后自身除10,循环上述操作,直到数字等于0,跳出循环
    /// </summary>
    /// <param name="number">需要检测的数字</param>
    /// <returns>返回数字number中出现的个数</returns>
    private int NumberOf1(int number)
    {
        int count = 0;
        //每次检测其个位的值是否为1,检测后令数字位数往后移(百位变十位,十位变个位)
        while(number!=0)
        {
            if (number % 10 == 1)
                count++;
            number /= 10;
        }
        return count;
    }
}

        在上述思路中,我们队每个数字都要做除法和求余操作,这会使时间复杂度达到O(n*logn)(输入n个数,每个数有logn位)。当输入的n非常大的时候,需要大量的计算,运算效率不高,因此我们需要更简单的方法。

从数字规律着手明显提高时间效率的解法

        如果希望不用计算每个数字的1的个数来求解,我们就只能从数字中寻找规律了。为了找到规律,我们可以假设一个非常大的数,比如:21345作为例子来分析。我们把1~21345分成两部分,一部分为1~1345,一部分为1346~21345 。
        首先观察1346~21345部分,1出现的情况分为两种。首先分析1出现在最高位的情况。在1346~21345的数字中,1出现在10000~19999这10000个数字的万位中,也就是出现了10000(10^4)次。
        值得注意的是,并不是对所有5位数而言都是10000次。对于万位数是1的数字如数字12345来说,出现的次数仅仅为2346次(10000~12345),也就是除去最高位后剩下的数字加上1 。
        接下来分析1出在除最高位之外的其他4位数中的情况。例子中1346~21345,这两万个数字中后4位中1出现的次数是8000次。由于最高位是2,我们可以再把1346~21345分成两段:1346~11345和11346~21345。每一段剩下的4位数字中,其中一位为1,其他三位可以在0~9中任意选,因此根据排序组合原则,总共出现的次数是2*4*10^3=8000 。2是分成的两部分,4为可选择1的位置,10的3次方表示出去1的位置其他3个位置可以从0~9任意排列选择。
        至于1~1345中1出现的次数,我们可以通过递归来实现求得。这也是我们为什么要把1~21345分为1~1345和1346~21345两部分的原因。因为把21345的最高位去掉就变成了1345了,便于我们使用递归来实现。
因此我们可以把数字转化为字符串然后使用一个递归函数来实现:

class Solution2
{
    public int NumberOf1Between1AndN_Solution(int n)
    {
        if (n <= 0)
            return 0;

        StringBuilder sb = new StringBuilder(n.ToString());
        return NumberOf1(sb, 0);
    }

    private int NumberOf1(StringBuilder sb, int digitIndex)
    {
        //判断输入是否正确
        if (sb == null || sb.Length <= 0 || sb[digitIndex] < '0' || sb[digitIndex] > '9')
            return 0;

        //获取当前位数的字符与剩余可变化的字符数
        int first = sb[digitIndex] - '0';
        int length = sb.Length - digitIndex;

        //如果剩余长度仅剩1个数字,那么判断这个数字是否大于0,如果大于0,至少有1个1,如果等于0,则返回0,一个1都没有
        if (length == 1 && first == 0)
            return 0;
        else if (length == 1 && first > 0)
            return 1;

        //numFirstDigit 用于储存1出现在当前位的次数,当前万位上1出现的次数
        int numFirstDigit = 0;

        //如果当前位的数字大于1,那么说明从10...0-19.9的会出现10的n次个1在当前位
        //以“21345”为例
        //例中万位上1因10000-19999可出现10的4次方次,4=当前位后面剩余数字的个数
        if (first > 1)
            numFirstDigit = PowerBase10(length - 1);
        //如果当前位等于1,则出现的次数就是1后面的数字了
        //例如“12345”,当前位为1的话,后面出现的次数就是0000-2345,共2346次
        else if (first == 1)
            numFirstDigit = GetNextNum(sb, digitIndex + 1) + 1;

        //numOtherDigits用于储存其他位出现1的次数
        int numOtherDigits = first * (length - 1) * PowerBase10(length - 2);

        //除去最高位之后出现的1的次数
        int numRecursive = NumberOf1(sb, digitIndex + 1);

        return numFirstDigit + numOtherDigits + numRecursive;
    }

    /// <summary>
    /// 把剩余位数的数字转换成数字,再返回这个数
    /// </summary>
    /// <param name="sb">传入的字符</param>
    /// <param name="index">开始计算的位</param>
    /// <returns>转换后得到的数字</returns>
    private int GetNextNum(StringBuilder sb, int index)
    {
        int num = 0;
        for (int i = index; i <= sb.Length - 1; i++)
        {
            num = num * 10 + (sb[i] - '0');
        }
        return num;
    }

    /// <summary>
    /// 求10的v次阶
    /// </summary>
    /// <param name="v">10的阶数</param>
    /// <returns>返回计算得到的结果</returns>
    private int PowerBase10(int v)
    {
        int result = 1;
        for (int i = 0; i < v; i ++)
            result *= 10;

        return result;
    }
}

上述的这种思路,递归的次数和位数相同,一个数字n有O(logn)位,因此这种思路的时间复杂度是O(logn),比前面的原始方法要好太多。

猜你喜欢

转载自blog.csdn.net/qq_33575542/article/details/80905261