我被要求完成此CodeStepByStep问题。以下是一个简短的摘要:编写一个名为count_even_digits的函数,该函数接受两个整数作为
参数,并返回第一个
数字中的偶数位数。偶数数字是0、2、4、6或8。第二个
值表示该数字有多少个数字。第二个值是
保证与第一个数字中的位数匹配。

例如,数字8546587具有四个偶数位数(两个8,即
4,和6),因此调用count_even_digits(8346387, 7)应该返回
4。

您可以假定传递给函数的值是非负的。


我的实现:

def count_even_digits(digit, nums):
    count = 0
    digits = str(digit)
    for i in range(nums):
        if int(digits[i]) % 2 == 0:
            count += 1

    return count


我正在寻找有关实现的任何反馈,以及如何使此代码更高效。我不得不将num强制转换为字符串,然后查看每个字符,将其转换为int,然后确定是否为偶数,这似乎很奇怪。对我来说,不必强制执行(int => string => analyzing as char => int)

#1 楼

您应该像本地人一样看一下Loop!显式遍历索引通常不是在Python中执行某些操作的最佳方法。通过直接遍历n的字符串表示形式,您甚至不需要第二个参数(如果赋值/定义的接口不需要它,则应将其删除)。当您开始使用无法索引的生成器或其他可迭代变量时,这将为您提供很多帮助。和sum以及一个生成器表达式,可以使您的函数更短:
不使用强制转换为True == 1的替代方法是创建False == 0函数:速度非常快,但转换为docstring的速度仍然更快,目前已测试到10 ** 308。我还添加了@vurmux答案和@ spyr03答案中显示的功能,它们甚至比这两个解决方案还快:



有趣的是,手动str循环比使用digits的生成器表达式要快。


对于具有很多数字的数字,它变得有点复杂。这包括一个在我关于SO的问题的答案中提出的函数,该问题为何str循环比for更好。此功能甚至更快,但我只建议在需要最后一点速度时使用它。

def count_even_digits(n, n_digits):
    """Return the number of digits of n which are even.
    Second argument (number of digits) is unused.
    """
    return sum(int(d) % 2 == 0 for d in str(n))




评论


\ $ \ begingroup \ $
虽然这是一个更好的解决方案,但如果您不打算实现n_digits,则不应将其作为参数
\ $ \ endgroup \ $
–user172231
19年5月23日在13:06

\ $ \ begingroup \ $
@MitchelPaulin作业需要它。
\ $ \ endgroup \ $
– Christophh Frings
19年5月23日在13:12

\ $ \ begingroup \ $
与使用整数算术相比,在字符串之间来回转换是否有优势(速度)? (类似于d // 10 ** i%2 == 0)
\ $ \ endgroup \ $
– Christophh Frings
19年5月23日在13:14

\ $ \ begingroup \ $
@ChristophFrings我目前正在将该替代方法添加到答案中
\ $ \ endgroup \ $
–地狱
19年5月23日在13:15

\ $ \ begingroup \ $
@ChristophFrings将其添加到答案中。
\ $ \ endgroup \ $
–地狱
19年5月23日在13:21

#2 楼

如另一个答案所述,在大多数情况下,在Python中使用索引进行迭代是一个坏主意。 Python有许多避免它的方法和模块。对于您的问题,Python具有强大的十进制模块来处理十进制数字。这是解决问题的方法:

from decimal import *

def count_even(number, length):
    return sum(
        d % 2 == 0
        for d in Decimal(number).as_tuple().digits
    )


Decimal(number).as_tuple().digits将返回数字的元组:

Decimal(123456).as_tuple().digits -> (1, 2, 3, 4, 5, 6)


PS此解决方案不使用数字的长度,也不需要它。 decimal模块可以完成整个工作。

评论


\ $ \ begingroup \ $
在时序图中添加了功能。它比我手动获取数字的方式快很多。
\ $ \ endgroup \ $
–地狱
19年5月23日在13:34

\ $ \ begingroup \ $
我喜欢总和用法,但是使用Decimal对于imo来说似乎过分了。
\ $ \ endgroup \ $
– Quelklef
19年5月24日在4:37

#3 楼

由于您要处理数字,因此您必须在某个时候选择一个底数(在本例中为10)。因此,将数字转换为字符串并不是获取数字的不错方法。我会保持简单,只检查每个数字,看看它是否是您想要的数字之一。

# Incorporating the other suggestions to loop over the chars directly rather than using an index
def count_even_digits(n, n_digits):
    count = 0
    for c in str(n):
        if c in "02468":
            count += 1
    return count


您可以跟进另一个建议使用内置总和

def count_even_digits(n, n_digits):
    return sum(c in "02468" for c in str(n))


使用这两种方法的优势在于,您仅从int => str进行转换,而不是(隐含十进制) int => str => int。

评论


\ $ \ begingroup \ $
我喜欢这样做是为了提高可读性,但感觉它可能会违反一些最佳做法规则
\ $ \ endgroup \ $
– JollyJoker
19年5月24日在12:21

#4 楼

这个答案的目的是说明,用其他语言做事的最有效方法可能与Python不同,在这种情况下就是。如果对问题的这种不同的低层观点是IDK的一个很好的代码审查答案,但是将其放在此处以防人们发现它有趣。转换为字符串,char和int会导致效率低下。在Python中执行此操作的原因是可以使用str() 1访问有效的已编译C循环,以将数字分解为基数为10的数字。大概这将得到很好的优化,因为它在实际程序中得到了很多使用。

在一种编译语言中,您只需要对10进行重复的除法/模运算,一次得到十进制数(首先是LSD),然后随便累加低位。或者,由于您希望计数低位= 0(偶数)的数字,因此请对几率进行计数并从循环外的总计数中减去。或者由于调用者无缘无故地向我们提供了总位数(生成十进制数字时我们总会发现这一点),因此我们可以利用它并从中开始递减计数,并在循环内保存1个操作。

例如在C中使用(假定固定宽度整数,不像Python的任意精度整数可以具有多个32位块):

 // unsigned division by a constant is more efficient, and the number is non-negative
int count_evens(unsigned x, int ndigits) {
    int count = ndigits;   // we don't need the caller to tell us how many decimal digits
                           // but take advantage if we have it.
    do {
        count -= (x%10) & 1;    // remove odd digits from the count
        x /= 10;                // remove the least significant decimal digit
    } while (x!=0);
    return count;
}
 


(这是为了“乐观”地编写,因此不必在第一次迭代之前检查x==0,因为在这种特殊情况下,运行循环的1次迭代而不是0仍然可以得到正确的答案。出来的检查会减少针对common(?)情况(非零输入)的指令。考虑使用C语言编写代码时理想的asm是可选的,但我很喜欢。)


有趣的事实:C编译器实际上不会使用整数除法指令,因为10是编译时常量。他们将使用乘法逆,这在现代CPU上效率更高。因此,以这种方式编写循环可能仅对C或其他编译语言有效,而对Python无效。

有趣的事实2:gcc发现了针对x86-64和Godbolt编译器资源管理器上的AArch64:x%10是冗余的。如果x%10为奇数,则x也为奇数。因为10是2的倍数,所以它只需要检查x的低位,而不实际从商和除数计算余数。 (定点乘法-逆向技巧仅提供商,余数是分开的。)尽早检测循环结束条件。使用x>=9可能有助于让CPU找出循环结束的时间,并减少/避免分支对最后一次循环迭代的错误预测惩罚。

如果调用者曾经使用过十进制数,而没有完成此功能的某些工作(或者可能只是对10的幂进行比较),或者已将其从十进制字符串转换而来。字符串,计算其中的奇/偶数本来会更有效。例如如果它是8位数字,则在asm中,您可以一次加载全部8个字节,并与while(--ndigits)和popcnt一起水平累加这些字节。 (对于可变长度,您可以进行较大的负载并使用移位来移出其值不属于字符串的字节。)


脚注1:

Python支持比2 ^ 32大得多的整数。它以2 ^ 30的基本格式(32位块)存储它们。 (https://rushter.com/blog/python-integer-implementation/)

基本的10 ^ 9格式将允许更有效地除以10(的幂),特别是转换为十进制可以将每个32位块分别处理为9个十进制数字,仅执行32位除法。 >但是2 ^ 30允许更有效的左/右移位。高效地与本地C 64位整数之间进行转换!

无论哪种方式,使用小于32位整数全宽度的东西都可以更轻松地实现“随身携带”(进位(进位和进位))。但是,这意味着在具有随加指令的机器上,尤其是可以在64位块上运行的64位机器上,最佳asm明显要差得多。

无论如何,实际除以10的整数必须查看所有较高的位,因为10并非2的幂。 ,但在Python循环中将0x0101010101010101乘10并不十分有效也就不足为奇了。

评论


\ $ \ begingroup \ $
您不需要取模10。最后一位的奇偶校验与数字的奇偶校验相同。同样,整数逆仅适用于奇数。因此,必须将“ / 10”设置为“ >> 1”,然后除以5。
\ $ \ endgroup \ $
– grovkin
19年5月24日在19:10

\ $ \ begingroup \ $
@grovkin:关于不需要%10的观察是我稍后回答的第二个有趣事实。 :)我把它留给了人类,因为编译器对其进行了优化。回复:除以5与10:由编译器为我们处理。对于所有输入值都适用的精确除法无论如何都需要将32x32 => 64位乘法的高半部分移位,是的x / 10和x / 5使用相同的常数但具有不同的移位计数。 godbolt.org/z/a0LwoE。我不认为知道这对我们使用C或Python都有帮助,因为用Python评估表达式的每一步都比进行硬件除法还要昂贵。
\ $ \ endgroup \ $
– Peter Cordes
19年5月24日19:48



#5 楼

您可以使用python lambda函数

count_even_digits= lambda x,y:len([i for i in str(x) if int(i)%2==0])
count_even_digits(n,n_digits)


这将创建一个lambda(inline)函数,该函数接受两个输入x和y并执行列表推导,从输入中提取数字如果它们是偶数,则返回number(x),然后返回此列表的长度,即偶数个数字。