将 std::p artition 与快速排序结合使用

Using std::partition with Quick Sort

本文关键字:结合 快速排序 artition std      更新时间:2023-10-16

使用下面分区的quickSort方法有什么问题?第 N 个元素似乎工作正常,但我认为分区也可以工作。我看到一个有 2 个分区调用的示例,但我不应该只需要一个吗?

#include <iostream>
#include <algorithm>
#include <iterator>
template <typename It>
void quickSortWorks (const It& lowerIt, const It& upperIt) 
{
  auto d = upperIt - lowerIt ;
  if ( d < 2 )
   return;
  auto midIt = lowerIt + d / 2;
  std::nth_element ( lowerIt, midIt, upperIt);
  quickSortWorks( lowerIt, midIt );
  quickSortWorks( midIt+1, upperIt );
}

template <typename It>
void quickSort (const It& lowerIt, const It& upperIt) 
{
  auto d = upperIt - lowerIt ;
  if ( d < 2 )
   return;
  auto midIt = lowerIt + d / 2;
  auto pIt = std::partition ( lowerIt, upperIt, [midIt](int i) { return i < *midIt; } );
  quickSort( lowerIt, pIt );
  quickSort( pIt + 1, upperIt );
}
int main ( )
{
  unsigned int N = 10;
  std::vector<int> v(N);
  // srand (time(nullptr));
  for_each(v.begin(), v.end(), [](int& cur){ cur = rand()%100; });
  std::vector<int> vorig(v);
  auto print_vec = [](std::ostream& out, const std::vector<int>& v) {
    std::copy(v.begin(), v.end(), std::ostream_iterator<int>(out, ", "));
    out << std::endl;
  };
  std::cout << " Using Partition: " << std::endl;
  std::cout << " Before: " << std::endl;
  print_vec(std::cout,v);
  quickSort(v.begin(), v.end());
  std::cout << " After: " << std::endl;
  print_vec(std::cout,v);
  v = vorig;
  std::cout << " Using Nth Element: " << std::endl;
  std::cout << " Before: " << std::endl;
  print_vec(std::cout,v);
  quickSortWorks(v.begin(), v.end());
  std::cout << " After: " << std::endl;
  print_vec(std::cout,v);
}

输出:

Using Partition: 
 Before: 
83, 86, 77, 15, 93, 35, 86, 92, 49, 21, 
 After: 
21, 15, 77, 35, 49, 83, 86, 92, 86, 93, 
Using Nth Element: 
 Before: 
83, 86, 77, 15, 93, 35, 86, 92, 49, 21, 
 After: 
15, 21, 35, 49, 77, 83, 86, 86, 92, 93, 

编写的代码只会意外工作,原因如下,

std::partition通过谓词完成其工作,前向序列包含计算结果为 true 的元素,其余的计算结果为 false。这意味着std::partition将大于透视的元素和等于透视的元素视为等效。

不能保证序列[middle,last)的顺序!

显然这不是你想要的。您希望压缩等于枢轴到序列[middle,last)前面的元素。这就是为什么您查看的示例代码使用第二个分区来将此顺序强加给尾随序列的原因(至少您需要将枢轴元素置于正确的位置(。

为了清楚

起见,
template<typename Ran>
  void quicksort(Ran first, Ran last)
  {
    typedef typename std::iterator_traits<Ran>::value_type value_type;
    if (last - first < 2)
       return;
    Ran middle = first + (last - first)/2;
    // save pivot.
    std::iter_swap(middle, last-1);
    middle = std::partition(first, last-1,
        [last] (const value_type& x) { return x < *(last-1); });
    // restore pivot.
    std::iter_swap(middle, last-1);
    quicksort(first, middle);
    quicksort(middle+1, last);
  }
即使

使用 lambda 修复,它也不会起作用,因为std::partitionstd::nth_element 不同,它不返回适合分而治之递归的迭代器。

std::partition 的调用的返回值是分区"上层"范围内第一个值的迭代器,其中谓词失败。 除非偶然,否则这不会是该范围内"最小">的迭代器。

相比之下,std::nth_element中的"透视"操作正是实现了这一点,这对于分叉递归也是必要的。

可以通过手动处理第一次迭代的示例来查看失败。 使用此包含 10 个元素的测试序列:

 83, 86, 77, 15, 93, 35, 86, 92, 49, 21

第一个"枢轴"将是第 6 个元素(索引 0 + 10/2 = 5(,即 35。 使用条件"小于 35"std::partition将在第一步将数组重新排列为

 21, 15, 77, 86, 93, 35, 86, 92, 49, 83

并返回指向第三个元素 (=77( 的指针。 很明显,值 77 将在算法持续时间内保持在第三个位置。 这显然是错误的。

我刚刚遇到了同样的问题。在花了几个小时进行分析后,我终于找到了解决方案,瞧!!

如前所述std::partition 使用提议的谓词会将数组分成两部分,第一部分由小于枢轴的元素组成,第二部分由大于或等于枢轴的元素组成。但不能保证枢轴将是第二部分中的第一个元素。

好吧,std::stable_partition可以完成这项工作。只需将枢轴作为first element并应用stable_partition即可。因为它将在分区时保持出现的顺序。现在可以保证枢轴将是第二部分中的第一个元素。

PS:不要与两部分混淆。我用这个词来更清楚地解释事情。

template <typename It>
void quickSort (const It& lowerIt, const It& upperIt) 
{
  auto d = upperIt - lowerIt ;
  if ( d < 2 )
   return;
  auto pIt = lowerIt;
  auto pValue = *pIt; 
  pIt = std::stable_partition ( lowerIt, upperIt, [pValue](int i) { return i < pValue; } );
  quickSort( lowerIt, pIt );
  quickSort( pIt + 1, upperIt );
}

您的闭包应该捕获 *midIt 的值,而不是 midIt:数量 "*midIt" 将在分区期间发生变化。

int midValue = *midIt;
std::partition(lowerIt, upperIt, [midValue](int i)...