如何有效地从std::集中选择随机元素
How to efficiently select a random element from a std::set
如何有效地从std::set
中选择随机元素?
std::set::iterator
不是随机访问迭代器。所以我不能像对std::deque
或std::vector
那样直接索引随机选择的元素
我可以使用从std::set::begin()
返回的迭代器,并在[0
,std::set::size()
)范围内随机递增多次,但这似乎做了很多不必要的工作。对于接近集合大小的"索引",我最终会遍历内部树结构的整个前半部分,即使已知在那里找不到元素。
有更好的方法吗
以效率的名义,我愿意定义";"随机";因为的随机性比我在向量中选择随机索引的任何方法都要小。称之为";"合理随机";。
编辑
下面有许多精辟的答案。
简短的版本是,即使您可以在log(n)时间中找到特定的元素,但您也无法通过std::set
接口在该时间中找到任何任意的
使用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)插入和删除的损失。
find
(或lower_bound
)的谓词会导致随机树遍历吗?你必须告诉它集合的大小,这样它才能估计树的高度,有时甚至在叶节点之前终止。
编辑:我意识到这个问题是std::lower_bound
接受了一个谓词,但没有任何类似树的行为(在内部它使用了std::advance
,这在另一个答案的注释中进行了讨论)。std::set<>::lower_bound
使用集合的谓词,该谓词不能是随机的并且仍然具有类似集合的行为。
Aha,不能使用不同的谓词,但可以使用可变谓词。由于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
来查找不需要的模式。
如果您可以访问底层的红黑树(假设存在一个),那么您可以访问O(logn)中的随机节点,选择L/R作为ceil(log2(n))
位随机整数的连续位。但是,您不能这样做,因为标准没有公开底层数据结构。
Xeo在向量中放置迭代器的解决方案是O(n)时间和空间来设置,但总体摊销为常数。这与作为O(n)时间的std::next
相比是有利的。
您可以使用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);
如果集合不频繁更新或不需要频繁运行此算法,请在vector
中保留数据的镜像副本(或根据需要将集合复制到向量中),然后从中随机选择。
另一种方法,如注释中所示,是将迭代器的向量保留在集合中(它们只在set
的元素删除时无效),并随机选择一个迭代器。
最后,如果不需要基于树的集合,可以使用vector
或deque
作为底层容器,并在需要时对ify进行排序/唯一化。
您可以通过维护一个正常的值数组来实现这一点;当您插入到集合中时,您将元素附加到数组的末尾(O(1)),然后当您想要生成一个随机数时,您也可以从O(1)中的数组中获取它。
当您希望从数组中删除元素时,就会出现此问题。最简单的方法是采用O(n),这可能足够满足您的需求。但是,可以使用以下方法将其改进为O(logn);
对于数组中的每个索引i
,保留prfx[i]
,它表示数组中范围0...i
中未删除元素的数量。保留一个分段树,在其中可以保留每个范围中包含的最大prfx[i]
。
每次删除都可以在O(logn)中更新段树。现在,当您想要访问随机数时,您可以查询段树来查找该数的"真实"索引(通过查找最大prfx
等于随机索引的最早范围)。这使得复杂性的随机数生成O(logn)。
现有容器的平均O(1)/O(log N)(可散列/不可散列)插入/删除/采样
想法很简单:使用拒绝采样,同时上限拒绝率,这可以通过摊销O(1)压实操作实现。
然而,与基于增广树的解决方案不同,这种方法不能扩展到支持加权采样。
template <typename T>
class UniformSamplingSet {
size_t max_id = 0;
std::unordered_set<size_t> unused_ids;
std::unordered_map<size_t, T> id2value;
std::map<T, size_t> value2id;
void compact() {
size_t id = 0;
std::map<T, size_t> new_value2id;
std::unordered_map<size_t, T> new_id2value;
for (auto [_, value] : id2value) {
new_value2id.emplace(value, id);
new_id2value.emplace(id, value);
++id;
}
max_id = id;
unused_ids.clear();
std::swap(id2value, new_id2value);
std::swap(value2id, new_value2id);
}
public:
size_t size() {
return id2value.size();
}
void insert(const T& value) {
size_t id;
if (!unused_ids.empty()) {
id = *unused_ids.begin();
unused_ids.erase(unused_ids.begin());
} else {
id = max_id++;
}
if (!value2id.emplace(value, id).second) {
unused_ids.insert(id);
} else {
id2value.emplace(id, value);
}
}
void erase(const T& value) {
auto it = value2id.find(value);
if (it == value2id.end()) return;
unused_ids.insert(it->second);
id2value.erase(it->second);
value2id.erase(it);
if (unused_ids.size() * 2 > max_id) {
compact();
};
}
// uniform(n): uniform random in [0, n)
template <typename F>
T sample(F&& uniform) {
size_t i;
do { i = uniform(max_id); } while (unused_ids.find(i) != unused_ids.end());
return id2value.at(i);
}
- 如何使用默认参数等选择模板专业化
- 如何(从固定列表中)选择一个数字序列,该序列将与目标数字相加
- 选择要调用的构造函数
- C++选择排序算法中的逻辑错误
- QTreeView幻灯片多选后无法使用单击选择
- 无法获取菜单选择以运行函数.C++
- Qt C++静态thread_local QNetworkAccessManager是线程应用程序的好选择吗
- 如何从存储在std::映射中的std::集中删除元素
- 在C++中,如何通过几种类型从元组中选择多个元素
- 讨论 - 创建矩阵时的数组与向量的向量 - 什么是最实用的选择
- 对可变参数使用声明.如何选择正确的功能
- 选择选举获胜者的程序
- 如何选择在 csv 文件中输出的位置
- 根据用户回答声明"Players"。用户选择玩家数量。播放器是结构体
- 将非平凡的项目放在多集中
- 程序在尝试猜测它选择的随机数时进入无限循环?
- 是否可以从定义的数字集中选择随机数
- 最佳的c++方式来随机选择设置位在位集中的位置
- 如何有效地从std::集中选择随机元素
- 我想为每个组合框选择分配整数值,并将它们集中打印到文本文件中