如何从std :: set中有效地选择随机元素

时间:2012-09-05 19:32:41

标签: c++ algorithm stl

如何从std::set

中有效地选择随机元素

std::set::iterator 不是随机访问迭代器。所以我无法直接为随机选择的元素编制索引,就像std::dequestd::vector

一样

可以std::set::begin()返回迭代器并将其0增加到std::set::size()-1次,但这似乎做了很多不必要的工作。对于接近集合大小的“索引”,我最终将遍历树的整个前半部分,即使已经知道该元素将不会在那里找到。

有更好的方法吗?

以效率的名义,我愿意将“随机”定义为较少随机,而不是用于在向量中选择随机索引的任何方法。称之为“合理随机”。

修改...

以下许多有见地的答案。

简短版本是即使您可以在 log(n)时间内找到特定的元素,也无法找到任意那个时候通过std::set界面的元素。

6 个答案:

答案 0 :(得分:7)

改为使用boost::container::flat_set

boost::container::flat_set<int> set;
// ...
auto it = set.begin() + rand() % set.size();

虽然插入和删除成为O(N),但我不知道这是否有问题。您仍然有O(log N)查找,并且容器是连续的这一事实提供了总体改进,通常超过O(log N)插入和删除的丢失。

答案 1 :(得分:4)

导致随机树遍历的find(或lower_bound)的谓词怎么样?您必须告诉它集合的大小,以便它可以估计树的高度,有时在叶节点之前终止。

编辑:我意识到这个问题是std::lower_bound采用谓词但没有任何类似树的行为(内部使用std::advance,这在另一个答案的评论中讨论过)。 std::set<>::lower_bound使用集合的谓词,该谓词不能是随机的,并且仍然具有类似集合的行为。

啊哈,你不能使用不同的谓词,但你可以使用一个可变的谓词。由于std::set按值传递谓词对象,因此您必须使用predicate &作为谓词,以便可以进入并修改它(将其设置为“随机化”模式)。

这是一个准工作的例子。不幸的是,我无法将我的大脑包裹在正确的随机谓词周围,所以我的随机性不是很好,但我相信有人可以解决这个问题:

#include <iostream>
#include <set>
#include <stdlib.h>
#include <time.h>

using namespace std;

template <typename T>
struct RandomPredicate {
    RandomPredicate() : size(0), randomize(false) { }
    bool operator () (const T& a, const T& b) {
        if (!randomize)
            return a < b;

        int r = rand();
        if (size == 0)
            return false;
        else if (r % size == 0) {
            size = 0;
            return false;
        } else {
            size /= 2;
            return r & 1;
        }
    }

    size_t size;
    bool randomize;
};

int main()
{
    srand(time(0));

    RandomPredicate<int> pred;
    set<int, RandomPredicate<int> & > s(pred);
    for (int i = 0; i < 100; ++i)
        s.insert(i);

    pred.randomize = true;
    for (int i = 0; i < 100; ++i) {
        pred.size = s.size();
        set<int, RandomPredicate<int> >::iterator it = s.lower_bound(0);
        cout << *it << endl;
    }
}

我的半成品随机性测试是./demo | sort -u | wc -l,看看我得到了多少独特的整数。使用较大的样本集,请尝试./demo | sort | uniq -c | sort -n查找不需要的模式。

答案 2 :(得分:2)

如果你可以访问底层的红黑树(假设一个存在),那么你可以在O(log n)中访问一个随机节点,选择L / R作为{的连续位{1}} - 位随机整数。但是,您不能,因为标准没有公开底层数据结构。

Xeo将迭代器放置在向量中的解决方案是设置O(n)时间和空间,但总体上是摊销的常量。这有利于ceil(log2(n)),即O(n)时间。

答案 3 :(得分:1)

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

set <int> myset;
//insert some elements into myset
int rnd = rand() % myset.size();
set <int> :: const_iterator it(myset.begin());
advance(it, rnd);
//now 'it' points to your random element

另一种方法,可能不那么随意:

int mini = *myset().begin(), maxi = *myset().rbegin();
int rnd = rand() % (maxi - mini + 1) + mini;
int rndresult = *myset.lower_bound(rnd);

答案 4 :(得分:1)

如果集合不经常更新或者您不需要经常运行此算法,请在vector中保留数据的镜像副本(或者只需将集合复制到需要的向量)并随机选择。

另一种方法,如注释中所示,是将迭代器的向量保存到集合中(它们仅在set s的元素删除时失效)并随机选择迭代器。

最后,如果您不需要基于树的集合,则可以使用vectordeque作为基础容器,并在需要时使用sort / unique-ify。

答案 5 :(得分:1)

您可以通过维护正常的值数组来完成此操作;当你插入到集合中时,你将元素追加到数组的末尾( O(1)),然后当你想要生成一个随机数时,你可以从 O(1)。

如果要从阵列中删除元素,则会出现此问题。最天真的方法是 O(n),这可能足以满足您的需求。但是,可以使用以下方法将其改进为 O(log n);

对于数组中的每个索引i,保留prfx[i],它表示数组中0...i范围内未删除元素的数量。保留一个分段树,保持每个范围中包含的最大prfx[i]

每次删除时,可以在 O(log n)中更新分段树。现在,当您想要访问随机数时,查询分段树以查找数字的“真实”索引(通过查找最大prfx等于随机索引的最早范围)。这使得随机数生成复杂度 O(log n)