it-swarm.com.ru

Правильно ли использовать метод JavaScript Array.sort () для перемешивания?

Я помогал кому-то с его JavaScript-кодом, и мои глаза привлекли внимание к следующему разделу:

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

Сначала я подумал: эй, это не может сработать! Но потом я немного поэкспериментировал и обнаружил, что это действительно, по крайней мере, похоже на случайную выборку. Результаты.

Затем я выполнил поиск в Интернете и почти наверху обнаружил статью , из которой этот код был скопирован наиболее странно. Выглядел как довольно респектабельный сайт и автор ...

Но мое внутреннее чувство говорит мне, что это должно быть неправильно. Тем более что алгоритм сортировки не указан стандартом ECMA. Я думаю, что разные алгоритмы сортировки приведут к различным неравномерным перемешиваниям. Некоторые алгоритмы сортировки могут даже бесконечно зацикливаться ...

Но что вы думаете?

И как еще один вопрос ... как бы я теперь пошел и измерил, насколько случайны результаты этой техники тасования?

обновление: Я сделал несколько измерений и опубликовал результаты ниже как один из ответов.

124
Rene Saarsoo

Это никогда не был мой любимый способ тасования, отчасти потому, что, как вы говорите, он зависит от конкретной реализации . В частности, мне кажется, что я помню, что стандартная библиотека, сортирующая из Java или .NET (не уверен, какой), часто может обнаружить, если вы столкнулись с противоречивым сравнением между некоторыми элементами (например, вы сначала утверждаете A < B и B < C, но затем C < A).

Это также приводит к более сложному (с точки зрения времени выполнения) перемешиванию, чем вам действительно нужно.

Я предпочитаю алгоритм перемешивания, который эффективно разбивает коллекцию на "перемешанные" (в начале коллекции, изначально пустые) и "не перемешанные" (остальная часть коллекции). На каждом шаге алгоритма выберите случайный элемент без перемешивания (который может быть первым) и поменяйте его местами с первым элементом без перемешивания - затем обработайте его как перемешанный (то есть мысленно переместите раздел, чтобы включить его).

Это O(n) и ​​требует только n-1 вызовов генератора случайных чисел, что приятно. Он также производит настоящую случайную последовательность - любой элемент имеет шанс 1/n оказаться в каждом пространстве, независимо от его исходного положения (при условии разумного значения ГСЧ). Сортированная версия приближает к равномерному распределению (при условии, что генератор случайных чисел не выбирает одно и то же значение дважды, что весьма маловероятно, если он возвращает случайное значение). удваивается) но мне проще рассуждать о случайной версии :)

Этот подход называется случай Фишера-Йейтса .

Я бы посчитал наилучшей практикой один раз кодировать этот случайный порядок и использовать его везде, где вам нужно перемешивать элементы. Тогда вам не нужно беспокоиться о реализации сортировки с точки зрения надежности или сложности. Это всего лишь несколько строк кода (которые я не буду пытаться в JavaScript!)

статья в Википедии о перемешивании (и, в частности, раздел алгоритмов перемешивания) говорит о сортировке случайной проекции - стоит прочитать раздел о плохих реализациях перемешивания в целом, так что вы знаете, чего следует избегать.

109
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). В зависимости от накладных расходов на выполнение кода JS по сравнению с собственной функцией sort() это может привести к заметной разнице в производительности , которая должна увеличиваться с увеличением размеров массива.


В комментариях к ответ Бобобо я заявил, что рассматриваемый алгоритм может не давать равномерно распределенные вероятности (в зависимости от реализации 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! является углом случай), если нет, все ставки выключены.

116
Christoph

Я сделал несколько измерений того, насколько случайны результаты этого случайного рода ...

Моя техника состояла в том, чтобы взять небольшой массив [1,2,3,4] и создать все (4! = 24) его перестановки. Затем я применил бы функцию перемешивания к массиву большое количество раз и посчитал, сколько раз генерируется каждая перестановка. Хороший алгоритм перетасовки распределяет результаты довольно равномерно по всем перестановкам, в то время как плохой не приведет к такому равномерному результату.

Используя приведенный ниже код, я протестировал Firefox, Opera, Chrome, IE6/7/8.

Удивительно для меня, случайная сортировка и реальное перемешивание создали одинаково равномерное распределение. Похоже, что (как многие и предполагали) основные браузеры используют сортировку слиянием. Это, конечно, не означает, что не может быть браузера, который работает по-другому, но я бы сказал, что это означает, что этот метод случайной сортировки достаточно надежен для использования на практике.

Правка: Этот тест не правильно измерял случайность или ее отсутствие. Смотрите другой ответ, который я написал.

Но с точки зрения производительности функция случайного выбора, предложенная Кристофом, была явным победителем. Даже для небольших четырехэлементных массивов реальное перемешивание выполнялось примерно в два раза быстрее, чем случайная сортировка!

 // Функция перемешивания, опубликованная Кристофом. 
 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 () {
 return Math.round (Math.random ()) - 0.5; 
}; 
 var randSort = function (A) {
 return A.sort (rnd); 
}; 
 
 var permutations = function (A) {
 if (A.length == 1) {
 возвращает [A]; 
} 
 else {
 var perms = []; 
 для (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 = перестановки (xs); 
 для (var j = 0 ; j <subperms. длина; j ++) {
 perms.Push (x.concat (subperms [j])); 
} 
} 
 возврат перми; 
} 
}; 
 
 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 (); 
 для (var i = 0; i <итерации; i ++) {
 var shuffled = func (A); 
 stats ["" + shuffled] ++; 
} 
 var end = new Date (); 
 
 // форматировать результат 
 var arr = []; 
 for (var i in stats) {
 arr.Push (i + "" + stats [i] ); 
} 
 return arr.join ("\ n") + "\ n\nВремя выполнения:" + ((конец - начало)/1000) + "секунды."; 
}; 
 
 alert ("случайная сортировка:" + test ([1,2,3,4], 100000, randSort)); 
 alert ("shuffle : "+ test ([1,2,3,4], 100000, shuffle)); 
16
Rene Saarsoo

Интересно, что 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"));
11
Rene Saarsoo

Я разместил простую тестовую страницу на моем веб-сайте, показывающую смещение вашего текущего браузера по сравнению с другими популярными браузерами, использующими различные методы для перетасовки. Он показывает ужасную предвзятость при использовании 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;
}

Результаты теста: http://jsperf.com/optimized-fisher-yates

9
Phrogz

Я думаю, что это хорошо для случаев, когда вы не разборчивы в распространении и хотите, чтобы исходный код был небольшим.

В JavaScript (где источник передается постоянно), small влияет на стоимость полосы пропускания.

5
Nosredna

Это взлом, конечно. На практике алгоритм с бесконечной петлей маловероятен. Если вы сортируете объекты, вы можете перебрать массив координат и сделать что-то вроде:

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

Прошло четыре года, но я хотел бы отметить, что метод случайного сравнения не будет правильно распределен, независимо от того, какой алгоритм сортировки вы используете.

Доказательство

  1. Для массива элементов n существует в точности перестановка n! (то есть возможные тасования).
  2. Каждое сравнение в случайном порядке - это выбор между двумя наборами перестановок. Для случайного компаратора есть шанс 1/2 выбора каждого набора.
  3. Таким образом, для каждой перестановки p вероятность оказаться с перестановкой p является дробью со знаменателем 2 ^ k (для некоторого k), поскольку она является суммой таких дробей (например, 1/8 + 1/16 = 3/16 ).
  4. Для n = 3 существует шесть одинаково вероятных перестановок. Таким образом, вероятность каждой перестановки равна 1/6. 1/6 не может быть выражена в виде дроби со степенью 2 в качестве знаменателя.
  5. Поэтому сортировка монет никогда не приведет к справедливому распределению перемешиваний.

Единственные размеры, которые могут быть правильно распределены, это n = 0,1,2.


В качестве упражнения попробуйте нарисовать дерево решений различных алгоритмов сортировки для n = 3.


В доказательстве есть пробел: если алгоритм сортировки зависит от согласованности компаратора и имеет неограниченное время выполнения с несовместимым компаратором, он может иметь бесконечную сумму вероятностей, которая может суммироваться до 1/6, даже если каждый знаменатель в сумме является степенью 2. Попытайтесь найти один.

Кроме того, если у компаратора есть фиксированный шанс дать любой ответ (например, (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