【问题标题】:Standard library partition algorithm标准库分区算法
【发布时间】:2013-11-15 16:38:33
【问题描述】:

我写了这个分区函数:

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 

【问题讨论】:

  • STL 功能已“优化”。
  • 您使用的是后缀 ++ 和 --,而 STL 使用的是前缀 ++ 和 --。如果你有一个特别大的迭代器,后缀形式可能会更慢,因为它返回一个值(修改前迭代器的副本)而不是引用。如果您不需要后缀语义,请首选前缀形式。
  • 只是好奇,哪个 STL 库以这种方式实现了“分区”?

标签: c++ algorithm vector stl


【解决方案1】:

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

【讨论】:

  • 感谢您指出这一点。我应该放一个 if 语句测试 begfirst 是否相等。
  • 事实上,在这种情况下,您将包含来自 stl 实现的代码 while (pred(*first)) { ++first; if (first==last) 先返回; }
  • @Dochevsky 此检查仅对 Vlad 在答案中提到的极端情况有意义。对于其他情况,这只是浪费 CPU 周期。
【解决方案2】:

来自STL partition description

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

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

【讨论】:

  • 即使在我防止交换相同元素的更正后,我的实现是否最终会交换更多内容?如果我在交换之前做了if(beg != first)
  • 事实上,在这种情况下,您将包含来自 stl 实现的代码 while (pred(*first)) { ++first; if (first==last) 先返回; }
【解决方案3】:

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

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

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

FTTTT

转发partition 会这样进行:

FTTTT
TFTTT
TTFTT
TTTFT
TTTTF

导致swap 在除第一次之外的每次迭代中,而双向分区将通过以下排列:

FTTTT
TTTTF

对于所有迭代只产生一个swap

此外,一般情况下,双向最多可以执行 N/2 swaps,而转发版本最多可以执行 ~N swaps。

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

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

如您所见,标准库的实现很可能会使用 Lomuto 的 partition 用于 ForwardIterators 和 Hoare 的 partition 用于 BidirectionalIterators。

Alexander Stepanov 在他的Notes on Programming 和与Paul McJones 合着的Elements of Programming 中讨论了partition 的问题


Live Demo

#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 moves)

【讨论】:

    猜你喜欢
    • 2021-04-04
    • 2013-07-14
    • 2011-05-03
    • 2010-09-23
    • 1970-01-01
    • 2020-12-30
    • 2017-03-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多