剑指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的数字。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值