【问题标题】:How to insert a new value to a set and erase another at the same time?如何在一个集合中插入一个新值并同时删除另一个?
【发布时间】:2014-12-02 12:15:27
【问题描述】:

每个集合都包含指定顺序的元素。我想为集合的大小指定一个界限,如果插入了一个严格小于(就顺序而言)的新元素并且已经达到指定的大小,自动删除最后一个元素。 p>

当然,我可以这样做:

class bounded_set
{
private:
    using set = std::set<Key, Compare, Allocator>;
    using iterator = typename set::iterator;

public:
    bounded_set(std::size_t size)
        : m_size(size)
    { }

    std::pair<iterator, bool> insert(Key const& value)
    {
        if (m_set.size() < m_size)
            return m_set.insert(value);

        auto last = std::prev(m_set.end());
        if (Compare()(value, *last))
        {
            m_set.erase(last);
            return m_set.insert(value);
        }
        return std::make_pair(last, false);
    }

private:
    set m_set;
    std::size_t m_size;
};

除了 bounded_set 不是最好的名称(因为 有界 容器在并发编程领域是众所周知的事情)之外,我还担心此实现中的内存分配.最有可能的是,首先,last 使用的空间将被释放。但是紧接着就需要为value分配新的内存。

我真正想做的是使用分配给last 的内存并将value 的数据复制到这个地方,同时保留顺序。

【问题讨论】:

  • 你可以用向量替换你的集合。除非您的数据集很大并且键具有昂贵的副本而没有廉价的移动,否则性能不会受到影响,并且实际上可能会更快,因为向量的动态内存分配比集合少。如果您保持向量排序,则可以在其上使用std::lower_bound 进行 log(n) 搜索。当然,您需要在插入等之前检查向量是否已经包含该值。将这些东西包装在“flat_set”类中。

标签: c++ c++11 stl set


【解决方案1】:

如果我正确理解了您的问题,具体取决于底层数据结构的工作方式,那么在您不必编写自定义内存分配器或使用库中的内存分配器的情况下,这不一定是可能的。例如,std::set 使用红黑树作为底层数据结构。因此,节点的内存位置和往返这些节点的关系指针本质上附加到树的总顺序。您不能重新使用来自“最小”值节点的内存并在那里放置另一个不是新的完全排序的“最小”值的值,而无需重新排序指向该节点的所有指针,以便它在树中该节点的值的适当位置。

如果您仍然担心内存使用并希望坚持使用 STL,而不是 std::set,也许您应该研究固定长度的优先级队列或使用基于数组的堆的类似性质的东西作为底层数据结构,因此内存不会不断分配和重新分配给新节点。

【讨论】:

  • -1 因为整个答案表明重用内存是不可能的,但实际上可以使用自定义分配器来完成。
  • 是的,自定义内存分配器可以解决问题,我已经更改了答案以反映这一点,但是当 STL 有其他工具更容易获得、测试等时,它似乎不必要地复杂...... . 没有?
  • MSVC 和 boost 一样带有许多分配器。 GCC 可能也会这样做。同时,使用 STL 来做到这一点需要向量+排序的奇怪组合。基于数组的堆可能是可以接受的,这取决于他为什么说他需要特定的排序。无论哪种方式,都会比set 拥有更多的移动/副本。
【解决方案2】:

我为您看到了几个选项,标准委员会错失了一个可以轻松解决您问题的机会。

N3586 为您的问题提出了解决方案。

std::pair<iterator, bool> insert(Key const& value)
{
    if (m_set.size() < m_size)
        return m_set.insert(value);

    auto last = std::prev(m_set.end());
    if (Compare()(value, *last))
    {
        auto temp = m_set.remove(last);
        *temp = value;
        return m_set.insert(temp);
    }
    return std::make_pair(last, false);
}

在这个假设的重写中,temp 是一个node_ptr,它允许非常量访问节点的value_type。您可以删除、写入和重新插入节点,所有这些都无需为节点分配任何内容。

委员会礼貌地拒绝了这个提议。

std::set 的自定义分配器可以以不那么优雅的方式来解决问题。这样的分配器只会缓存节点,而您现有的 insert 将正常工作。这种方法的一个小缺点是,虽然自定义分配器可以防止您的节点被释放,但它不能防止您的 Key 在您更改它时被破坏,然后再被构造。某些类型在分配中比在破坏-构造循环中更有效。有时前者可以是noexcept,而后者不能。

总的来说,我认为自定义分配器方法是最后的手段。你可以让它工作。但这需要一些精心策划的、非直观的代码。

想到了push_heappop_heap 的使用。但是,如果您确实需要一个迭代器来插入或返回相等的元素,那么它的使用会很尴尬。如果您可以处理 void 返回类型,它可能看起来像:

void insert(Key const& value)
{
    if (m_set.size() < m_size)
    {
        m_set.push_back(value);
        std::push_heap(m_set.begin(), m_set.end(), Compare{});
    }

    if (Compare()(value, m_set.front()))
    {
        std::pop_heap(m_set.begin(), m_set.end(), Compare{});
        m_set.back() = value;
        std::push_heap(m_set.begin(), m_set.end(), Compare{});
    }
}

但是在堆中搜索新插入的值很麻烦,push_heap 不提供此信息。

还有一个选项是排序向量+插入排序。您必须自己编写插入排序,但这是一项相对较小的编程任务。您想要插入排序的原因是您将始终对除最后一个元素之外的排序数组进行排序。而插入排序最适合这项工作。

这些解决方案都不是完美的,除了N3586 之外,没有一个提供任何接近“开箱即用”解决方案的东西,即只需要几行代码的解决方案。而N3586 不存在。如果您认为它应该存在,请联系您的 C++ 国家机构代表,并告诉他们。或者自己加入 C++ 委员会,并为之游说。

【讨论】:

  • 为什么这样的提议会被拒绝有点令人难以置信:这对我来说似乎完全没有干扰。委员会给出了什么理由?
  • @TemplateRex:人们担心如何在不调用未定义行为的情况下以可移植的方式实现它。为了我的钱,这正是你把它放在标准库中的原因。 std::lib 实现者编写不可移植的代码,以便其余部分不必(至少不是那么多)。但委员会由许多不同的人组成,他们有许多不同的想法。甚至很难将最好的想法标准化。移动语义花了十年时间,从来没有争议。标准化node_ptr 的关键是要有精力和毅力来推动它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-25
相关资源
最近更新 更多