如何求“1”的数目

本题是一道某研究院的题目,看似简单,但想要求出高效算法,是也有一定难度的

给定一个十进制正整数N,写下从1开始,到N的所有整数,然后数一下其中出现的所有“1”的个数。

例如: N=2, 写下1,2。这样只出现了1个“1”。

N=12,我们会写下1,2,3,4,5,6,7,8,9,10,11,12 这样,1的个数为5.

问题是:

1. 写一个函数f(N), 返回1到N之间出现的“1”的个数,比如f(12)=5。


分析与解法

【问题1的解法一】

看上去并不复杂,一个最简单的方法就是遍历从1到N,把每一个数中含有“1”的个数加起来就是最终结果。参考如下代码(JAVA)

	private int count(int n) {
		int total = 0;
		while (n != 0) {
			total += (n % 10 == 1) ? 1 : 0;
			n /= 10;
		}
		return total;
	}

	public int f(int n) {
		int total = 0;
		for (int i = 1; i <= n; i++) {
			total += count(i);
		}
		return total;
	}

此方法虽简单易懂,但却非常低效,其时间复杂度为O(N*log2N),随着N的增大而线性增长,如下为我机器上的测试结果(本机为win10 64bit)

		long start = System.nanoTime();
		System.out.println("在N=100000000的时,其中1的个数为:" + t.f(100000000));
		long end = System.nanoTime();
		System.out.println("运行时间(毫秒):" + (double) (end - start) / 1000000);


		// 输出结果
		// 在N=100000000的时,其中1的个数为:80000001
		// 运行时间(毫秒):2579.713748	


【问题1的解法二】

先从简单情况开始观察,总结规律。

先看1位数的情况。

如果N=3,即从1到3所有数字1,2,3, 只有个位数字上可能出现1,且只会出现1 次,

结论:如果N>=1 那么f(N)=1, 如果N=0,则f(N)=0.


再看2位数的情况。

如果N=13, 即从1到13所有数字1,2,3,4,5,6,7,8,9,10,11,12,13. 个数与十位数都可能有1,将它们分开考虑,个位出现1的次数有两次,1和11, 十位出现1的次数有4次10,11,12,13,所以f(N)=2+4=6.  再考虑23的情况,它和N=13有点不同,十位出现1的次数为10次,从10到19,个位出现1的次数为1,11,21,所以f(N)=3+10=13, 

通过分析不难看出,个位数出现1的次数不仅与个位数相关,还与十位数相关。如果N的个位数=0,则个位出现1的次数=十位数的数字,如果个位>=1,则个位数出现1的次数=十位数的数字+1。 十位数上出现1的次数不仅与十位数有关,还与个位数有关:如果十位数字=1,则十位数上出现1的次数=个位数的数字+1;如果十位数>1,则十位数上出现1的次数为10。得出如下结论

f(13)=2+4=6

f(23)=3+10=13

f(33)=4+10=14

..

f(93)=10+10=20



再看3位数情况。

如果N=123, 个位数出现1的个数为13。 十位出现1的个数为20。百位出现1的个数为24 

f(123) = 个位数出现1的个数 + 十位数出现1的个数 + 百位数出现1的个数=13+20+24=54


同理可分析4位数,5位数, 经过推导得出一般情况下, 从N到f(N)的计算方法为:

假设N=abcde, 这里a, b, c, d, e分别是十进制N的各个数位上的数字。计算百位上出现1的次数将受3个因素影响:百位上的数字, 百位以下的数字,百位以上的数字。

如果百位以上的数字为0,则百位上出现1的次数由更高位决定,比如12013,则百位出现1的情况可能是100-199, 1100-1199, 2100-2199,。。。,11100-11199,共1200个。也就是说由更高位(12)决定,并且等于更高位12X当前位数100,

 如果百位上数字为1,则百位上出现1的次数不仅受更高位影响,还受低位影响,也就是由高位和低位共同决定 。如12113, 百位出现1的情况是100-199,1100-1199,2100-2199,。。。,11100-11199,共12000个,和上面一样, 等于高位数字12X当前位数100, 但它还受低位影响,百位出现1的情况是12100-12113, 共124个,等于位数113+1;

如果百位上数字大于1,则百位上出现1的次数也是由高位决定,如12213,出现1的可能性为100-199,1100-1199, 2100-2199, 。。。,11100-11199, 12100-12199,共1300个,并且等于更高位数字+1(12+1)X当前位数(100)。

通过归纳总结,我们可以写出更高效的算法。如下所示(JAVA)


	public int sum1(int n) {
		int iCount = 0;
		int iFactor = 1;
		int iLowerNum = 0;
		int iCurrNum = 0;
		int iHigherNum = 0;

		while (n / iFactor != 0) {
			iLowerNum = n - (n / iFactor) * iFactor;
			iCurrNum = (n / iFactor) % 10;
			iHigherNum = n / (iFactor * 10);
			switch (iCurrNum) {
			case 0:
				iCount += iHigherNum * iFactor;
				break;
			case 1:
				iCount += iHigherNum * iFactor + iLowerNum + 1;
				break;
			default:
				iCount += (iHigherNum + 1) * iFactor;
				break;

			}
			iFactor *= 10;
		}

		return iCount;
	}

此方法只要分析N就可以得到f(N), 避开了1到N的遍历,时间复杂度为O(len), 即O(ln(n)/ln(10) + 1). 在本地环境(win10 + 64bit) 测试结果如下, 可见速度提升了几个数量级。。。

		long start = System.nanoTime();
		System.out.println("在N=100000000的时,其中1的个数为:" + t.sum1(100000000));
		long end = System.nanoTime();
		System.out.println("运行时间(毫秒):" + (double) (end - start) / 1000000);

		// 输出结果
		// 在N=100000000的时,其中1的个数为:80000001
		// 运行时间(毫秒):0.272
















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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值