Python:创建大小为 n^2 的元组的时间和空间复杂度

Posted

技术标签:

【中文标题】Python:创建大小为 n^2 的元组的时间和空间复杂度【英文标题】:Python: Time and space complexity of creating size n^2 tuples 【发布时间】:2018-09-04 06:19:33 【问题描述】:

这是我学校过去一年的期中论文中的一个问题。下面附上一张图表,显示机器人将如何移动,来自同一张纸。橙色部分说明了我的担忧。

基本上,机器人在遇到左侧未访问的方格时会向前移动并左转。

机器人横穿 3 号网格的指令顺序为: ('F','T','F','T','F','F','T','F','F','T','F','F',' F') 其中“F”表示向前移动一格,“T”表示向左转 90 度。请注意,最后一条指令会导致机器人退出网格。 函数 gen_seq 将网格的大小作为输入,并返回一系列指令供机器人横穿网格。指令序列是一个包含字符串“F”和“T”的元组,代表前进和转向命令。

提供函数 gen_seq 的递归或迭代实现。 提示:召回 int 可以与 tuple 相乘。

根据您的实施时间和空间说明增长顺序并解释您的答案。

这些是评分方案中建议的答案。

def gen_seq(n): # recursive
    if n == 1:
        return ('F',)
    else:
        side = ('T',) + (n-1)*('F',)
        return gen_seq(n-1) + side + side + ('F',)

def gen_seq(n): # iterative
    seq = ('F',)
    for i in range(2, n+1):
        side = ('T',) + (n-1)*('F',)
        seq += side + side + ('F',)
    return seq

时间:O(n^3)。在每个函数调用(递归)或循环(迭代)中,都会创建一个螺旋的每个“层”的路径长度的新元组。由于螺旋的长度是 n^2,并且有 n 个函数调用或循环运行 n 次,所以总时间是 n^2*n = O(n3)。 换句话说,它是平方和:1^2+2^2+3^2+: : :+n^2 = O(n^3)

空间:O(n^2)。一天结束时,创建并返回一个大小为 n^2 的新元组。

1)我是否正确推断形成元组的时间复杂度的推导似乎是每次递归/迭代后更新元组的长度之和。

如果我想形成一个大小为 n^2(拉直螺旋的大小)的元组,首先必须形成 1^2,然后是 2^2…n^2,导致上述平方和。

我看到一篇关于字符串而不是元组的相关帖子,在这种情况下时间= 1+2+…n=n^2,这支持了我的推断。

Time complexity of string concatenation in Python

2)对于涉及切片/连接的递归函数的空间复杂度,空间等于它们的时间,在这种情况下为 O(n^3),我说是否正确。我对这种情况的解释是:由于有 n 个递归占用了堆栈上的空间,并且每个递归都由串联形成一个长度为 n^2 的新元组(此处没有看到切片),空间将为 O(n *n^2)。

我还认为 O(n^2) 的建议空间仅适用于没有观察到堆栈帧并且空间中仅包含最终元组 (n^2) 的长度的迭代解决方案。

【问题讨论】:

你有什么问题? @jhpratt 它写在橙色部分,编号 您的实际问题是什么?那个 android 移动方案似乎是无关的——一个合适的解决方案是使用生成器而不是生成大量的元组。您不确定您的 O 计算吗?如果是这样,你能让你的问题更清晰更短吗?令人困惑的是,您担心“时间复杂度的推导”,“似乎是什么”,“正确的[...]说什么” ",切片与非切片情况的关系等等。 @MisterMiyagi 关于正确的解决方案,由于我在一个基本的编程模块中,我们坚持使用原始方法。是的,我不确定我的 O 计算,因为我没有通过适当的算法分析课程,我们主要依靠直觉。我不知道如何在不削减细节的情况下缩短它(1)为什么我有这个问题,肯定需要考试 qns 的来源。 (2)如果我不为我的答案提供解释……有人会问我是如何得出答案的。 (3)实际的qns是橙色部分的is it correct qns,提供背景。 【参考方案1】:

TLDR:您的时间复杂度是正确的,尽管递归 gen_seq 的 O(n^3) 空间过于悲观(它仍然更加浪费)。请注意,最佳静态解决方案是 O(n^2),因为这是答案的大小。如果不需要静态答案,空间复杂度可以降低到 O(1)。


让我们从建立一些基本的复杂性开始。以下适用于时间和空间复杂度:

创建一个字符字面量是 O(1)。 创建一个大小为n 的元组是O(n)。 创建一个空元组或单元素元组是 O(1)。 连接两个长度为nm 的元组是O(n+m)。 连接两个长度为n^2m的元组,为O(n^2+m) = O(n^2)。

迭代:

def gen_seq(n): # iterative
    seq = ('F',)
    for i in range(2, n+1):
        side = ('T',) + (i-1)*('F',)  # note: `i-1` instead of `n-1`
        seq += side + side + ('F',)
    return seq

复杂性的关键点是:

range(const, n+1) 循环是 O(n) 时间 复杂度和 O(1) 空间side 在 i->n 的大小为 n 时重新构造。空间被重复使用,最大 O(n) 空间。所有 n 次迭代都消耗时间,O(n*n) = O(n^2) 时间seq 在所有 n 次迭代中都与一个 n 元组连接。空间被重复使用,最大 O(n*n) = O(n^2) 空间。所有 n 次迭代都消耗时间,因为 O(n*n^2) = O(n^3) 时间

最大复杂度获胜,因此迭代使用O(n^2) 空间O(n^3) 时间


递归:

def gen_seq(n): # recursive
    if n == 1:
        return ('F',)
    else:
        side = ('T',) + (n-1)*('F',)
        return gen_seq(n-1) + side + side + ('F',)

复杂性的关键点是:

递归从 n->1 开始重复,意思是 O(n) 时间side 以 n 大小重新构造。空间没有被重复使用,因为每个side都是在之前递归构造的,最大为O(n*n) = O(n^2)空间。所有 n 次迭代都消耗时间,O(n*n) = O(n^2) 时间。 return 值在所有 n 次迭代中与一个 n 元组连接。空间被重用,因为每个return值都是在递归之后构造的,最大为O(n*n) = O(n^2)空间。所有 n 次迭代都消耗时间,因为 O(n*n^2) = O(n^3) 时间

最大复杂度获胜,因此迭代使用O(n^2) 空间O(n^3) 时间


时间复杂度的限制是每一步的结果必须在下一步重复。在 Python 中,这可以使用生成器来规避 - 这允许您返回中间结果并继续生成更多结果:

def gen_seq(n):
    yield 'F'
    for i in range(1, n):
        yield 'T'
        yield from ('F' for _ in range(i))
        yield 'T'
        yield from ('F' for _ in range(i))

seq = tuple(gen_seq(m))

复杂性的关键点是:

range(n) 循环是 O(n) 时间 复杂度和 O(1) 空间yield from ... range(i) 循环是 O(n) 时间和 O(1) 空间。空间重用将其保留在 O(1) 空间。重复 n 次得到 O(n*n) = O(n^2) 时间。 通过tuple一次连接所有结果是 O(n^2 * 1) = O(n^2) 空间

最大复杂度获胜,因此迭代使用 O(n^2) 空间和 O(n^2) 时间。如果结果不存储直接使用,则只使用 O(1) 空间。

【讨论】:

以上是关于Python:创建大小为 n^2 的元组的时间和空间复杂度的主要内容,如果未能解决你的问题,请参考以下文章

PySpark UDF 返回可变大小的元组

Python:元组列表:比较所有元组并检索元组的元素不等于任何其他元组的元组

为后来成为特定类型的元组的空元组设置正确的类型

如何从具有子元组的元组创建列表?

Python的元组列表截取

Python:扁平化包含来自函数的另一个元组的元组的最简单方法