1.背景
筆記試験中に、アルゴリズムの問題に遭遇しました。それは、n個の異なる数から繰り返されないほぼランダムな数のmです。シャッフリングアルゴリズムは、元の配列を分割することです。これにより、特定の数の元の配列が、粉砕された配列の各位置に等しい確率で表示され、問題を解決できます。
2.シャッフルアルゴリズム
3つのシャッフリングアルゴリズムは、カードドロー、カードスワップ、およびカード挿入から派生します。そのうち、ドローとカードスワップは、それぞれフィッシャー-イェーツシャッフルとクヌース-ダーステンフェルドシュッフルアルゴリズムに対応します。
2.1フィッシャー-イェーツシャッフル
このシャッフリング方法を最初に提案したのは、ロナルドA.フィッシャーとフランクイェーツ、つまりフィッシャーイェーツシャッフルでした。基本的な考え方は、次のように、元の配列から新しい配列に以前に取得されたことのない番号をランダムに選択することです。
- 元の配列と新しい配列を初期化します。元の配列の長さはn(既知)です。
- 未処理の配列から(kが残っていると仮定して)、[0、k)の要素pをランダムに生成します(要素に0から始まる番号が付けられていると仮定します)
- 残りのk個の数字からp番目の数字を取ります
- 数がなくなるまで2と3を繰り返します
- ステップ3で取得したシーケンスは、スクランブルされたシーケンスです。
以下は、そのランダム性を証明します。つまり、新しい配列に配置された各要素のi番目の位置は1 / nです(配列のサイズがnであると仮定)。
証明:要素mがi番目の位置に配置される確率P =要素が最初のi-1位置で選択されたときにmが選択されない確率* mがi番目の位置で選択される確率、つまり、
たとえば、5つの数値がある場合シーケンスでは、各番号が最初の番号として取り出される確率は1/5、つまり1 / nです
。2番目に取り出された番号は新しい配列の2番目に配置され、各番号は2番目の番号として取得されます。取り出す確率は4/5 * 1/4 = 1/5、つまり1 / nです。
#define N 10
#define M 5
void Fisher_Yates_Shuffle(vector<int>& arr,vector<int>& res)
{
srand((unsigned)time(NULL));
int k;
for (int i=0;i<M;++i)
{
k = rand() % arr.size();
res.push_back(arr[k]);
arr.erase(arr.begin()+k);
}
}
時間の複雑さはO(n * n)であり、空間の複雑さはO(n)です。
2.2クヌース-ダーステンフェルドシャッフル
KnuthとDurstenfeldは、Fisher et al。に基づいてアルゴリズムを改善し、元の配列の数値と相互作用して、余分なO(n)スペースを節約しました。アルゴリズムの基本的な考え方はフィッシャーの考え方と似ています。未処理のデータからランダムに番号が取り出されるたびに、その番号は配列の最後に配置されます。つまり、処理された番号は配列の最後に保存されます。
アルゴリズムの手順は次のとおりです。
- 配列サイズがnの配列arrを作成し、それぞれ1からnまでの値を格納します。
- 0からn-1までのランダムな数xを生成します; //最後のビットも取得できます
- 最初のランダム番号である添え字xを付けて値を出力します
- arrの現在のテール要素をx番目の要素と交換します
- 2と同じように、0からn-2までのランダムな数xを生成します
- 出力添え字は、2番目のランダム番号であるxの値です。
- arrの最後から2番目の要素を、添え字がxである要素と交換します;
…
上記のように、m個の数値が出力されるまで
このアルゴリズムは、古典的なシャッフリングアルゴリズムです。その証明は次のとおりです
。arr[i]の場合、シャッフル後にn-1番目の位置にある確率は1 / n(最初の交換のランダム数はi)であり
、n-2の位置にある確率は[(n- 1)/ n] * [1 /(n-1)] = 1 / n、(最初に交換されるランダム番号はiではなく、2回目はarr [i]の場所です(i = nの場合は注意してください) -1、最初の交換arr [n-1]はランダムな位置に変更されます))
nk番目の位置にある確率は[(n-1)/ n] * [(n-2)/(n- 1)] … [(n-k + 1)/(n-k + 2)] * [1 /(n-k + 1)] = 1 / n
(最初のランダム数はiであってはならず、2番目はiであってはなりませんarr [i]の場所ではありません(交換によって変更される場合があります)...... nk番目の時間はarr [i]の場所です)。
void Knuth_Durstenfeld_Shuffle(vector<int>& arr){
for(int i = arr.size() - 1; i >= 0; i--){
srand(unsigned)time(NULL);
swap(arr[i], arr[rand() % (i + 1)];
}
}
グラフィカルな証明
時間の複雑さはO(n)で、空間の複雑さはO(1)です。欠点は配列nの長さを知っている必要があります。
元の配列が変更されています。これは順序を入れ替えるアルゴリズムです。アルゴリズムの時間の複雑さもフィッシャーアルゴリズムによるものです。 O(n2)はO(n)にプロモートされます。後ろから前にスキャンするため、長さがわからないアレイや動的に大きくなるアレイを処理することはできません。