列表展平的时间复杂度

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),它与递归版本一样是二次的。

以上是关于列表展平的时间复杂度的主要内容,如果未能解决你的问题,请参考以下文章

用嵌套列表和嵌套字典列表展平一个非常大的 Json

使用 Databricks 和 ADF 展平复杂的 json

在 Python 中展平复杂的目录结构

如何使用 jq 展平复杂的 json 结构?

GDB:打印/转储到文件时自动展平结构

展平/取消展平嵌套 JSON 对象的最快方法