【问题标题】:Difference of the two versions of partition used in quicksort快速排序中使用的两个分区版本的区别
【发布时间】:2013-09-24 05:01:09
【问题描述】:

第一个很简单,从两边走,直到找到一个回归。

/*C++ version, [first, last), last needs --first to fetch the last element*/
/*returns the middle of partitioning result*/
int* partition( int *first, int *last, int pivot ) {
    while (true) {
        while (*first < pivot) ++first;
        --last;//Don't edit this, it's true.
        while (pivot < *last) --last;
        if (!(first < last)) return first;
        swap(*first, *last);
        ++first;
    }
}

第二个(见“算法简介”)是:

int* partition( int a[], int n, int pivot ) {
    bound = 0;
    for ( i = 1; i != n; ++i )
        if ( a[i] < pivot )
            swap( &a[i], &a[++bound]);
    swap(a, a + bound);
    return a + bound;
}

第二个的不变量是“All elements before bound is less than pivot”。

问:两个版本的优缺点是什么?

我先给出一个,第二个需要对迭代器(指针)进行++操作,所以它可以应用于一些ForwardIterator,比如链表的迭代器。其他提示?

【问题讨论】:

  • 第一个真的是对的吗,是不是第三行的--last放错地方了?
  • @Joni 不,这是一个 c++ 版本,其中 last 是一个遍历最后一个元素的迭代器。
  • 在这种情况下,我建议给出完整的实现,包括函数定义和 firstlastpivot 的声明,以及函数应该返回的规范 (第一个有return语句,第二个没有)
  • 第一个是用于查找位于枢轴槽错误(相反)侧的两个元素并交换它们的常用算法。第二个旨在选择枢轴槽最终的位置,逐步调整它并在此过程中将所有小于枢轴的值移动到其下方。在这两者中,第二个通常更容易被初学者理解,并且不太容易出现错误的实现。第二个可以触发更多的交换,但两者具有相似的复杂性。
  • @Joni 很好,完成了。抱歉我的含糊陈述。

标签: c++ c algorithm quicksort partition


【解决方案1】:

就这两种算法的基本思想而言,两者都是正确的。他们将进行相同数量的比较,但第二个将比第一个进行更多的交换。

您可以通过逐步执行算法来查看这一点,因为它们使用 5 作为轴对数组 1 9 2 8 3 7 4 6 5 进行分区。当第一个算法交换两个数字时,它再也不会触及任何一个。第二种算法首先交换 9 和 2,然后交换 9 和 3,以此类推,进行多次交换以将 9 移动到其最终位置。

还有其他区别。如果我没有犯任何错误,这就是第一个算法对数组进行分区的方式:

1 9 2 8 3 7 4 6 5
f                 l
1 9 2 8 3 7 4 6 5  # swap 9,5
  f             l
1 5 2 8 3 7 4 6 9  # swap 8,4
      f     l
1 5 2 4 3 7 8 6 9  # return f = 5
        l f

这是第二种算法对数组进行分区的方式:

1 9 2 8 3 7 4 6 5  # 1<5, swap 1,1
bi      
1 9 2 8 3 7 4 6 5  # 9>5, no swap
  bi
1 9 2 8 3 7 4 6 5  # 2<5, swap 9,2
  b i
1 2 9 8 3 7 4 6 5  # 8>5, no swap
    b i
1 2 9 8 3 7 4 6 5  # 3<5, swap 9,3
    b   i
1 2 3 8 9 7 4 6 5  # 7>5, no swap
      b   i
1 2 3 8 9 7 4 6 5  # 4<5, swap 8,4
      b     i
1 2 3 4 9 7 8 6 5  # 6>5, no swap
        b     i
1 2 3 4 9 7 8 6 5  # 5=5, exit loop, swap 9,5
        b       i
1 2 3 4 5 7 8 6 9  # return b = 4
        b       i

注意它是如何进行 5 次交换的,而其他算法只有 2 次。它还将数组中的最后一项移动到中间数组。在这种情况下,最后一项恰好是枢轴,所以它是移动到中间的枢轴,但这不是一般情况。

【讨论】:

  • 这个不错。第二个例子是最后一次交换背后的原因。通常在开始时选择枢轴索引x,然后完成swap(a[x],a[len-1]) 以将枢轴值“存储”在最后一个位置。循环的测试条件通常是if ( a[i] &lt; a[len-1] ),因为这是保持枢轴“值”的地方。并且a[bound] 将始终是循环完成后枢轴值需要到达的位置,并通过最后一次交换将其移动到那里。同样的算法也可以从上到下执行,将主值保持在a[0],直到循环结束并发生最终交换。
  • 可以在此处查看作为基于迭代器的模板实现的第二个示例:See It Live。不错的文章(+1)
  • @WhozCraig 按照惯例,模板参数应命名为 ForwardIterator,表示迭代器的最低要求。这不是一个不错的选择,但是 STL 做了更多的优化,包括消除尾递归、限制递归深度、在某个阈值下使用堆排序或插入排序,这被称为 introsort (DR Musser, " Introspective Sorting and Selection Algorithms”,Software Practice and Experience 27(8):983, 1997.)。更多详情请参考sgi.com/tech/stl/sort.html
  • @zoujyjs 当然。 std::sort 的一些实现是完全惊人的。我的实现,来自 MIT 的 LLVM 实现)非常出色。他们为较小的分区排序所采取的步骤是大量工作的结果,并且完全按照您的描述进行。他们甚至为内联的 2、3、4 和 5 元素展开加倍努力。在可配置的限制之外,它是插入排序,包括临时部分排序等。这就像阅读有关排序算法的科学期刊。
  • @WhozCraig 第二个递归写成quicksort(++pvt, last)不是更好吗,pvt+1要求迭代器是RandomAccessIterator,因为它使用operator+
猜你喜欢
  • 2010-12-06
  • 2015-10-28
  • 1970-01-01
  • 1970-01-01
  • 2018-09-13
  • 2022-07-01
  • 2021-06-11
相关资源
最近更新 更多