计算2 ^ n

时间:2019-04-04 07:12:20

标签: python time-complexity pow

我正在尝试计算时间复杂度并将其与实际计算时间进行比较。

如果我没记错的话,时间复杂度是O(log(n)),但是从实际的计算时间来看,它看起来更像O(n)甚至O(nlog(n))。

造成这种差异的原因是什么?

def pow(n):
    """Return 2**n, where n is a nonnegative integer."""
    if n == 0:
        return 1
    x = pow(n//2)
    if n%2 == 0:
        return x*x
    return 2*x*x

理论时间复杂度

enter image description here

实际运行时间

enter image description here

7 个答案:

答案 0 :(得分:8)

我怀疑您的时间计算不准确,所以我使用abort()进行了计算,这是我的统计数据:

timeit

更新

enter image description here

好吧,代码确实以O(n * log(n))...运行!可能的解释是,对于大数,乘法/除法不是O(1),因此该部分不成立:

import timeit
# N
sx = [10, 100, 1000, 10e4, 10e5, 5e5, 10e6, 2e6, 5e6]
# average runtime in seconds
sy = [timeit.timeit('pow(%d)' % i, number=100, globals=globals()) for i in sx]

乘除法实验:

T(n) = 1 + T(n//2)
     = 1 + 1 + T(n//4)
#      ^   ^
#     mul>1
#         div>1
# when n is large

mul = lambda x: x*x div = lambda y: x//2 s1 = [timeit.timeit('mul(%d)' % i, number=1000, globals=globals()) for i in sx] s2 = [timeit.timeit('div(%d)' % i, number=1000, globals=globals()) for i in sx] mul相同的图-它们不是O(1)(?)小整数似乎更有效,但对大整数没有太大区别。我不知道那是什么原因。 (不过,如果有帮助,我应该在这里保留答案)

enter image description here

答案 1 :(得分:5)

迭代次数将为log(n,2),但每次迭代都需要在两个数字之间执行乘法,该数字是前一次迭代的两倍。

可变精度数字的最佳乘法算法在O(N * log(N) * log(log(N)))O(N^log(3))中执行,其中N是表示该数字所需的位数(位或字)。实际上,这两种复杂性相结合会产生比O(log(n))大的执行时间。

每个迭代中两个数字的位数为2 ^ i。因此,总时间将是经过log(n)次迭代的数字的乘法(x * x)复杂度之和

要基于Schönhage–Strassen乘法算法计算函数的时间复杂度,我们需要使用以下公式添加每次迭代的时间复杂度:O(N * log(N)* log(log(N))):< / p>

∑ 2^i * log(2^i) * log(log(2^i)) [i = 0...log(n)]

∑ 2^i * i * log(i) [i = 0...log(n)]

那将是相当复杂的,所以让我们看一个更简单的场景。

如果Python的可变精度乘法使用最幼稚的O(N ^ 2)算法,则最坏情况的时间可以表示为:

∑ (2^i)^2 [i = 0...log(n)]

∑ 4^i [i = 0...log(n)]    

(4^(log(n)+1)-1)/3    # because ∑K^i [i=0..n] = (K^(n+1)-1)/(K-1)

( 4*4^log(n) - 1 ) / 3

( 4*(2^log(n))^2 - 1 ) / 3    

(4*n^2-1)/3   # 2^log(n) = n

(4/3)*n^2-1/3

这将是O(n ^ 2),这表明log(n)迭代时间会抵消自身,而有利于乘法的复杂度分布。

如果将此推理应用于唐津乘法算法,我们将得到相同的结果:O(N ^ log(3)):

∑ (2^i)^log(3)    [i=0..log(n)]

∑ (2^log(3))^i    [i=0..log(n)]

∑ 3^i             [i=0..log(n)]

( 3^(log(n)+1) - 1 ) / 2   # because ∑K^i [i=0..n] = (K^(n+1)-1)/(K-1)

( 3*3^log(n) - 1 ) / 2

( 3*(2^log(3))^log(n) - 1 ) / 2

( 3*(2^log(n))^log(3) - 1 ) / 2

(3/2)*n^log(3) - 1/2

对应于O(n ^ log(3))并证实了这一理论。

请注意,由于您正在以指数形式取得 n 进步,因此衡量表的最后一列具有误导性。这改变了t [i] / t [i-1]的含义及其对时间复杂度评估的解释。如果N [i]和N [i-1]之间的级数呈线性关系,那将更有意义。
考虑到计算中的N [i] / N [i-1]比率,我发现结果似乎与O(n ^ log(3))的相关性更高,这表明Python将Karatsuba用于大型整数乘法。 (对于MacOS上的3.7.1版)但是,这种相关性非常弱。

最终答案:O(log(N))

经过更多测试后,我意识到乘大数所花费的时间存在巨大差异。有时,较大的数字比较小的数字花费的时间要少得多。这使得时序图令人怀疑,并且基于小而不规则的样本与时间复杂度的相关性将不会是决定性的。
对于更大且分布更均匀的样本,时间与(n)强烈相关(0.99)。这意味着乘法开销带来的差异仅影响值范围内的固定点。故意选择相距几个数量级的N值会加剧这些固定点的影响,从而使结果产生偏差。

因此您可以忽略我上面编写的所有不错的理论,因为数据表明时间复杂度的确是Log(n)。您只需要使用更有意义的样本(以及更好的变更率计算)。

答案 2 :(得分:4)

这是因为将2个小数乘以O(1)。但是要乘以2个长数(N-num)O(log(N)** 2)。 https://en.wikipedia.org/wiki/Multiplication_algorithm 因此,在每一步中,时间不要增加O(log(N))

答案 3 :(得分:1)

这可能很复杂,但是由于这是递归的,因此在不同情况下,您必须检查n的不同值。 https://en.wikipedia.org/wiki/Master_theorem_(analysis_of_algorithms)对此进行了解释。

答案 4 :(得分:1)

您必须考虑函数的实际输入大小。它不是n幅值,而是表示n所需的位数,其数量级为对数。也就是说,将数字除以2不会将输入大小减半:只会将其减少1位。这意味着对于一个n位数字(其值在2 ^ n和2 ^(n + 1)之间),运行时间的大小实际上是对数的,但是 linear 位的数量。

    n       lg n                 bits to represent n
    --------------------------------------
    10     between 2 and 3      4 (1010)
   100     between 4 and 5      7 (1100100)
  1000     just under  7       10 (1111101000)
 10000     between 9 and 10    14 (10011100010000)

每次将n乘以10时,您只会将输入大小增加3-4位,大约是2倍,而不是10倍。

答案 5 :(得分:1)

对于某些整数值,python将在内部使用“ long reperesentation”,并且在您的情况下,此情况发生在n=63之后,因此您的理论时间复杂度应仅对n < 63的值正确。

对于“长表示形式”,两个数字(x * y)相乘的复杂度大于O(1)

    对于x == y(例如x*x)来说,
  • 的复杂度为around O(Py_SIZE(x)² / 2)

  • 对于x != y(例如2*x)的
  • 像“ Schoolbook long multiplication”那样执行乘法,因此复杂度将为O(Py_SIZE(x)*Py_SIZE(y))。在您的情况下,它也可能会稍微影响性能,因为2*x*x将执行(2*x)*x,而更快的方法将是执行2*(x*x)

因此,对于n> = 63,理论复杂度还必须考虑乘法的复杂度。

如果可以将乘法的复杂度降低到pow,则可以测量自定义O(1)的“纯”复杂度(忽略乘法的复杂度)。例如:

SQUARE_CACHE = {}
HALFS_CACHE = {}


def square_and_double(x, do_double=False):
    key = hash((x, do_double))
    if key not in SQUARE_CACHE:
        if do_double:
            SQUARE_CACHE[key] = 2 * square_and_double(x, False)
        else:
            SQUARE_CACHE[key] = x*x
    return SQUARE_CACHE[key]


def half_and_remainder(x):
    key = hash(x)
    if key not in HALFS_CACHE:
        HALFS_CACHE[key] = divmod(x, 2)
    return HALFS_CACHE[key]


def pow(n):
    """Return 2**n, where n is a non-negative integer."""
    if n == 0:
        return 1
    x = pow(n//2)
    return square_and_double(x, do_double=bool(n % 2 != 0))


def pow_alt(n):
    """Return 2**n, where n is a non-negative integer."""
    if n == 0:
        return 1
    half_n, remainder = half_and_remainder(n)
    x = pow_alt(half_n)
    return square_and_double(x, do_double=bool(remainder != 0))

import timeit
import math


# Values of n:
sx = sorted([int(x) for x in [100, 1000, 10e4, 10e5, 5e5, 10e6, 2e6, 5e6, 10e7, 10e8, 10e9]])

# Fill caches of `square_and_double` and `half_and_remainder` to ensure that complexity of both `x*x` and of `divmod(x, 2)` are O(1):
[pow_alt(n) for n in sx]

# Average runtime in ms:
sy = [timeit.timeit('pow_alt(%d)' % n, number=500, globals=globals())*1000 for n in sx]

# Theoretical values:
base = 2
sy_theory = [sy[0]]
t0 = sy[0] / (math.log(sx[0], base))
sy_theory.extend([
    t0*math.log(x, base)
    for x in sx[1:]
])
print("real timings:")
print(sy)
print("\ntheory timings:")
print(sy_theory)

print('\n\nt/t_prev:')
print("real:")
print(['--' if i == 0 else "%.2f" % (sy[i]/sy[i-1]) for i in range(len(sy))])
print("\ntheory:")
print(['--' if i == 0 else "%.2f" % (sy_theory[i]/sy_theory[i-1]) for i in range(len(sy_theory))])
# OUTPUT:
real timings:
[1.7171500003314577, 2.515988002414815, 4.5264500004122965, 4.929114998958539, 5.251838003459852, 5.606903003354091, 6.680275000690017, 6.948587004444562, 7.609975000377744, 8.97067000187235, 16.48820400441764]

theory timings:
[1.7171500003314577, 2.5757250004971866, 4.292875000828644, 4.892993172417281, 5.151450000994373, 5.409906829571465, 5.751568172583011, 6.010025001160103, 6.868600001325832, 7.727175001491561, 8.585750001657289]


t/t_prev:
real:
['--', '1.47', '1.80', '1.09', '1.07', '1.07', '1.19', '1.04', '1.10', '1.18', '1.84']

theory:
['--', '1.50', '1.67', '1.14', '1.05', '1.05', '1.06', '1.04', '1.14', '1.12', '1.11']

结果仍不完美,但接近理论O(log(n))

答案 6 :(得分:0)

如果您计算得出的结果,采取的步骤,则可以生成类似教科书的结果:

def pow(n):
    global calls
    calls+=1
    """Return 2**n, where n is a nonnegative integer."""
    if n == 0:
        return 1
    x = pow(n//2)
    if n%2 == 0:
        return x*x
    return 2*x*x

def steppow(n):
    global calls
    calls=0
    pow(n)
    return calls

sx = [math.pow(10,n) for n in range(1,11)]
sy = [steppow(n)/math.log(n) for n in sx]
print(sy)

然后它会产生如下内容:

[2.1714724095162588, 1.737177927613007, 1.5924131003119235, 1.6286043071371943, 1.5634601348517065, 1.5200306866613815, 1.5510517210830421, 1.5200306866613813, 1.4959032154445342, 1.5200306866613813]

1.52 ...似乎是最喜欢的。

但是实际的运行时还包括看似无害的数学运算,随着内存中物理数量的增加,运算也变得越来越复杂。 CPython使用许多乘法实现分支到各个点:

long_mul是条目:

if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {
    stwodigits v = (stwodigits)(MEDIUM_VALUE(a)) * MEDIUM_VALUE(b);
    return PyLong_FromLongLong((long long)v);
}

z = k_mul(a, b);

如果数字适合CPU字,则它们会相乘(但结果可能更大,因此LongLong(*)),否则它们将使用k_mul()代表Karatsuba乘法,它还会根据大小和值检查几件事:

i = a == b ? KARATSUBA_SQUARE_CUTOFF : KARATSUBA_CUTOFF;
if (asize <= i) {
    if (asize == 0)
        return (PyLongObject *)PyLong_FromLong(0);
    else
        return x_mul(a, b);
}

对于较短的数字,将使用经典算法x_mul(),并且短路检查还取决于乘积是平方,因为x_mul()具有用于计算x*x的优化代码路径类似的表达式。但是,在一定的内存大小以上时,该算法会保留在本地,但随后会再次检查两个值的大小差异如何:

if (2 * asize <= bsize)
    return k_lopsided_mul(a, b);

可能分支到另一种算法k_lopsided_mul(),该算法仍为Karatsuba,但已进行了优化,可将数量级相差很大的数字相乘。

简而言之,即使2*x*x也很重要,如果将其替换为x*x*2,则timeit的结果也会有所不同:

2*x*x: [0.00020009249478223623, 0.0002965123323532072, 0.00034258906889154733, 0.0024181753953639975, 0.03395215528201522, 0.4794894526936972, 4.802882867816082]
x*x*2: [0.00014974939375012042, 0.00020265231347948998, 0.00034002925019471775, 0.0024501731290706985, 0.03400164511014836, 0.462764023966729, 4.841786565730171]

(测量为

sx = [math.pow(10,n) for n in range(1,8)]
sy = [timeit.timeit('pow(%d)' % i, number=100, globals=globals()) for i in sx]

顺便说一句,

(*),因为结果的大小经常被高估(就像刚开始时,long*long之后可能适合或可能不适合long),所以有一个{ {3}}函数也可以使用,最后它确实花费时间来释放额外的内存(请参见上面的注释),但是仍然为内部对象设置了正确的大小,这涉及到在实际数字之前循环计数零。 / p>