シーケンスのすべてのインデックスを生成することは、特に選択する数値の比率MAX
が低い場合(複雑さがによって支配される場合)、時間がかかる可能性があるため、一般的には悪い考えO(MAX)
です。選択する数の比率がMAX
1に近づくと、これはさらに悪化します。選択したインデックスをすべてのシーケンスから削除することもコストがかかるためです(アプローチしますO(MAX^2/2)
)。ただし、少数の場合、これは一般にうまく機能し、特にエラーが発生しやすいわけではありません。
コレクションを使用して生成されたインデックスをフィルタリングすることも悪い考えです。シーケンスにインデックスを挿入するのに時間がかかり、同じ乱数を数回描画できるため、進行が保証されません(ただし、十分MAX
に大きい場合はほとんどありません)。 )。これは複雑さ
O(k n log^2(n)/2)
に近い可能性があり、重複を無視し、コレクションが効率的なルックアップのためにツリーを使用すると仮定します(ただしk
、ツリーノードの割り当てにかなりの一定のコストがかかり、場合によってはリバランスが必要になります))。
もう1つのオプションは、最初からランダムな値を一意に生成して、進行が確実に行われるようにすることです。つまり、最初のラウンドで、ランダムなインデックス[0, MAX]
が生成されます。
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
2番目のラウンドで[0, MAX - 1]
は、(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)
次に、インデックスの値を調整する必要があります。2番目のインデックスがシーケンスの後半(最初のインデックスの後)にある場合は、ギャップを考慮してインクリメントする必要があります。これをループとして実装し、任意の数の一意のアイテムを選択できるようにします。
短いシーケンスの場合、これは非常に高速な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]
。オリジナルの2つの要素間の最小距離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());
}
}
私はこれを3つのベンチマークでテストしました。最初に、7つのアイテムから3つの数値が選択され、選択されたアイテムのヒストグラムが10,000回の実行にわたって累積されました。
4265 4229 4351 4267 4267 4364 4257
これは、7つの項目のそれぞれがほぼ同じ回数選択されたことを示しており、アルゴリズムによる明らかなバイアスはありません。すべてのシーケンスは、正確性(内容の一意性)についてもチェックされました。
2番目のベンチマークでは、5000項目から7つの数字を選択しました。アルゴリズムのいくつかのバージョンの時間は、10,000,000回の実行にわたって累積されました。結果は、コード内のコメントにとして示されますb1
。アルゴリズムの単純なバージョンはわずかに高速です。
3番目のベンチマークでは、5000個のアイテムから700個の数字を選択しました。アルゴリズムのいくつかのバージョンの時間が再び累積され、今回は10,000回を超えました。結果は、コード内のコメントにとして示されますb2
。アルゴリズムの二分探索バージョンは、単純なものより2倍以上高速になりました。
2番目の方法は、私のマシンで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
で2番目になり、バイナリ検索を使用したアプローチよりも約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());
完全なソースコードはこちらです。