假设我们要从总大小m
中选择一个大小为n
的随机子集。由于可以使用S = {0, 1, 2, ..., (n - 1)}
中的唯一索引来标识总集中的每个元素。问题相当于从m
中随机选择S
个不同的元素。
一个简单的算法将重复调用伪随机数生成器rand
以从S
生成随机数。如果之前已生成过数字,请再试一次。算法终止,直到生成m
个不同的数字。此算法的最佳空间复杂度为O(1)
,但可能会rand
调用m
次以上。
我更关心的是时间复杂度而不是空间复杂性,如果合理的话,我会乐意交换空间。所以我实现了以下算法。它会rand
完全调用min{m, (n - m)}
次,但价格会增加O(n)
的空间复杂度。 (原始代码可以找到here)
template <typename Clock = std::chrono::high_resolution_clock>
auto tick_count() {
return Clock::now().time_since_epoch().count();
}
template <typename OutIt, typename RAND = std::minstd_rand,
typename Uint = typename RAND::result_type>
void random_subset(std::size_t m, std::size_t n, OutIt it, RAND&& rand =
RAND(static_cast<Uint>(tick_count()))) {
assert(n - 1 <= rand.max());
assert(m <= n);
if (m == 0) return;
auto swapped = false;
auto tmp = n - m;
if (tmp < m) {
m = tmp;
swapped = true;
}
std::vector<std::size_t> indices(n);
std::iota(indices.begin(), indices.end(), static_cast<std::size_t>(0));
auto back_it = indices.end();
for (std::size_t i = 0; i < m; ++i) {
auto idx = rand() % (n - i);
std::swap(indices[idx], *--back_it);
}
swapped ? std::copy(indices.begin(), back_it, it) :
std::copy(back_it, indices.end(), it);
}
我想知道算法是否可以在性能方面进一步改进。对通用实现的改进也很受欢迎。
答案 0 :(得分:2)
也许您可以使用Fisher-Yates algorithm的非常小的变体进行随机改组,特别是second variant of the Durstendfeld version:
-- To shuffle an array a of n elements (indices 0..n-1):
for i from 0 to n−2 do
j ← random integer such that 0 ≤ j < n-i
exchange a[i] and a[i+j]
只需将循环终止从 n - 2 更改为您需要的。
在证明中,循环不变量是指一旦传递了索引 i ,直到它的数组就是随机混乱。因此,您可以提前终止所需的结果。