tl; dr:是否可以有效地在双向链表上实现快速排序?在考虑之前我的理解是,不,不是。
前几天我有机会考虑基本排序算法的迭代器要求。基本的O(N²)非常简单。
快速排序
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;
是否可以仅使用双向迭代器相当有效地实现快速排序?递归深度仍然可以保护。
NB。我还没有尝试过实际的实现。
答案 0 :(得分:2)
您需要随机访问迭代器,因为您通常希望从列表中间选择pivot元素。如果您选择第一个或最后一个元素作为枢轴,则双向迭代器就足够了,但是对于预先排序的列表,Quicksort会退化为O(n ^ 2)。
答案 1 :(得分:1)
tl;博士:是
正如你所说的,问题是要找到pivot元素,它是中间的元素,用随机访问找到它需要O(1),用双向迭代器找到它需要O(n)(n / 2次操作) ,确切地说)。但是,在每个步骤中,您必须创建子容器,左侧和右侧分别包含越来越小的数字。这是快速排序的主要工作,对吗?
现在,在构建子容器时(对于递归步骤),我的方法是创建一个指向它们各自前面元素的迭代器h
。现在,无论何时选择下一个元素转到子容器,只需每隔一次提前h
。一旦准备好下降到新的递归步骤,这将h
指向pivot元素。
你只需要找到第一个无关紧要的枢轴,因为O(n log n + n / 2)= O(n log n)。
实际上这只是一个运行时优化,但对复杂性没有影响,因为你是否迭代列表一次(将每个值放在相应的子容器中)或两次(找到枢轴然后放置每个值)在相应的子容器中)是完全相同的:O(2n)= O(n) 这只是一个执行时间问题(而不是复杂性)。
答案 2 :(得分:1)
在双向链表上实施快速排序策略绝对没有问题。 (我认为它也可以很容易地适应单链表)。传统的快速排序算法中唯一依赖于随机访问要求的地方是设置阶段,它使用“棘手”的东西来选择枢轴元素。实际上,所有这些“技巧”只不过是可以用几乎同样有效的顺序方法取代的启发式方法。
我之前已对链表实施过快速排序。没有什么特别之处,你只需要密切注意正确的元素重新连接。您可能已经理解,列表排序算法的大部分价值来自于您可以通过重新链接重新排序元素,而不是显式值交换。它不仅可以更快,而且(通常 - 更重要的是)保留可能附加到列表元素的外部引用的值 - 有效性。
P.S。但是,我要说的是,对于链表,合并排序算法可以实现更优雅的实现,具有同样出色的性能(除非您正在处理某些特定快速排序表现更好的情况)。