셔플 링에 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 < B및 B < 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.