列表展平的时间复杂度
Posted
技术标签:
【中文标题】列表展平的时间复杂度【英文标题】:Time Complexity of list flattening 【发布时间】:2018-08-23 12:23:26 【问题描述】:我有两个函数,这两个函数都可以展平 Python 中任意嵌套的列表列表。
我正在尝试计算两者的时间复杂度,看看哪个更有效,但到目前为止我还没有找到任何关于 SO 的确定性。有很多关于列表列表的问题,但不是第 n 级嵌套。
函数 1(迭代)
def flattenIterative(arr):
i = 0
while i < len(arr):
while isinstance(arr[i], list):
if not arr[i]:
arr.pop(i)
i -= 1
break
else:
arr[i: i + 1] = arr[i]
i += 1
return arr
函数 2(递归)
def flattenRecursive(arr):
if not arr:
return arr
if isinstance(arr[0], list):
return flattenRecursive(arr[0]) + flattenRecursive(arr[1:])
return arr[:1] + flattenRecursiveweb(arr[1:])
我的想法如下:
功能 1 复杂性
我认为迭代版本的时间复杂度是O(n * m)
,其中n
是初始数组的长度,m
是嵌套的数量。我认为O(n)
的空间复杂度n
是初始数组的长度。
函数 2 复杂度
我认为递归版本的时间复杂度为O(n)
,其中n
是输入数组的长度。我认为O(n * m)
的空间复杂度,其中n
是初始数组的长度,m
是嵌套的深度。
总结
所以,对我来说,迭代函数似乎更慢,但空间效率更高。相反,递归函数更快,但空间效率较低。这是正确的吗?
【问题讨论】:
最终的扁平化列表的长度将是O(n*m)
,对吧?因此,任何返回列表(而不是惰性迭代器)的算法几乎必须至少是O(n*m)
空间。
此外,您似乎将诸如删除和插入列表中间、连接两个列表或复制列表尾部之类的事情计算为恒定时间步骤。但是其中每一个实际上都需要O(p)
为长度为 p 的列表工作。
顺便说一下,如果您知道如何编写 yield from flatten(elem)
惰性递归版本,您可能想先尝试分析一下,因为它可能更容易处理——没有列表移位或连接操作,除了堆栈没有临时存储,只是计数O(1)
步骤。
啊,我不知道O(p)
。你在说类似的东西:def iter_flatten(iterable): it = iter(iterable) for e in it: if isinstance(e, list): for f in iter_flatten(e): yield f else: yield e
?
如果 n 是初始列表长度,则不可能有 O(n)
解决方案,考虑到 [[[[[[[[[[0]]]]]]]]]]
情况,其中 n 为 1,但最小可能步数为 9。我认为最好解决方案是O(n*m)
(或者,如果您使用n
作为最终列表大小而不是初始大小,则O(n+m)
)。我认为你可以使用iter_flatten
来获得它,如果你使用像单链表而不是数组这样的常量可拼接的东西,你也可以使用flattenIterable
来获得它。但如果不考虑更多,我不确定。
【参考方案1】:
我不这么认为。有 N 个元素,因此您需要至少访问每个元素一次。总体而言,您的算法将运行 O(N) 次迭代。决定因素是每次迭代会发生什么。
你的第一个算法有 2 个循环,但如果你仔细观察,它仍然在每个元素上迭代 O(1) 次。然而,正如@abarnert 指出的那样,arr[i: i + 1] = arr[i]
将每个元素从arr[i+1:]
向上移动,这又是 O(N)。
您的第二个算法类似,但在这种情况下您要添加列表(在前一种情况下,这是一个简单的切片分配),不幸的是,列表添加的复杂性是线性的。
总之,您的两种算法都是二次的。
【讨论】:
啊,第一句话现在说得通了。关于您的第二点,为什么每次迭代O(1)
而不是 O(m)
次?
@ColinRicardo 因为您迭代每个元素一次,以重新分配它。
是的,对不起,我把自己弄糊涂了。感谢您的澄清。
但是arr[i: i + 1] = arr[i]
不是固定时间。它将每个元素从[i+1:]
向上移动,这需要O(n)
移动(然后它将len(arr[i]))
值复制到现在空置的插槽中,这需要多于O(1)
但少于O(n)
,所以我认为我们可以忽略)。
另外,我认为你错过了深度可以任意高的事实。例如,考虑[[[[[[[[[[0]]]]]]]]]]
。显然,只需 1 步即可将其展平。为什么?因为要复制0
,必须先复制[0]
,再复制[[0]]
,以此类推。因此,对于每个元素,这一步是最坏的情况O(m)
步骤。把它和另一点放在一起,你计算的n
步数每个都需要n+m
次,所以它是O(n**2 + n*m)
,它与递归版本一样是二次的。以上是关于列表展平的时间复杂度的主要内容,如果未能解决你的问题,请参考以下文章