标准库分区算法

Standard library partition algorithm

本文关键字:算法 分区 标准      更新时间:2023-10-16

我写了这个分区函数:

template <class I, class P> I partition(I beg, I end, P p)
{
    I first = beg;
    while(beg != end) {
        if(!p(*beg))
            beg++;
        else {
            // if(beg != first) - EDIT: add conditional to prevent swapping identical elements
            std::swap(*beg, *first);
            first++;
            beg++;
        }
    }
    return first;
}

我已经用一些输出测试了它,我没有发现任何问题。

标准库分区功能相当于:

template <class BidirectionalIterator, class UnaryPredicate>
  BidirectionalIterator partition (BidirectionalIterator first,
                                   BidirectionalIterator last, UnaryPredicate pred)
{
  while (first!=last) {
    while (pred(*first)) {
      ++first;
      if (first==last) return first;
    }
    do {
      --last;
      if (first==last) return first;
    } while (!pred(*last));
    swap (*first,*last);
    ++first;
  }
  return first;
}

后者似乎要复杂得多,并且具有嵌套循环。我的版本有问题吗?如果不是为什么更复杂的版本?

下面是使用以下谓词的一些输出:

bool greaterthantwo(double val)
{
    return val > 2;
}

MAIN

std::vector<double> test{1,2,3,4,2,5,6,7,4,8,2,4,10};
std::vector<double>::iterator part = ::partition(test.begin(), test.end(), greaterthantwo);
for(const auto &ref:test)
    std::cout << ref << " ";
std::cout << std::endl;
for(auto it = part; it != test.end(); it++)
    std::cout << *it << " ";
std::cout << std::endl;

OUTPUT

3 4 5 6 7 4 8 4 10 2 2 2 1 
2 2 2 1 

您的版本接近 Nico Lomuto partition .这种partitionForwardIterator上工作并且是半稳定的(第一部分是稳定的,在某些情况下可能很有用)。

您引用的标准库实现版本接近 C. A. R. Hoare 在他的论文"Quicksort"中描述partition。它适用于BidirectionalIterator,并不意味着任何稳定性。

让我们在以下情况下比较它们:

FTTTT

转发partition将像这样进行:

FTTTT
TFTTT
TTFTT
TTTFT
TTTTF

导致每次迭代都swap,但第一次除外,而双向分区将经历以下排列:

FTTTT
TTTTF

导致所有迭代仅产生一个swap

此外,在一般情况下,双向最多可以做 N/2 swap 秒,而正向版本最多可以做 ~N swap 秒。

std::partition 在 C++98/03 中适用于 BidirectionalIterator s,但在 C++11 中,他们将要求放宽到 ForwardIterator s(尽管它不一定是半稳定的)。复杂性要求:

复杂性:如果ForwardIterator满足BidirectionalIterator的要求,则最多(last - first)/2次掉期;否则最多last - first掉期。正好最后 - 谓词的第一次应用完成。

如您所见,标准库的实现很可能会将 Lomuto 的 partition用于 ForwardIterator s,将 Hoare 的 partition用于 BidirectionalIterator s。

亚历山大·斯捷潘诺夫(Alexander Stepanov partition与保罗·麦克琼斯(Paul McJones)合著的《编程笔记》(Notes on Programming and Elements of Programming)中讨论了这个问题


现场演示

#include <initializer_list>
#include <forward_list>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <list>
using namespace std;
int counter = 0;
struct T
{
    int value;
    T(int x = 0) : value(x) {}
    T(const T &x)
    {
        ++counter;
        value = x.value;
    }
    T &operator=(const T &x)
    {
        ++counter;
        value = x.value;
        return *this;
    }
};
auto pred = [](const T &x){return x.value;};
template<typename Container>
void test()
{
    Container l = {0, 1, 1, 1, 1};
    counter = 0;
    partition(begin(l), end(l), pred);
    cout << "Moves count: " << counter << endl;
}
int main()
{
    test<forward_list<T>>();
    test<list<T>>();
}

输出为:

Moves count: 12
Moves count: 3

swap 是 3 move 秒)

您的函数存在严重缺陷。如果序列的初始元素满足谓词,它将满足谓词的每个元素与自身交换。

来自 STL 分区说明

复杂性第一个和最后一个之间的距离是线性的:将 pred 应用于每个元素,并可能交换其中一些元素(如果迭代器类型是双向的,则最多交换一半,否则最多交换那么多)。

在您的实现中,您可以交换更多。