web-dev-qa-db-ja.com

シャッフルにJavaScript Array.sort()メソッドを使用するのは正しいですか?

私は彼のJavaScriptコードで誰かを助けていましたが、次のようなセクションに目が留まりました。

function randOrd(){
  return (Math.round(Math.random())-0.5);
}
coords.sort(randOrd);
alert(coords);

私の最初のことは:ちょっと、これはうまくいかないかもしれない!しかし、その後、私はいくつかの実験を行い、それが実際に少なくともうまくうまくランダム化された結果を提供するようであることがわかりました。

その後、私はいくつかのウェブ検索を行い、ほぼ最上部で article を見つけました。このコードから、このコードが最もよくコピーされました。かなり立派なサイトと著者のように見えました...

しかし、私の直感は、これは間違っているに違いないと私に告げています。特に、ソートアルゴリズムはECMA標準で指定されていないためです。ソートのアルゴリズムが異なると、不均一なシャッフルが異なると思います。いくつかのソートアルゴリズムはおそらく無限ループすることさえあります...

しかし、あなたはどう思いますか?

そして別の質問として...このシャッフル技術の結果がどれほどランダムであるかを今どのように測定しますか?

pdate:いくつかの測定を行い、回答の1つとして以下の結果を投稿しました。

124
Rene Saarsoo

あなたが言うように実装固有であるため、それは私のシャッフルのお気に入りの方法ではありませんでした。特に、Javaまたは.NET(どちらが不明か)からの標準ライブラリのソートでは、いくつかの要素間で一貫性のない比較が行われるかどうかをしばしば検出できることを覚えているようです。請求 A < BおよびB < C、 しかしその後 C < A)。

また、実際に必要なものよりも(実行時間の点で)複雑なシャッフルになります。

コレクションを「シャッフル」(コレクションの開始時、最初は空)と「アンシャッフル」(コレクションの残り)に効果的に分割するシャッフルアルゴリズムを好みます。アルゴリズムの各ステップで、ランダムなシャッフルされていない要素(最初の要素である可能性があります)を選択し、最初のシャッフルされていない要素と交換します-それをシャッフルされたものとして扱います(つまり、パーティションを精神的に動かしてそれを含めます)。

これはO(n)であり、Niceである乱数ジェネレーターへのn-1回の呼び出しのみを必要とします。真のシャッフルも生成します。元の位置に関係なく、各スペースで(合理的なRNGを想定)ソートされたバージョンは、偶数分布に近似します(乱数ジェネレーターが'同じ値を2回選択しないでください。ランダムなdoubleを返す場合はほとんどありませんが)シャッフルバージョンについては簡単に推論できます:)

このアプローチは Fisher-Yates shuffle と呼ばれます。

このシャッフルを一度コーディングし、アイテムをシャッフルする必要があるすべての場所で再利用するのがベストプラクティスだと思います。そうすれば、信頼性や複雑さの点でソートの実装について心配する必要はありません。それはほんの数行のコードです(JavaScriptでは試しません!)

Wikipediaのシャッフルに関する記事 (特にシャッフルアルゴリズムのセクション)では、ランダムな投影の並べ替えについて説明しています。シャッフルの一般的な実装の貧弱なセクションを読む価値があります。

108
Jon Skeet

ジョンがすでに 理論をカバー にした後、ここに実装があります:

_function shuffle(array) {
    var tmp, current, top = array.length;

    if(top) while(--top) {
        current = Math.floor(Math.random() * (top + 1));
        tmp = array[current];
        array[current] = array[top];
        array[top] = tmp;
    }

    return array;
}
_

アルゴリズムはO(n)ですが、ソートはO(n log n)でなければなりません。ネイティブsort()関数と比較してJSコードを実行するオーバーヘッドに応じて、これは パフォーマンスの顕著な差 につながる可能性があり、これは配列サイズとともに増加するはずです。


bobobobo's answer へのコメントで、問題のアルゴリズムは(sort()の実装に応じて)均等に分散された確率を生成しない可能性があると述べました。

私の議論はこれらの行に沿っています:ソートアルゴリズムには、特定の数のc比較が必要です。たとえば、Bubblesortの場合はc = n(n-1)/2です。ランダム比較関数により、各比較の結果が等しくなる可能性が高くなります。つまり、_2^c_等しい確率の結果があります。ここで、各結果は、配列のエントリの_n!_順列の1つに対応する必要があります。これにより、一般的なケースでは均等な分散が不可能になります。 (必要な実際の比較の数は入力配列に依存するため、これは単純化ですが、アサーションは保持されるはずです。)

ジョンが指摘したように、乱数ジェネレーターは有限数の擬似乱数値を_n!_順列にマップするため、これだけではsort()を使用するよりもFisher-Yatesを好む理由にはなりません。しかし、Fisher-Yatesの結果は依然として優れているはずです。

Math.random()は、範囲_[0;1[_の擬似乱数を生成します。 JSは倍精度浮動小数点値を使用するため、これは_2^x_の可能な値に対応します。ここで_52 ≤ x ≤ 63_(実際の数を見つけるのが面倒です)。 Math.random()を使用して生成された確率分布は、アトミックイベントの数が同じ桁の場合、正常に動作しなくなります。

Fisher-Yatesを使用する場合、関連するパラメーターは配列のサイズです。実際の制限により、_2^52_に近づくことはありません。

ランダム比較関数でソートする場合、関数は基本的に戻り値が正または負であるかどうかのみを考慮するため、これは問題になりません。しかし、類似したものがあります:比較関数は行儀が良いため、_2^c_の可能性のある結果は、述べたように、同様に可能性があります。 _c ~ n log n_の場合2^c ~ n^(a·n)ここで_a = const_で、少なくとも_2^c_が_n!_と同じ(またはそれよりも小さい)ことを可能にします。したがって、並べ替えアルゴリズムが順列に均等にマッピングされる場合でも、不均一な分布につながります。これに実用的な影響があるかどうかは私を超えています。

本当の問題は、並べ替えアルゴリズムが順列に均等にマッピングされるとは限らないことです。 Mergesortが対称的であるのは容易にわかりますが、Bubblesortや、さらに重要なのはQuicksortやHeapsortのようなものについての推論はそうではありません。


一番下の行:sort()がMergesortを使用している限り、shouldは、コーナーケースを除いて合理的に安全です(少なくとも私は_2^c ≤ n!_がコーナーケースであることを期待して)、そうでない場合、すべてのベットがオフになります。

116
Christoph

このランダムな並べ替えの結果がどれほどランダムであるかを測定しました...

私のテクニックは、小さな配列[1,2,3,4]を取り、そのすべて(4!= 24)の順列を作成することでした。次に、配列にシャッフル関数を何度も適用し、各順列が生成される回数をカウントします。良いシャッフルアルゴリズムは、すべての順列にわたって結果を非常に均等に分配しますが、悪いアルゴリズムはその均一な結果を作成しません。

以下のコードを使用して、Firefox、Opera、Chrome、IE6/7/8でテストしました。

驚いたことに、ランダムソートと実際のシャッフルの両方で、均等に均一な分布が作成されました。 (多くの人が示唆しているように)メインブラウザはマージソートを使用しているようです。もちろん、これはブラウザが存在しないことを意味するわけではなく、異なることをしますが、このランダムソートメソッドは実際に使用するのに十分な信頼性があることを意味します。

編集:このテストは、実際にランダム性またはその欠如を正しく測定しませんでした。私が投稿した他の回答を参照してください。

ただし、パフォーマンスの面では、Cristophが提供するシャッフル機能が明確な勝者でした。小さい4要素配列の場合でも、実際のシャッフルはランダムソートの約2倍の速さで実行されました。

 // Cristoph。
 var shuffle = function(array){
 var tmp、current、top = array.length; 
 [。によって投稿されたシャッフル関数。 ____。] if(top)while(-top){
 current = Math.floor(Math.random()*(top + 1)); 
 tmp = array [current]; 
 array [current] = array [top]; 
 array [top] = tmp; 
} 
 
 return array; 
}; 
 
 //ランダムソート関数
 var rnd = function(){
 return Math.round(Math.random())-0.5; 
}; 
 var randSort = function(A){
 return A.sort(rnd); 
}; 
 
 var permutations = function(A){
 if(A.length == 1){
 return [A]; 
} 
 else {
 var perms = []; 
 for(var i = 0; i <A.length; i ++){
 var x = A.slice(i、i + 1); 
 var xs = A.slice(0、i).concat(A.slice(i + 1)); 
 var subperms = permutations(xs); 
 for(var j = 0 ; j <subperms。長さ; j ++){
 perms.Push(x.concat(subperms [j])); 
} 
} 
 return perms; 
} 
}; 
 
 var test = function(A、iterations、func){
 // init permutations 
 var stats = {}; 
 var perms = permutations(A); 
 for(var i in perms){
 stats ["" + perms [i]] = 0; 
} 
 
 //何度もシャッフルして統計を収集します
 var start = new Date(); 
 for(var i = 0; i <iterations; i ++){
 var shuffled = func(A); 
 stats ["" + shuffled] ++; 
} 
 var end = new Date(); 
 
 //フォーマット結果
 var arr = []; 
 for(stats in var i){
 arr.Push(i + "" + stats [i] ); 
} 
 return arr.join( "\ n")+ "\ n\n所要時間:" +((end-start)/ 1000)+ "seconds。"; 
}; 
 
 alert( "random sort:" + test([1,2,3,4]、100000、randSort)); 
 alert( "shuffle : "+ test([1,2,3,4]、100000、shuffle)); 
16
Rene Saarsoo

興味深いことに、Microsoftはpick-random-browser-pageで同じ手法を使用しました。

彼らはわずかに異なる比較関数を使用しました:

function RandomSort(a,b) {
    return (0.5 - Math.random());
}

私にはほとんど同じように見えますが、ランダムではないことが判明しました...

そのため、リンクされた記事で使用されたのと同じ方法で再度テストランを行ったところ、実際に-ランダムなソート方法が欠陥のある結果を生み出したことが判明しました。ここに新しいテストコード:

function shuffle(arr) {
  arr.sort(function(a,b) {
    return (0.5 - Math.random());
  });
}

function shuffle2(arr) {
  arr.sort(function(a,b) {
    return (Math.round(Math.random())-0.5);
  });
}

function shuffle3(array) {
  var tmp, current, top = array.length;

  if(top) while(--top) {
    current = Math.floor(Math.random() * (top + 1));
    tmp = array[current];
    array[current] = array[top];
    array[top] = tmp;
  }

  return array;
}

var counts = [
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0],
  [0,0,0,0,0]
];

var arr;
for (var i=0; i<100000; i++) {
  arr = [0,1,2,3,4];
  shuffle3(arr);
  arr.forEach(function(x, i){ counts[x][i]++;});
}

alert(counts.map(function(a){return a.join(", ");}).join("\n"));
11
Rene Saarsoo

私のウェブサイトに 簡単なテストページ を配置しました。現在のブラウザと他の一般的なブラウザとでは異なる方法でシャッフルするバイアスを示しています。 Math.random()-0.5、バイアスされていない別の「ランダム」シャッフル、および上記のFisher-Yatesメソッドを使用するだけのひどいバイアスを示しています。

一部のブラウザでは、特定の要素が「シャッフル」中にまったく場所を変更しない可能性が50%ほどあることがわかります。

注:@ChristophによるFisher-Yatesシャッフルの実装は、コードを次のように変更することで、Safariでわずかに高速化できます。

function shuffle(array) {
  for (var tmp, cur, top=array.length; top--;){
    cur = (Math.random() * (top + 1)) << 0;
    tmp = array[cur]; array[cur] = array[top]; array[top] = tmp;
  }
  return array;
}

テスト結果: http://jsperf.com/optimized-fisher-yates

9
Phrogz

ディストリビューションにこだわりがなく、ソースコードを小さくしたい場合には問題ないと思います。

JavaScript(ソースが絶えず送信される)では、小さな値が帯域幅コストに違いをもたらします。

5
Nosredna

確かにハックです。実際には、無限ループのアルゴリズムはほとんどありません。オブジェクトをソートしている場合、coords配列をループして、次のようなことを行うことができます。

for (var i = 0; i < coords.length; i++)
    coords[i].sortValue = Math.random();

coords.sort(useSortValue)

function useSortValue(a, b)
{
  return a.sortValue - b.sortValue;
}

(そして、再度ループしてsortValueを削除します)

それでもハック。あなたがそれをうまくやりたいなら、あなたは難しい方法でそれをしなければなりません:)

2
Thorarin

4年が経ちましたが、どのソートアルゴリズムを使用しても、ランダムコンパレーター方式は正しく分散されないことを指摘したいと思います。

証明:

  1. n要素の配列の場合、n!順列(シャッフルの可能性)。
  2. シャッフル中のすべての比較は、2組の順列から選択できます。ランダムコンパレータの場合、各セットを選択する可能性は1/2です。
  3. したがって、各順列pについて、順列pで終わる可能性は、分母2 ^ k(一部のk)の分数です(分数の合計(1/8 + 1/16 = 3/16など)であるため) )。
  4. N = 3の場合、6つの同等に近い順列があります。したがって、各順列の可能性は1/6です。 1/6は、分母として2のべき乗を持つ分数として表現できません。
  5. したがって、コインフリップソートでは、シャッフルが公平に分散されることはありません。

おそらく正しく分散できる唯一のサイズは、n = 0,1,2です。


演習として、n = 3の異なるソートアルゴリズムの決定木を描画してみてください。


証明にはギャップがあります:ソートアルゴリズムがコンパレーターの一貫性に依存し、一貫性のないコンパレーターで無制限のランタイムを持っている場合、確率の無限の合計を持つことができます。合計のすべての分母は2の累乗です。1を見つけてください。

また、コンパレータがどちらかの答えを与える可能性が固定されている場合(例:(Math.random() < P)*2 - 1、定数P)の場合、上記の証明が成り立ちます。代わりに、コンパレータが以前の回答に基づいてオッズを変更する場合、公正な結果を生成できる可能性があります。特定のソートアルゴリズムに対応するこのようなコンパレータを見つけることは、研究論文になる可能性があります。

2
leewz

D3を使用している場合、組み込みのシャッフル機能があります(Fisher-Yatesを使用)。

var days = ['Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi','Dimanche'];
d3.shuffle(days);

そして、ここでマイクはそれについて詳しく説明します:

http://bost.ocks.org/mike/shuffle/

1
Renaud

Array.sort() 関数を使用して配列をシャッフルできますか?はい。

結果は十分にランダムですか?いいえ。

次のコードスニペットを検討してください。

var array = ["a", "b", "c", "d", "e"];
var stats = {};
array.forEach(function(v) {
  stats[v] = Array(array.length).fill(0);
});
//stats = {
//    a: [0, 0, 0, ...]
//    b: [0, 0, 0, ...]
//    c: [0, 0, 0, ...]
//    ...
//    ...
//}
var i, clone;
for (i = 0; i < 100; i++) {
  clone = array.slice(0);
  clone.sort(function() {
    return Math.random() - 0.5;
  });
  clone.forEach(function(v, i) {
    stats[v][i]++;
  });
}

Object.keys(stats).forEach(function(v, i) {
  console.log(v + ": [" + stats[v].join(", ") + "]");
})

サンプル出力:

a [29, 38, 20,  6,  7]
b [29, 33, 22, 11,  5]
c [17, 14, 32, 17, 20]
d [16,  9, 17, 35, 23]
e [ 9,  6,  9, 31, 45]

理想的には、カウントは均等に分散される必要があります(上記の例では、すべてのカウントは約20である必要があります)。しかし、そうではありません。どうやら、分布はブラウザによって実装されている並べ替えアルゴリズムと、並べ替えのために配列アイテムをどのように反復するかによって異なります。

この記事では、さらなる洞察を提供します。
Array.sort()を使用して配列をシャッフルしないでください

0
Salman A

単一の配列を使用するアプローチは次のとおりです。

基本的なロジックは次のとおりです。

  • N要素の配列で始まる
  • 配列からランダムな要素を削除し、配列にプッシュします
  • 配列の最初のn-1個の要素からランダムな要素を削除し、配列にプッシュします
  • 配列の最初のn-2個の要素からランダムな要素を削除し、配列にプッシュします
  • ...
  • 配列の最初の要素を削除し、配列にプッシュします
  • コード:

    for(i=a.length;i--;) a.Push(a.splice(Math.floor(Math.random() * (i + 1)),1)[0]);
    
    0
    ic3b3rg