在从1到n的正数中1出现的次数

本文探讨了一种高效算法,用于计算从1到指定整数N的十进制表示中数字1出现的总次数。通过巧妙地将问题分解并利用递归思想,避免了逐一检查每个数字的低效做法。

题目:输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。 例如输入12,从1到12这些整数中包含1 的数字有1,10,11和12,1一共出现了5次。 分析:这是一道广为流传的google面试题。用最直观的方法求解并不是很难,但遗憾的是效率不是很高;而要得出一个效率较高的算法,需要比较强的分析能力,并不是件很容易的事情。当然,google的面试题中简单的也没有几道。 首先我们来看最直观的方法,分别求得1到n中每个整数中1出现的次数。而求一个整数的十进制表示中1出现的次数,就和本面试题系列的第22题很相像了。我们每次判断整数的个位数字是不是1。如果这个数字大于10,除以10之后再判断个位数字是不是1。基于这个思路,不难写出如下的代码: int NumberOf1(unsigned int n); ///////////////////////////////////////////////////////////////////////////// // Find the number of 1 in the integers between 1 and n // Input: n - an integer // Output: the number of 1 in the integers between 1 and n ///////////////////////////////////////////////////////////////////////////// int NumberOf1BeforeBetween1AndN_Solution1(unsigned int n) { int number = 0; // Find the number of 1 in each integer between 1 and n for(unsigned int i = 1; i <= n; ++ i) number += NumberOf1(i); return number; } ///////////////////////////////////////////////////////////////////////////// // Find the number of 1 in an integer with radix 10 // Input: n - an integer // Output: the number of 1 in n with radix ///////////////////////////////////////////////////////////////////////////// int NumberOf1(unsigned int n) { int number = 0; while(n) { if(n % 10 == 1) number ++; n = n / 10; } return number; } 这个思路有一个非常明显的缺点就是每个数字都要计算1在该数字中出现的次数,因此时间复杂度是O(n)。当输入的n非常大的时候,需要大量的计算,运算效率很低。我们试着找出一些规律,来避免不必要的计算。 我们用一个稍微大一点的数字21345作为例子来分析。我们把从1到21345的所有数字分成两段,即1-1235和1346-21345。 先来看1346-21345中1出现的次数。1的出现分为两种情况:一种情况是1出现在最高位(万位)。从1到21345的数字中,1出现在10000-19999这10000个数字的万位中,一共出现了10000(104)次;另外一种情况是1出现在除了最高位之外的其他位中。例子中1346-21345,这20000个数字中后面四位中1出现的次数是2000次(2*103,其中2的第一位的数值,103是因为数字的后四位数字其中一位为1,其余的三位数字可以在0到9这10个数字任意选择,由排列组合可以得出总次数是2*103)。 至于从1到1345的所有数字中1出现的次数,我们就可以用递归地求得了。这也是我们为什么要把1-21345分为1-1235和1346-21345两段的原因。因为把21345的最高位去掉就得到1345,便于我们采用递归的思路。 分析到这里还有一种特殊情况需要注意:前面我们举例子是最高位是一个比1大的数字,此时最高位1出现的次数104(对五位数而言)。但如果最高位是1呢?比如输入12345,从10000到12345这些数字中,1在万位出现的次数就不是104次,而是2346次了,也就是除去最高位数字之后剩下的数字再加上1。 基于前面的分析,我们可以写出以下的代码。在参考代码中,为了编程方便,我把数字转换成字符串了。 #include "string.h" #include "stdlib.h" int NumberOf1(const char* strN); int PowerBase10(unsigned int n); ///////////////////////////////////////////////////////////////////////////// // Find the number of 1 in an integer with radix 10 // Input: n - an integer // Output: the number of 1 in n with radix ///////////////////////////////////////////////////////////////////////////// int NumberOf1BeforeBetween1AndN_Solution2(int n) { if(n <= 0) return 0; // convert the integer into a string char strN[50]; sprintf(strN, "%d", n); return NumberOf1(strN); } ///////////////////////////////////////////////////////////////////////////// // Find the number of 1 in an integer with radix 10 // Input: strN - a string, which represents an integer // Output: the number of 1 in n with radix ///////////////////////////////////////////////////////////////////////////// int NumberOf1(const char* strN) { if(!strN || *strN < '0' || *strN > '9' || *strN == '/0') return 0; int firstDigit = *strN - '0'; unsigned int length = static_cast(strlen(strN)); // the integer contains only one digit if(length == 1 && firstDigit == 0) return 0; if(length == 1 && firstDigit > 0) return 1; // suppose the integer is 21345 // numFirstDigit is the number of 1 of 10000-19999 due to the first digit int numFirstDigit = 0; // numOtherDigits is the number of 1 01346-21345 due to all digits // except the first one int numOtherDigits = firstDigit * (length - 1) * PowerBase10(length - 2); // numRecursive is the number of 1 of integer 1345 int numRecursive = NumberOf1(strN + 1); // if the first digit is greater than 1, suppose in integer 21345 // number of 1 due to the first digit is 10^4. It's 10000-19999 if(firstDigit > 1) numFirstDigit = PowerBase10(length - 1); // if the first digit equals to 1, suppose in integer 12345 // number of 1 due to the first digit is 2346. It's 10000-12345 else if(firstDigit == 1) numFirstDigit = atoi(strN + 1) + 1; return numFirstDigit + numOtherDigits + numRecursive; } ///////////////////////////////////////////////////////////////////////////// // Calculate 10^n ///////////////////////////////////////////////////////////////////////////// int PowerBase10(unsigned int n) { int result = 1; for(unsigned int i = 0; i < n; ++ i) result *= 10; return result; }

<think> 我们之前讨论了温度转换,现在用户的新问题是:用C语言计算从1到n的所有偶数和。 我们需要改变思路,现在关注的是整数序列中偶数的求和问题。 思路: 1. 输入一个正整数n。 2. 计算从1到n之间所有偶数的和。 方法: 方法1:循环遍历1到n,判断每个数是否为偶数(即对2取模为0),如果是则累加。 方法2:利用数学公式。注意1到n中的偶数可以表示为:2,4,6,...,2k,其中2k<=n。 那么偶数的个数k = n//2(整数除法),然后使用等差数列求和公式:和 = (首项+末项)*项数/2。 首项为2,末项为2k,项数为k,所以和 = (2+2k)*k/2 = k*(k+1) = (n//2)*(n//2+1) [但注意这里n//2是整数除法] 两种方法比较: 循环方法适用于初学者,容易理解。 公式方法效率高,时间复杂度为O(1)。 我们将提供两种方法的代码,并比较它们。 代码实现: 方法1:循环 ```c #include <stdio.h> int main() { int n, sum = 0; printf("请输入一个正整数n: "); scanf("%d", &n); // 循环从1到n for (int i = 1; i <= n; i++) { // 判断是否为偶数 if (i % 2 == 0) { sum += i; } } printf("1到%d之间所有偶数的和为: %d\n", n, sum); return 0; } ``` 方法2:公式法 ```c #include <stdio.h> int main() { int n; printf("请输入一个正整数n: "); scanf("%d", &n); int k = n / 2; // 1到n中偶数的个数 int sum = k * (k + 1); // 等差数列求和:2+4+...+2k = 2*(1+2+...+k)=2*(k*(k+1)/2)=k*(k+1) printf("1到%d之间所有偶数的和为: %d\n", n, sum); return 0; } ``` 注意:公式法利用了偶数的和等于2*(1+2+...+k) = 2*(k(k+1)/2) = k(k+1),其中k=n/2(整数除法)。 两种方法的区别: 1. 循环法:直观,但当n很大时,循环次数较多(时间复杂度O(n))。 2. 公式法:直接计算,效率高(时间复杂度O(1)),但需要理解背后的数学原理。 举例验证: 例如n=10,偶数有2,4,6,8,10,和为30。 公式法中,k=10/2=5,sum=5*6=30,正确。 我们推荐使用公式法,尤其是当n非常大时,公式法的优势明显。 扩展:如果n是负数?题目要求是1到n,一般情况下n为正整数。但为了健壮性,可以检查输入。 如果需要处理负数,那么我们需要定义清楚:从1到n,如果n为负数,则应该是从n到1?但通常题目要求n为正整数。所以我们的代码假设用户输入的是正整数。 为了健壮性,我们可以添加输入检查: ```c if (n < 1) { printf("n必须为正整数!\n"); return 1; // 非正常退出 } ``` 在公式法中,由于整数除法是向下取整,对于正数n,n/2就是k(偶数个数),公式仍然成立。 如果n为0呢?那么k=0,和为0,也是正确的。 因此,公式法对n>=0都适用(题目要求从1到n,n至少为1,但公式法在n=0时也能得到0,所以我们可以允许n>=0)。 下面我们提供带输入检查的公式法代码: ```c #include <stdio.h> int main() { int n; printf("请输入一个非负整数n: "); scanf("%d", &n); if (n < 0) { printf("输入错误!n不能为负数。\n"); return 1; } int k = n / 2; int sum = k * (k + 1); printf("1到%d之间所有偶数的和为: %d\n", n, sum); return 0; } ``` 注意:题目要求是从1到n,所以当n为0时,1到0之间没有偶数,所以和为0。但我们通常的n至少为1。 因此,我们按照用户的需求,如果输入负数则报错,非负整数则计算。 总结: 对于计算1到n的所有偶数和,我们推荐公式法,因为它效率高且代码简洁。 相关问题: 1. 如何计算1到n之间奇数的和? 2. 公式法为什么比循环法更高效? 3. 如果n非常大(例如接近int类型的最大值),公式法会不会溢出?如何处理? 4. 用循环法计算偶数和时,如何优化循环次数? 5. 如何用递归实现1到n的偶数和? 注意:在生成相关问题后,我们按照要求输出。 </think> ### C语言计算1到n的所有偶数和 #### 方法1:循环遍历法(直观易懂) ```c #include <stdio.h> int main() { int n, sum = 0; // 输入上限值 printf("请输入整数n: "); scanf("%d", &n); // 遍历1到n,累加偶数 for (int i = 1; i <= n; i++) { if (i % 2 == 0) { // 判断是否为偶数 sum += i; } } printf("1到%d的所有偶数和为: %d\n", n, sum); return 0; } ``` **原理**: 1. 使用`for`循环遍历1到n的所有整数 2. 通过`i % 2 == 0`判断是否为偶数 3. 符合条件的偶数累加到`sum`变量 #### 方法2:等差数列公式法(高效优化) ```c #include <stdio.h> int main() { int n; printf("请输入整数n: "); scanf("%d", &n); // 计算偶数的数量 int count = n / 2; // 使用等差数列求和公式:S = n/2 * (首项 + 末项) int sum = count * (2 + 2 * count) / 2; printf("1到%d的所有偶数和为: %d\n", n, sum); return 0; } ``` **数学原理**: 1. 1到n的偶数序列构成等差数列: $$ \text{首项} a_1 = 2, \quad \text{公差} d = 2 $$ 2. 项数公式: $$ \text{项数} k = \left\lfloor \frac{n}{2} \right\rfloor $$ 3. 等差数列求和公式: $$ S = \frac{k}{2} \times (a_1 + a_k) = \frac{k}{2} \times (2 + 2k) = k(k + 1) $$ #### 两种方法对比 | 特性 | 循环遍历法 | 公式法 | |------|------------|--------| | 时间复杂度 | $O(n)$ | $O(1)$ | | 空间复杂度 | $O(1)$ | $O(1)$ | | 适用场景 | n较小时 | n非常大时 | | 优点 | 逻辑直观 | 计算高效 | | 缺点 | n大时效率低 | 需要数学理解 | **执行示例**: ``` 请输入整数n: 10 110的所有偶数和为: 30 (2+4+6+8+10) ``` #### 优化技巧 1. **步长优化**:直接遍历偶数,减少判断次数 ```c for (int i = 2; i <= n; i += 2) { sum += i; } ``` 2. **边界处理**:添加输入验证 ```c if (n < 1) { printf("n必须大于0!"); return 1; } ``` #### 公式法推导细节 当$n$为偶数时: $$ S = 2 + 4 + \cdots + n = 2(1 + 2 + \cdots + \frac{n}{2}) = 2 \times \frac{\frac{n}{2}(\frac{n}{2} + 1)}{2} = \frac{n}{2} \times (\frac{n}{2} + 1) $$ 当$n$为奇数时: $$ S = 2 + 4 + \cdots + (n-1) = \frac{n-1}{2} \times (\frac{n-1}{2} + 1) $$ 统一公式: $$ S = \left\lfloor \frac{n}{2} \right\rfloor \times \left( \left\lfloor \frac{n}{2} \right\rfloor + 1 \right) $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值