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

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

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

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

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

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

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

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

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



답변

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

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

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

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

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

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

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


답변

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!.


답변

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

내 기술은 작은 배열 [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, 셔플));


답변

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


답변

내 웹 사이트에 간단한 테스트 페이지 를 배치 하여 다른 방법으로 셔플하는 다른 브라우저를 사용하여 현재 브라우저의 바이어스를 보여줍니다. 그것은 바이어스를 사용 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


답변

배포판에 대해 까다 롭지 않고 소스 코드를 작게 만들고 싶을 때 좋습니다.

JavaScript (소스가 지속적으로 전송되는 곳)에서 작 으면 대역폭 비용이 달라집니다.


답변

확실히 해킹입니다. 실제로 무한 루프 알고리즘은 불가능합니다. 객체를 정렬하는 경우 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를 제거하십시오)

그래도 해킹. 잘하고 싶다면 열심히해야합니다 🙂