如何计算整数范围内的每个数字?
想象一下,你出售那些用于房屋,储物柜门,酒店房间等的金属数字。当你的客户需要门牌号码时,你需要找出每个数字有多less个数字:
- 1到100
- 51至300
- 1到2,000,左边是零
显而易见的解决scheme是从第一个到最后一个数字执行一个循环,将计数器转换为左侧或没有零的string,提取每个数字,并将其用作索引来递增10个整数的数组。
我想知道是否有更好的方法来解决这个问题,而不必遍历整个整数范围。
任何语言或伪代码的解决scheme都是受欢迎的。
编辑:
答案审查
CashCommons和Wayne Conrad的 John评论说,我目前的做法很好,也足够快。 让我用一个愚蠢的比喻:如果你在1分钟内完成棋盘上方块的计算任务,你可以通过逐个计算方块来完成任务,但更好的解决scheme是计算边和做一个乘法,因为你以后可能会被要求去计算build筑物中的瓷砖。
亚历克斯·雷斯纳(Alex Reisner)指出了一个非常有趣的math定律,不幸的是,这个问题似乎并不相关。
Andresbuild议我使用相同的algorithm,但用%10操作而不是子string提取数字。
约翰在CashCommons和phordbuild议预先计算所需的数字并将它们存储在查找表中,或者对于原始速度来说,它是一个数组。 如果我们有一个绝对的,不可移动的,最大的整数值,这可能是一个很好的解决scheme。 我从来没有见过其中之一。
高性能标记和filter计算了各种范围所需的数字。 一毫秒的结果似乎表明有一个比例,但其他数字的结果显示不同的比例。
filter发现了一些公式,可以用来计数十位数的数字。 Robert Harvey在MathOverflow上发表了一个非常有趣的经历。 math家之一用math符号写了一个解决scheme。
Aaronaught使用math开发和testing了一个解决scheme。 发布后,他回顾了从math溢出发起的公式,并发现它的缺陷(指向Stackoverflow :)。
noahlavine开发了一个algorithm,并以伪代码的forms呈现。
一个新的解决scheme
读完所有的答案,并做了一些实验,我发现,从1到10 n -1的整数范围:
- 对于数字1至9,需要n * 10 (n-1)个片段
- 对于数字0,如果不使用前导零,则需要n * 10 n-1 – ((10 n -1)/ 9)
- 对于数字0,如果使用前导零,则需要n * 10 n-1 -n
第一个公式是filter (也许是其他人)发现的,我发现了另外两个是通过反复试验(但是可能包含在其他答案中)。
例如,如果n = 6,范围是1到999,999:
- 对于数字1到9,我们需要6 * 10 5 =每个600,000
- 对于数字0,没有前导零,我们需要6 * 10 5 – (10 6 -1)/ 9 = 600,000 – 111,111 = 488,889
- 对于数字0,前导零,我们需要6 * 10 5 – 6 = 599,994
这些数字可以使用高性能标记结果进行检查。
使用这些公式,我改进了原来的algorithm。 它仍然从整数范围的第一个到最后一个数字循环,但是,如果它find一个十的幂的数字,它使用公式添加到数字中计算1到9的全部范围的数量或1到99或1到999等。下面是伪代码中的algorithm:
整数首先,最后//范围中的第一个和最后一个数字 整数数字//循环中的当前数字 整数功率//功率是公式中10 ^ n中的n 整数Nines // Nines是10 ^ n - 1,10 ^ 5 - 1 = 99999的结果 整数前缀//数字中的第一个数字。 对于14,200,前缀是142 数组0..9位//将保持所有数字的计数 数字=首先到最后 CALL TallyDigitsForOneNumber WITH Number,1 //计数每个数字的计数 //在数字中增加1 //开始优化。 注释是数字= 1,000和最后= 8,000。 功率=数字结尾处的零//对于1,000,功率= 3 中频功率> 0 //数字以0 00 000等结尾 Nines = 10 ^ Power-1 // Nines = 10 ^ 3 - 1 = 1000 - 1 = 999 IF Number + Nines <= Last //如果1,000 + 999 <8,000,则添加一整套 数字[0-9] + =功率* 10 ^(功率-1)//将数字0至9加3 * 10 ^(3-1)= 300 数字[0] - = - 电源//调整数字0(前导零公式) 前缀= Number的第一个数字//对于1000,前缀是1 CALL TallyDigitsForOneNumber WITH Prefix,Nines // Tally每个的计数 //前缀中的数字, //增加999 数字+ =九个//将循环计数器增加999个周期 万一 万一 //优化结束 ENDFOR SUBROUTINE TallyDigitsForOneNumber PARAMS数字,计数 重复 数字[数字%10] + =数字 Number = Number / 10 UNTIL号码= 0
例如,对于范围786到3,021,计数器将递增:
- 从786到790(5个周期)
- 从790到799(1个周期)
- 从799增加到800
- 从800到899的99
- 从899增加到900
- 从99到999
- 从1到999到1000
- 1000到1999年999
- 从1999年到2000年1
- 从2000年到2999年999
- 从2999到3000由1
- 从3000到3010(10个周期)
- 从3010到3019的9个(1个周期)
- 从3019到3021(2个周期)
总计:28个周期没有优化:2,235个周期
请注意,这个algorithm解决了这个问题,而不用引导零。 要用前导零来使用它,我用了一个黑客:
如果范围在700到1000之间,需要前导零,请使用10,700到11,000的algorithm,然后从数字1的计数中减去1000 – 700 = 300。
基准和源代码
我testing了原始方法,使用%10的相同方法以及一些大范围的新解决scheme,结果如下:
原来的104.78秒 用%10 83.66 随着十大0.07的权力
基准testing应用程序的屏幕截图:
替代文字http://clarion.sca.mxhttp://img.dovov.comstories/digitsbench.png
如果您想查看完整的源代码或运行基准testing,请使用以下链接:
- 完整的源代码(在号angular ): http : //sca.mx/ftp/countdigits.txt
- 可编译项目和win32 exe: http ://sca.mx/ftp/countdigits.zip
接受的答案
noahlavine解决scheme可能是正确的,但我只是不能遵循伪代码,我认为有一些细节丢失或没有完全解释。
Aaronaught的解决scheme似乎是正确的,但代码太复杂了,我的口味。
我接受了filter的答案,因为他的思路引导我开发这个新的解决scheme。
为了从一个数字卷起数字,我们只需要做一个昂贵的string转换,如果我们不能做一个国防部,数字可以最快速地推动一个数字是这样的:
feed=number; do { digit=feed%10; feed/=10; //use digit... eg. digitTally[digit]++; } while(feed>0)
该循环应该是非常快的,并且可以放置在开始到结束数字的循环内,以最简单的方式来计算数字。
为了更快,对于更大范围的数字,即时寻找一个优化的方法,统计所有数字从0到数字* 10 ^意义(从一开始就结束我的声音)
这里是一个表格,显示一些有效数字的数字符号。这些数字包括0,但不包括顶部值本身 – 这是一个疏忽,但它可能更容易看到模式(在这里没有最高值的数字)这些符号不包括尾随零,
1 10 100 1000 10000 2 20 30 40 60 90 200 600 2000 6000 0 1 1 10 190 2890 1 2 3 4 6 9 30 110 490 1690 1 0 1 20 300 4000 1 12 13 14 16 19 140 220 1600 2800 2 0 1 20 300 4000 0 2 13 14 16 19 40 220 600 2800 3 0 1 20 300 4000 0 2 3 14 16 19 40 220 600 2800 4 0 1 20 300 4000 0 2 3 4 16 19 40 220 600 2800 5 0 1 20 300 4000 0 2 3 4 16 19 40 220 600 2800 6 0 1 20 300 4000 0 2 3 4 6 19 40 120 600 1800 7 0 1 20 300 4000 0 2 3 4 6 19 40 120 600 1800 8 0 1 20 300 4000 0 2 3 4 6 19 40 120 600 1800 9 0 1 20 300 4000 0 2 3 4 6 9 40 120 600 1800
编辑:清理我的原始思想:
从蛮力表显示从0(包括)到poweroTen(notinc)的统计,可以看出十大力量的大小:
increments tally[0 to 9] by md*tp*10^(tp-1) increments tally[1 to md-1] by 10^tp decrements tally[0] by (10^tp - 10) (to remove leading 0s if tp>leadingzeros) can increment tally[moresignificantdigits] by self(md*10^tp) (to complete an effect)
如果对每个有效数字应用这些计数调整,则计数应该从0到1结束
调整可以颠倒去除前面的范围(起始数字)
感谢Aaronaught为您的完整和经过testing的答案。
对于这样的问题有一个明确的math解决scheme。 我们假设这个值是零填充到最大位数(不是,但我们稍后会补偿),通过它的原因:
- 从0到9,每个数字出现一次
- 从0-99开始,每个数字出现20次(位置1为10x,位置2为10x)
- 从0-999,每个数字出现300次(P1为100x,P2为100x,P3为100x)
如果范围是从0到10的幂,则给定数字的明显模式是N * 10 N-1 ,其中N是10的幂。
如果范围不是10的幂数呢? 从10的最低功率开始,然后工作。 最容易处理的情况是399的最大值。我们知道,对于每个100的倍数,每个数字至less出现20次,但是我们必须补偿出现在最高有效位数的次数,对于数字0-3,这将精确地为100,并且对于所有其他数字精确地为零。 具体而言,要添加的额外数字是相关数字的10 N.
把它归为一个公式,对于小于10的幂(即399,6999等)的某个倍数的上界,它变为: M * N * 10 N-1 + iif(d <= M,10 N ,0)
现在你只需要处理其余部分(我们将称之为R )。 以445为例。 无论结果是399还是400-445。 在这个范围内,MSD出现更多次,所有的数字(包括MSD)也出现在他们从范围[0 – R ]相同的频率上。
现在我们只需要补偿前导零。 这种模式很简单 – 只是:
10 N + 10 N-1 + 10 N-2 + … + ** 10 0
更新:这个版本正确地考虑到了“填充零”,即在处理余数([4 0 0,4 0 1,4 0 2,…])时处于中间位置的零。 找出填充零有点难看,但修改后的代码(C风格的伪代码)处理它:
function countdigits(int d, int low, int high) { return countdigits(d, low, high, false); } function countdigits(int d, int low, int high, bool inner) { if (high == 0) return (d == 0) ? 1 : 0; if (low > 0) return countdigits(d, 0, high) - countdigits(d, 0, low); int n = floor(log10(high)); int m = floor((high + 1) / pow(10, n)); int r = high - m * pow(10, n); return (max(m, 1) * n * pow(10, n-1)) + // (1) ((d < m) ? pow(10, n) : 0) + // (2) (((r >= 0) && (n > 0)) ? countdigits(d, 0, r, true) : 0) + // (3) (((r >= 0) && (d == m)) ? (r + 1) : 0) + // (4) (((r >= 0) && (d == 0)) ? countpaddingzeros(n, r) : 0) - // (5) (((d == 0) && !inner) ? countleadingzeros(n) : 0); // (6) } function countleadingzeros(int n) { int tmp= 0; do{ tmp= pow(10, n)+tmp; --n; }while(n>0); return tmp; } function countpaddingzeros(int n, int r) { return (r + 1) * max(0, n - max(0, floor(log10(r))) - 1); }
正如你所看到的那样,它变得有点丑陋,但它仍然运行在O(log n)时间,所以如果你需要处理数十亿的数字,这仍然会给你即时的结果。 :-)如果你在范围[0 – 1000000]上运行它,你会得到与高性能标记所发布的完全相同的分布,所以我几乎肯定它是正确的。
仅供参考, inner
variables的原因是前导零function已经recursion,所以只能在countdigits
的第一次执行中countdigits
。
更新2:如果代码难以阅读,下面是每行countdigits
返回语句的含义的参考(我尝试了内联注释,但他们使代码更难阅读):
- 任何数字的频率最高为10(0-99等)
- MSD的频率高于10的最高功率的任何倍数(100-399)
- 剩下的任何数字的频率(400-445,R = 45)
- 其余MSD的频率
- 在剩下的范围内计算中间位置的零(404,405 …)
- 只减去前导零(在最外圈)
我假设你想要一个数字在一个范围内的解决scheme,你有起始和结束的数字。 想象一下,从开始数字开始计数直到达到最终数字 – 它会起作用,但速度会很慢。 我认为一个快速algorithm的诀窍是要认识到,为了在10 ^ x的地方上升一个数字,并保持其他所有的相同,你需要使用10 ^ x之前的所有数字加上全部数字0 -9 10 ^(x-1)次。 (除了你的计数可能涉及到第x个数字的进位 – 我在下面进行纠正。)
这是一个例子。 说你从523到1004。
- 首先,你从523到524.这使用数字5,2和4每个一次。
- 第二,从524到604.从最右边的数字开始,所有的数字都是6个循环,所以你需要每个数字6个副本。 第二位数字通过数字2到0,10次。 第三位数字是6 5次和5 100-24次。
- 第三,从604到1004数。最右边的数字做40个周期,所以每个数字增加40个副本。 第二个从右边数字做4个周期,所以每个数字添加4个副本。 最左边的数字分别是7,8和9中的100,加上0中的5以及6中的100 – 5。最后一个数字是1 5次。
为了加快速度,请看最右边的两个地方。 它使用每个数字10 + 1次。 一般来说,1 + 10 + … + 10 ^ n =(10 ^(n + 1) – 1)/ 9,我们可以用来加速计数。
我的algorithm是从开始数到结束数(使用10进制计数),但使用上面的事实来快速完成。 你可以遍历起始号码的数字,从最小到最重要的数字,在每一个地方,你的数字和结尾的数字是一样的。 在每一点上,n是你在进场之前需要做的向上计数的数量,以及之后你需要做的数量。
现在让我们假设伪代码作为一种语言。 在这里,那就是我要做的事情:
将开始和结束数字转换为数字数组start []和end [] 创build一个数组count []与10个元素存储的副本数量 每个数字,你需要 从右到左迭代开始编号。 在第i位, 让d是你必须从这个数字得到的位数 到结尾数字中的第i位数字。 (即减去当量 数字mod 10) 在count中添加d *(10 ^ i - 1)/ 9。 设m是该数字右边所有数字的数值, n是10 ^ i - m。 对于起始数字左边的每个数字e,直至包括该数字 第i位数字,将n加到该数字的计数上。 对于1到1中的j 将第i位数字加1,包括做任何载体 对于起始数字左边的每个数字e,直到并包括在内 第i位数字,加上10 ^我的数字的数字 对于起始数字左边的每个数字e,直到并包括该数字 第i位数字,将m加到该数字的计数上。 将起始号码的第i位数字设置为结尾的第i位数字 数。
哦,因为我的价值每增加1,跟踪你的旧10 ^我只是乘以10来得到新的,而不是每次幂。
这是一个非常糟糕的答案,我很惭愧地发布。 我问Mathematica统计所有数字从1到1,000,000,没有领先的0。 这是我得到的:
0 488895 1 600001 2 600000 3 600000 4 600000 5 600000 6 600000 7 600000 8 600000 9 600000
下一次你在硬件商店订购粘性数字时,按这些比例sorting,你不会有太大的错误。
我在math溢出问了这个问题 ,并因为问这样一个简单的问题而被打屁股。 其中一位网友对我表示同情,并表示,如果我把它发给解决问题的艺术 ,他会回答。 所以我做了。
这是他发布的答案:
http://www.artofproblemsolving.com/Forum/viewtopic.php?p=1741600#1741600
令人尴尬的是,我的math不足以理解他所发表的内容(那个19岁的男孩……真令人沮丧)。 我真的需要参加一些math课。
好的一面是,这个方程是recursion的,所以应该是一个简单的事情,通过懂math的人把它变成一个只有几行代码的recursion函数。
你的方法很好。 我不知道为什么你需要比你所描述的更快的东西。
或者,这会给你一个即时的解决scheme:在你真正需要它之前,计算你需要从1到某个最大数量。 您可以存储每一步所需的号码。 如果你有第二个例子的范围,这将是1到300所需要的,减去1到50所需要的。
现在你有一个可以随意调用的查询表。 做到10000只需要几MB,几分钟计算一次?
我知道这个问题有一个被接受的答案,但我的任务是编写面试的代码,我想我想出了一个快速的替代解决scheme,不需要循环,可以根据需要使用或丢弃前导零。
这其实很简单,但不容易解释。
如果你列出了前n个数字
1 2 3 . . . 9 10 11
通常从左到右的顺序开始从开始房间号码到结束房间号码所需的数字,因此对于上面我们有一个1,一个2,一个3 …一个9,两个1的一个零,四个1等。我见过的大多数解决scheme使用这种方法进行了一些优化,以加快速度。
我所做的就是纵向数列,如数百,数十和单位。 你知道最高的房间号码,所以我们可以通过一个单独的分区来计算数百列中的每个数字有多less,然后recursion计算十列中的多less等等。如果我们愿意的话,我们可以减去前导零。
如果您使用Excel写出数字,但是为数字的每个数字使用单独的列,则更容易可视化
ABC - - - 0 0 1 (assuming room numbers do not start at zero) 0 0 2 0 0 3 . . . 3 6 4 3 6 5 . . . 6 6 9 6 7 0 6 7 1 ^ sum in columns not rows
因此,如果最高房间号是671,那么百列将垂直地有100个零,接着是100个,如此多达71个六分之一,如果需要的话忽略100个零,因为我们知道这些都是领先的。
然后caching到数十,并执行相同的操作,我们知道将会有10个零,接着是10个等等,重复6次,然后最终时间下降到2个七分之一。 再次可以忽略前10个零,因为我们知道他们是领导。 最后当然要做单位,根据需要忽略第一个零。
所以没有循环,一切都是用分割来计算的。 我使用recursion来“向上”移动列,直到达到最大值(在这种情况下为数百),然后退回总计。
我用C#编写了这个代码,如果有兴趣的人可以发布代码,但是没有做任何基准testing,但是对于10 ^ 18个房间的值来说,这个时间本质上是即时的。
无法find这里或其他地方提到的这种方法,所以认为这可能是有用的人。
这并不能回答你确切的问题,但是按照本福德定律 ,第一位数字的分布很有意思。 例如,如果您随机select一组数字,则其中的30%将以“1”开始,这有点违反直觉。
我不知道任何描述后续数字的分布,但是你也许能够凭经验确定这个数字,并且得到一个简单的公式来计算任何数字范围所需的近似数字位数。
如果“更好”的意思是“更清楚”,那么我怀疑它。 如果意思是“更快”,那么是的,但是我不会使用更快的algorithm来代替更清晰的algorithm,而没有令人信服的需求。
#!/usr/bin/ruby1.8 def digits_for_range(min, max, leading_zeros) bins = [0] * 10 format = [ '%', ('0' if leading_zeros), max.to_s.size, 'd', ].compact.join (min..max).each do |i| s = format % i for digit in s.scan(/./) bins[digit.to_i] +=1 unless digit == ' ' end end bins end p digits_for_range(1, 49, false) # => [4, 15, 15, 15, 15, 5, 5, 5, 5, 5] p digits_for_range(1, 49, true) # => [13, 15, 15, 15, 15, 5, 5, 5, 5, 5] p digits_for_range(1, 10000, false) # => [2893, 4001, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000]
Ruby 1.8是一种被称为“狗慢”的语言,在0.135秒内运行上述代码。 这包括加载解释器。 除非你需要更快的速度,否则不要放弃一个明显的algorithm。
如果你在很多迭代中需要原始速度,请尝试查找表:
- build立一个2维数组:10个最大房子号
int nDigits[10000][10] ; // Don't try this on the stack, kids!
- 填写每行所需的数字从零到达该数字。
提示:使用上一行作为开始:
n=0..9999: if (n>0) nDigits[n] = nDigits[n-1] d=0..9: nDigits[n][d] += countOccurrencesOf(n,d) //
- “两个数字之间的位数”变成简单的减法。
对于范围= 51到300,计数为300并减去计数为50。 0's = nDigits [300] [0] - nDigits [50] [0] 1's = nDigits [300] [1] - nDigits [50] [1] 2's = nDigits [300] [2] - nDigits [50] [2] 3's = nDigits [300] [3] - nDigits [50] [3] 等等
您可以分隔每个数字( 例如 , 查看这里 ),创build一个条目从0到9的直方图(它将计算一个数字中出现多less个数字)并乘以问号的数字。
但如果不是你要找的东西,你能举一个更好的例子吗?
编辑:
现在我觉得我得到了这个问题。 我认为你可以认为这(伪C):
int histogram[10]; memset(histogram, 0, sizeof(histogram)); for(i = startNumber; i <= endNumber; ++i) { array = separateDigits(i); for(j = 0; k < array.length; ++j) { histogram[k]++; } }
分开的数字实现链接中的function。
直方图的每个位置将有每个数字的数量。 例如
histogram[0] == total of zeros histogram[1] == total of ones
…
问候