生成序列的所有索引通常是一个坏主意,因为它可能需要花费很多时间,特别是如果要选择的数字与MAX的比率较低(复杂性由n < rand_num[j] - j主导)。 如果要选择的数字与rand_num[j] - j的比率接近1会变得更糟,因为从所有序列中移除所选择的指数也变得昂贵(我们接近rand_num[j])。 但对于小数字,这通常效果很好,并不是特别容易出错。
通过使用集合过滤生成的索引也是一个坏主意,因为花费一些时间将索引插入到序列中,并且无法保证进度,因为可以多次绘制相同的随机数(但是对于足够大的MAX它是不太可能)。 这可能接近复杂性
n < rand_num[j] - j,忽略重复项并假设集合使用树进行有效查找(但是具有分配树节点且可能必须重新平衡的显着恒定成本rand_num[j] - j)。
另一种选择是从一开始就唯一地生成随机值,保证正在取得进展。 这意味着在第一轮中,生成MAX中的随机索引:
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
在第二轮中,仅生成MAX(因为已选择一个项目):
items i0 i1 i3 i4 i5 i6 (total 6 items)
idx 1 ^^ (index 2 out of these 6, but 3 out of the original 7)
然后需要调整指数的值:如果第二个指数落在序列的后半部分(在第一个指数之后),则需要递增以考虑差距。 我们可以将其实现为循环,允许我们选择任意数量的唯一项。
对于短序列,这是非常快的MAX算法:
void RandomUniqueSequence(std::vector &rand_num,
const size_t n_select_num, const size_t n_item_num)
{
assert(n_select_num <= n_item_num);
rand_num.clear(); // !!
// b1: 3187.000 msec (the fastest)
// b2: 3734.000 msec
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
// get a random number
size_t n_where = i;
for(size_t j = 0; j < i; ++ j) {
if(n + j < rand_num[j]) {
n_where = j;
break;
}
}
// see where it should be inserted
rand_num.insert(rand_num.begin() + n_where, 1, n + n_where);
// insert it in the list, maintain a sorted sequence
}
// tier 1 - use comparison with offset instead of increment
}
其中MAX是您的5和n < rand_num[j] - j是您的rand_num[j] - j. rand_num[j]在rand_num(含)中返回随机整数。 如果通过使用二分搜索来找到插入点来选择许多项(例如,不是5而不是500),则可以使这更快一些。 为此,我们需要确保满足要求。
我们将使用比较MAX进行二进制搜索
n < rand_num[j] - j.我们需要显示rand_num[j] - j仍然是排序序列rand_num[j]的排序序列。幸运的是很容易显示,因为原始rand_num的两个元素之间的最小距离是一个(生成的数字是唯一的,所以总是存在差异 至少1)。 同时,如果我们从所有元素中减去指数j
rand_num[j],索引的差异正好是1.因此在“最差”的情况下,我们得到一个恒定的序列 - 但从不减少。 因此可以使用二进制搜索,产生O(n log(n))算法:
struct TNeedle { // in the comparison operator we need to make clear which argument is the needle and which is already in the list; we do that using the type system.
int n;
TNeedle(int _n)
:n(_n)
{}
};
class CCompareWithOffset { // custom comparison "n < rand_num[j] - j"
protected:
std::vector::iterator m_p_begin_it;
public:
CCompareWithOffset(std::vector::iterator p_begin_it)
:m_p_begin_it(p_begin_it)
{}
bool operator ()(const int &r_value, TNeedle n) const
{
size_t n_index = &r_value - &*m_p_begin_it;
// calculate index in the array
return r_value < n.n + n_index; // or r_value - n_index < n.n
}
bool operator ()(TNeedle n, const int &r_value) const
{
size_t n_index = &r_value - &*m_p_begin_it;
// calculate index in the array
return n.n + n_index < r_value; // or n.n < r_value - n_index
}
};
最后:
void RandomUniqueSequence(std::vector &rand_num,
const size_t n_select_num, const size_t n_item_num)
{
assert(n_select_num <= n_item_num);
rand_num.clear(); // !!
// b1: 3578.000 msec
// b2: 1703.000 msec (the fastest)
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
// get a random number
std::vector::iterator p_where_it = std::upper_bound(rand_num.begin(), rand_num.end(),
TNeedle(n), CCompareWithOffset(rand_num.begin()));
// see where it should be inserted
rand_num.insert(p_where_it, 1, n + p_where_it - rand_num.begin());
// insert it in the list, maintain a sorted sequence
}
// tier 4 - use binary search
}
我在三个基准测试中对此进行了测试。 首先,从7个项目中选择3个数字,所选项目的直方图累计超过10,000次:
4265 4229 4351 4267 4267 4364 4257
这表明7个项目中的每一个被选择大致相同的次数,并且没有由算法引起的明显偏差。 还检查所有序列的正确性(内容的唯一性)。
第二个基准涉及从5000个项目中选择7个数字。 算法的几个版本的时间累计超过10,000,000次运行。 结果在代码中的注释中表示为MAX.算法的简单版本稍微快一些。
第三个基准涉及从5000个项目中选择700个数字。 再次积累了几个版本的算法的时间,这次超过10,000次运行。 结果在代码中的注释中表示为MAX.算法的二进制搜索版本现在比简单版本快两倍以上。
第二种方法开始在我的机器上选择超过cca 75项目的速度更快(注意,任一算法的复杂性不依赖于项目数量,MAX)。
值得一提的是,上述算法按升序生成随机数。 但是添加另一个数组会很简单,数字将按生成顺序保存,然后返回(以可忽略的额外成本set)。 没有必要改变输出:这会慢得多。
请注意,源代码是用C ++编写的,我的机器上没有Java,但概念应该是清楚的。
编辑:
为了娱乐,我还实现了生成包含所有索引的列表的方法
set,随机选择它们并从列表中删除它们以保证唯一性。 由于我选择了相当高的b2(5000),性能是灾难性的:
// b1: 519515.000 msec
// b2: 20312.000 msec
std::vector all_numbers(n_item_num);
std::iota(all_numbers.begin(), all_numbers.end(), 0);
// generate all the numbers
for(size_t i = 0; i < n_number_num; ++ i) {
assert(all_numbers.size() == n_item_num - i);
int n = n_Rand(n_item_num - i - 1);
// get a random number
rand_num.push_back(all_numbers[n]); // put it in the output list
all_numbers.erase(all_numbers.begin() + n); // erase it from the input
}
// generate random numbers
我还用set(一个C ++集合)实现了这个方法,实际上它在基准测试b2上排名第二,比二进制搜索的方法慢了大约50%。 这是可以理解的,因为set使用二叉树,其插入成本类似于二进制搜索。 唯一的区别是获得重复项目的机会,这会减慢进度。
// b1: 20250.000 msec
// b2: 2296.000 msec
std::set numbers;
while(numbers.size() < n_number_num)
numbers.insert(n_Rand(n_item_num - 1)); // might have duplicates here
// generate unique random numbers
rand_num.resize(numbers.size());
std::copy(numbers.begin(), numbers.end(), rand_num.begin());
// copy the numbers from a set to a vector
完整的源代码在这里。