Правильно ли использовать метод JavaScript Array.sort () для перемешивания? - PullRequest
124 голосов
/ 08 июня 2009

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

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

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

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

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

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

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

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

Ответы [ 12 ]

116 голосов
/ 08 июня 2009

После того, как Джон уже рассмотрел теорию , вот реализация:

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

При использовании Фишера-Йейтса соответствующим параметром является размер массива, который никогда не должен приближаться к 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! это угловой случай), если нет все ставки сняты.

109 голосов
/ 08 июня 2009

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

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

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

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

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

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

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

16 голосов
/ 08 июня 2009

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

Моя техника состояла в том, чтобы взять небольшой массив [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));
11 голосов
/ 02 марта 2010

Интересно, что 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"));
9 голосов
/ 17 ноября 2010

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

5 голосов
/ 08 июня 2009

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

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

2 голосов
/ 11 ноября 2013

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

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

  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 голосов
/ 08 июня 2009

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

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)

Все еще хак. Если вы хотите сделать это красиво, вы должны сделать это трудным путем:)

1 голос
/ 22 октября 2013

Если вы используете D3, есть встроенная функция тасования (с использованием Фишера-Йейтса):

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

А вот Майк подробно расскажет об этом:

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

0 голосов
/ 24 ноября 2013

Вот подход, который использует один массив:

Основная логика:

Начиная с массива из n элементов Удалить случайный элемент из массива и вставить его в массив Удалить случайный элемент из первых n - 1 элементов массива и вставить его в массив Удалить случайный элемент из первых n - 2 элементов массива и вставить его в массив ... Удалить первый элемент массива и вставить его в массив

Код:

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