select 随机子集的通用算法实现
Generic algorithm implementation to select a random subset
假设我们要从总大小 n
中 select 大小 m
的随机子集。由于可以使用 S = {0, 1, 2, ..., (n - 1)}
中的唯一索引来标识总集中的每个元素。该问题等同于 select m
个来自 S
.
的随机元素
一个简单的算法将重复调用伪随机数生成器 rand
以从 S
生成随机数。如果之前已经生成过数字,请重试。算法终止,直到生成 m
个不同的数字。此算法的最佳 space 复杂度为 O(1)
,但可能会调用 rand
次以上 m
次。
我更关心时间复杂度而不是 space 复杂度,如果合理的话,我很乐意用 space 换取时间。所以我实现了以下算法。它调用 rand
恰好 min{m, (n - m)}
次,但代价是 O(n)
的 space 复杂性增加。 (原代码可见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);
}
我想知道是否可以在性能方面进一步改进算法。也欢迎对通用实现进行改进。
也许您可以使用 Fisher-Yates algorithm for random shuffling, specifically the 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已经通过,到它的数组是随机洗牌。因此,您可以提前终止并获得所需的结果。
假设我们要从总大小 n
中 select 大小 m
的随机子集。由于可以使用 S = {0, 1, 2, ..., (n - 1)}
中的唯一索引来标识总集中的每个元素。该问题等同于 select m
个来自 S
.
一个简单的算法将重复调用伪随机数生成器 rand
以从 S
生成随机数。如果之前已经生成过数字,请重试。算法终止,直到生成 m
个不同的数字。此算法的最佳 space 复杂度为 O(1)
,但可能会调用 rand
次以上 m
次。
我更关心时间复杂度而不是 space 复杂度,如果合理的话,我很乐意用 space 换取时间。所以我实现了以下算法。它调用 rand
恰好 min{m, (n - m)}
次,但代价是 O(n)
的 space 复杂性增加。 (原代码可见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);
}
我想知道是否可以在性能方面进一步改进算法。也欢迎对通用实现进行改进。
也许您可以使用 Fisher-Yates algorithm for random shuffling, specifically the 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已经通过,到它的数组是随机洗牌。因此,您可以提前终止并获得所需的结果。