从1到N整数中1出现的次数--我的代码

问题: 输入一个整数n,求从1 到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11和12,1一共出现了5次。

这是《剑指offer 名企面试官精讲典型编程题》一书中的第32题。书中Herry给出了递归算法。我自己写的是循环算法。虽然有着不同写法,但大体上思路是一致的。我的算法并不比Herry的强。写下来只是提供另外的一个解法。

 

分析:要得到一个整数中出现的1的次数,其实就是算出1在个位,十位,百位等等中出现的次数总和。在个位上,1在0至9中出现的频率只是1次,其它023456789都和1无关。但是整个个位的循环次数是除去个位的整数值加1(或不加)。如,23, 个位的1出现的次数是3 (23去掉3再加1),即01,11,21;123个位的1出现次数是13(123去掉3加1),即01,11,21,31,41,51,61,71,81,91,101,111,121;以此类推。

 

再来看十位, 1在0至9中出现的频率也只是1次,但是和个位不同的是,十位中的1却要因为个位的原因至多循环10次(或不到10次),如10,11,12,13,14,15,16,17,18,19,所以整个十位的循环次数是除去个位和十位的整数值乘以10(或不乘)。

百位是也是如此,百位上的1要为十位和个位循环至多10*10次。

由此,我们就可以得出了算法。

时间复杂性是O(n)

 

       private int GetOneOccurence( int piNum ) {
            String lsNum = piNum.ToString();
            int liResult = 0;

            // calculate result from first digit to last one 
            for ( int i = lsNum.Length - 1; i >= 0; i-- ) {
                liResult += GetOneOccurence( lsNum, i );
            }

            return liResult;
        }

        private int GetOneOccurence( String psNum, int piLoopPosition ) {
            String lsSuffix;
            int liResult = 0;

            // current: digit in current position that is specified by piPos
            // Suffix: get following digits from current digit 
            // Prefix: get digits before current digit
            //      e.g. 456791, current number is 6, the lsSuffix is 791, prefix is 45.
            lsSuffix = psNum.Substring( piLoopPosition + 1 );

            int liCurNum = int.Parse( psNum.Substring( piLoopPosition, 1 ) ) ;
            int liPrefix = ( piLoopPosition == 0 ? 0 : int.Parse( psNum.Substring( 0, piLoopPosition )  ) );
            int liSuffix = ( String.IsNullOrEmpty( lsSuffix ) ? 0 : int.Parse( lsSuffix ) );

            // Get power suffix of current number 
            // e.g: 1,234 , power of suffix is 100 when current number is 2
            int liSufPow = (int)Math.Pow( 10, lsSuffix.Length );

            // if current number is great than 1, then 1 occurence in this position is ( prefix + 1 ) * power of suffix
            //   e.g: 12345, the current number is 3 now. then 1 occurence in third position is ( 12 + 1 ) * 100
            //          that is: 01100 to 12100, plus 100 to 199
            // if current number is equal to 1, then occurence is prefix * power of suffice + suffix + 1
            //   e.g: 21345, current number is 1. the value is 2 * 1000 + 345 + 1
            //          that is: 1000 to 1345, plus 11000 to 21000
            // the last situation is that current number is equal to 0. So, occurence is simple to prefix * power of suffix
            //   e.g: 12045. the result should be 12 * 100
            //          that is 00100 - 00199, 01100 - 01199, 02100 - 02199,... ,11100 - 11199.
            if ( liCurNum > 1 ) {
                liResult = ( liPrefix + 1 ) * liSufPow;
            } else if ( liCurNum == 1 ) {
                liResult = liPrefix * liSufPow + ( liSuffix + 1 ); 
            } else {
                liResult = liPrefix * liSufPow;
            }

            return liResult;
        }

 

上述的算法基本上体现了分析思路, LeetCode有高手给出的最简化的程序。
算法中的循环是为了得到数字的位数,既个位,十位,百位,千位等等,那么我们可以用i * 10来替代。
For( long 1 = 1; i <= piNum; I *= 10 )
每次用 piNum / i  就能得到当前位数之前的数字,再 % 10 得到当前位数。(这里用的是long,不是int。i会超过最大的int值。)
这道题唯一要注意的是当前位数的数字大于1,等于1,还是小于1。用上面的23例子来说,个位的1出现的次数是3 (23去掉3得到2再2+1=3),既01,11,21。如果是20的话,就不能用20去掉0再2+1=3了,个位上的1不会出现21这个数字的,所以只能是01,11。LeetCode上用+8来解决这个问题, 既(23 + 8 )/10, (20 +8)/10 。为什么不用+9呢,因为当前数字如果是1的话要单独处理。
如果我们要处理213的十位,当前位数的数字是1。那么十位上的1不是(2+1)*10,这样就把214到219一并算进去了。只能先得到2*10,在把(2)10 至(2)13十位上出现的1加上去。得到2*10的计算公式是(21 + 8 )/10 * 10,如果用+9而不是+8就不对了。所以公式是(21 + 8 ) / 10 * 10 + ( 214 % 10 + 1 )。(别忘了210,所以最后要再加个1)。

public class Solution {
    public int CountDigitOne(int n) {
        long lintResult = 0;
        
        for( long i = 1; i <= n; i *= 10 ) {
            lintResult += ( n / i + 8 ) / 10 * i + ( ( n / i ) % 10 == 1 ? ( n % i + 1 ) : 0 ) ;
        }
        
        return (int)lintResult;
    }
}


 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值