这个问题很笼统,但也有一个问题:
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)
最坏情况
空间复杂度:???
因此对于预期的时间复杂度,最好的情况是当 left
和 right
被平均分割时,以下系列将适用于 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)
或者它与辅助数据的关系是别的什么?
我是否可以得出结论,当同时存在辅助数据结构和递归堆栈时,我们只需计算两者中较大的一个?
答案 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_side
或 right_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)