【问题标题】:Equivalent of Python's list sort with key / Schwartzian transform等效于 Python 的列表排序与键/施瓦茨变换
【发布时间】:2015-06-15 09:49:43
【问题描述】:

在 Python 中,给定一个列表,我可以通过一个关键函数对其进行排序,例如:

>>> def get_value(k):
...     print "heavy computation for", k
...     return {"a": 100, "b": 30, "c": 50, "d": 0}[k]
...
>>> items = ['a', 'b', 'c', 'd']
>>> items.sort(key=get_value)
heavy computation for a
heavy computation for b
heavy computation for c
heavy computation for d
>>> items
['d', 'b', 'c', 'a']

如您所见,列表不是按字母数字排序的,而是按get_value() 的返回值排序的。

C++ 中是否有等价物? std::sort() 只允许我提供自定义比较器(相当于 Python 的 items.sort(cmp=...)),而不是关键功能。如果没有,是否有任何经过良好测试、高效、公开可用的等效实现我可以放入我的代码中?

请注意,Python 版本每个元素仅调用一次 key 函数,而不是每次比较调用两次。

【问题讨论】:

  • Python的key函数基本上封装了一个Schwartzian transform。也许这是一个有用的 Google 搜索字词?
  • 但是 Python 最初有一个 cmp 比较函数,这实际上是一个 C 构造。
  • @MartijnPieters:很好,我从来不知道这个词!谢谢。
  • 这个问题可能会有所帮助stackoverflow.com/questions/19842035/…

标签: python c++ sorting equivalent


【解决方案1】:

您可以自己滚动:

template <typename RandomIt, typename KeyFunc>
void sort_by_key(RandomIt first, RandomIt last, KeyFunc func) 
{
    using Value = decltype(*first);
    std::sort(first, last, [=](const ValueType& a, const ValueType& b) {
        return func(a) < func(b);
    });
}

如果KeyFunc 太贵,您必须使用这些值创建一个单独的向量。

我们甚至可以组合一个类,让我们仍然可以使用std::sort

template <typename RandomIter, typename KeyFunc>
void sort_by_key(RandomIter first, RandomIter last, KeyFunc func)
{
    using KeyT = decltype(func(*first));
    using ValueT = typename std::remove_reference<decltype(*first)>::type;

    struct Pair {
        KeyT key;
        RandomIter iter;
        boost::optional<ValueT> value;

        Pair(const KeyT& key, const RandomIter& iter)
            : key(key), iter(iter)
        { }

        Pair(Pair&& rhs)
            : key(std::move(rhs.key))
            , iter(rhs.iter)
            , value(std::move(*(rhs.iter)))
        { }

        Pair& operator=(Pair&& rhs) {
            key = std::move(rhs.key);
            *iter = std::move(rhs.value ? *rhs.value : *rhs.iter);
            value = boost::none;
            return *this;
        }

        bool operator<(const Pair& rhs) const {
            return key < rhs.key;
        }
    };

    std::vector<Pair> ordering;
    ordering.reserve(last - first);

    for (; first != last; ++first) {
        ordering.emplace_back(func(*first), first);
    }

    std::sort(ordering.begin(), ordering.end());
}

或者,如果这太 hacky,这是我的原始解决方案,这需要我们自己编写 sort

template <typename RandomIt, typename KeyFunc>
void sort_by_key_2(RandomIt first, RandomIt last, KeyFunc func)
{
    using KeyT = decltype(func(*first));
    std::vector<std::pair<KeyT, RandomIt> > ordering;
    ordering.reserve(last - first);

    for (; first != last; ++first) {
        ordering.emplace_back(func(*first), first);
    }

    // now sort this vector by the ordering - we're going
    // to sort ordering, but each swap has to do iter_swap too
    quicksort_with_benefits(ordering, 0, ordering.size());
}

虽然现在我们必须重新实现快速排序:

template <typename Key, typename Iter>
void quicksort_with_benefits(std::vector<std::pair<Key,Iter>>& A, size_t p, size_t q) {
    if (p < q) {
        size_t r = partition_with_benefits(A, p, q);
        quicksort_with_benefits(A, p, r);
        quicksort_with_benefits(A, r+1, q);
    }
}

template <typename Key, typename Iter>
size_t partition_with_benefits(std::vector<std::pair<Key,Iter>>& A, size_t p, size_t q) {
    auto key = A[p].first;
    size_t i = p;
    for (size_t j = p+1; j < q; ++j) {
        if (A[j].first < key) {
            ++i;
            std::swap(A[i].first, A[j].first);
            std::iter_swap(A[i].second, A[j].second);
        }
    }

    if (i != p) {
        std::swap(A[i].first, A[p].first);
        std::iter_swap(A[i].second, A[p].second);
    }
    return i;
}

举个简单的例子:

int main()
{
    std::vector<int> v = {-2, 10, 4, 12, -1, -25};

    std::sort(v.begin(), v.end());
    print(v); // -25 -2 -1 4 10 12

    sort_by_key_2(v.begin(), v.end(), [](int i) { return i*i; }); 
    print(v); // -1 -2 4 10 12 -25
}

【讨论】:

  • 是的,但如果func 计算繁重的东西,那效率就不高了。 func 每次比较都会调用两次,而不是像 Python 版本中那样每个元素调用一次(我将更新问题以提及这一点)
  • @Claudiu 这是 CPU 和内存使用率之间的权衡,你不能两者兼得。如果关键功能是轻量级的,那么 Barry 的方法会胜出,因为它不需要额外的内存。如果键函数是重量级的,Python 的方法更好,代价是必须为预先计算的键分配另一个列表。可以在 C++ 中模拟 Python 的方法,方法是计算应用于列表元素的键的中间向量,并通过该向量中的索引进行比较。
  • 编辑后的答案包含快速排序的实现,这表明实际上不可能实现 Python 风格的基于键的排序 std::sort ,并保留后者的通用性。 std::sort 的接口受到限制,因为它传递对值的引用而不是实际的迭代器(可用于在预计算键的向量中查找键)。
  • @Claudiu 仅计算一次func 的最佳方法依赖于(1)制作pairs 的vector 和(2)实现您自己的sort 方法。有用。好不好完全取决于KeyFunc有多贵。
  • 有趣 - 该实现 +1。你能解释一下为什么不能重用 std::sort 吗?比如说,您能否将迭代器的附加映射添加到它们的最终位置,然后将比较器传递给std::sort,它会在该映射中查找迭代器?确实,这将是额外的开销,但如果 std::sort 实现得更好,那么它可能是值得的。不过,到那时,可能是过早的优化成为万恶之源。
【解决方案2】:

如果密钥类型不是非常大(如果是,我会说衡量标准),你可以保存一个

std::vector< std::pair<key_type, value_type>> vec;

而不是您的“正常”值向量。然后,您可以只计算和保护密钥一次,然后只需使用 std::sort

另一种侵入性的方法是将密钥作为成员提供,然后将其缓存。这样做的好处是您无需在每次访问向量时都弄乱pairs。

【讨论】:

  • 如果你改为使用std::pair&lt;key_type, value_type&gt;,那么你甚至不需要将比较对象传递给std::sort。
  • @Claudiu 谢谢,这确实更好。
猜你喜欢
  • 1970-01-01
  • 2011-06-08
  • 2010-10-10
  • 1970-01-01
  • 1970-01-01
  • 2021-10-04
  • 2011-12-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多