셔플 링에 JavaScript Array.sort () 메소드를 사용하는 것이 맞습니까?


126

나는 그의 JavaScript 코드로 누군가를 돕고 있었고 내 눈은 다음과 같은 섹션에 매료되었습니다.

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

내 첫 번째는 : 이봐, 아마도 작동하지 않을 수 있습니다! 그러나 나는 몇 가지 실험을 해본 결과 적어도 무작위로 무작위로 결과를 제공하는 것으로 보입니다.

그런 다음 웹 검색을 수행하고 거의 맨 위에이 코드가 가장 정교하게 복사 된 기사 를 찾았습니다 . 꽤 존경받는 사이트와 저자처럼 보였습니다 ...

그러나 내 직감은 이것이 잘못되었다는 것을 말해줍니다. 특히 정렬 알고리즘이 ECMA 표준에 의해 지정되지 않았기 때문에. 다른 정렬 알고리즘으로 인해 다른 균일하지 않은 셔플이 발생한다고 생각합니다. 일부 정렬 알고리즘은 아마도 무한 반복 될 수도 있습니다 ...

하지만 어떻게 생각하세요?

그리고 또 다른 질문으로 ... 이제 어떻게이 셔플 링 기법의 결과가 무작위인지 측정 할 수 있습니까?

업데이트 : 몇 가지 측정을 수행하고 아래 결과를 답변 중 하나로 게시했습니다.


단지 부호 수만 반올림하는 것은 쓸모가 없다는 것을 알아 차리는 것
bormat

2
" 나는 그것이 무작위로 좋은 결과를 제공하는 것으로 보인다. "- 정말 ???
Bergi

답변:


109

그것은 부분적으로 있기 때문에, 셔플의 내가 가장 좋아하는 방법은 본 적이 없네 입니다 당신이 말한대로 구현 고유의. 특히, Java 또는 .NET에서 정렬하는 표준 라이브러리 (어떤 것이 확실하지 않은지)는 종종 일부 요소 (예 : 먼저 청구 A < BB < C, 그러나 C < A) 간의 비교가 일치하지 않는지를 감지 할 수 있음을 기억합니다 .

또한 실제로 필요한 것보다 더 복잡한 (실행 시간 측면에서) 셔플로 끝납니다.

컬렉션을 "셔플 링"(컬렉션 시작시 처음에는 비어 있음)과 "셔플되지 않은"(컬렉션 나머지)으로 효과적으로 분할하는 셔플 알고리즘을 선호합니다. 알고리즘의 각 단계에서 무작위 셔플되지 않은 요소 (첫 번째 요소 일 수 있음)를 선택하고 첫 번째 셔플되지 않은 요소와 교체 한 다음 셔플 된 것으로 처리합니다 (즉, 파티션을 포함하도록 정신적으로 이동).

이것은 O (n)이며 난수 생성기에 대한 n-1 호출 만 필요합니다. 또한 진정한 셔플을 생성합니다. 모든 요소는 원래 위치에 관계없이 1 / n의 확률로 종료됩니다 (적절한 RNG 가정). 정렬 된 버전은 근사 하지만 셔플 버전에 대한 이유를 쉽게 찾을 수 있습니다 (이 임의의 두 배를 돌려 않다면 난수 생성기는 매우 가능성이있는, 두 번 같은 값을 선택하지 않는 가정) 더 분포 :)

이 접근법을 Fisher-Yates shuffle 이라고합니다 .

이 셔플을 한 번 코딩하고 항목을 셔플하는 데 필요한 모든 곳에서 재사용하는 것이 가장 좋습니다. 그러면 안정성이나 복잡성 측면에서 정렬 구현에 대해 걱정할 필요가 없습니다. 그것은 단지 몇 줄의 코드입니다 (JavaScript에서는 시도하지 않을 것입니다!)

셔플 링 (특히 셔플 알고리즘 섹션) 대한 Wikipedia 기사에서는 랜덤 프로젝션을 정렬하는 방법에 대해 설명합니다. 일반적으로 셔플 링의 잘못된 구현에 대한 섹션을 읽으면 가치가 있습니다.


5
Raymond Chen은 정렬 비교 기능이 규칙을 따르는 중요성에 대해 심층적으로 설명합니다. blogs.msdn.com/oldnewthing/archive/2009/05/08/9595334.aspx
Jason Kresowaty

1
내 추론이 맞으면 정렬 된 버전 에서 '정품'셔플이 생성 되지 않습니다 !
Christoph

@Christoph : rand (x)가 정확히 그 범위를 넘어서는 것이 보장된다면 Fisher-Yates조차도 "완벽한"분포를 줄 것 입니다. 일부 x에 대해 RNG에 대해 일반적으로 2 ^ x 가능한 상태가 있다고 가정하면 rand (3)에 대해서도 정확히 같지 않다고 생각합니다 .
Jon Skeet

@Jon : 그러나 Fisher-Yates는 2^x각 배열 인덱스에 대해 상태를 생성합니다. 예를 들어 총 2 ^ (xn) 상태가 2 ^ c보다 상당히 커야합니다. 자세한 내용은 수정 된 답변 참조
Christoph

@Christoph : 나는 나 자신을 제대로 설명하지 않았을 수도 있습니다. 요소가 3 개라고 가정합니다. 당신은 모든 3 중에서 첫 번째 원소를 무작위로 골라냅니다. 완전히 균일 한 분포 를 얻으려면 [0,3) 범위 내에서 완전히 균일하게-그리고 PRNG가 2 ^ n 인 경우 난수를 선택할 수 있어야합니다 가능한 상태에서는 그렇게 할 수 없습니다. 하나 또는 두 가지 가능성이 약간 더 높아질 가능성이 있습니다.
Jon Skeet

118

Jon이 이미 이론을 다루고 나면 구현은 다음과 같습니다.

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). 네이티브 sort()함수 와 비교하여 JS 코드를 실행하는 오버 헤드에 따라 성능에 눈에 띄는 차이 가 발생하여 배열 크기가 커질 수 있습니다.


bobobobo의 답변에 대한 의견 에서 문제의 알고리즘이 (의 구현에 따라) 균등하게 분산 된 확률을 생성하지 않을 수 있다고 언급했습니다 sort().

나의 주장은 다음과 c같은 내용 c = n(n-1)/2을 따른다 : 정렬 알고리즘은 특정한 수 의 비교 가 필요하다 ( 예 : Bubblesort). 우리의 무작위 비교 함수는 각 비교의 ​​결과를 똑같이 가능하게 만듭니다. 즉 2^c 똑같이 가능한 결과가 있습니다. 이제 각 결과는 n!배열 항목 의 순열 중 하나와 일치해야 하므로 일반적인 경우 고른 분포가 불가능합니다. (필요한 실제 비교 수는 입력 배열에 따라 다르지만 어설 션은 여전히 ​​유지되어야하므로 이는 단순화 된 것입니다.)

Jon이 지적한 것처럼 sort()난수 생성기가 유한 수의 의사 난수 값을 n!순열에 매핑하기 때문에 이것만으로 Fisher-Yates를 선호하는 이유는 없습니다 . 그러나 Fisher-Yates의 결과는 여전히 더 좋습니다.

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!, 따라서 불균일 한 분포를 초래 정렬 알고리즘은 여기서 균등 permutaions에 매핑하는 경우에도. 이것이 실질적인 영향을 미친다면 저를 넘어선 것입니다.

실제 문제는 정렬 알고리즘이 순열에 균등하게 매핑되는 것이 보장되지 않는다는 것입니다. Mergesort가 대칭 적 인 것처럼 보이지만 Bubblesort 또는 Quicksort 또는 Heapsort와 같은 것에 대한 추론은 그렇지 않습니다.


결론 : sort()Mergesort 를 사용하는 한 코너 케이스 (적어도 코너 케이스 인 경우)를 제외하고는 합리적으로 안전 해야 합니다 2^c ≤ n!.


구현해 주셔서 감사합니다. 엄청나게 빠릅니다! 특히 내가 저 자신이 쓴 느린 쓰레기와 비교할 때.
Rene Saarsoo

1
underscore.js 라이브러리를 사용하는 경우 위의 Fisher-Yates 셔플 방법으로 확장하는 방법은 다음과 같습니다. github.com/ryantenney/underscore/commit/…
Steve

이것에 대해 대단히 감사합니다. 당신과 Johns의 대답은 저와 동료가 거의 4 시간을 함께 보냈던 문제를 해결하는 데 도움이되었습니다! 우리는 원래 OP와 비슷한 방법을 사용했지만 무작위 화가 매우 비정상적이라는 것을 알았습니다. 따라서 약간의 jquery로 작동하여 이미지 목록 (슬라이더 용)을 뒤섞어 약간의 변경을 가져 왔습니다. 굉장한 무작위 화.
Hello World

16

이 무작위 정렬 결과가 얼마나 무작위인지 측정했습니다.

내 기술은 작은 배열 [1,2,3,4]을 취하고 그것의 모든 (4! = 24) 순열을 만드는 것이었다. 그런 다음 셔플 링 함수를 배열에 여러 번 적용하고 각 순열이 몇 번 생성되는지 계산합니다. 좋은 셔플 링 알고리즘은 모든 순열에 대해 결과를 균등하게 분배하지만 나쁜 것은 균일 한 결과를 생성하지 않습니다.

아래 코드를 사용하여 Firefox, Opera, Chrome, IE6 / 7 / 8에서 테스트했습니다.

놀랍게도, 무작위 정렬과 실제 셔플은 모두 똑같이 균일 한 분포를 만들었습니다. 그래서 (많은 사람들이 제안했듯이) 주요 브라우저는 병합 정렬을 사용하고있는 것 같습니다. 물론 이것은 다른 브라우저를 사용할 수 없다는 것을 의미하지는 않지만,이 임의 정렬 방법이 실제로 사용하기에 충분히 신뢰할 수 있음을 의미합니다.

편집 : 이 테스트는 실제로 무작위 또는 그 부족을 올바르게 측정하지 못했습니다. 내가 게시 한 다른 답변을 참조하십시오.

그러나 성능 측면에서 크리스토프가 제공하는 셔플 기능은 확실한 승자였습니다. 작은 4 요소 어레이의 경우에도 실제 셔플은 임의 정렬보다 약 2 배 빠릅니다!

// Cristoph가 게시 한 셔플 기능.
var shuffle = 함수 (배열) {
    var tmp, current, top = array.length;

    if (top) while (-top) {
        전류 = Math.floor (Math.random () * (top + 1));
        tmp = 배열 ​​[현재];
        배열 [현재] = 배열 ​​[위];
        배열 [상단] = tmp;
    }

    배열 반환;
};

// 랜덤 정렬 함수
var rnd = function () {
  반환 Math.round (Math.random ())-0.5;
};
var randSort = 함수 (A) {
  return A.sort (rnd);
};

var 순열 = function (A) {
  if (A.length == 1) {
    반환 [A];
  }
  다른 {
    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);
      for (var j = 0; j <subperms.length; j ++) {
        perms.push (x.concat (subperms [j]));
      }
    }
    반환 파마;
  }
};

var test = 함수 (A, 반복, 기능) {
  // 초기화 순열
  var 통계 = {};
  var perms = 순열 (A);
  for (var i in perms) {
    통계 [ ""+ perms [i]] = 0;
  }

  // 여러 번 섞어 통계를 수집
  var start = new Date ();
  for (var i = 0; i <iterations; i ++) {
    var shuffled = func (A);
    통계 [ ""+ shuffled] ++;
  }
  var end = new Date ();

  // 결과 형식
  var arr = [];
  for (통계의 var i) {
    arr.push (i + ""+ stats [i]);
  }
  return arr.join ( "\ n") + "\ n \ n 걸린 시간 :"+ ((end-start) / 1000) + "seconds.";
};

alert ( "무작위 정렬 :"+ 테스트 ([1,2,3,4], 100000, randSort));
alert ( "셔플 :"+ 테스트 ([1,2,3,4], 100000, 셔플));

11

흥미롭게도 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"));

왜 0.5이어야하는지 모르겠습니다-Math.random (), 왜 Math.random ()이 아닙니까?
Alexander Mills

1
@AlexanderMills : 전달 된 비교기 함수 sort()a및 의 비교에 따라 0보다 크거나 작거나 같은 숫자를 반환해야합니다 b. ( developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… )
LarsH

@LarsH 네, 이해가 되겠습니다
Alexander Mills

9

내 웹 사이트에 간단한 테스트 페이지 를 배치 하여 다른 방법으로 셔플하는 다른 브라우저를 사용하여 현재 브라우저의 바이어스를 보여줍니다. 그것은 바이어스를 사용 Math.random()-0.5하지 않는 또 다른 '임의'셔플과 위에서 언급 한 Fisher-Yates 방법을 사용하는 것의 끔찍한 편견을 보여줍니다 .

일부 브라우저에서는 '셔플'중에 특정 요소가 전혀 바뀌지 않을 확률이 50 % 나 높다는 것을 알 수 있습니다!

참고 : 코드를 다음과 같이 변경하면 Safari에서 @Christoph에 의해 Fisher-Yates 셔플을 약간 더 빠르게 구현할 수 있습니다.

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 셔플 변형도 있습니다.
Daniel Martin

@DanielMartin 하나의 라이너가 답이되어야합니다. 또한 구문 분석 오류를 피하려면 다음과 같이 두 개의 세미콜론을 추가해야합니다 arr = arr.map(function(n){return [Math.random(),n];}).sort().map(function(n){return n[1];});.
Giacomo1968

2

확실히 해킹입니다. 실제로 무한 루프 알고리즘은 불가능합니다. 객체를 정렬하는 경우 coords 배열을 반복하여 다음과 같은 작업을 수행 할 수 있습니다.

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

4 년이 지났지 만 사용하는 정렬 알고리즘에 관계없이 임의 비교기 방법이 올바르게 배포되지 않을 것이라고 지적하고 싶습니다.

증명:

  1. n요소 배열의 경우 정확히 n!순열 (즉, 가능한 셔플)이 있습니다.
  2. 셔플 중 모든 비교는 두 세트의 순열 중에서 선택합니다. 랜덤 비교기의 경우 각 세트를 선택할 확률이 1/2입니다.
  3. 따라서 각 순열 p에 대해 순열 p로 끝나는 확률은 분모가 2 ^ k 인 분수입니다 (일부 k의 경우). 이는 분수의 합이므로 (예 : 1/8 + 1/16 = 3/16 ).
  4. n = 3의 경우, 똑같이 6 개의 순열이 있습니다. 따라서 각 순열의 확률은 1/6입니다. 1/6은 분모가 2의 거듭 제곱 인 분수로 표현 될 수 없습니다.
  5. 따라서 코인 플립 정렬은 셔플의 공정한 분배를 초래하지 않습니다.

올바르게 분배 될 수있는 유일한 크기는 n = 0,1,2입니다.


연습으로, n = 3에 대해 다른 정렬 알고리즘의 의사 결정 트리를 작성하십시오.


증명에 차이가 있습니다. 정렬 알고리즘이 비교기의 일관성에 의존하고 일관성이없는 비교기로 런타임에 제한이없는 경우 무한한 확률의 합을 가질 수 있으며,이 경우에도 1/6까지 추가 할 수 있습니다 합계의 모든 분모는 2의 거듭 제곱입니다. 1을 찾으십시오.

또한, 비교기가 어느 하나의 대답 (예를 들어 (Math.random() < P)*2 - 1, 상수를 위해 P) 을 줄 고정 된 기회를 갖는 경우 , 상기 증거는 유지된다. 비교기가 이전 답변을 기반으로 확률을 변경하면 공정한 결과를 생성 할 수 있습니다. 주어진 분류 알고리즘에 대한 이러한 비교기를 찾는 것이 연구 논문이 될 수 있습니다.


1

D3을 사용하는 경우 내장 셔플 기능이 있습니다 (Pfisher-Yates 사용).

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

그리고 여기 Mike가 이에 대해 자세히 설명합니다.

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

    구현에는 많은 수의 요소를 건드리지 않을 위험이 높습니다. 그들은 열등한 요소의 양이 맨 위로 밀려서 전체 배열에서 이동합니다. 셔플 링에 그려진 패턴이있어 신뢰할 수 없습니다.
    Kir Kanos

    @ 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 ()에 전달하는 함수 는 일반적으로 다음과 같습니다.

    sortingFunc (first, second) 함수
    {
      // 예:
      첫 번째-두 번째 반환;
    }
    

    sortingFunc의 직업은 다음을 반환하는 것입니다.

    • 첫 번째가 두 번째보다 빠르면 음수
    • 두 번째 이후에 먼저 가야하는 경우 양수
    • 완전히 같으면 0

    위의 정렬 기능은 순서대로 정렬합니다.

    당신이 가진 것과 무작위로-와 +를 반환하면 임의의 순서를 얻습니다.

    MySQL처럼 :

    ORDER BY rand () 테이블에서 SELECT *
    

    5
    거기에 있다 뭔가 잘못이 방법으로 다음 JS 구현에 의해 사용되는 정렬 알고리즘에 따라 확률이 동등하게 분배되지 않습니다!
    Christoph

    우리가 실제로 걱정하는 것입니까?
    bobobobo 2016 년

    4
    @bobobobo : 응용 프로그램에 따라, 예, 가끔 그렇습니다. 또한 올바르게 작동 shuffle()하는 것은 한 번만 작성하면되므로 실제로 문제가되지는 않습니다. 코드 저장소에 스 니펫을 넣고 필요할 때마다 발굴하십시오
    Christoph
    당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
    Licensed under cc by-sa 3.0 with attribution required.