为啥 QuickSort 使用 O(log(n)) 额外空间?

Posted

技术标签:

【中文标题】为啥 QuickSort 使用 O(log(n)) 额外空间?【英文标题】:Why does QuickSort use O(log(n)) extra space?为什么 QuickSort 使用 O(log(n)) 额外空间? 【发布时间】:2012-09-16 09:39:51 【问题描述】:

我已经实现了下面的快速排序算法。网上我读到它有 O(log(n)) 的空间要求。为什么会这样?我没有创建任何额外的数据结构。

是不是因为我的递归会使用堆栈上的一些额外空间?如果是这种情况,是否可以通过不递归(而不是使其迭代)来减少内存?

private static void quickSort (int[] array, int left, int right) 
    int index = partition(array, left, right);

    //Sort left half
    if (left < index - 1)
        quickSort(array, left, index - 1);

    //Sort right half
    if (index < right)
        quickSort(array, index , right);


private static int partition (int array[], int left, int right) 
    int pivot = array[(left + right) / 2]; //Pick pivot point
    while (left <= right) 
        //Find element on left that should be on right
        while (array[left] < pivot)
            left++;

        //Find element on right that should be on left
        while (array[right] > pivot)
            right--;

        //Swap elements and move left and right indices
        if (left <= right) 
            int temp = array[left];
            array[left] = array[right];
            array[right] = temp;
            left++;
            right--;
        
    
    return left;

【问题讨论】:

“在线我已阅读”...具体在哪里? ***:en.wikipedia.org/wiki/Quicksort第一段和右边的小方框 【参考方案1】:

正确,额外的空间是 log(n) 堆栈帧。来自Wikipedia article of Quicksort:

有一个更复杂的版本 [...] 可以使用 O(log n) 空间(不是 计算输入)平均(对于调用堆栈)

虽然您可以以迭代方式实现快速排序(即,使用循环而不是递归),但您随后需要维护一个辅助堆栈,因为快速排序有两个递归调用而不仅仅是一个。

最后,正如其他答案所指出的那样,O(log(n)) 对于几乎所有实际应用来说都非常非常小。每个常量因素,例如数据结构的开销,都会对内存使用产生更大的影响。

【讨论】:

最近发现一个有趣的注释,即使在 O(N^2) 时间运行的情况下,您也可以进行快速排序以仅使用 O(log(N)) 空间(这种情况发生在,例如,枢轴始终是最小元素等 - 只是提醒最坏的情况是 O(N^2) 用于快速排序)。当您在分区后对“左”和“右”部分进行递归时,您可以重新排列调用以使较小的部分进行递归并将另一部分优化为尾递归调用。以这种方式,在最大为原始集合大小的一半时需要递归 => 总是 O(log N) 递归调用。 当然可以使用辅助堆栈制作快速排序的迭代版本。 我不完全确定使用语句“使其迭代是不可能的,因为它不是尾递归”有点不准确。您可以将非尾递归函数转换为尾递归函数,然后再转换为迭代版本。 @joe_04_04 感谢您的评论。你是对的,是否尾递归是函数的属性,而不是算法的属性。我更新了我的答案。【参考方案2】:

要摆脱递归调用,您必须在代码中使用堆栈数据结构,它仍会占用log(n) 空间。

【讨论】:

【参考方案3】:

如果您进一步阅读 Wikipedia 文章,您会发现更多 thorough discussion of space complexity。他们特别写道:

在进行任何递归调用之前,具有就地和不稳定分区的快速排序仅使用恒定的额外空间。快速排序必须为每个嵌套递归调用存储恒定数量的信息。由于最好的情况最多进行 O(log n) 嵌套递归调用,因此它使用 O(log n) 空间。但是,如果没有 Sedgewick 限制递归调用的技巧,在最坏的情况下,快速排序可能会进行 O(n) 次嵌套递归调用,并且需要 O(n) 次辅助空间。

实际上,O(log n) 内存什么都不是。例如,如果要对 10 亿个整数进行排序,存储它们需要 4 GB,但堆栈只需要大约 30 个堆栈帧,大约 40 个字节,因此总共大约 1200 个字节。

【讨论】:

【参考方案4】:

是的,这是因为堆栈帧,是的,可以通过做一些非常聪明的事情将其转换为迭代算法(尽管什么都没有立即出现)。但为什么? O(log(n)) 空间几乎没有。作为参考,即使您有一个 Java 允许的最大大小的数组,也就是 2^31 个元素,大约 8 GB。快速排序需要 31 个堆栈帧。 Ballpark,也许每帧 100 字节?所以总共 3 KB,与实际数组的内存相比,这不算什么。

实际上,几乎任何时候都是 O(log(n)),它几乎与常数相同。

【讨论】:

你说得对,log(n) 确实非常小,但我不会说它“与常数几乎相同”。 ;-) 此外,最大堆栈大小通常小于最大堆大小。 @rolve,对于给定的真实世界机器,它实际上是恒定的,因为最大堆栈深度不能超过序列中元素数量的 log2,正如 Joe K(几乎)所说。 (不过,Joe K,您确实知道 31 * 100 字节是 3.1 Kb,而不是 300Kb。错字,不是吗?)假设您有 64 位架构和 Google 大小的预算,也许您可​​以将它推到 50 个堆栈帧,但这仍然会小于 8K,这“可能是恒定的”。然而,理论上它是 O(log n)。此外,您只能通过首先在较小的分区上递归来实现这一点。 (证据留给读者。)【参考方案5】:

很抱歉再次提出这个老问题,但我刚刚在planetmath.org 上找到了一个完全不同(但有点愚蠢)的答案来回答你的问题:

任何在连续数组上运行的排序算法都需要 O⁢(log ⁡n) 额外空间,因为这是表示数组索引所需的 [sic]。

【讨论】:

【参考方案6】:

子列表的大小在每次连续递归调用时减半,并且当子列表为 1 时递归终止。因此您对元素的操作次数等于您之前可以将 n 除以 2 的次数达到1;日志(n)。

【讨论】:

以上是关于为啥 QuickSort 使用 O(log(n)) 额外空间?的主要内容,如果未能解决你的问题,请参考以下文章

为啥代码的时间复杂度为 O(log n)?

为啥要对字符串进行排序 O(n log n)? [复制]

为啥即使 Eratothenes 筛子的时间复杂度为 O(nlog(log(n))),程序对于 n=100000 也不起作用

为啥自上而下的堆构建方法比自下而上的方法效率低,即使它的增长顺序比 O(n) 低 O(log n)?

八大排序算法

quicksort