此快速排序实现的空间复杂度

时间:2021-01-08 11:42:46

标签: python algorithm big-o space-complexity

这个问题很笼统,但也有一个问题:

def quick_sort(lst):
    if len(lst) < 2: return lst
    pivot_lst = lst[0]
    left_side = [el for el in lst[1:] if el < pivot_lst]
    right_side = [el for el in lst[1:] if el >= pivot_lst]
    return quick_sort(left_side) + [pivot_lst] + quick_sort(right_side)

时间复杂度:O(nlog(n)) 预期,O(n^2) 最坏情况

空间复杂度:???

因此对于预期的时间复杂度,最好的情况是当 leftright 被平均分割时,以下系列将适用于 n 大小的输入:

n + n/2 + n/4 + n/8 +...  +1 
= n(1 + 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + ... . )
= O(n)

因此,在最坏的情况下,即选择的枢轴点是列表中的最大值或最小值时,这将适用:

n + (n-1) + (n-2) +... + 1
= (n^2 + n) / 2 
= O(n^2)

我的问题是,上面的序列是否分别代表了 O(n)O(n^2) 的预期和最差空间复杂度?

我正在努力思考堆栈帧内存如何在这里发挥作用。 我们会添加它吗?

所以,如果它是 O(log(n)),那么空间复杂度是 O(n) + O(log(n)) -> O(n)

或者它与辅助数据的关系是别的什么?

我是否可以得出结论,当同时存在辅助数据结构和递归堆栈时,我们只需计算两者中较大的一个?

1 个答案:

答案 0 :(得分:1)

总结

在快速排序的这个实现中,是的——预期的 auxiliary 空间复杂度是 O(n),最坏情况的辅助空间复杂度是 O(n^2)

<块引用>

我正在努力思考堆栈帧内存如何在这里发挥作用。我们会添加它吗?

所以,如果它是 O(log(n)),那么空间复杂度是 O(n) + O(log(n)) -> O(n)

[...]

我是否可以得出结论,当同时存在辅助数据结构和递归堆栈时,我们只需计算两者中较大的一个?

没有

我认为您正确地注意到递归堆栈深度在预期情况下是 O(log(n)),但错误地认为这意味着它的空间复杂度是在预期的情况下也是 O(log(n))。这不一定是真的。

  • 单个堆栈帧可以表示比 O(1) 更多的空间。
  • 一个框架代表多少空间可能因框架而异。

因此,在查找算法的总空间复杂度时,您无法将其递归深度与其数据需求分开分析,然后在最后将两者相加。你需要一起分析它们。

一般来说,您需要了解:

  • 递归的深度 - 将有多少堆栈帧。
  • 对于每个堆栈帧,其空间复杂度是多少。这包括函数参数、局部变量等。

然后,您可以将同时处于活动状态的所有堆栈帧的空间复杂度相加。

示例:预期情况

想象一下 n=8 的这个函数调用树。我使用符号 quick_sort(n) 表示“使用 n 元素列表进行快速排序。”

quick_sort(8)
    quick_sort(4)
        quick_sort(2)
            quick_sort(1)
            quick_sort(1)
        quick_sort(2)
            quick_sort(1)
            quick_sort(1)
    quick_sort(4)
        quick_sort(2)
            quick_sort(1)
            quick_sort(1)
        quick_sort(2)
            quick_sort(1)
            quick_sort(1)

由于您的实现是单线程的,因此一次只有一个分支处于活动状态。在最深处,这看起来像:

quick_sort(8)
    quick_sort(4)
        quick_sort(2)
            quick_sort(1)

或者,一般来说:

quick_sort(n)
    quick_sort(n/2)
        quick_sort(n/4)
            ...
                quick_sort(1)

让我们看看每帧将消耗的空间。

<calling function>
    lst: O(n)
    
    quick_sort(n)
        lst: O(1)
        pivot_lst: O(1)
        left_side: O(n/2)
        right_side: O(n/2)
        
        quick_sort(n/2)
            lst: O(1)
            pivot_lst: O(1)
            left_side: O(n/4)
            right_side: O(n/4)
            
            quick_sort(n/4)
                lst: O(1)
                pivot_lst: O(1)
                left_side: O(n/8)
                right_side: O(n/8)
                
                ...
                    quick_sort(1)
                        lst: O(1)

请注意,我认为 lst 参数始终具有 O(1) 的空间复杂度,以反映 Python 列表是按引用传递的。如果我们将它设为 O(n)O(n/2) 等,我们会重复计算它,因为它实际上与调用函数的 left_sideright_side 是同一个对象。这对于这个特定算法的最终结果来说并不重要,但总的来说,您需要牢记这一点。

我在符号上也很草率。编写 O(n/2) 很容易立即将其简化为 O(n)。暂时不要这样做:如果这样做,最终会夸大总空间复杂度。

稍微简化一下:

<calling function>
    lst: O(n)

    quick_sort(n)
        everything: O(n/2)

        quick_sort(n/2)
            everything: O(n/4)

            quick_sort(n/4)
                everything: O(n/8)

                ...
                    quick_sort(1)
                        everything: O(1)

将它们相加:

O(n) + O(n/2) + O(n/4) + O(n/8) + ... + O(1)
= O(n)

示例:最坏情况

使用与上述相同的方法,但为简洁起见跳过了一些步骤:

<calling function>
    lst: O(n)

    quick_sort(n)
        everything: O(n-1)

        quick_sort(n-1)
            everything: O(n-2)

            quick_sort(n-2)
                everything: O(n-3)

                ...
                    quick_sort(1)
                        everything: O(1)
O(n) + O(n-1) + O(n-2) + O(n-3) + ... + O(1)
= O(n^2)
相关问题