生成序列的所有索引通常不是一个好主意,因为这可能会花费很多时间,尤其是在要选择的数字与数字的比率MAX
很低的情况下(复杂度由决定O(MAX)
)。如果要选择的数字与MAX
一个数字的比率接近一个,这将变得更糟,因为从所有序列中删除选择的索引也变得很昂贵(我们接近O(MAX^2/2)
)。但是对于少量数字,此方法通常效果很好,并且不太容易出错。
使用集合过滤生成的索引也是一个坏主意,因为花费一些时间将索引插入序列中,并且由于可以绘制相同的随机数次而不能保证进度(但是对于足够大的情况,MAX
这不太可能)。这可能接近复杂性
O(k n log^2(n)/2)
,忽略重复项并假设该集合使用树进行有效查找(但k
分配树节点的成本相当高,并且可能不得不重新平衡)。
另一种选择是从一开始就唯一地生成随机值,以确保取得进展。这意味着在第一轮中,将[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();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
size_t n_where = i;
for(size_t j = 0; j < i; ++ j) {
if(n + j < rand_num[j]) {
n_where = j;
break;
}
}
rand_num.insert(rand_num.begin() + n_where, 1, n + n_where);
}
}
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的差异)。同时,如果我们j
从所有元素中减去索引
rand_num[j]
,则索引之间的差异恰好为1。因此,在“最差”的情况下,我们得到一个恒定的序列-但从不减少。因此可以使用二进制搜索,产生O(n log(n))
算法:
struct TNeedle {
int n;
TNeedle(int _n)
:n(_n)
{}
};
class CCompareWithOffset {
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;
return r_value < n.n + n_index;
}
bool operator ()(TNeedle n, const int &r_value) const
{
size_t n_index = &r_value - &*m_p_begin_it;
return n.n + n_index < r_value;
}
};
最后:
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();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
std::vector<int>::iterator p_where_it = std::upper_bound(rand_num.begin(), rand_num.end(),
TNeedle(n), CCompareWithOffset(rand_num.begin()));
rand_num.insert(p_where_it, 1, n + p_where_it - rand_num.begin());
}
}
我已经在三个基准测试中对此进行了测试。首先,从7个项目中选择3个数字,并在10,000次运行中累积所选项目的直方图:
4265 4229 4351 4267 4267 4364 4257
这表明,这7个项目中的每一个都大约选择了相同的次数,并且没有明显的由算法引起的偏差。还检查所有序列的正确性(内容唯一性)。
第二个基准测试涉及从5000个项目中选择7个数字。该算法的多个版本的时间累计超过10,000,000次。结果在代码的注释中表示为b1
。该算法的简单版本稍快一些。
第三个基准测试涉及从5000个项目中选择700个数字。再次累积了该算法的多个版本的时间,这次运行了10,000多次。结果在代码的注释中表示为b2
。现在,该算法的二进制搜索版本比简单的算法快两倍以上。
第二种方法开始在我的机器上选择超过cca 75个项目时变得更快(请注意,这两种算法的复杂性都不取决于项目的数量MAX
)。
值得一提的是,上述算法以升序生成随机数。但是,添加另一个数组将按其生成的顺序将数字保存起来很简单,然后将其返回(可以忽略不计的额外费用O(n)
)。不必对输出进行混洗:那会慢得多。
请注意,源代码是C ++,我的计算机上没有Java,但是概念应该很清楚。
编辑:
出于娱乐目的,我还实现了一种生成带有所有索引的列表,
0 .. MAX
随机选择它们并将其从列表中删除以确保唯一性的方法。由于我选择了很高的值MAX
(5000),因此性能是灾难性的:
std::vector<int> all_numbers(n_item_num);
std::iota(all_numbers.begin(), all_numbers.end(), 0);
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);
rand_num.push_back(all_numbers[n]);
all_numbers.erase(all_numbers.begin() + n);
}
我还用set
(一个C ++集合)实现了该方法,该方法实际上在基准测试中排名第二b2
,仅比使用二进制搜索的方法慢约50%。这是可以理解的,因为set
使用了二叉树,其插入成本类似于二叉搜索。唯一的不同是获得重复项目的机会,这会减慢进度。
std::set<int> numbers;
while(numbers.size() < n_number_num)
numbers.insert(n_Rand(n_item_num - 1));
rand_num.resize(numbers.size());
std::copy(numbers.begin(), numbers.end(), rand_num.begin());
完整的源代码在这里。