web-dev-qa-db-fra.com

Est-il correct d'utiliser la méthode JavaScript Array.sort () pour le mélange?

J'aidais quelqu'un avec son code JavaScript et mes yeux ont été capturés par une section qui ressemblait à ça:

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

Ma première était cependant: hé, cela ne peut pas fonctionner ! Mais ensuite j'ai fait quelques expérimentations et j'ai trouvé qu'en effet, au moins, il semble fournir des résultats bien randomisés.

Ensuite, j'ai fait une recherche sur le Web et presque en haut j'ai trouvé un article à partir duquel ce code a été le plus copieusement copié. Ressemblait à un site et à un auteur assez respectable ...

Mais mon instinct me dit que cela doit être faux. D'autant plus que l'algorithme de tri n'est pas spécifié par la norme ECMA. Je pense que différents algorithmes de tri entraîneront des brassages non uniformes différents. Certains algorithmes de tri peuvent même boucler à l'infini ...

Mais que pensez-vous?

Et comme autre question ... comment pourrais-je maintenant mesurer le degré aléatoire des résultats de cette technique de mélange?

mise à jour: J'ai fait quelques mesures et publié les résultats ci-dessous comme l'une des réponses.

124
Rene Saarsoo

Cela n'a jamais été ma façon préférée de mélanger, en partie parce que est spécifique à l'implémentation comme vous le dites. En particulier, je semble me souvenir que le tri de bibliothèque standard à partir de Java ou .NET (je ne sais pas lequel)) peut souvent détecter si vous vous retrouvez avec une comparaison incohérente entre certains éléments (par exemple, vous commencez par prétendre A < B et B < C, mais alors C < A).

Il se transforme également en un mélange plus complexe (en termes de temps d'exécution) que ce dont vous avez vraiment besoin.

Je préfère l'algorithme de lecture aléatoire qui partitionne efficacement la collection en "mélangé" (au début de la collection, initialement vide) et "non mélangé" (le reste de la collection). À chaque étape de l'algorithme, choisissez un élément aléatoire non mélangé (qui pourrait être le premier) et échangez-le avec le premier élément non mélangé - puis traitez-le comme mélangé (c'est-à-dire déplacez mentalement la partition pour l'inclure).

Ceci est O(n) et ne nécessite que n-1 appels au générateur de nombres aléatoires, qui est Nice. Il produit également un véritable shuffle - tout élément a 1/n chance de finir dans chaque espace, quelle que soit sa position d'origine (en supposant un RNG raisonnable). La version triée se rapproche d'une distribution paire (en supposant que le générateur de nombres aléatoires ne fait pas ne choisissez pas deux fois la même valeur, ce qui est très improbable s'il renvoie des doubles aléatoires) mais je trouve plus facile de raisonner sur la version aléatoire :)

Cette approche est appelée shuffle de Fisher-Yates .

Je considérerais comme la meilleure pratique de coder ce mélange une fois et de le réutiliser partout où vous devez mélanger des éléments. Ensuite, vous n'avez pas à vous soucier des implémentations de tri en termes de fiabilité ou de complexité. Ce ne sont que quelques lignes de code (que je n'essaierai pas en JavaScript!)

Le article Wikipedia sur le shuffling (et en particulier la section des algorithmes de shuffle) parle de trier une projection aléatoire - cela vaut la peine de lire la section sur les mauvaises implémentations du shuffling en général, donc vous savez quoi éviter.

108
Jon Skeet

Après que Jon a déjà couvert la théorie , voici une implémentation:

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;
}

L'algorithme est O(n), tandis que le tri doit être O(n log n). En fonction de la surcharge d'exécution du code JS par rapport à la fonction native sort(), cela peut conduire à une différence notable de performances qui devrait augmenter avec la taille des tableaux.


Dans les commentaires à réponse de bobobobo , j'ai déclaré que l'algorithme en question pourrait ne pas produire de probabilités uniformément réparties (selon l'implémentation de sort()).

Mon argument va dans ce sens: Un algorithme de tri nécessite un certain nombre c de comparaisons, par exemple c = n(n-1)/2 pour Bubblesort. Notre fonction de comparaison aléatoire rend le résultat de chaque comparaison également probable, c'est-à-dire qu'il y a 2^c résultats tout aussi probables . Maintenant, chaque résultat doit correspondre à l'une des permutations n! Des entrées du tableau, ce qui rend une distribution uniforme impossible dans le cas général. (Il s'agit d'une simplification, car le nombre réel de comparaisons nécessaires dépend du tableau d'entrée, mais l'assertion doit toujours être vérifiée.)

Comme Jon l'a souligné, ce n'est pas une raison en soi pour préférer Fisher-Yates à l'utilisation de sort(), car le générateur de nombres aléatoires mappera également un nombre fini de valeurs pseudo-aléatoires aux permutations n! . Mais les résultats de Fisher-Yates devraient encore être meilleurs:

Math.random() produit un nombre pseudo-aléatoire dans la plage [0;1[. Comme JS utilise des valeurs à virgule flottante double précision, cela correspond aux valeurs possibles de 2^x52 ≤ x ≤ 63 (Je suis trop paresseux pour trouver le nombre réel). Une distribution de probabilité générée à l'aide de Math.random() cessera de bien se comporter si le nombre d'événements atomiques est du même ordre de grandeur.

Lorsque vous utilisez Fisher-Yates, le paramètre pertinent est la taille du tableau, qui ne devrait jamais approcher 2^52 En raison de limitations pratiques.

Lors du tri avec une fonction de comparaison aléatoire, la fonction ne se soucie que si la valeur de retour est positive ou négative, ce ne sera donc jamais un problème. Mais il y en a une similaire: Parce que la fonction de comparaison se comporte bien, les résultats possibles de 2^c Sont, comme indiqué, tout aussi probables. Si c ~ n log n Alors 2^c ~ n^(a·n)a = const, Ce qui permet au moins que 2^c Soit de même ampleur que (ou même moins que) n! Et conduisant ainsi à une distribution inégale, même si l'algorithme de tri permet de mapper uniformément sur les permutations. Si cela a un impact pratique, cela me dépasse.

Le vrai problème est que les algorithmes de tri ne sont pas garantis pour correspondre uniformément aux permutations. Il est facile de voir que Mergesort fait comme il est symétrique, mais le raisonnement sur quelque chose comme Bubblesort ou, plus important encore, Quicksort ou Heapsort, ne l'est pas.


L'essentiel: tant que sort() utilise Mergesort, vous devriez être raisonnablement sûr sauf dans les cas d'angle (au moins je suis en espérant que 2^c ≤ n! est un cas de coin), sinon, tous les paris sont désactivés.

116
Christoph

J'ai fait quelques mesures sur la façon dont les résultats de ce type aléatoire sont aléatoires ...

Ma technique consistait à prendre un petit tableau [1,2,3,4] et à en créer toutes les permutations (4! = 24). Ensuite, j'appliquerais la fonction de brassage au tableau un grand nombre de fois et je compterais combien de fois chaque permutation est générée. Un bon algorithme de brassage répartirait les résultats de manière assez uniforme sur toutes les permutations, tandis qu'un mauvais ne créerait pas ce résultat uniforme.

En utilisant le code ci-dessous, j'ai testé dans Firefox, Opera, Chrome, IE6/7/8.

Étonnamment pour moi, le tri aléatoire et le véritable mélange ont tous deux créé des distributions également uniformes. Il semble donc que (comme beaucoup l'ont suggéré) les principaux navigateurs utilisent le tri par fusion. Bien sûr, cela ne signifie pas qu'il ne peut pas y avoir de navigateur, cela fait différemment, mais je dirais que cela signifie que cette méthode de tri aléatoire est suffisamment fiable pour être utilisée dans la pratique.

EDIT: Ce test n'a pas vraiment mesuré correctement le caractère aléatoire ou l'absence de celui-ci. Voir l'autre réponse que j'ai postée.

Mais du côté de la performance, la fonction de lecture aléatoire donnée par Cristoph a été un gagnant clair. Même pour les petits tableaux à quatre éléments, le vrai mélange a été exécuté environ deux fois plus vite que le tri aléatoire!

 // La fonction shuffle publiée par 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; 
}; 
 
 // la fonction de tri aléatoire 
 var rnd = function () {
 return Math.round (Math.random ()) - 0,5; 
}; 
 var randSort = fonction (A) {
 renvoie A.sort (rnd); 
}; 
 
 var permutations = fonction (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 <sous-conditions. longueur; j ++) {
 perms.Push (x.concat (subperms [j])); 
} 
} 
 return perms; 
} 
}; 
 
 var test = fonction (A, itérations, func) {
 // init permutations 
 var stats = {}; 
 var perms = permutations (A); 
 for (var i in perms) {
 stats ["" + perms [i]] = 0; 
} 
 
 // mélanger plusieurs fois et rassembler les statistiques 
 var start = new Date (); 
 for (var i = 0; i <iterations; i ++) {
 var shuffled = func (A); 
 stats ["" + shuffled] ++; 
} 
 var end = new Date (); 
 
 // formatage du résultat 
 var arr = []; 
 for (var i in stats) {
 arr.Push (i + "" + stats [i] ); 
} 
 return arr.join ("\ n") + "\ n\nTemps pris:" + ((fin - début)/1000) + "secondes."; 
}; 
 
 alert ("tri aléatoire:" + test ([1,2,3,4], 100000, randSort)); 
 alert ("shuffle : "+ test ([1,2,3,4], 100000, shuffle)); 
16
Rene Saarsoo

Fait intéressant, Microsoft a utilisé la même technique dans sa page de sélection de navigateur aléatoire.

Ils ont utilisé une fonction de comparaison légèrement différente:

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

Ça me ressemble presque, mais il s'est avéré que ce n'était pas si aléatoire ...

J'ai donc fait à nouveau quelques tests avec la même méthodologie utilisée dans l'article lié, et en effet - il s'est avéré que la méthode de tri aléatoire a produit des résultats erronés. Nouveau code de test ici:

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

J'ai placé ne simple page de test sur mon site Web montrant le biais de votre navigateur actuel par rapport à d'autres navigateurs populaires utilisant différentes méthodes pour mélanger. Il montre le terrible biais de l'utilisation de Math.random()-0.5, un autre mélange aléatoire qui n'est pas biaisé, et la méthode Fisher-Yates mentionnée ci-dessus.

Vous pouvez voir que sur certains navigateurs, il y a jusqu'à 50% de chances que certains éléments ne changent pas de place du tout pendant le `` shuffle ''!

Remarque: vous pouvez rendre l'implémentation du shuffle Fisher-Yates par @Christoph légèrement plus rapide pour Safari en changeant le code en:

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;
}

Résultats des tests: http://jsperf.com/optimized-fisher-yates

9
Phrogz

Je pense que c'est bien pour les cas où vous n'êtes pas pointilleux sur la distribution et que vous voulez que le code source soit petit.

En JavaScript (où la source est transmise en permanence), petit fait une différence dans les coûts de bande passante.

5
Nosredna

C'est un hack, certainement. En pratique, un algorithme en boucle infinie est peu probable. Si vous triez des objets, vous pouvez parcourir le tableau coords et faire quelque chose comme:

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;
}

(puis parcourez-les à nouveau pour supprimer la valeur de tri)

Encore un hack cependant. Si vous voulez bien le faire, vous devez le faire à la dure :)

2
Thorarin

Cela fait quatre ans, mais je voudrais souligner que la méthode du comparateur aléatoire ne sera pas correctement distribuée, quel que soit l'algorithme de tri que vous utilisez.

Preuve:

  1. Pour un tableau d'éléments n, il y a exactement n! permutations (c'est-à-dire remaniements possibles).
  2. Chaque comparaison lors d'un shuffle est un choix entre deux ensembles de permutations. Pour un comparateur aléatoire, il y a 1/2 chance de choisir chaque ensemble.
  3. Ainsi, pour chaque permutation p, la chance de se retrouver avec la permutation p est une fraction de dénominateur 2 ^ k (pour certains k), car c'est une somme de telles fractions (par exemple 1/8 + 1/16 = 3/16 ).
  4. Pour n = 3, il y a six permutations également probables. La chance de chaque permutation est donc 1/6. 1/6 ne peut pas être exprimé comme une fraction avec une puissance de 2 comme dénominateur.
  5. Par conséquent, le type de retournement de pièces de monnaie n'entraînera jamais une répartition équitable des brassages.

Les seules tailles qui pourraient éventuellement être correctement réparties sont n = 0,1,2.


Comme exercice, essayez de dessiner l'arbre de décision de différents algorithmes de tri pour n = 3.


Il y a une lacune dans la preuve: si un algorithme de tri dépend de la cohérence du comparateur et a un temps d'exécution illimité avec un comparateur incohérent, il peut avoir une somme infinie de probabilités, qui peut s'additionner à 1/6 même si chaque dénominateur de la somme est une puissance de 2. Essayez d'en trouver un.

De plus, si un comparateur a une chance fixe de donner l'une ou l'autre réponse (par exemple (Math.random() < P)*2 - 1, pour la constante P), la preuve ci-dessus est valable. Si le comparateur change à la place ses cotes en fonction des réponses précédentes, il peut être possible de générer des résultats équitables. Trouver un tel comparateur pour un algorithme de tri donné pourrait être un document de recherche.

2
leewz

Si vous utilisez D3, il existe une fonction de lecture aléatoire intégrée (en utilisant Fisher-Yates):

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

Et voici Mike qui entre dans les détails à ce sujet:

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

1
Renaud

Pouvez-vous utiliser la fonction Array.sort() pour mélanger un tableau - Oui.

Les résultats sont-ils assez aléatoires - Non.

Considérez l'extrait de code suivant:

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(", ") + "]");
})

Exemple de sortie:

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]

Idéalement, les comptes devraient être répartis également (pour l'exemple ci-dessus, tous les comptes devraient être autour de 20). Mais ce n'est pas le cas. Apparemment, la distribution dépend de l'algorithme de tri implémenté par le navigateur et de la façon dont il itère les éléments du tableau pour le tri.

Plus d'informations sont fournies dans cet article:
Array.sort () ne doit pas être utilisé pour mélanger un tablea

0
Salman A

Voici une approche qui utilise un seul tableau:

La logique de base est:

  • Commençant par un tableau de n éléments
  • Supprimer un élément aléatoire du tableau et le pousser sur le tableau
  • Supprimer un élément aléatoire des n-1 premiers éléments du tableau et le pousser sur le tableau
  • Supprimer un élément aléatoire des n - 2 premiers éléments du tableau et le pousser sur le tableau
  • ...
  • Retirez le premier élément du tableau et poussez-le sur le tableau
  • Code:

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