题目描述
求出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),比前面的原始方法要好太多。