是否可以实现 O(1) 空间复杂度的快速排序?

Posted

技术标签:

【中文标题】是否可以实现 O(1) 空间复杂度的快速排序?【英文标题】:Is it possible to implement quicksort with O(1) space complexity? 【发布时间】:2012-07-12 09:42:22 【问题描述】:

根据我在Wikipedia's explanation of quicksort's space complexity 中的理解,快速排序的空间复杂性来自其递归性质。我很好奇是否有可能以非递归方式实现快速排序,并且在这样做的过程中,以恒定的空间复杂度实现它。

【问题讨论】:

@trumpetlicks: O(1) 空间复杂度. @j_random_hacker - 好点,我在考虑算法复杂性:-) 是的,您只需要以能够预测分区大小的方式选择枢轴元素。 @Daniel:事实证明,您不能基于时间复杂度低于 O(N log N) 的比较进行排序。 @salva:中位数的中位数会做到这一点——但它需要 O(log N) 空间来选择这些枢轴...... 【参考方案1】:

完全有可能以非递归方式实现它,但您可以通过实现与普通函数调用/返回堆栈分开的堆栈来实现。它可以通过只存储基本信息而不是大量(大部分相同的)函数返回地址来节省一些空间,但它的大小仍然是对数的,而不是恒定的。

【讨论】:

fwiw,有一种方法可以就地进行快速排序。我想你可以有效地保存枢轴等。 @DennisMeng:是的,您可以有效地将枢轴保存在堆栈中。假设您总是先对较小的分区进行排序,那么您必须保存的枢轴数限制为 O(log N)。 IOW,您需要的额外空间量是 O(log N)。 这个答案不正确。请参阅下面的版本。您可以将范围存储在数据中,而不会影响算法的复杂性。 @JerryCoffin。我描述的算法对数据进行分区,然后对每个分区进行排序。与经典快速排序的区别在于它通过循环而不是递归(根据问题这是必要的)来完成此操作。另一个区别是它调整了每个分区的两个值的位置,而不是将信息存储在堆栈中。我根本看不到与堆排序的关系。该算法在选择拆分器时存在完全相同的问题,并且具有与快速排序相同的性能特征。 @GordonLinoff:嗯,我想你可以持有任何你想要的意见。如果你问 10 位经验丰富的程序员:“如果我愿意,我可以合理地将其称为快速排序吗?”这可能是真的,至少有一半可能至少会同意你可以。另一方面,如果您说“这种类型的名称是什么”,如果不止一个人说它是一种快速排序,我会感到惊讶(即使有人说它,也可能是因为他们没有仔细阅读小心)。声称我的答案是错误的(或明显的反对票)并不是一个合理的依据。【参考方案2】:

***并不总是错误的。而且,正如该部分所建议的,有一种方法可以使用常量空间进行快速排序或类似的操作。重要的一点。快速排序本身可以定义为递归分区算法。如果是这样,那么根据定义,它将需要 O(n) 堆栈空间。但是,我假设您没有使用这种迂腐的定义。

简单回顾一下分区的工作原理。给定一个数组、一个起点和一个终点,选择一个分区值。然后拆分数组中的数据元素,因此小于分区值的所有内容都在左侧,而大于分区值的所有内容都在右侧。这样做的一个好方法是从每一端开始,找到第一个不属于的值,然后交换它们。顺便说一下,这使用了常量空间。

所以,算法的每一步都在遍历数组。让我们记住这个事实。

现在,我们可以做一个有趣的观察。如果我们以深度优先的方式进行递归分区,那么我们只需要存储每个范围的端点。在下降的过程中,数组的左边缘始终是开始。终点逐渐接近起点,直到只有两个可以交换的元素。此时,开始移动了两个槽,但我们不知道结束。因此,查找结尾并继续该过程。然后在下一步“向上”时,我们需要下一个端点,以此类推。

问题是:除了将实际值存储在堆栈中之外,我们还能通过其他方式找到终点吗?

嗯,答案是“是”。

递归分区算法的每一步都会读取所有数据。我们可以对数据做一些额外的计算。特别是,我们可以计算最大值和第二大值。 (我也会计算最小值,但这是一种优化。

我们对值所做的是标记范围。在第一次拆分时,这意味着将第二个最大值放在拆分点,将最大值放在范围的末尾。在返回树的路上,你知道范围从哪里开始。范围的结尾是第一个大于该值的值。

瞧!您可以在不存储任何数据的情况下向上移动“递归”树。您只是在使用所提供的数据。

完成此操作后,您只需将算法从递归算法更改为 while 循环。 while 循环重新排列数据,在每一步设置起点和终点。它选择一个拆分器,拆分数据,标记起点和终点,然后在数据的左侧重复。

当它到达最小单位时,它会检查它是否完成(它是否到达数据的末尾)。如果不是,它会查看一个单位的数据点以找到第一个标记。然后它通过数据寻找终点。顺便说一下,这种搜索在复杂性上等同于数据的分区,因此它不会增加复杂性的顺序。然后它遍历这个数组,继续这个过程直到它完成。

如果数据中有重复项,则该过程会稍微复杂一些。但是,如果有 log(N) 个重复项,我几乎会主张删除重复项,使用剩余的插槽作为堆栈对数据进行排序,然后将它们重新合并。

为什么这是快速排序。快速排序算法是一种分区交换算法。该算法通过选择一个拆分器值、在两侧划分数据并重复此过程来进行。正如杰弗里在他的回答中指出的那样,递归不是必需的。非常方便。

此算法以完全相同的方式进行。分区遵循相同的基本规则,左侧记录较小,右侧记录较大。唯一的区别是在每个分区内,特定的值被选择在分区的边缘。通过仔细放置这些值,不需要额外的“每步”存储。由于这些值属于分区,因此根据分区和重复的快速排序原则,这是一个有效的分区。

如果有人认为快速排序必须使用递归,那么这将无法通过严格的测试(并且原始问题的答案是微不足道的)。

【讨论】:

【参考方案3】:

Branislav Ďurian 在 1986 年提出了 Quicksort 的恒定空间版本。请参阅他的论文“Quicksort without a stack”。在 J. Gruska、B. Rovan 和 J. Wiedermann,编辑,计算机科学数学基础论文集,计算机科学讲义第 233 卷,第 283-289 页。施普林格出版社,1986 年。

其他几位作者对此进行了跟进。您可以查找 Bing-Chao 和 Knuth (1986);韦格纳(1987); Kaldewaij 和 Udding(1991 年);格里斯 (1994)。

【讨论】:

以上是关于是否可以实现 O(1) 空间复杂度的快速排序?的主要内容,如果未能解决你的问题,请参考以下文章

是否是原地(就地)命令式快速排序?

常用排序算法的时间复杂度和空间复杂度

时间/空间复杂度,基础排序算法(冒泡选择快速插入)

排序算法(快速排序)

常用的排序算法的时间复杂度和空间复杂度

常用的排序算法的时间复杂度和空间复杂度