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

C++ 如何有效地从std::set中选择随机元素,c++,algorithm,stl,C++,Algorithm,Stl,如何有效地从std::set中选择随机元素 std::set::iterator不是随机访问迭代器。因此,我不能像为std::deque或std::vector那样直接索引随机选择的元素 我可以使用从std::set::begin()返回的迭代器,并将其0增加到std::set::size()-1次,但这似乎做了很多不必要的工作。对于接近集合大小的“索引”,我将遍历整个树的前半部分,即使已经知道在那里找不到元素 有更好的方法吗? 在效率的名义下,我愿意将“随机”定义为比我在向量中选择随机索引的任

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

std::set::iterator
不是随机访问迭代器。因此,我不能像为
std::deque
std::vector
那样直接索引随机选择的元素

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

有更好的方法吗?

在效率的名义下,我愿意将“随机”定义为比我在向量中选择随机索引的任何方法都更少的随机性。称之为“合理随机”

编辑…

下面是许多有见地的答案


简短的版本是,即使您可以在log(n)time中找到特定的元素,您也无法通过
std::set
接口在该时间内找到任意元素。

对于导致随机树遍历的
find
(或
下限
)的谓词如何?您必须告诉它集合的大小,以便它可以估计树的高度,有时在叶节点之前终止

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

Aha,不能使用不同的谓词,但可以使用可变谓词。由于
std::set
通过值传递谓词对象,因此必须使用
谓词&
作为谓词,以便可以在中访问并修改它(将其设置为“随机化”模式)

下面是一个准工作示例。不幸的是,我的大脑不能围绕正确的随机谓词,所以我的随机性不是很好,但我相信有人能弄明白:

#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;
    }
}
#包括
#包括
#包括
#包括
使用名称空间std;
模板
结构随机谓词{
RandomPredicate():大小(0),随机化(false){
布尔运算符()(常数T&a、常数T&b){
如果(!随机化)
返回acout您可以使用
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
如果您可以访问底层红黑树(假设存在),那么您可以访问O(logn)中的随机节点,选择L/R作为
ceil(log2(n))
位随机整数的连续位。但是,您不能,因为标准没有公开底层数据结构

Xeo在向量中放置迭代器的解决方案是设置O(n)个时间和空间,但总的来说是摊销常数。这与
std::next
,即O(n)个时间相比是有利的。

使用:

boost::container::flat\u set;
// ...
auto it=set.begin()+rand()%set.size();

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

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

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


最后,如果您不需要基于树的集合,您可以使用
vector
deque
作为基础容器,并在需要时进行排序/唯一化。

您可以通过维护一个正常的值数组来实现这一点;插入集合时,将元素附加到数组的末尾(O(1)),然后当您想要生成一个随机数时,您也可以从O(1)中的数组中获取它

当您要从数组中删除元素时,会出现问题。最简单的方法是使用O(n),这可能足够满足您的需要。但是,可以使用以下方法将其改进为O(log n)

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


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

将集合中的所有迭代器放入
std::vector
中,并从中随机选择?问:如果您想“访问
int mini = *myset().begin(), maxi = *myset().rbegin();
int rnd = rand() % (maxi - mini + 1) + mini;
int rndresult = *myset.lower_bound(rnd);
boost::container::flat_set<int> set;
// ...
auto it = set.begin() + rand() % set.size();