生成序列的所有索引通常不是一个好主意,因为它可能需要很多时间,特别是如果要选择的数字与MAX 的比率很低(复杂性由O(MAX) 主导)。如果要选择的数字与 MAX 的比率接近 1,则情况会变得更糟,因为从所有序列中删除所选索引也会变得昂贵(我们接近 O(MAX^2/2))。但对于少数人来说,这通常效果很好,而且不会特别容易出错。
使用集合过滤生成的索引也是一个坏主意,因为将索引插入序列中会花费一些时间,并且不能保证进度,因为可以多次绘制相同的随机数(但对于足够大的MAX 不太可能)。这可能接近于复杂性
O(k n log^2(n)/2),忽略重复项并假设集合使用树进行高效查找(但分配树节点的固定成本k 和可能不得不rebalance)。
另一种选择是从一开始就生成唯一的随机值,以确保取得进展。这意味着在第一轮中,会在[0, MAX] 中生成一个随机索引:
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
第二轮只生成[0, MAX - 1](因为已经选择了一项):
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)
然后需要调整索引的值:如果第二个索引落在序列的后半部分(在第一个索引之后),则需要增加它以弥补差距。我们可以将其实现为一个循环,允许我们选择任意数量的唯一项目。
对于短序列,这是相当快的O(n^2/2) 算法:
void RandomUniqueSequence(std::vector<int> &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
}
n_select_num 是你的 5,n_number_num 是你的 MAX。 n_Rand(x) 在[0, x](含)中返回随机整数。如果通过使用二分搜索找到插入点来选择很多项目(例如,不是 5 个而是 500 个),这可以更快一些。为此,我们需要确保满足要求。
我们将进行二分搜索,比较 n + j < rand_num[j] 与
n < rand_num[j] - j 相同。我们需要证明rand_num[j] - j 仍然是排序序列rand_num[j] 的排序序列。幸运的是,这很容易显示,因为原始 rand_num 的两个元素之间的最小距离为 1(生成的数字是唯一的,因此始终存在至少 1 的差异)。同时,如果我们从所有元素
rand_num[j] 中减去索引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<int>::iterator m_p_begin_it;
public:
CCompareWithOffset(std::vector<int>::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<int> &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<int>::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 次运行。结果在代码中用 cmets 表示为b1。算法的简单版本稍快。
第三个基准测试涉及从 5000 个项目中选择 700 个数字。该算法的几个版本的时间再次被累积,这次超过 10,000 次运行。结果在代码中的 cmets 中表示为b2。该算法的二分查找版本现在比简单版本快两倍以上。
第二种方法在我的机器上选择超过 cca 75 个项目时开始变得更快(请注意,任何一种算法的复杂性都不取决于项目的数量,MAX)。
值得一提的是,上述算法是按升序生成随机数的。但是添加另一个数组会很简单,数字将按照生成的顺序保存到该数组中,然后将其返回(附加成本可以忽略不计O(n))。没有必要对输出进行洗牌:那会慢得多。
请注意,源代码是 C++,我的机器上没有 Java,但概念应该很清楚。
编辑:
为了娱乐,我还实现了生成包含所有索引的列表
0 .. MAX 的方法,随机选择它们并将它们从列表中删除以保证唯一性。由于我选择了相当高的MAX(5000),所以性能是灾难性的:
// b1: 519515.000 msec
// b2: 20312.000 msec
std::vector<int> 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<int> 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
完整源代码为here。