剑指offer 学习笔记 1~n整数中1出现的次数
面试题43:1~n整数中1出现的次数。输入一个整数n,求1~n这n个整数的十进制表示中1出现的次数。
解法一:最直观的解法,累加1~n中每个1出现的次数,我们可以对10求余数判断整数的个位数字是不是1。如果这个数字大于10,在判断次低位时需要将数字除以10之后再判断除10后结果的个位数是不是1:
#include <iostream>
using namespace std;
int NumberOf1(unsigned num) {
int count = 0;
while (num) {
if (num & 1) {
++count;
}
num /= 10;
}
return count;
}
int NumberOf1Between1AndN(unsigned n) {
int sum = 0;
for (unsigned i = 1; i <= n; ++i) {
sum += NumberOf1(i);
}
return sum;
}
int main() {
cout << NumberOf1Between1AndN(12) << endl;
}
上例中,我们对每个数字都进行了除法和求余运算,如果输入的是n,n有O(logn)位,我们需要判断每一位是不是1,它的时间复杂度为O(nlogn),太慢了。
解法二:以21345分析,我们把数字分为两段:1~1345和1346~21345。我们先看后半段数字,即1346~21345,1的出现分为两种情况,首先分析1出现在最高位(本例中是万位)的情况,在这段数字中,1出现在10000~19999这10000个数字的万位中,一共出现了10000(10的4次方)次。值得注意的是,并不是对所有5位数而言在万位出现的次数都是10000次,对于万位是1的数字,如12345,1只出现在10000~12345的万位,出现了2346次,也就是除去最高数字之后剩下的数字再加上1。接下来分析1出现在最高位之外的其他4位数中的情况,例子中1346~21345这20000个数字中后4位中1出现的次数是8000次,由于最高位是2,我们可以把1346~21345分成两段:1346~11345和11346~21345,每一段剩下的4位数字中,选择其中一位是1,其余三位可以在0~9这10个数字中任意选择,根据排列组合,总共出现的次数是2x4x1000=8000次。
至于1~1345中1出现的次数,我们就可以用递归求得了。这也是为什么要把1~21345分成1~1345和1346~2134两段的原因,这样可以出现最大位的整数个数(万位的20000个数)。
#define _CRT_SECURE_NO_WARNINGS // 要定义在开头,否则可能失效
#include <iostream>
using namespace std;
int PowerBase10(int n) {
int res = 1;
for (int i = 0; i < n; ++i) {
res *= 10;
}
return res;
}
int NumberOf1(char* strN) {
if (strN == nullptr || *strN < '0' || *strN > '9' || *strN == '\0') {
return 0;
}
int first = *strN - '0';
int length = strlen(strN); // 返回strN的长度(不包括空字符)
// 与sizeof区别在于,sizeof是运算符,可用于任何类型,但strlen只能用于C风格字符串
// 并且用于C风格字符串时,sizeof返回值包括空字符
if (length == 1 && first == 0) {
return 0;
}
if (length == 1 && first > 0) { // 当递归到个位数时,如个位数大于1,说明存在一个1,因为总要经过1才能到大于1的数
return 1;
}
int numOfFirstDigit = 0; // 最大位为1的数的个数,以下以21345为例分析
if (first > 1) {
numOfFirstDigit = PowerBase10(length - 1); // 最大位大于1时,最大位为1的数范围为10000~19999,是1w个
}
if (first == 1) {
numOfFirstDigit = atoi(strN + 1) + 1; // 最大位为1时,最大位为1的数的范围为10000~strN表示的数个,即strN-10000+1个
}
int numOfOtherDigit = first * strlen(strN + 1) * PowerBase10(length - 2); // 1346~21345中除第一位外剩下的数字中有1的个数,这个区间正好有first*1w=2w个数
// 先来看1w个数(4位数0000~9999),固定其中的一个数为1,有strlen(strN+1)=4种方法
// 每种固定方式下,剩下的能活动的数的个数为length-2=3个,每位有10种可能,即10的3次方种可能
int numRecursive = NumberOf1(strN + 1); // 递归找到0~1345中1的个数
return numOfFirstDigit + numOfOtherDigit + numRecursive;
}
int NumberOf1Between1AndN(int n) {
if (n <= 0) {
return 0;
}
char strN[50]; // 为方便处理,将数字转化为字符串,50可以保证能完全装下最大的int数
sprintf(strN, "%d", n); // 使用第二个参数中的模式串初始化strN,第二个参数中的%个数应与第二个参数之后的参数个数相等
// 如成功,返回写入到strN中的字符总数(不包括最后的空字符),如失败,返回一个负数
// 此功能在VS2008中被禁用,因为可能会引起内存越界等错误,VS2015及以后版本提供了sprintf_s来代替它,功能相同,但不会数组越界
// 如果必须要用此函数,使用宏定义#define _CRT_SECURE_NO_WARNINGS
return NumberOf1(strN);
}
int main() {
cout << NumberOf1Between1AndN(12) << endl;
}
这种思路每次递归去掉最高位,递归的次数和位数相同,一个数字有O(logn)位,时间复杂度为O(logn)。
法二我第一次看时我觉得排列组合时1算重了,比如固定第一位为1,那后边也会出现1,那么到后边的那位固定为1时,就与之前重复了,我的想法是错的,题目中求的是1出现的次数,而非含1的数出现的次数,如果是后者,才会重复。更具体的例子是,如果求0到99之间的1出现的次数,则可以先固定个位,十位有10种数字排列,这十种数字都可以与个位的1产生数字中个位带1的数字,当固定十位时,同理也有十种数字可以产生十位带1的数字。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)