노트

피셔-예이츠 셔플

Fisher–Yates Shuffle

CS#algorithm · 연결된 개념 2개

쉽게 말하면

피셔-예이츠 셔플은 카드를 맨 뒤 자리부터 채우되, 아직 자리가 안 정해진 카드 중 하나를 무작위로 뽑아 그 자리에 놓는 방식이에요. 그래서 어떤 순서든 똑같은 확률로 나와요.

비유가 깨지는 곳 뽑는 범위가 핵심이에요. 아직 안 정해진 앞쪽이 아니라 배열 전체에서 고르면 일부 순서가 더 자주 나와요. sort(() => Math.random() - 0.5)도 공정하지 않아요.

배열을 모든 순열(Permutation)이 같은 확률로 나오도록 섞는 알고리즘. 끝에서부터 앞으로 오며, 현재 위치의 원소를 그 앞쪽(자기 포함) 중 무작위로 고른 원소와 바꾼다.

function shuffle<T>(arr: T[]): T[] {
  const a = [...arr]
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)) // 0 ≤ j ≤ i
    ;[a[i], a[j]] = [a[j], a[i]]
  }
  return a
}
  • 원소마다 한 번씩만 다루므로 O(n), 추가 공간 없이 제자리에서도 할 수 있다(위 예는 원본을 지키려고 복사했다)
  • j를 0 ≤ j ≤ i가 아니라 배열 전체에서 고르면 일부 순열이 더 자주 나오는 편향이 생긴다. 범위를 정확히 지키는 게 핵심이다
  • arr.sort(() => Math.random() - 0.5)도 흔히 쓰지만 공정하지 않다. 정렬 알고리즘은 일관된 비교를 전제로 하므로 결과가 엔진과 구현에 따라 한쪽으로 치우친다(정렬 알고리즘)
  • Math.random()은 암호학적으로 안전하지 않다. 추첨처럼 공정성이 중요한 곳에서는 crypto.getRandomValues()를 쓴다

도널드 크누스가 『컴퓨터 프로그래밍의 예술』(The Art of Computer Programming)에서 소개해 크누스 셔플이라고도 부른다. 복잡도 감각은 빅오 표기법.

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 투 포인터

    배열이나 문자열에서 위치를 가리키는 포인터 두 개를 두고, 조건에 따라 움직여 가며 답을 찾는 패턴. 추가 메모리 없이(O(1) 공간) 중첩 반복을 한 번의 순회로 줄인다. 보통 정렬된 입력에서 쓴다.

  • 해시 테이블

    키를 해시 함수(Hash Function)로 배열 인덱스로 바꿔 값을 저장하는 자료구조. 조회·추가·삭제가 평균 O(1)이다. JavaScript의 객체와 Map·Set, 파이썬의 dict·set(python-collections), 자바의 HashMap이 모두 해시 테이블이다.

  • 병합 정렬

    배열을 원소 하나가 될 때까지 반으로 나눈 뒤, 정렬된 조각 둘을 하나로 합치기를 반복하는 정렬. 원소 하나짜리 배열은 이미 정렬돼 있다는 점에서 출발한다. 분할 정복의 대표 예다.

  • 섣부른 최적화

    "섣부른 최적화는 모든 악의 근원이다." 도널드 크누스(Donald Knuth)가 1974년 글 「Structured Programming with go to Statements」에서 쓴 말이다. 원문의 맥락은 작은 효율은 대부분(약 97%)의 경우 잊으라는 것이고, 정말 중요한 3%는 놓치지 말라는 말이 이어진다.

  • 언어적 안티패턴

    이름, 타입, 주석이 말하는 것과 코드가 실제로 하는 일이 어긋나는 것. 읽는 사람을 잘못된 추측으로 이끈다.

보기 옵션