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


126

私は彼のJavaScriptコードで誰かを助けていて、私のようなものは次のようなセクションに捕らえられました。

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

私の最初のことでしたが、ちょっと、これはおそらく機能しません!しかし、私はいくつかの実験を行ったところ、確かに少なくともランダム化された結果が得られるように思えました。

それから私はいくつかのウェブ検索をしました、そして、ほとんどトップでこのコードが最も熱心にコピーされた記事を見つけました。かなり立派なサイトと著者のように見えました...

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

しかし、どう思いますか?

そして、別の質問として...このシャッフルテクニックの結果がどれだけランダムであるかをどのように測定しますか?

更新:私はいくつかの測定を行い、答えの1つとして以下の結果を投稿しました。


結果を符号の数だけ丸めるのは無意味であることに気づくだけです
ボルマット

2
うまくランダム化された結果が得られるようです。」- 本当に???
Bergi、2017年

回答:


109

あなたが言うようにそれ実装固有であるという理由もあって、これ私のお気に入りのシャッフル方法ではありませんでした。特に、Javaまたは.NET(どちらかわからない)からの標準ライブラリの並べ替えでは、一部の要素間で一貫性のない比較が行われた場合に検出されることが多いことを覚えているようです(たとえば、最初にとを宣言A < BB < C、次にC < A)。

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

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

これはO(n)であり、乱数発生器へのn-1呼び出しのみが必要です。また、本物のシャッフルを生成します-元の位置に関係なく、すべての要素は各スペースで1 / nの確率で終了します(妥当なRNGを想定)。並べ替えられたバージョンは、均等な分布に近似します(乱数ジェネレーターが同じ値を2回選択しないと仮定します。これは、ランダムなdoubleを返す可能性はほとんどありません)。しかし、シャッフルバージョンについての理由は簡単です:)

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

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

シャッフル上のWikipediaの記事あなたが避けるために何を知っているので、それの価値は、一般的にはシャッフルの貧困層の実装に関するセクションを読んで-ランダムプロジェクションをソートについて協議(特にシャッフルアルゴリズムセクション)。


5
Raymond Chenは、ソート比較関数がルールに従うという重要性について詳しく説明しています。blogs.msdn.com
Jason Kresowaty

1
私の推論が正しい場合、ソートされたバージョン「本物の」シャッフルを生成しません
クリストフ

@Christoph:考えてみれば、rand(x)がその範囲全体で正確に均一であることが保証されている場合に限り、Fisher-Yatesでも「完全な」分布が得られます。いくつかのxのRNGには通常2 ^ xの可能な状態があることを考えると、rand(3)の場合でも正確にそうなるとは思いません。
Jon Skeet、

@ジョン:しかし、Fisher-Yatesは2^x各配列インデックスの状態を作成します。つまり、合計2 ^(xn)の状態があり、これは2 ^ cよりかなり大きいはずです-詳細については編集した回答を参照してください
Christoph

@クリストフ:私は自分自身を正しく説明しなかったかもしれません。要素が3つだけあるとします。すべての3つから最初の要素をランダムに選択します。完全に均一な分布を得るには、[0,3)の範囲の乱数を完全に均一に選択できる必要があります。PRNGが2 ^ nの場合可能性のある状態、それを行うことはできません-1つまたは2つの可能性は、発生する可能性がわずかに高くなります。
ジョンスキート

118

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

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の回答へのコメントで、問題のアルゴリズムは(の実装に応じてsort())均等に分散された確率を生成しない可能性があると述べました。

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

Jonが指摘したsort()ように、乱数ジェネレーターは有限数の疑似乱数値をn!順列にマッピングするため、これだけでを使用するよりもFisher-Yatesを優先する理由にはなりません。しかし、Fisher-Yatesの結果はさらに優れているはずです。

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

フィッシャーイエイツを使用する場合、関連するパラメーターは配列のサイズです。これは2^52、実際的な制限のため、決して接近すべきではありません。

ランダム比較関数を使用してソートする場合、関数は基本的に戻り値が正か負かのみを考慮するため、これが問題になることはありません。しかし、似たようなものがあります。比較関数は適切に動作しているため、2^c可能な結果は、前述のように、ほぼ同じです。その場合c ~ n log n2^c ~ n^(a·n)where a = constは、並べ替えアルゴリズムが順列に均等にマッピングする場合でも、少なくとも2^c同じ大きさ(またはそれ未満)でn!あり、したがって不均一な分布を引き起こすことを可能にします。これが実際的な影響を与えるかどうかは私を超えています。

実際の問題は、並べ替えアルゴリズムが順列に均等にマッピングされることが保証されていないことです。Mergesortは対称的であることが簡単にわかるが、Bubblesortのようなもの、さらに重要なことにはQuicksortやHeapsortのようなものについての推論はそうではない。


sort()結論:Mergesort を使用している限り、コーナーケース(少なくとも私はそれがコーナーケースであることを望んでいます)を除いて、合理的に安全である必要2^c ≤ n!あります。そうでない場合は、すべてのベットがオフになります。


実装いただきありがとうございます。ものすごく速い!特に、その間私が自分で書いたその遅いがらくたと比較して。
Rene Saarsoo、2009年

1
underscore.jsライブラリーを使用している場合は、上記のフィッシャーイェーツのシャッフルメソッドで拡張する方法を次に示し
Steve

どうもありがとうございました。あなたとジョンズの回答を組み合わせることで、私と同僚が合わせて4時間近く費やしていた問題を解決できました。元々はOPと同様の方法がありましたが、ランダム化が非常に不安定であることがわかったため、メソッドを少し変更し、少しのjqueryで動作するように(スライダーの)画像のリストをごちゃまぜにして、いくつかを取得しました素晴らしいランダム化。
Hello World

16

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

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

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

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

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

しかし、パフォーマンスの面では、クリストフによって与えられたシャッフル機能が明らかに勝者でした。小さな4要素配列の場合でも、実際のシャッフルはランダムソートの約2倍の速度で実行されました。

//クリストフによって投稿されたシャッフル関数。
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;
    }

    配列を返す;
};

//ランダムソート関数
var rnd = function(){
  Math.round(Math.random())-0.5を返します。
};
var randSort = function(A){
  A.sort(rnd);を返します。
};

var permutations = function(A){
  if(A.length == 1){
    [A]を返す;
  }
  そうしないと {
    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.length; j ++){
        perms.push(x.concat(subperms [j]));
      }
    }
    パーマを返す;
  }
};

var test = function(A、iterations、func){
  //初期順列
  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 = [];
  (var i in stats){
    arr.push(i + "" + stats [i]);
  }
  return arr.join( "\ n")+ "\ n \ n所要時間:" +((end-start)/ 1000)+ "秒。";
};

alert( "ランダムソート:" + test([1,2,3,4]、100000、randSort));
alert( "shuffle:" + test([1,2,3,4]、100000、shuffle));

11

興味深いことに、Microsoftは、ランダムブラウザーの選択ページで同じ手法使用しました

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

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"));

なぜ0.5でなければならないのかわかりません-Math.random()、Math.random()だけではないのですか?
Alexander Mills

1
@AlexanderMills:に渡されるコンパレーター関数はsort()aおよびの比較に応じて、ゼロより大きい、小さい、またはゼロに等しい数を返すことになっていbます。(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
LarsH

@LarsHええ、それは理にかなっています
Alexander Mills

9

さまざまな方法を使用してシャッフルする他の人気のあるブラウザーに対する現在のブラウザーのバイアスを示す簡単なテストページをWebサイトに配置しまし。これは、バイアスをかけないMath.random()-0.5別の「ランダム」シャッフル、および上記のフィッシャーイェイツの方法を使用しただけの恐ろしいバイアスを示しています。

一部のブラウザーでは、「シャッフル」中に特定の要素がまったく変更されない可能性が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


5

配布にこだわりがなく、ソースコードを小さくしたい場合は問題ないと思います。

JavaScript(ソースが絶えず送信される)では、小さいと帯域幅コストに差が生じます。


2
物事は、あなたはあなたが思っているよりもほとんど常に配布についてより気を使います、そして「小さなコード」のためにarr = arr.map(function(n){return [Math.random(),n]}).sort().map(function(n){return n[1]});、それは常にあります。非常に圧縮されたKnuth / FYシャッフルバリアントもあります。
Daniel Martin

@DanielMartinそのワンライナーは答えになるはずです。また、解析エラーを回避するために、2つのセミコロンを追加する必要があるため、次のようになりますarr = arr.map(function(n){return [Math.random(),n];}).sort().map(function(n){return n[1];});
Giacomo1968 2015年

2

確かにハックです。実際には、無限ループアルゴリズムはありそうもありません。オブジェクトを並べ替える場合は、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

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)、上記の証明が成り立ちます。代わりに、コンパレータが以前の回答に基づいてオッズを変更した場合、公正な結果を生成できる可能性があります。与えられた分類アルゴリズムのためにそのようなコンパレータを見つけることは研究論文になるかもしれません。


1

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

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

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

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


0

これは、単一の配列を使用するアプローチです。

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

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

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

    実装には、かなりの数の要素をそのままにしておく高いリスクがあります。それらは、上にプッシュされた下位要素の量だけ配列全体でシフトされます。そのシャッフルに描かれたパターンが信頼できないものになっています。
    Kir Kanos 2014年

    @KirKanos、私はあなたのコメントを理解しているかどうかわかりません。私が提案するソリューションはO(n)です。それは間違いなくすべての要素に「触れる」でしょう。ここにデモンストレーションのフィドルがあります。
    ic3b3rg 2014年

    0

    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()を使用して配列をシャッフルしないでください。


    -3

    何も問題はありません。

    .sort()に渡す関数は通常、次のようになります。

    function sortingFunc(first、second)
    {
      //例:
      最初から2番目を返す。
    }
    

    sortingFuncでのあなたの仕事は以下を返すことです:

    • 最初が2番目の前になる場合は負の数
    • 最初が2番目に続く場合は正の数
    • 完全に等しい場合は0

    上記のソート関数は、物事を整理します。

    -と+をランダムに返すと、ランダムな順序になります。

    MySQLのように:

    SELECT *テーブルORDER BY rand()から
    

    5
    このアプローチに何か問題があります。JS実装で使用されているソートアルゴリズムによっては、確率が均等に分散されません。
    クリストフ

    それは私たちが実際に心配していることですか?
    ボボボボ2009年

    4
    @bobobobo:アプリケーションによっては、はい、ときどきそうします。また、正しく動作shuffle()するのは1回だけ記述する必要があるため、実際には問題ではありません。スニペットをコードボールトに配置し、必要なときにいつでも発掘できます
    Christoph
    弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
    Licensed under cc by-sa 3.0 with attribution required.