编程之美学习笔记(一): 1的数目

问题描述

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

思路一

这个问题初一看感觉就是不是很难,因为马上就能想到一个最简单的方法来计算了,那就是从1开始遍历到N,将其

每一个数中含有的“1”的个数加起来,这样便得到了从1到N所有“1”的个数的和。下面是实现代码

#include<iostream>
using namespace std;

int count1(int n) //计算整数n中“1”的个数
{
	int count = 0;
	while (n)
	{
		count += (n % 10 == 1) ? 1 : 0;
		n /= 10;
	}
	return count;
}

int main()
{
	long long n;
	long long sum;
	while (cin >> n)
	{
		sum = 0;
		for (long long i = 1; i <= n; i++)//遍历1到N
		{
			sum += count1(i);
		}
		cout << sum << endl;
	}    
	return 0;
}

这个方法很简单吧!相信大家都能想到,而且代码的实现也比较简单,容易理解。但是这个算法确有一个致命的问

题,那就是效率问题,它的时间复杂度是O(N*log2N)。如果给定的N较大的话,则需要很长的运算时间才能得出

结果。在我自己的电脑上算100 000 000,大概用了17秒多点。而且计算时间会随着N的增大而线性增大。

这样的效率显然不符合实际要求的,那么就得想办法进行优化了,显然这个算法效率低下的最终原因还是总是需

从1遍历到N,那么想要提高效率就必须得摈弃这种遍历方法啦。下面来看看编程之美上的思路吧,果然很犀利!


思路二

仔细分析这个问题,给定了N,似乎就可以通过分析“小于N等于的数在每一位上可能出现1的次数”之和来得到这个结

果。让我们先来分析一些特定的N,如何得到一个规律来分析在每个位上所有出现1的可能性,并求出得到最后的结

果。先从一些简单的情况开始观察,看看能不能发现什么规律。


先看N为1位数的情况:

如果N = 3,那么从1到3的所有数字:1、2、3,只有一位数上可能出现1,而且只有一次,进一步可以发现如果N是

个位数,如果N>=1,那么总数都等于1,如果N=0,则总数为0。

再看N为2位数的情况:

如果N = 12,那么从1到12的所有数字:1、2、3、4、5、6、7、8、9、10、11、12,个位和十位的数字上都可能有

1,我们可以将它们分开来考虑,个位出现1的次数有两次:1和11,十位出现1的次数有3次:10、11、12,所以总数

为5。要注意的是11这个数字在十位和个位都出现了1,但是11恰好在个位为1和十位为1中被计算了两次,所以不用

殊处理,是对的。在考虑23的情况,它和N=12有点不同,十位出现1的次数为10次,从10到19,个位出现1的次数

为1、11、21,所以总数为13。通过对两位数进行分析,我们发现,个位数出现1的次数不仅和个位数字有关,还和

十位数字有关。。。。。。(此次省略N字!!!)


上面省略的是编程之美上的接上的所有分析,这里就不在累赘啦。如果需要编程之美电子书的朋友可以给我留言,到

时直接发过来!

其实看到上面的分析基本已经知道了,这里采用的方法是先通过分析一些数据,找出N的各个数位上出现1的个数,然

后把这些加起来,就得到了最后的结果啦。大家自己在纸上写下,可以很容易知道,每个位上出现1的次数受4个因素

的影响:当前所在位数、比当前位更高位的数iHigherNum、当前位的数iCurrNum、比当前位更地位的iLowerNum。

比如N=2051,当计算十位上出现1的次数时,iHigherNum=20,iCurrNum=5,iLowerNum=3。接下来就是要找出这个规

律啦。就拿2051来说吧:

我们先来看看十位上出现1的情况吧:10-19、110-119......2010-2019,总数=10*21;

再看看百位上的情况:100-199、1100-1199,总数=100*2;

再看个位上的情况,对于最高(低)位有点不同,因为最高(低)位的更高(低)位没有,所以这里设为0:1、11、

21、31......2051,总数=206*1;

最后看下千位上的情况:1000-1999,总数=1*1000。

最后从1到N中所有出现1的次数=210+200+206+1000=1616。


我在纸上另外测试了很多数据,最后得出了规律,下面F(n)表示数字N从最低位开始第n位上1出现的次数,k表示10的

n次方。

iCurrNum = 0时,F(n) = iHigherNum * k;

iCurrNum = 1时,F(n) = iHigherNum * k + iLower + 1;

iCurrNum > 1时,F(n) = (iHigher+1) * k。

由此得出的算法实现代码如下:

#include<iostream>
using namespace std;

long long count1 (int n)
{
	long long iCount = 0;
	long long iFactor = 1;
	long long iLowerNum = 0;
	long long iHigherNum = 0;
	long long iCurrNum = 0;

	while (n / iFactor)
	{
		iLowerNum = n % 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;
}

int main()
{
	long long n;
	int sum;
	while (cin >> n)
	{
		sum = count1(n);
		cout << sum << endl;
	}
}

这个程序计算100 000 000在我机子上运行不到1毫秒就搞定啦,速度不知道提高了多少倍!



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值