同步按照第一个容器的元素对两个容器进行排序

7
给定两个容器:std::list< int > a;std::list< int > b;,— a.size() == b.size()。需要同步地对容器ab进行排序,即在a中进行元素交换时,应该对应交换b中相应的元素(在位置索引的意义上)。假设ab中的元素非常重,即你不能复制它们。

如何以完美的STL方式实现这一操作?如何使用std::sort来完成操作?如果aconst,该怎么办?

我目前的做法:

#include <iostream>
#include <iomanip>
#include <type_traits>
#include <utility>
#include <iterator>
#include <algorithm>
#include <list>
#include <vector>

#include <cstdlib>
#include <cassert>

template< typename first, typename second > 
void
sort_synchronously(first & f, second & s)
{
    std::size_t sz = f.size();
    assert(sz == s.size());
    struct P
    {
        typename first::iterator pfirst;
        typename second::iterator psecond;
        bool operator < (P const & p) const { return (*pfirst < *p.pfirst); }
        void swap(P & p) noexcept { std::iter_swap(pfirst, p.pfirst); std::swap(pfirst, p.pfirst); std::iter_swap(psecond, p.psecond); std::swap(psecond, p.psecond);  }
    };
    std::vector< P > p;
    p.reserve(sz); // O(N) additional memory
    auto fi = std::begin(f);
    auto si = std::begin(s);
    for (std::size_t i = 0; i < sz; ++i) {
        p.push_back({fi, si});
        ++fi;
        ++si;
    }
    std::sort(std::begin(p), std::end(p)); // O(N * log N) time
} 

int
main()
{
    std::list< int > a{5, 4, 3, 2, 1};
    std::list< int > b{1, 2, 3, 4, 5};
    std::copy(std::cbegin(a), std::cend(a), std::ostream_iterator< int >(std::cout, " ")); std::cout << std::endl;
    std::copy(std::cbegin(b), std::cend(b), std::ostream_iterator< int >(std::cout, " ")); std::cout << std::endl;
    sort_synchronously(a, b);
    std::copy(std::cbegin(a), std::cend(a), std::ostream_iterator< int >(std::cout, " ")); std::cout << std::endl;
    std::copy(std::cbegin(b), std::cend(b), std::ostream_iterator< int >(std::cout, " ")); std::cout << std::endl;
    return EXIT_SUCCESS;
}

但是我无法为struct P提供基于P::swap的免费swap函数。这是语言的无法避免的限制(我无法在函数作用域内定义非lambda函数,但可以定义非模板类)。
额外信息:
我发现存在对swap自由函数重载的要求并不是std::sort函数的类型要求。只有MoveConstructible和MoveAssignable是必须的。因此the code更为合适(但仍不完整)。真正困难的问题是:提供给std::sort的范围中元素的交换(显然)被分成一系列组成操作:T tmp(std::move(lhs)); lhs = std::move(rhs); rhs = std::move(tmp);。因此,我不能交换容器本身引用元素的位置(在std::sort期间),而只能交换它们的迭代器。

“对应的”元素是什么意思?是指在相同位置上的元素,还是具有相同值的元素? - CinCout
@GargAnkit 是的,当然可以。 - Tomilov Anatoliy
很遗憾,boot::zip_iterator不符合排序迭代器的要求 :-( - Jarod42
4个回答

3

一个相对简单的解决方案是构建一个指向列表迭代器的向量v,并对其进行排序。然后,v的第i个元素指向应该占据排序后列表中第i个位置的元素,在此基础上可以重建排序后的列表。由于使用了辅助容器,性能可能不够优化,但易于理解。

void ZippedSort(std::list<A>& a, std::list<B>& b) {

    using PairOfIts = pair<decltype(a.begin()), decltype(b.begin())>;

    vector<PairOfIts> v;
    auto i = a.begin();
    auto j = b.begin();
    for (; i != a.end(); ++i, ++j)
        v.push_back(make_pair(i, j));

    std::sort(v.begin(), v.end(), [](PairOfIts const& i, PairOfIts const& j) { return *i.first < *j.first; } );

    list<A> sortedA;
    list<B> sortedB;
    for (auto& x : v) {
        sortedA.splice(sortedA.end(), a, x.first);
        sortedB.splice(sortedB.end(), b, x.second);
    }

    swap(sortedA, a);
    swap(sortedB, b);
}

我认为*O(N)*内存是不可避免的代价。但是这里_N * 3_ =(. - Tomilov Anatoliy
@Orient 我修改了示例代码,使用splice()来获取元素,而不是push_back。这样应该更好。 - edflanders
std::forward_list 没有 splice 方法。 - Tomilov Anatoliy
@Orient:内存开销确实为O(N)。但是,“假设a和b中的元素非常重。也就是说,您无法制作它们的副本”-额外的迭代器比元素本身便宜得多。 - MSalters

2

在处理这个问题时,最好的STL方式是使用std::pair填充vector,并创建一个自定义比较器,只比较对中的第一个元素。然后您将获得排序后的一组对。


我不想制作源元素的副本。只允许对它们进行移动或交换。 - Tomilov Anatoliy
2
你可以使用索引(1、2、3...)作为对的第二个元素。当对的向量被排序时,只需交换第二个数组中应该存在的元素即可。 - Heavy
1
由于源容器“first”和“second”通常没有下标运算符operator [](只有ForwardIterator,而不是RandomAccessIterator),因此您无法使用索引。建议使用std::forward_list - Tomilov Anatoliy
1
@Orient,这会不会是个问题呢?因为std::sort需要随机访问迭代器。 - lisyarus
std::sort 能够正常工作。下面有很多答案表明,无法避免使用额外的内存(以某种方式保存“顺序”)。可以使用 std::vectorstd::deque 或另一个具有 RandomAccessIterator 的容器。 - Tomilov Anatoliy
显示剩余4条评论

2
正确的做法是创建一个迭代器类,其value_typestd::pair<T1 &, T2 &>。它可能应该包含每个要排序的序列上的一个迭代器,并正确地将操作传递给它们。
实际上,这正是boost::zip_iterator所做的。我建议使用适当的比较器来使用它;或者至少使用boost::zip_iterator作为它应该如何工作的示例。

0

好的,完成了。但看起来这是一个(不太糟糕的)hack:在T tmp(std::move(lhs)); lhs = std::move(rhs); rhs = std::move(tmp);链中的std::swap实现中,我让std::sort算法仅执行中间操作(其他两个都是无操作):

#include <iostream>
#include <iomanip>
#include <type_traits>
#include <utility>
#include <iterator>
#include <algorithm>
#include <vector>
#include <forward_list>

#include <cstdlib>
#include <cassert>

template< typename first, typename second > 
void
sort_synchronously(first & f, second & s)
{
    std::size_t sz = static_cast< std::size_t >(std::distance(std::cbegin(f), std::cend(f)));
    assert(sz == static_cast< std::size_t >(std::distance(std::cbegin(s), std::cend(s))));
    struct P
    {
        typename first::iterator pfirst;
        typename second::iterator psecond;
        bool signal;
        bool operator < (P const & p) const { return (*pfirst < *p.pfirst); }
        P(typename first::iterator pf, typename second::iterator ps)
            : pfirst(pf)
            , psecond(ps)
            , signal(false)
        { ; }
        P(P &&) : signal(true) { ; }
        void operator = (P && p) { if (!p.signal) { std::iter_swap(pfirst, p.pfirst); std::iter_swap(psecond, p.psecond); } }
    };
    std::vector< P > p;
    p.reserve(sz);
    auto fi = std::begin(f);
    auto si = std::begin(s);
    for (std::size_t i = 0; i < sz; ++i) {
        p.emplace_back(fi, si);
        ++fi;
        ++si;
    }
    std::sort(std::begin(p), std::end(p));
} 

int
main()
{
    std::forward_list< int > a{5, 4, 3, 2, 1};
    std::forward_list< int > b{10, 20, 30, 40, 50};
    std::copy(std::cbegin(a), std::cend(a), std::ostream_iterator< int >(std::cout, " ")); std::cout << std::endl;
    std::copy(std::cbegin(b), std::cend(b), std::ostream_iterator< int >(std::cout, " ")); std::cout << std::endl;
    sort_synchronously(a, b);
    std::cout << std::endl;
    std::copy(std::cbegin(a), std::cend(a), std::ostream_iterator< int >(std::cout, " ")); std::cout << std::endl;
    std::copy(std::cbegin(b), std::cend(b), std::ostream_iterator< int >(std::cout, " ")); std::cout << std::endl;
    return EXIT_SUCCESS;
}

我相信对于`static_assert(std::is_const{})`的修改显而易见(只需将`typename first::iterator`更改为`typename first::const_iterator`并执行`std::swap(pfirst,p.pfirst)`而不是`std::iter_swap(pfirst,p.pfirst)`)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接