快速排序迭代器要求

Posted

技术标签:

【中文标题】快速排序迭代器要求【英文标题】:quick-sorts iterator requirements 【发布时间】:2011-09-28 16:46:19 【问题描述】:

tl;dr: 是否可以有效地在双向链表上实现快速排序?我想之前的理解是,不,不是。

前几天我有机会考虑基本排序算法的迭代器要求。基本的 O(N²) 相当简单。

冒泡排序 - 2 个前向迭代器会做得很好,一个接一个地拖动。 插入排序 - 2 个双向迭代器即可。一个用于乱序元素,一个用于插入点。 选择排序 - 有点棘手,但前向迭代器可以解决问题。

快速排序

std::sort 中的 introsort_loop(如 gnu 标准库/hp(1994)/silicon graphics(1996) 中)要求它是 random_access。

__introsort_loop(_RandomAccessIterator __first,
         _RandomAccessIterator __last,
         _Size __depth_limit, _Compare __comp)

正如我所期望的那样。

现在经过仔细检查,我找不到需要快速排序的真正原因。唯一明确需要 random_access_iterators 的是 std::__median 调用,它需要计算中间元素。常规的普通快速排序计算中位数。

分区由检查组成

 if (!(__first < __last))
    return __first;

对于双向检查并不是一个真正有用的检查。然而,人们应该能够用一个简单的条件

来替换之前的分区旅行(从左到右/从右到左)中的检查
if ( __first == __last ) this_partitioning_is_done = true;

是否可以仅使用双向迭代器相当有效地实现快速排序?递归深度还是可以保护的。

注意。我还没有尝试过实际的实现。

【问题讨论】:

对于插入排序,前向迭代器就足够了。您可以使用std::rotatestd::upper_bound 的组合来实现插入,并且这两种成分只需要前向迭代器。当然仍然是 O(N^2)。 【参考方案1】:

您需要随机访问迭代器,因为您通常希望从列表中间选择枢轴元素。如果您选择第一个或最后一个元素作为枢轴,双向迭代器就足够了,但是对于预排序列表,快速排序会退化为 O(n^2)。

【讨论】:

所以如果我们使用递归深度保护,双向排序可以 O(N log N) 吗? (已编辑:) 实际上,您可以使用双向迭代器轻松地从 $O(n)$ 中的序列中间选择枢轴,而不会影响整体时间复杂度。 @Captain Giraffe:您仍然可以从中间选择枢轴元素 - 然后选择枢轴是 O(n) 而不是 O(1) 来选择数组的中间,但是不会影响整体复杂性,因为无论如何您都要为每个枢轴执行 O(n) 步骤:分区。只是额外的遍历可以将总时间乘以大约 1.5 以获取中间或随机选择的枢轴。或者更糟糕的是,如果您想采用 9 个等距点的中位数或其他一些令人兴奋的枢轴选择规则。 @Steve 是的,我现在看到了 :-) avakar,也是如此。 那么为什么会有 list.sort(),而不是 std::sort(bidirectional_iterator begin, end)?【参考方案2】:

tl;博士:是的

正如你所说,问题是找到枢轴元素,也就是中间的元素,用随机访问找到它需要 O(1),用双向迭代器找到它需要 O(n)(n/2 次操作,准确地说)。但是,在每个步骤中,您必须创建子容器,左侧和右侧分别包含较小和较大的数字。这就是快速排序的主要工作,对吧?

现在,在构建子容器(用于递归步骤)时,我的方法是创建一个迭代器 h 指向它们各自的前端元素。现在,每当您选择下一个元素进入子容器时,只需每秒钟推进一次h。一旦您准备好下降到新的递归步骤,这将使 h 指向枢轴元素。

你只需要找到第一个无关紧要的支点,因为 O(n log n + n/2) = O(n log n)。

实际上这只是运行时优化,但对复杂性没有影响,因为无论您是迭代列表一次(将每个值放入相应的子容器中)还是两次(找到枢轴然后放入每个值)在各自的子容器中)都是一样的:O(2n) = O(n)。 这只是执行时间的问题(而不是复杂性)。

【讨论】:

(运行时)优化在这种情况下非常重要。我没有注意到链表可以在 N log N 中排序这一事实。您建议将中值枢轴增加半步是聪明的,但我的印象是它产生的开销与我对分区的建议相同。 这是常识吗?在我读过的所有东西中,我还没有遇到过一个在链表上进行 N log N 排序的例子。现在我需要检查 :-) list::sort 当然是 N​​logN。 嗯,Omega(n log n) 是基于比较的排序的下限,并且可以实现。列表唯一没有的是随机访问。因此,如果您可以模拟随机访问(正如我所建议的),我认为在讨论排序速度时,您没有理由强调 在链表上。对于您的间接怀疑:基准测试是否真的很重要——没有其他方法可以判断。【参考方案3】:

在双向链表上实施快速排序策略绝对没有问题。 (我认为它也可以很容易地适应单链表)。传统快速排序算法中唯一依赖于随机访问要求的地方是设置阶段,它使用“棘手”的东西来选择枢轴元素。实际上,所有这些“技巧”只不过是可以用几乎同样有效的顺序方法代替的启发式方法。

我之前已经为链表实现了快速排序。它没有什么特别之处,您只需要密切注意适当的元素重新链接。正如您可能理解的那样,列表排序算法的大部分价值来自您可以通过 重新链接 重新排序元素,而不是显式的值交换。它不仅可以更快,而且(而且通常 - 更重要的是)保留了可能附加到列表元素的外部引用的值有效性。

附:但是,我想说的是,对于链表,合并排序算法会产生一个更加优雅的实现,它具有同样出色的性能(除非您正在处理某些特定情况下使用快速排序表现更好的情况)。

【讨论】:

指针参数对于许多参考风格的语言来说非常重要,这里也是如此。但是,单链表的实现必须非常棘手。 “单一”开销至少应该与随机到双向开销一样大? 在这些条件下,哪些情况下快速排序比合并排序更好?除非空间是一个巨大的限制因素。

以上是关于快速排序迭代器要求的主要内容,如果未能解决你的问题,请参考以下文章

python实现迭代的快速排序(Iterative Quick Sort)

迭代快速排序方法的分区算法问题

算法——快速排序迭代式和递归式的Java实现

经典算法复习快速排序的应用

排序方法——快速排序

机房解疑 | 快速排序