Чи правильно використовувати метод Array.sort () JavaScript для перетасовування?


126

Я допомагав комусь із його JavaScript-кодом, і мої очі були схоплені на такий розділ:

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

Моє перше, хоча: було , це, можливо, не може працювати! Але потім я зробив кілька експериментів і виявив, що це, принаймні, здається, дає непогано рандомізовані результати.

Потім я здійснив деякий пошук в Інтернеті і майже вгорі знайшов статтю, з якої цей код було найбільш копійовано скопійовано. Виглядав як досить поважний сайт та автор ...

Але почуття моєї кишки говорить мені, що це має бути неправильно. Тим більше, що алгоритм сортування не визначений стандартом ECMA. Я думаю, що різні алгоритми сортування призведуть до різних нерівномірних перетасовок. Деякі алгоритми сортування, ймовірно, можуть нескінченно циклічно ...

Але що ти думаєш?

І як ще одне питання ... як би я зараз пішов і виміряв, наскільки випадкові результати цієї методи перетасування?

оновлення: я зробив кілька вимірювань і розмістив результати нижче, як одну з відповідей.


просто зауважимо, що марно
підбивати

2
" Я виявив, що це, здається, дає непогано рандомізовані результати ". - Справді ???
Бергі

Відповіді:


109

Це ніколи не було моїм улюбленим способом переміщення, частково тому, що це , як ви кажете, залежить від впровадження. Зокрема, я , здається, пам'ятаю , що стандартна бібліотека сортування з будь Java або .NET (не впевнений , який) часто можна виявити , якщо ви в кінцевому підсумку з несумісним порівняння між деякими елементами (наприклад , ви перший претензії A < Bі B < C, але потім C < A).

Це також закінчується як більш складна (з точки зору часу виконання) переміщення, ніж вам потрібно.

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

Це O (n) і вимагає лише n-1 викликів до генератора випадкових чисел, що добре. Він також створює справжній перетасування - будь-який елемент має 1 / n шанс потрапити в кожен простір, незалежно від його вихідного положення (якщо прийнятний RNG). Впорядкована версія наближається до рівномірного розподілу (якщо припустити, що генератор випадкових чисел не вибирає однакове значення двічі, що дуже малоймовірно, якщо він повертає випадкові подвоєння), але мені легше міркувати про переміщення версії :)

Такий підхід називається перемиканням Фішера-Йейта .

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

Стаття Вікіпедії про перетасування (і зокрема розділ алгоритмів перетасовки) розповідає про сортування випадкової проекції - варто прочитати розділ про погані реалізації перетасовки взагалі, щоб ви знали, чого уникати.


5
Реймонд Чен розглядає важливість того, що функції порівняння сортування відповідають правилам: blogs.msdn.com/oldnewthing/archive/2009/05/08/9595334.aspx
Jason Kresowaty

1
якщо мої міркування правильні, відсортована версія не призводить до «справжнього» переміщення!
Крістоф

@Christoph: думати про це, навіть Fisher-Yates буде тільки дати «ідеальний» розподіл , якщо рандів (х) гарантовано буде точно навіть на його діапазоні. Зважаючи на те, що для RNG зазвичай є 2 ^ х можливих станів для деякого x, я не думаю, що це буде точно навіть для rand (3).
Джон Скіт

@Jon: але Fisher-Yates створить 2^xстани для кожного індексу масиву, тобто усього буде 2 ^ (xn) станів, що має бути трохи більше, ніж 2 ^ c - детальніше див. Мою відредаговану відповідь
Крістоф

@Christoph: Я, можливо, не пояснив себе належним чином. Припустимо, у вас всього 3 елемента. Ви вибираєте перший елемент випадковим чином, з усіх 3. Щоб отримати повністю рівномірний розподіл, вам доведеться вибирати випадкове число в діапазоні [0,3) повністю рівномірно - і якщо PRNG має 2 ^ n Можливі стани, ви не можете цього зробити - одна чи дві можливості матимуть трохи більшу ймовірність виникнення.
Джон Скіт

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). Залежно від накладних витрат виконання JS-коду порівняно з нативною sort()функцією, це може призвести до помітної різниці в продуктивності, яка повинна збільшуватися з розмірами масиву.


У коментарях до відповіді bobobobo я заявив, що алгоритм, про який йдеться, може не створювати рівномірно розподілених ймовірностей (залежно від реалізації 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!і, таким чином, веде до нерівномірного розподілу, навіть якщо алгоритм сортування, де відображати перестановки рівномірно. Якщо це має якийсь практичний вплив, це поза мною.

Справжня проблема полягає в тому, що алгоритми сортування не гарантують рівномірне відображення перестановок. Неважко помітити, що Мергезорт виглядає як симетричний, але міркування про щось на зразок Bubblesort або, що ще важливіше, Quicksort або Heapsort, це не так.


Суть: Якщо ви sort()користуєтеся Mergesort, ви повинні бути в безпеці, за винятком випадків кутового (принаймні, я сподіваюся, що 2^c ≤ n!це кутовий випадок), якщо ні, всі ставки відключені.


Дякуємо за реалізацію. Це палає швидко! Особливо порівняно з тим повільним лаєм, про який я тим часом писав сам.
Рене Саарсоо

1
Якщо ви використовуєте бібліотеку underscore.js, ось як її поширити за допомогою вищезазначеного методу перемішування Fisher-Yates: github.com/ryantenney/underscore/commit/…
Стів

Дякую вам за це, поєднання вашої та відповіді Джонса допомогло мені виправити проблему, яку я та колега провели майже 4 години разом! Спочатку ми мали подібний метод до ОП, але виявили, що рандомізація була дуже лускатою, тому ми взяли ваш метод і трохи змінили його, щоб попрацювати з трохи запитань, щоб збільшити список зображень (для слайдера), щоб отримати деякі дивовижна рандомізація.
Hello World

16

Я зробив кілька вимірювань того, наскільки випадкові результати цього випадкового сортування ...

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

За допомогою наведеного нижче коду я перевірив Firefox, Opera, Chrome, IE6 / 7/8.

Дивно, але для мене випадкові сортування та реальна змішання створили однаково розподілені розподіли. Отож, схоже, що (як багато хто з них запропонував) основні браузери використовують сортування злиття. Це, звичайно, не означає, що там не може бути браузера, це робить інакше, але я б сказав, це означає, що цей метод випадкового сортування досить надійний, щоб використовувати його на практиці.

EDIT: Цей тест насправді не вимірював правильно випадковість чи відсутність. Дивіться іншу відповідь, яку я опублікував.

Але з боку виступу функція перемішування, яку дав Крістоф, стала очевидним переможцем. Навіть для невеликих чотирьохелементних масивів справжнє переміщення виконується приблизно вдвічі швидше, ніж випадкове сортування!

// Функція переміщення, розміщена Крістофом.
var shuffle = функція (масив) {
    var tmp, current, top = array.length;

    якщо (верх), а (- верх) {
        current = Math.floor (Math.random () * (top + 1));
        tmp = масив [поточний];
        масив [поточний] = масив [верх];
        масив [верх] = tmp;
    }

    повернутий масив;
};

// функція випадкового сортування
var rnd = function () {
  повернути Math.round (Math.random ()) - 0,5;
};
var randSort = функція (A) {
  повернути A.sort (rnd);
};

var permutations = function (A) {
  якщо (A.length == 1) {
    повернути [А];
  }
  ще {
    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 = перестановки (xs);
      for (var j = 0; j <subperms.length; j ++) {
        perms.push (x.concat (підперми [j]));
      }
    }
    повернення хімічних речовин;
  }
};

var test = функція (A, ітерації, функція) {
  // init перестановки
  var stats = {};
  var perms = перестановки (A);
  for (var i in perms) {
    stats ["" + perms [i]] = 0;
  }

  // Перетасовувати багато разів і збирати статистику
  var start = нова дата ();
  для (var i = 0; i <ітерації; i ++) {
    var shuffled = func (A);
    stats ["" + перетасовується] ++;
  }
  var end = нова дата ();

  // результат форматування
  var arr = [];
  для (var i in stats) {
    arr.push (i + "" + stats [i]);
  }
  повернути arr.join ("\ n") + "\ n \ nТайм, взятий:" + ((кінець - початок) / 1000) + "секунди.";
};

попередження ("випадкове сортування:" + тест ([1,2,3,4], 100000, randSort));
попередження ("перетасування:" + тест ([1,2,3,4], 100000, перетасування));

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 ()?
Олександр Міллс

1
@AlexanderMills: Функція порівняння, яка передається sort(), повинна повертати число, яке перевищує, менше або дорівнює нулю залежно від порівняння aта b. ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… )
LarsH

@LarsH Так, це має сенс
Олександр Міллз

9

Я розмістив просту тестову сторінку на своєму веб-сайті, де відображається ухил вашого поточного веб-переглядача порівняно з іншими популярними браузерами, використовуючи різні способи переміщення. Це показує жахливі упередження просто використання Math.random()-0.5, черговий "випадковий" переміс, який не є упередженим, і метод Фішера-Йейта, згаданий вище.

Ви можете бачити, що в деяких браузерах існує 50% шансів, що певні елементи взагалі не зміняться під час "перетасовки"!

Примітка. Ви можете зробити реалізацію перемішування Fisher-Yates від @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

Я думаю, це добре для тих випадків, коли ви не вимогливі до розповсюдження і хочете, щоб вихідний код був невеликим.

У JavaScript (там, де джерело передається постійно), малі впливають на витрати на пропускну здатність.


2
Справа в тому, що ви майже завжди дбайливіші щодо розповсюдження, ніж ви думаєте, що ви є, а для "малого коду" завжди є те arr = arr.map(function(n){return [Math.random(),n]}).sort().map(function(n){return n[1]});, що має перевагу в тому, що не надто страшно набагато довше і насправді правильно розподілене. Існують також дуже стислі варіанти перемикання Knuth / FY.
Даніель Мартін

@DanielMartin Цей однорядковий варіант повинен відповісти. Крім того , щоб уникнути помилок синтаксичного аналізу, два з коми потрібно додати , так це виглядає наступним чином : arr = arr.map(function(n){return [Math.random(),n];}).sort().map(function(n){return n[1];});.
Giacomo1968

2

Це, звичайно, хак. На практиці нескінченний цикл алгоритму є малоймовірним. Якщо ви сортуєте об'єкти, ви можете провести цикл через масив координат і зробити щось на кшталт:

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

Минуло чотири роки, але я хотів би зазначити, що метод випадкового порівняння не буде правильно розподілений, незалежно від того, яким алгоритмом сортування ви користуєтесь.

Доказ:

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


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

    Ваша реалізація має високий ризик залишити значну кількість елементів недоторканими. Вони будуть просто зміщені у всьому масиві на кількість неповноцінних елементів, що були висунуті зверху. У цьому перетасуванні є малюнок, який робить його ненадійним.
    Кір Канос

    @KirKanos, я не впевнений, що я розумію твій коментар. Я пропоную рішення - O (n). Це, безумовно, буде "торкатися" кожного елемента. Ось загадка для демонстрації.
    ic3b3rg

    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 (), зазвичай виглядає приблизно так

    функція сортуванняFunc (перша, друга)
    {
      // приклад:
      повернути перше - друге;
    }
    

    Ваше завдання по сортуваннюFunc - повернути:

    • від’ємне число, якщо перше йде перед другим
    • додатне число, якщо перше повинно пройти після другого
    • і 0, якщо вони повністю рівні

    Вищеописана функція сортування наводить порядок.

    Якщо ви повернете випадки і + як випадково, як у вас є, ви отримаєте випадкове впорядкування.

    Як у MySQL:

    SELECT * з таблиці ORDER BY rand ()
    

    5
    там є що - то не так з цим підходом: в залежності від алгоритму сортування в використанні по реалізації JS, імовірності не будуть рівномірно розподілені!
    Крістоф

    Це те, про що ми практично хвилюємося?
    bobobobo

    4
    @bobobobo: залежно від програми, так, іноді ми робимо; Крім того, правильно працюючий shuffle()повинен бути написаний лише один раз, тому це насправді не проблема: просто покладіть фрагмент у сховище коду та розкопайте його, коли вам це потрібно
    Крістоф
    Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
    Licensed under cc by-sa 3.0 with attribution required.