Правильно ли использовать метод JavaScript Array.sort() для перетасовки?
Я помогал кому-то с его кодом JavaScript, и мои глаза были пойманы секцией, которая выглядела так:
function randOrd(){
return (Math.round(Math.random())-0.5);
}
coords.sort(randOrd);
alert(coords);
Мой первый, хотя был: эй, это не сработает!. Но затем я немного экспериментировал и обнаружил, что он действительно по крайней мере, кажется, дает хорошо рандомизированные результаты.
Затем я сделал некоторый поиск в Интернете и почти в верхней части нашел статью из которой этот код был наиболее скопирован. Похоже на довольно респектабельный сайт и автора...
Но мое чувство кишки говорит мне, что это должно быть неправильно. Тем более, что алгоритм сортировки не указан стандартом ECMA. Я думаю, что разные алгоритмы сортировки приведут к различным неравномерным перетасовкам. Некоторые алгоритмы сортировки могут, вероятно, даже бесконечно...
Но как вы думаете?
И как еще один вопрос... как я теперь пойду и измерим, насколько случайны результаты этой техники перетасовки?
update: Я сделал некоторые измерения и опубликовал результаты ниже в качестве одного из ответов.
Ответы
Ответ 1
Это никогда не был моим любимым способом перетасовки, отчасти потому, что он специфичен для реализации, как вы говорите. В частности, я, похоже, помню, что стандартная библиотека, сортировка из Java или .NET(не уверенная), может часто обнаруживать, если вы закончите несогласованное сравнение между некоторыми элементами (например, вы сначала утверждаете A < B
и B < C
, но тогда C < A
).
Он также заканчивается как более сложный (с точки зрения времени выполнения) перетасовка, чем вам действительно нужно.
Я предпочитаю алгоритм перетасовки, который эффективно разбивает коллекцию на "перетасованный" (в начале коллекции, изначально пустой) и "unshuffled" (остальная часть коллекции). На каждом шаге алгоритма выберите случайный unshuffled элемент (который может быть первым) и поменяйте его на первый неубранный элемент - затем обработайте его как перетасованный (т.е. Мысленно переместите раздел, чтобы включить его).
Это O (n) и требует только n-1 вызовов генератора случайных чисел, что приятно. Он также создает подлинную перетасовку - любой элемент имеет 1/n шанс оказаться в каждом пространстве независимо от его исходного положения (при условии разумного RNG). Сортированная версия приближается к четному распределению (если предположить, что генератор случайных чисел не выбирает одно и то же значение дважды, что маловероятно, если он возвращает случайные удвоения), но мне легче рассуждать о версии в случайном порядке:)
Этот подход называется Fisher-Yates shuffle.
Я рассматривал бы это как наилучшую практику для того, чтобы скопировать этот случайный танец один раз и повторно использовать его везде, где вам нужно перетасовать элементы. Тогда вам не нужно беспокоиться о реализации сортировки с точки зрения надежности или сложности. Это всего лишь несколько строк кода (которые я не буду пытаться использовать в JavaScript!)
Статья в Википедии о перетасовке (и, в частности, раздел алгоритмов перетасовки) рассказывает о сортировке случайной проекции - стоит прочитать раздел о бедных реализаций перетасовки в целом, поэтому вы знаете, чего следует избегать.
Ответ 2
После того, как Джон уже рассмотрел теорию, вот реализация:
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)
. В зависимости от накладных расходов на выполнение JS-кода по сравнению с нативной функцией sort()
это может привести к заметной разнице в производительности, которая должна увеличиваться с размерами массивов.
В комментариях к bobobobo answer я заявил, что данный алгоритм может не давать равномерно распределенных вероятностей (в зависимости от реализации sort()
).
Мой аргумент идет по этим строкам: для алгоритма сортировки требуется определенное число c
сравнений, например c = n(n-1)/2
для Bubblesort. Наша функция случайного сравнения делает результаты каждого сравнения одинаково вероятными, т.е. Есть 2^c
равновероятные результаты. Теперь каждый результат должен соответствовать одной из перестановок n!
записей массива, что делает равномерное распределение невозможным в общем случае. (Это упрощение, так как фактическое количество выбранных сравнений зависит от входного массива, но утверждение должно сохраняться.)
Как отметил Джон, это не является основанием для предпочтения Фишера-Йейта с использованием sort()
, поскольку генератор случайных чисел также отображает конечное число псевдослучайных значений в подстановки n!
. Но результаты Фишера-Йейта должны все же быть лучше:
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, вы должны быть разумно безопасны, за исключением случаев с угловыми шкафами (по крайней мере, я надеюсь, что 2^c ≤ n!
- это угловой случай), если нет, все ставки отключены.
Ответ 3
Я сделал некоторые измерения того, насколько случайны результаты этого случайного сортирования...
Моя техника заключалась в том, чтобы взять небольшой массив [1,2,3,4] и создать все (4!= 24) перестановки. Затем я бы применял функцию перетасовки к массиву много раз и подсчитывал, сколько раз каждая перестановка создается. Хороший алгоритм перетасовки распределял бы результаты довольно равномерно по всем перестановкам, в то время как плохое не создавало бы такой однородный результат.
Используя приведенный ниже код, я тестировал в Firefox, Opera, Chrome, IE6/7/8.
Удивительно, но случайная сортировка и реальная перетасовка создали одинаково равномерные распределения. Таким образом, кажется, что (как многие из них предложили) основные браузеры используют сортировку слияния. Это, конечно, не означает, что там не может быть браузера, который работает по-другому, но я бы сказал, это означает, что этот метод случайной сортировки является достаточно надежным для использования на практике.
РЕДАКТИРОВАТЬ: Этот тест на самом деле не правильно измерил случайность или отсутствие такового. См. Другой ответ, который я разместил.
Но с точки зрения производительности функция тасования, данная Кристофом, была явным победителем. Даже для небольших четырехэлементных массивов реальная тасовка выполнялась примерно в два раза быстрее, чем случайная сортировка!
// The shuffle function posted by 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;
};
// the random sort function
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.length; 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;
}
// shuffle many times and gather stats
var start=new Date();
for (var i=0; i<iterations; i++) {
var shuffled = func(A);
stats[""+shuffled]++;
}
var end=new Date();
// format result
var arr=[];
for (var i in stats) {
arr.push(i+" "+stats[i]);
}
return arr.join("\n")+"\n\nTime taken: " + ((end - start)/1000) + " seconds.";
};
alert("random sort: " + test([1,2,3,4], 100000, randSort));
alert("shuffle: " + test([1,2,3,4], 100000, shuffle));
Ответ 4
Интересно, что 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"));
Ответ 5
Я разместил простую тестовую страницу на своем веб-сайте, показывающую смещение вашего текущего браузера по сравнению с другими популярными браузерами, использующими различные методы перемешивания. Он показывает ужасную предвзятость при использовании Math.random()-0.5
, еще одной "случайной" случайной случайной последовательности, а также метода Фишера-Йейтса, упомянутого выше.
Вы можете видеть, что в некоторых браузерах 50% -ная вероятность того, что некоторые элементы не будут меняться местами вообще во время "перемешивания"!
Примечание: вы можете сделать реализацию Fisher-Yates shuffle с помощью @Christoph немного быстрее для 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;
}
Ответ 6
Я думаю, что это хорошо для случаев, когда вы не придирчивы к распределению, и вы хотите, чтобы исходный код был небольшим.
В JavaScript (где источник передается постоянно), малый делает разницу в стоимости полосы пропускания.
Ответ 7
Это, конечно, хак. На практике алгоритм с бесконечным циклом маловероятен.
Если вы сортируете объекты, вы можете пройти через массив коордов и сделать что-то вроде:
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)
Тем не менее, взломать. Если вы хотите сделать это красиво, вам нужно сделать это нелегко:)
Ответ 8
Прошло четыре года, но я хотел бы отметить, что метод случайного компаратора не будет правильно распределен, независимо от того, какой алгоритм сортировки вы используете.
Доказательство:
- Для массива из
n
элементов существует ровно n!
перестановки (т.е. возможные перетасовки). - Каждое сравнение во время перетасовки представляет собой выбор между двумя наборами перестановок. Для случайного компаратора существует вероятность 1/2 выбора каждого набора.
- Таким образом, для каждой перестановки p вероятность завершения перестановки p является дробью с знаменателем 2 ^ k (для некоторого k), так как это сумма таких дробей (например, 1/8 + 1/16 = 3/16).
- Для n = 3 существует шесть одинаково вероятных перестановок. Таким образом, вероятность каждой перестановки равна 1/6. 1/6 не может быть выражена как дробь с силой 2 в качестве знаменателя.
- Поэтому сортировка монет не будет приводить к справедливому распределению тасов.
Единственные размеры, которые могут быть правильно распределены, равны n = 0,1,2.
В качестве упражнения попробуйте вычеркнуть дерево решений различных алгоритмов сортировки для n = 3.
В доказательстве есть пробел: если алгоритм сортировки зависит от согласованности компаратора и имеет неограниченное время выполнения с непоследовательным компаратором, он может иметь бесконечную сумму вероятностей, что позволяет добавить до 1/6, даже если каждый знаменатель в сумме является степенью 2. Попробуйте найти ее.
Кроме того, если у компаратора есть фиксированная вероятность дать ответ (например, (Math.random() < P)*2 - 1
, для постоянной P
), то доказательство выше. Если компаратор вместо этого изменит свои коэффициенты на основе предыдущих ответов, может быть возможным получить справедливые результаты. Поиск такого компаратора для данного алгоритма сортировки может быть исследовательской.
Ответ 9
Если вы используете D3, есть встроенная функция перетасовки (используя Fisher-Yates):
var days = ['Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi','Dimanche'];
d3.shuffle(days);
И вот Майк подробно рассказывает об этом:
http://bost.ocks.org/mike/shuffle/
Ответ 10
Можете ли вы использовать функцию 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(", ") + "]");
})
Ответ 11
Здесь используется подход, который использует один массив:
Основная логика:
Начиная с массива из n элементов
Удалите случайный элемент из массива и нажмите его на массив
Удалите случайный элемент из первых n - 1 элементов массива и надавите на массив
Удалите случайный элемент из первых n - 2 элементов массива и надавите на массив
...
Удалите первый элемент массива и надавите на массив
код:
for(i=a.length;i--;) a.push(a.splice(Math.floor(Math.random() * (i + 1)),1)[0]);
Ответ 12
Addi Osmani внедрил эту версию Fisher-Yates shuffle:
function shuffle(array) {
var rand, index = -1,
length = array.length,
result = Array(length);
while (++index < length) {
rand = Math.floor(Math.random() * (index + 1));
result[index] = result[rand];
result[rand] = array[index];
}
return result;
}
Ответ 13
Для тех, кто возвращается, чтобы посмотреть на это, вот доказательство того, что sort() не работает для рандомизации: http://phrogz.net/JS/JavaScript_Random_Array_Sort.html
Ответ 14
В этом нет ничего плохого.
Функция, которую вы передаете .sort(), обычно выглядит примерно как
function sortingFunc( first, second )
{
// example:
return first - second ;
}
Ваша работа в sortingFunc должна возвращаться:
- отрицательное число, если сначала идет до второго
- положительное число, если сначала следует перейти после второго
- и 0, если они полностью равны
Вышеуказанная функция сортировки приводит порядок вещей.
Если вы вернетесь - и + случайно, как у вас, вы получите случайный порядок.
Как и в MySQL:
SELECT * from table ORDER BY rand()