在他们的演讲"Quicksort is Optimal"中,Sedgewick和Bentley引用了一个名为Bentley-McIlroy三向分区的快速分配步骤的修改版本。此版本的分区步骤可以优雅地适应包含相同键的输入,方法是始终从剩余的内容中提取pivot元素的副本,确保在包含重复项的数组上调用时算法仍能很好地运行。
此分区步骤的C代码在此处重新打印:
void threeWayPartition(Item a[], int l, int r)
{
int i = l-1, j = r, p = l-1, q = r; Item v = a[r];
if (r <= l) return;
for (;;)
{
while (a[++i] < v) ;
while (v < a[--j]) if (j == l) break;
if (i >= j) break;
exch(a[i], a[j]);
if (a[i] == v) { p++; exch(a[p], a[i]); }
if (v == a[j]) { q--; exch(a[j], a[q]); }
}
exch(a[i], a[r]); j = i-1; i = i+1;
for (k = l; k < p; k++, j--) exch(a[k], a[j]);
for (k = r-1; k > q; k--, i++) exch(a[i], a[k]);
}
我有兴趣将此版本的quicksort实现为STL算法(仅用于我自己的启发,而不是替代非常快的std::sort
)。为了做到这一点,我理想地接受算法的输入,一系列STL迭代器定义要排序的范围。因为quicksort不需要随机访问,我希望这些迭代器是双向迭代器,因为这会使算法更通用,并允许我对std::list
和其他只支持双向访问的容器进行排序。
然而,这有一个小问题。请注意,三向分区算法的第一行包含:
int i = l-1, p = l-1;
这最终会创建两个在要分区的范围之前的整数,这很好,因为在循环体中它们在使用之前会递增。但是,如果我用双向迭代器替换这些索引,则此代码不再具有已定义的行为,因为它在要排序的范围开始之前将迭代器备份。
我的问题如下 - 没有实质上重写算法的核心,是否有一种方法可以使这个代码适应STL样式的迭代器,因为该算法首先通过备份迭代器来实现范围的开始?现在,我唯一的想法是引入额外的变量来“假装”我们在第一步备份迭代器,或者用特殊的迭代器适配器装饰迭代器,允许你在开始之前通过跟踪来备份在范围开始之前您有多少逻辑步骤。这些都不是很优雅......我错过了什么吗?有一个简单的解决方案吗?
谢谢!
答案 0 :(得分:2)
目前排名靠前的答案的一个主要问题是,对std::distance
的调用会使迭代在最坏的情况下呈二次方式。没有唯一键的序列会导致更糟糕的案例行为,这尤其令人遗憾,因为这正是三向分区旨在加速的情况。
以最佳方式实现Bentley-McIlroy三向分区,以便与双向迭代器一起使用,
template <typename Bi1, typename Bi2>
Bi2 swap_ranges_backward(Bi1 first1, Bi1 last1, Bi2 last2)
{
typedef typename std::reverse_iterator<Bi1> ri1;
typedef typename std::reverse_iterator<Bi2> ri2;
return std::swap_ranges(ri1(last1), ri1(first1), ri2(last2)).base();
}
template <typename Bi, typename Cmp>
std::pair<Bi, Bi>
partition3(Bi first, Bi last,
typename std::iterator_traits<Bi>::value_type pivot, Cmp comp)
{
Bi l_head = first;
Bi l_tail = first;
Bi r_head = last;
Bi r_tail = last;
while ( true )
{
// guarded to avoid overruns.
//
// @note this is necessary since ordered comparisons are
// unavailable for bi-directional iterator types.
while ( true )
if (l_tail == r_head)
goto fixup_final;
else if (comp(*l_tail, pivot))
++l_tail;
else
break;
--r_head;
while ( true )
if (l_tail == r_head)
goto fixup_right;
else if (comp(pivot, *r_head))
--r_head;
else
break;
std::iter_swap(l_tail, r_head);
// compact equal to sequence front/back.
if (!comp(*l_tail, pivot))
std::iter_swap(l_tail, l_head++);
if (!comp(pivot, *r_head))
std::iter_swap(r_head, --r_tail);
++l_tail;
}
fixup_right:
// loop exited before chance to eval.
if (!comp(pivot, *r_head))
++r_head;
fixup_final:
// swap equal to partition point.
if ((l_tail - l_head) <= (l_head - first))
l_tail = std::swap_ranges(l_head, l_tail, first);
else
l_tail = swap_ranges_backward(first, l_head, l_tail);
if ((r_tail - r_head) <= (last - r_tail))
r_head = swap_ranges_backward(r_head, r_tail, last);
else
r_head = std::swap_ranges(r_tail, last, r_head);
// equal range in values equal to pivot.
return std::pair<Bi, Bi>(l_tail, r_head);
}
注意:这已使用Bentley验证套件进行了测试。保护前进的一个很好的副作用是,该功能对于通用目的是安全的(对pivot
或序列长度没有限制)。
使用示例,
template<typename Bi, typename Cmp>
void qsort_bi(Bi first, Bi last, Cmp comp)
{
auto nmemb = std::distance(first, last);
if (nmemb <= 1)
return;
Bi pivot = first;
std::advance(pivot, std::rand() % nmemb);
std::pair<Bi, Bi> equal = partition3(first, last, *pivot, comp);
qsort_bi(first, equal.first, comp);
qsort_bi(equal.second, last, comp);
}
template<typename Bi>
void qsort_bi(Bi first, Bi last)
{
typedef typename std::iterator_traits<Bi>::value_type value_type;
qsort_bi(first, last, std::less<value_type>());
}
虽然上述排序可能有效,但它说明了另一个答案已经做出的一点,即双向迭代器和快速排序不合适。
如果没有能力在恒定时间内选择合适的支点,性能的提升使得quicksort成为一个低劣的选择。此外,双向迭代器对于链表上的最佳排序来说过于笼统,因为它们无法利用列表的优势,例如恒定时间插入和拼接。最后,另一个更微妙(可能是有争议的)问题是,用户希望链接列表上的排序是稳定的。
我的推荐? sgi STL使用的自下而上的迭代mergesort。它经过验证,稳定,简单,快速(保证n * log(n))。不幸的是,这个算法似乎没有唯一的名称,我无法单独找到实现的链接,所以在这里重复。
这是一个非常灵活的算法,它的工作方式类似于二进制计数器(非空列表等于1)。计数器保存大小为2 ^索引的列表(即1,2,4,8 ...)。随着每个元素(位)的添加,可以启动一个进位,它将级联到更高阶的列表中(二进制加法)。
template <typename Tp>
void msort_list(std::list<Tp>& in)
{
std::list<Tp> carry;
std::list<Tp> counter[64];
int fill = 1;
while (!in.empty()) {
carry.splice(carry.begin(), in, in.begin());
int i = 0;
for (; !counter[i].empty(); i++) {
// merge upwards for stability.
counter[i].merge(carry);
counter[i].swap(carry);
}
counter[i].swap(carry);
if (i == fill) ++fill;
}
for (int i = 1; i < fill; i++)
counter[i].merge(counter[i-1]);
in.swap(counter[fill-1]);
}
注意:此版本在几个方面与原版有所不同。 1)我们在一个而不是零处开始fill
,这允许我们跳过大小检查并使最终交换工作,而不会影响行为。 2)原始内部循环条件添加i < fill
,此检查是无关紧要的(可能是计数器数组是动态的版本的保留)。
答案 1 :(得分:1)
<强> without substantially rewriting the core of the algorithm
强>
这几乎限制了你试图破解边界问题的方法,所以你需要使用自定义迭代器适配器,或者将迭代器包装在boost::optional
或类似的东西中,这样你就知道它什么时候了第一次访问。
更好的方法是修改算法以适应手头的工具(这正是STL的用途,对不同的迭代器类型使用不同的算法)。
我不知道是否this is correct,但它以不同的方式描述了算法,不需要迭代器超出范围。
<小时/> 编辑:话虽如此,我已经开始了。这段代码是未经测试的,因为我不知道在给定输入的情况下输出应该是什么样的 - 请参阅注释以获取详细信息。它只能为双向/随机访问迭代器工作。
#include <algorithm>
#include <iterator>
template <class Iterator>
void three_way_partition(Iterator begin, Iterator end)
{
if (begin != end)
{
typename Iterator::value_type v = *(end - 1);
// I can initialise it to begin here as its first use in the loop has
// changed to post-increment (its pre-increment in your original
// algorithm).
Iterator i = begin;
Iterator j = end - 1;
// This should be begin - 1, but thats not valid, I set it to end
// to act as a sentinal value, that way I know when im incrementing
// p for the first time, and can set it to begin.
Iterator p = end;
Iterator q = end - 1;
for (;;)
{
while (*(i++) < v);
while (v < *(--j))
{
if (j == begin)
{
break;
}
}
if (std::distance(i, j) <= 0)
{
break;
}
if (*i == v)
{
if (p == end)
{
p = begin;
}
else
{
++p;
}
std::iter_swap(p, i);
}
if (v == *j)
{
--q;
std::iter_swap(j, q);
}
}
std::iter_swap(i, end - 1);
j = i - 1;
i++;
for (Iterator k = begin; k < p; ++k, --j)
{
std::iter_swap(k, j);
}
for (Iterator k = end - 2; k > q; --k, ++i)
{
std::iter_swap(i, k);
}
}
}
答案 2 :(得分:0)
不幸的是,表达式“k&lt; p”对于双向迭代器是非法的(需要随机访问)。在我看来,这是你面临的真正限制。例如
if (i >= j) break;
必须离开并替换为
if (i == j) break;
这意味着您需要在“内部”循环中添加额外条件,以确保j(特别是)不会减少太多。 Net / net在使这个算法为双向迭代器运行时,不能满足你的约束“没有实质上的重写”。
答案 3 :(得分:0)
考虑到功能所做的所有交换,只是执行以下操作不会更容易(也许更有效),
template <typename For, typename Cmp>
std::pair<For, For>
partition_3way(For first, For last,
typename std::iterator_traits<For>::value_type pivot, Cmp comp)
{
For lower = std::partition(first, last, std::bind2nd(comp, pivot));
For upper = std::partition(lower, last,
std::not1(std::bind1st(comp, pivot)));
// return equal range for elements equal to pivot.
return std::pair<For, For>(lower, upper);
}