【问题标题】:How to select a random element in std::set?如何在 std::set 中选择随机元素?
【发布时间】:2011-03-04 10:13:47
【问题描述】:

如何在std::set 中选择随机元素?

我天真地尝试过这个:

int GetSample(const std::set<int>& s) {
  double r = rand() % s.size();
  return *(s.begin() + r); // compile error
}

但是operator+是不允许的。

【问题讨论】:

标签: c++ iterator set


【解决方案1】:

您可以使用std::advance 方法。

#include <set>
#include <algorithm>

int main() {
  using namespace std;
  // generate a set...
  set<int> s;
  for( int i = 0; i != 10; ++i ) s.insert(i);
  auto r = rand() % s.size(); // not _really_ random
  auto n = *select_random(s, r);
}

在哪里

template<typename S>
auto select_random(const S &s, size_t n) {
  auto it = std::begin(s);
  // 'advance' the iterator n times
  std::advance(it,n);
  return it;
}

【讨论】:

  • 任何解决方案都是 O(N)。证明留作练习,提示:在恒定时间内可以达到多少个 std::set 元素?
  • 可能是 O(logN)。 std::set 存储在某种树中,可能有一个解决方案只在其中一个分支上下降,然后就完成了。
  • 我的答案中使用排序向量的方法是 O(1)。
  • @Kiscsirke 您说得对,使用平衡搜索树,您可以使用 O(log(N)) 进行插入、删除和随机访问。然而,后者要求节点存储他们在左边或右边有多少孩子。这需要在插入、删除和重新平衡期间进行更新。由于std::setstd::map 对用户隐藏了树内部结构,因此它们不能用于实现此目的。我最终实现了自己的搜索树。绝对有可能得到 O(log(N)) 查找。
  • @Timofey 是的。 OP的“问题”不是他问的问题:)。相应地更新了我的答案。
【解决方案2】:

第一个解决方案:时间O(log n) /空间O(1)(不均匀!)

上面评论中的一个假设,它可以在 O(log(n)) 中完成(vs O(n) for std::advance)没有向量(使用 O(n) 更多空间)通过使用我描述的方法here

基本上,你:

  • 检查集合是否为空(如果是,则没有希望)
  • 生成随机值
  • 如果已经存在则返回,否则插入
  • 获取一个迭代器 it 就可以了
  • 如果最后是it,则将随机元素设为*(it++)*(set.begin())
  • 在删除你插入的元素之前不要返回

n.b :正如 Aaron 所指出的,该元素不是随机选择的统一。您需要构建与集合中的元素具有相同分布的随机元素以接近统一轮询。

第二种解决方案:时间O(1) /空间O(n)(均匀)

davidhigh 已经用向量给出了解决方案,但存在一个问题,因为当您 pop 堆栈中的一个元素时,您将不得不在 O(n) 或者您可以在每次想要检索随机元素时重新构建向量,但这也是 O(n)

为避免此问题并将插入/删除保持在O(log n),您可以保留std::unordered_set 并在第一个解决方案中使用similar method 以获取随机元素O(1).

p.s :如果您的元素很大,您可以使用一组无序的指针(带有修改过的哈希)来节省一些内存。

【讨论】:

  • 这是随机的,是的,但不是从集合的当前元素随机一致。我们可以假设提问者想要统一。虽然也许这不是完全必要的
  • 确实,如果您生成的元素的分布看起来像接近它的集合。 unordered_set 没有这个问题(请参阅答案中的链接)。需要考虑一下...
【解决方案3】:

如果随机访问很重要,并且您可以忍受 O(N) 平均插入工作量,那么this paper 中给出的解决方法可能很方便。

主要思想是使用排序向量,然后查找函数std::lower_bound。就像在正常集合中一样,查找需要 O(log N)。此外,(随机)插入需要 O(N),因为所有后续元素必须像在法线向量中一样移动(并且可能执行重新分配)。然而,在后面的插入是恒定的(除了重新分配。您可以通过调用reserve() 来避免这种情况,并具有足够大的存储空间)。

最后,问题的重点:随机访问是O(1)。 只需从[0, V.size()-1]中的均匀分布中抽取一个随机数i,并返回对应的元素@ 987654327@。

这是论文中的代码基础,它实现了这个排序的向量。根据需要扩展它:

template <class T, class Compare = std::less<T> >
struct sorted_vector {
 using std::vector;
 using std::lower_bound;
 vector<T> V;
 Compare cmp; 
 typedef typename vector<T>::iterator iterator;
 typedef typename vector<T>::const_iterator const_iterator;
 iterator begin() { return V.begin(); }
 iterator end() { return V.end(); }
 const_iterator begin() const { return V.begin(); }
 const_iterator end() const { return V.end(); }

 //...if needed, implement more by yourself

 sorted_vector(const Compare& c = Compare()) : V(), cmp(c) {}
 template <class InputIterator>
 sorted_vector(InputIterator first, InputIterator last, Const Compare& c = Compare())
 : V(first, last), cmp(c)
 {
 std::sort(begin(), end(), cmp);
 }

 //...

 iterator insert(const T& t) {
     iterator i = lower_bound(begin(), end(), t, cmp);
     if (i == end() || cmp(t, *i))
        V.insert(i, t);
      return i;
 }
 const_iterator find(const T& t) const {
     const_iterator i = lower_bound(begin(), end(), t, cmp);
      return i == end() || cmp(t, *i) ? end() : i;
 }
};

对于更复杂的实现,您还可以考虑this page

编辑:或者更好的是,使用boost::container::flat_set,它使用上面的想法实现集合,即作为排序向量。

【讨论】:

  • 如果你知道set在你开始随机采样后不会改变,或者它很少改变,你也可以在vector改变时缓存它,然后从中选择那里。您可以将缓存的set 包装起来以使其透明(写入无效缓存,如果读取无效则重建缓存)。
【解决方案4】:

C++17 std::sample

这将是一种方便但效率不高 (O(n)) 的方法:

#include <algorithm>
#include <iostream>
#include <random>
#include <set>
#include <vector>

int main() {
    std::set<int> in{1, 2, 3, 5, 7};
    std::vector<int> out;
    std::sample(in.begin(), in.end(), std::back_inserter(out),
                3, std::mt19937{std::random_device{}()});
    for (auto i : out)
        std::cout << i << std::endl;
}

但我认为为了提高效率,您只需复制到另一种类型的结构:How to select a random element in std::set in less than O(n) time?

【讨论】:

    【解决方案5】:

    要从集合中获取随机元素,首先使用 rand() 函数取一个随机数,然后按集合大小取一个模数 (%),这样我们的迭代器就不会越界。现在,要获取随机元素,只需迭代 idx=rand() % s.size() 次即可获取随机元素。在这种方法中,每个元素都有相同的发生概率。

    // making set
    unordered_set<int> s;
    s.insert(1);
    s.insert(2);
    s.insert(3);
    s.insert(4);
    
    // logic
    int idx = rand()%s.size();
    auto it = s.begin();
    for (int i = 0; i < idx; i++)
    {
        it++;
    }
    return *it;
    

    【讨论】:

      【解决方案6】:
      int GetSample(const std::set<int>& s) {
        double r = rand() % s.size();
        std::set<int>::iterator it = s.begin();
        for (; r != 0; r--) it++;
        return *it;
      }
      

      将是一种方法,虽然不漂亮;

      【讨论】:

      • 此代码不正确,您不能简单地检查 double 是否相等。为什么要在这里翻倍?
      猜你喜欢
      • 2012-08-30
      • 1970-01-01
      • 2012-01-08
      • 1970-01-01
      • 2015-01-17
      • 1970-01-01
      • 1970-01-01
      • 2010-12-18
      • 2021-12-30
      相关资源
      最近更新 更多