在采访中有人问我这个问题:

检查数字N是否为K的幂。
示例:
N = 32,K = 2 => True
N = 40,K = 5 =>错误

我写了下面的代码,但得到的反馈是,可以改善复杂度,如何提高其复杂度?
def check_kth_power(n, k):
        while n%k == 0:
            n = n/k

        if n != 1:
            return False
        return True

        
print check_kth_power(128, 5)


评论

您知道N = 40,K = 5为假,因为如果5为奇数,则每个幂。而且偶数的所有幂都是偶数。

名称“ check_kth_power”似乎在询问是否存在某些x,其中n = x ^ k。我建议使用check_power_of_k作为更好的名称。

#1 楼

您可能已经使用了这样一个事实:如果\ $ n = k ^ x \ $,则\ $ \ log_k(n)= x \ $是整数。但是,由于Python中float的精度有限,这会产生一些问题。为了解决这个问题,我们可以同时测试对数ceilfloor,看看是否可以取回n。这样,我们最多只需要进行三个昂贵的计算。

from math import log, ceil, floor

def check_kth_power(n, k):
    kth_log = log(n, k)
    return k ** int(ceil(kth_log)) == n or \
           k ** int(floor(kth_log)) == n


现在,无需进行两次幂运算,我们可以意识到k ** int(ceil(kth_log)) = k * k ** int(floor(kth_log))的速度很小-在大约50%的情况下提高效率(由于@KyleGullion:

def check_kth_power(n, k):
    candidate = k ** int(log(n, k))
    return candidate == n or k * candidate == n


这似乎适用于相当大的值(我检查了大约\ $ 2 ^ {32000 },2 ^ {20000}-1,3 ^ {20000} \ $)。这是下表中的时间表。

要获得更多的速度提升,您可以窃取@pantarei的答案是个不错的过早退出方法,如果数字是k的幂,这会牺牲一些速度,但如果不是x % 1 == 0的话,则会获得约2的系数:

def check_kth_power_gp(n, k):
    if n%k != 0:
        return False
    candidate = k ** int(log(n, k))
    return candidate == n or k * candidate == n



最后是一些时间,它比较了我的代码和@ user1825464的答案,@ pantarei的答案,@ RyanMill的答案,@ Eevee的答案和@JamesMishra的答案的代码:

+-------------+---+----------+-------------+-----------+------------+---------+--------------+
|      n      | k | Graipher | user1825464 | panta rei | Ryan Mills |  Eevee  | James Mishra |
+-------------+---+----------+-------------+-----------+------------+---------+--------------+
| 2**1000     | 2 | 1.73 µs  | 9.9 µs      | 105 µs    | 12.9 µs    | 388 µs  | 3.23 µs      |
| 2**1000 - 1 | 2 | 1.99 µs  | 2.5 µs      | 619 ns    | 15.7 µs    | 765 ns  | 3.09 µs      |
| 3**1000     | 2 | 2.41 µs  | 4.26 µs     | 854 ns    | 22.4 µs    | 1.04 µs | 4.08 µs      |
| 3**1000     | 3 | 2.81 µs  | 12.6 µs     | 125 µs    | 13.8 µs    | 556 µs  | 4.51 µs      |
+-------------+---+----------+-------------+-----------+------------+---------+--------------+


因此,日志并不关心它是否实际上是k次方,而循环在这种情况下必须做更多的工作,但可能会发现它的性能不是更快。


您可以使用!= 0来检查整数,否则使用float来检查整数(感谢@Peilonrayz在注释中):

from math import log

def check_kth_power(n, k):
    return log(n, k) % 1 == 0


但是,正如注释中所述,这仅在log的精度不再足以区分n的结果之前有效不是整数。

对于形式为\ $ 2 ^ x-1 \ $的数字,如@GarethReese所述,这发生在\ $ x = 48 \ $,所以您应该问如果k和q4312079q有任何限制。

评论


\ $ \ begingroup \ $
评论不用于扩展讨论;此对话已移至聊天。
\ $ \ endgroup \ $
– Jamal♦
17年4月17日在9:34

\ $ \ begingroup \ $
通过使用模幂运算来检查除数,可以节省很多工作。即返回pow(k,int(ceil(kth_log)),n)== 0或pow(k,int(floor(kth_log)),n)== 0
\ $ \ endgroup \ $
–凯尔G
17年4月17日在16:50

\ $ \ begingroup \ $
@KyleGullion您确定吗?对于2 ** 50和2 ** 50-1,您的版本要慢50%。
\ $ \ endgroup \ $
– kyrill
17-4-17在23:53



\ $ \ begingroup \ $
测试用例n = 16,k = -2呢?我敢打赌,很多答案都会打破。
\ $ \ endgroup \ $
– Peter Taylor
17年4月19日在9:51



\ $ \ begingroup \ $
正确是正确的:16 ==(-2)^ 4。其他有趣的测试用例是n = 8,k = -2(正确答案为False)和n = -8,k = -2(正确答案为True)。
\ $ \ endgroup \ $
– Peter Taylor
17-4-19在11:12



#2 楼

def check_kth_power(n, k):
    if n == 1:
        return True

    div = k
    while div * div <= n:
        div = div * div

    if n % div != 0:
        return False
    return check_kth_power(n / div, k)


其复杂度为\ $ O(log(log(N))\ $,相比之下,\ $ O(log(N))\ $为简单除法或对数。

这背后的原因是,如果\ $ N \ $是\ $ K \ $的幂,则它在基数\ $ K \ $中的表示类似于\ $ 000100 ..00 \ $。通过检查形式为\ $ K ^ {2 ^ i} \ $的\ $ N \ $的最大除数,进行二进制搜索来查找该位置(如果存在),进行除法,然后执行递归地返回,直到它不除(不是\ $ K \ $的幂),或者返回1(并且是\ $ K \ $的幂)。

评论


\ $ \ begingroup \ $
我真的以为我可以将其纳入递归限制,但是它适用于令人惊讶的大数字。在更新后的答案中,请查看我在计算机上进行的时间比较,以比较您的计算机和(固定)算法。
\ $ \ endgroup \ $
–地狱
17年4月17日在9:02

\ $ \ begingroup \ $
@Graipher尾部呼叫优化怎么样?是不是Python优化了对“跳转”的调用?
\ $ \ endgroup \ $
– kyrill
17年4月17日在15:55

\ $ \ begingroup \ $
@kyrill Python不会进行尾部调用优化,而且可能永远不会做。参见stackoverflow.com/questions/13591970/…
\ $ \ endgroup \ $
–地狱
17年4月17日在19:05

\ $ \ begingroup \ $
@Graipher谢谢,这是一些有趣的阅读。
\ $ \ endgroup \ $
– kyrill
17年4月17日在19:20

\ $ \ begingroup \ $
当然,手工完成转换尾部调用并不难。
\ $ \ endgroup \ $
–噪声
17年4月17日在23:02

#3 楼

有点大胆的猜测,但也许他们正在寻找使用divmod的原因,因为具有相同操作数的%/本质上是相同的操作?

def check_kth_power(n, k):
    while True:
        q, r = divmod(n, k)
        if r == 0:
            n = q
        else:
            break

    return n == 1


评论


\ $ \ begingroup \ $
欢迎使用代码审查!很高兴看到排名靠前的SO用户来到这里。 :)
\ $ \ endgroup \ $
– Peilonrayz
17-4-17在13:38



\ $ \ begingroup \ $
我刚刚经过了,因为这引起了我的注意,但是现在我觉得有些必须去逛逛了,啊:)
\ $ \ endgroup \ $
–伊芙
17年4月18日在7:42

\ $ \ begingroup \ $
我将您的函数添加到了时序表中,该值的展度是千分之一次幂,对于此变体而言,甚至没有更大。很有趣的看到。
\ $ \ endgroup \ $
–地狱
17年4月18日在10:06

\ $ \ begingroup \ $
哈,看起来像函数调用开销使您节省了不分割两次的钱
\ $ \ endgroup \ $
–伊芙
17年4月18日在11:31

#4 楼

这是@Graipher答案的一个版本,但是它更多地依赖于Python int的属性。

 from math import log
def check_kth_power(n, k):
    test_pow = int(log(n, k))
    test_n_floor = k ** test_pow
    test_n_ceil = k ** (test_pow + 1)
    return n == test_n_floor or n == test_n_ceil
 


如果test_pow遇到浮点问题,则test_n_floortest_n_ceil都不等于n。这是因为两个int上的Python指数运算符将产生intlong类型,并且不会丢失精度。

>>> log(2 ** 50, 2)
50.0
>>> log(2 ** 50 - 1, 2)
50.0
>>> check_kth_power(2 ** 50, 2)
True
>>> check_kth_power(2 ** 50 - 1, 2)
False
>>> 2 ** 50 - 1 == 2 ** int(log(2 ** 50-1, 2))
False
>>> check_kth_power(3 ** 5, 3)
True
>>> check_kth_power(3 ** 5 + 1, 3)
False
>>> check_kth_power(3 ** 5 - 1, 3)


False

贷方@Graphier在我的原始答案中发现了一个错误。我已经更新了修复程序。

评论


\ $ \ begingroup \ $
通过注意test_n_ceil == k * test_n_floor,您实际上可以在这里避免取幂。
\ $ \ endgroup \ $
–凯尔G
17年4月18日在2:58

\ $ \ begingroup \ $
@KyleGullion这是一个很好的建议,请在我的回答中予以实施。它的速度提升约为0.9到2,所以值得:)
\ $ \ endgroup \ $
–地狱
17年4月18日在11:54

\ $ \ begingroup \ $
将您添加到了时序表中(即使现在变为水平滚动)。我的代码更快(并且还没有进行Kyle的优化),因为我仅在第一个不等于n的情况下才计算第二个指数,因此如果在50%的情况下该数字是k的幂,则可以节省计算量(50%,因为有时浮点精度会导致其略高于或低于整数值)。
\ $ \ endgroup \ $
–地狱
17年4月18日在12:00

#5 楼

为什么不直接使用它呢?

def check_kth_power(n, k):
    if n%k != 0:
        return False

    pow = k
    while pow < n:
        pow = pow * k
    return pow == n

print check_kth_power(128, 5)


此代码更易于理解,并且使用了更有效的操作。

但是@Graipher的答案可能是最有效的方法。

评论


\ $ \ begingroup \ $
请参阅我的答案中的时间安排。如果数字不是k次幂,则您的代码将杀死它,如果不是,则失去它:)
\ $ \ endgroup \ $
–地狱
17年4月17日在9:31

\ $ \ begingroup \ $
@Graipher因此,您也可以在代码中添加小检查n%k!= 0,并且也将其杀死...
\ $ \ endgroup \ $
– kyrill
17年4月17日在15:57

\ $ \ begingroup \ $
@kyrill我尝试过,它是k的幂的情况从〜2us降低到〜3us,而如果不是,则降低到<1us。因此,这取决于您测试的大多数数字是否具有幂...
\ $ \ endgroup \ $
–地狱
17年4月18日在15:44

#6 楼

二进制搜索

他们对复杂性感兴趣。我很确定他们希望您使用二进制搜索。

这是一个面试问题。因此,您必须展示自己对算法和复杂性的了解。

仅通过重复乘法(或除法)进行幂运算就不会削减它。
仅使用math.log,这可能是您在实践中将如何做,也不会了解您的知识。

要进行二进制搜索,首先需要一种快速的方法来获得一个上限。然后通过移动上限(hi)和下限(lo),以标准教科书方式实现二进制搜索。

def got_the_power(n, k):
    '''
    Returns True if n is a power of k, otherwise returns False.    
    '''

    def get_upper_bound(n, k):
        '''
        Finds an upper bound for e where k**e = n.
        '''
        a, e = k**2, 2
        while a < n:
            a *= a
            e = e*2
              # The above line was e = e**2 in the original post.
              # Bug spotted by Graipher. Thanks! 
        return e

    hi = get_upper_bound(n, k)
    lo = 0
    while hi - lo > 1:    # standard binary search
        m = (hi - lo) / 2 + lo
        a = k**m
        if a == n:
            return True
        if a > n:
            hi = m
        else:
            lo = m
    return False

# Test it out
print got_the_power(64, 2)  # True
print got_the_power(63, 2)  # False


子例程和整体函数每个以\ $ \ mathcal {O}(log(e))\ $。运行,其中e是从n到以k为底的对数。但是,基本运算是取幂,它本身位于\ $ \ mathcal {O}(log(e))\ $中。因此,总体复杂度为\ $ \ mathcal {O}(log(e)^ 2)\ $。

评论


\ $ \ begingroup \ $
这似乎过于复杂且效率低下,因为它丢弃了很多中间值,然后再次使用。构建一个k ^(2 ^ i)列表直到获得超过n的值(有效地进行get_upper_bound但存储中间值),然后应用该逻辑,即如果list元素为如果它大于累加器,则可以将其丢弃,否则必须将其精确划分为下一个累加器。
\ $ \ endgroup \ $
– Peter Taylor
17年4月19日在9:48

\ $ \ begingroup \ $
没错,存储中间值肯定会消除一个常数因子。但这不会改变时间的复杂性。而且我将无法提供教科书二进制搜索作为代码的一部分。
\ $ \ endgroup \ $
–瑞安·米尔斯(Ryan Mills)
17年4月20日在12:39

#7 楼

如果n == 0或k == 1,则代码的复杂性非常非常糟糕,因为循环没有完成:-(因此应该解决。

当人们讨论复杂性时,最糟糕的是案例和平均案例在计算机科学中,人们通常主要关注最坏的情况。在现实生活中,平均案例更为重要。

在您的案例中,最坏的情况是n = k ^ m ,则需要m步,但是,如果随机选择n,则仅一步就停止的(k-1)/ k几率是两步,(k-1)/ k ^ 2 (k-1)/ k ^ 3三个步骤,依此类推。即使对于k = 2,平均步骤数也只有两个。

当然,这只是我说您的代码还可以。在求职面试中,您可能必须与面试官说的一切相处。

评论


\ $ \ begingroup \ $
或者,如果k ==0。更不用说k <0,这破坏了大多数答案。
\ $ \ endgroup \ $
– Peter Taylor
17年4月19日在9:50