Boyer–Moore 과반수 투표 알고리즘: 후보 하나와 카운터로 과반 찾기, 두 번째 패스가 필요한 이유

Boyer와 Moore가 1980년에 고안한 MJRTY는 후보 하나와 카운터 하나로 과반 원소 후보를 고릅니다. 동작 규칙, 정확성 논증, 과반이 없을 때 생기는 오답과 두 번째 패스를 원 논문 기준으로 정리합니다.

algorithmsmajority-votestreamingcomplexity

n개의 값 중 절반보다 많이 나온 값이 있는지, 있다면 무엇인지 알고 싶다고 합시다. 투표에서 과반 득표자를 찾는 문제이고, 배열에서 “과반 원소(majority element)“를 찾는 문제이기도 합니다. 여기서 과반은 정확히 n/2보다 많다는 뜻입니다. 13표 중 7표는 과반이고, 12표 중 6표는 과반이 아닙니다.

Robert S. Boyer와 J Strother Moore는 이 문제를 값 하나와 카운터 하나로 푸는 알고리즘을 1980년에 고안했고, 논문에서는 MJRTY라고 불렀습니다. 흔히 Boyer–Moore 과반수 투표 알고리즘(majority vote algorithm)이라고 합니다. 이 글은 두 저자의 1991년 논문 “MJRTY—A Fast Majority Vote Algorithm”(Bledsoe 기념 논문집 Automated Reasoning, 105–117쪽, 저자가 공개한 PostScript 사본)과 Moore가 공개한 설명 페이지를 기준으로 씁니다. 논문 후반의 Fortran 검증 시스템 세부 내용과, 이 알고리즘을 일반화한 후속 연구의 내용은 다루지 않습니다.

흔한 방법과 그 비용

논문은 먼저 뻔한 방법을 짚습니다. 표를 한 번 훑으면서 후보마다 득표수를 따로 세는 것입니다. 후보 수가 고정되어 있으면 이 방법은 n에 비례하는 시간에 끝납니다. 후보 수가 정해져 있지 않으면 사정이 다릅니다. 누적 득표를 저장하고 찾는 비용 때문에 실행 시간이 표의 수에 대해 선형보다 나빠질 수 있고, 논문은 n²에 비례할 수도 있다고 적습니다.

값을 정렬할 수 있다면 다른 길도 있습니다. 선형 시간 선택 알고리즘으로 중앙값을 찾고, 그 중앙값이 절반보다 많은 표를 받았는지 확인하는 방법입니다. 과반 원소가 있다면 정렬했을 때 반드시 가운데를 차지하기 때문입니다(이 이유는 이 글의 설명입니다). 논문은 이때 쓰는 Rivest–Tarjan 알고리즘의 비교 횟수 상한을 n > 32일 때 5.43n − 163으로 인용합니다. 이런 선형 시간 선택은 중앙값의 중앙값(BFPRT) 글에서 다뤘습니다.

MJRTY는 비교를 많아야 2n번 합니다. 그리고 값 사이의 대소 관계가 필요 없습니다. 논문의 표현대로 “같은가”만 비교합니다.

규칙은 두 줄이다

알고리즘은 값을 앞에서부터 하나씩 보면서 현재 후보(candidate)와 카운터 한 쌍을 유지합니다. 처음에는 후보가 정해져 있지 않고 카운터는 0입니다. 원소 e를 볼 때마다 다음을 합니다.

  • 카운터가 0이면 후보를 e로 바꾸고 카운터를 1로 둡니다.
  • 카운터가 0이 아니면, e가 현재 후보와 같을 때 카운터를 1 늘리고 다를 때 1 줄입니다.

끝까지 보고 난 뒤 남은 후보가, 과반 원소가 있다면 바로 그 원소입니다. “있다면”이라는 조건이 이 알고리즘에서 가장 중요한 부분이고, 뒤에서 따로 다룹니다.

논문은 이 과정을 전당대회장의 몸싸움에 빗댑니다. 대의원마다 지지 후보의 이름이 적힌 팻말을 들고 있고, 서로 다른 후보를 지지하는 두 사람이 마주치면 동시에 쓰러진다고 합시다. 어떤 후보의 지지자가 나머지 전부를 합친 것보다 많다면 마지막까지 서 있는 사람은 그 후보 지지자뿐입니다. 과반이 없으면 서 있는 사람이 있더라도 그들이 지지하는 후보가 과반이라는 보장은 없습니다. 그래서 의장은 남은 후보의 팻말을, 쓰러진 사람이 든 것까지 포함해 다시 세어야 합니다.

논문은 그래서 알고리즘을 두 부분으로 나눕니다. 서로 다른 표를 짝지어 지우는 “짝짓기(pairing) 단계”와, 남은 후보가 실제로 절반보다 많은 표를 받았는지 세는 “세기(counting) 단계”입니다. 논문은 짝짓기를 n번의 비교로 할 수 있고, 세기 단계도 많아야 n번의 비교가 든다고 설명합니다.

예제로 따라가기

Moore의 설명 페이지와 논문이 같이 쓰는 예제는 13개짜리 수열입니다.

A A A C C B B C C C B C C

각 원소를 본 직후의 상태를 “후보:카운터”로 적으면 설명 페이지의 단계별 그림과 같습니다. 카운터가 0이면 후보가 없다는 뜻으로 ?를 씁니다.

순서 원소 상태
1 A A:1
2 A A:2
3 A A:3
4 C A:2
5 C A:1
6 B ?:0
7 B B:1
8 C ?:0
9 C C:1
10 C C:2
11 B C:1
12 C C:2
13 C C:3

논문의 해설대로 읽으면, 4–6번째 원소(C 두 개와 B 하나)가 앞의 A 세 표를 상쇄해 카운터가 0이 되고, 7번째 B가 잠깐 앞서다가 8번째 C에 지워집니다. 이후 C가 앞서 나가 C:3으로 끝납니다. 이 수열에서 C는 13개 중 7개라서(직접 센 값입니다) 실제로 과반입니다.

마지막 카운터 3이 C의 득표수는 아니라는 점에 주의하세요. 카운터는 짝지어지지 않고 남은 C 표의 수입니다.

왜 맞는가

논문의 증명은 다음 불변식에 기댑니다. i번째 원소까지 처리한 뒤, 지금까지 본 원소는 두 무리로 나눌 수 있습니다.

  • 후보 cand를 지지하는 k개(k는 현재 카운터)
  • 서로 다른 값끼리 둘씩 짝지을 수 있는 나머지

이 불변식이 끝까지 유지되면 결론이 나옵니다. cand가 아닌 어떤 값 x가 n/2보다 많은 표를 받았다고 가정합시다. 둘째 무리는 서로 다른 값끼리 짝지어지므로 x는 그 무리에서 많아야 (n − k)/2표를 가집니다. 그러면 x가 n/2를 넘으려면 첫째 무리에서도 표를 받아야 하는데, 첫째 무리는 전부 cand의 표입니다. 모순이므로, 과반 원소가 있다면 그것은 cand입니다.

불변식 자체는 i에 대한 귀납법으로 보입니다. 논문의 경우 나눔을 요약하면 이렇습니다.

  • 카운터가 0일 때 새 원소를 보면, 지금까지의 원소는 모두 짝지어진 무리에 들어 있으므로 새 원소 하나만으로 첫째 무리를 새로 만듭니다.
  • 새 원소가 cand와 같으면 첫째 무리에 더합니다.
  • 새 원소가 cand와 다르면 첫째 무리에서 하나를 꺼내 새 원소와 짝지어 둘째 무리로 옮깁니다.

과반이 없으면 틀린 답이 나온다

알고리즘은 과반이 없는 입력에도 후보를 하나 내놓습니다. Moore의 설명 페이지는 위 예제에서 첫 번째 C를 A로 바꿔 보라고 합니다.

A A A A C B B C C C B C C

이 수열을 같은 규칙으로 따라가면 역시 C:3으로 끝납니다. 그런데 이제 C는 13개 중 6개이고 A는 4개, B는 3개입니다(직접 센 값입니다). 과반 원소가 없는데 후보는 C입니다.

논문에는 더 짧은 예도 있습니다. A A A B B B C에서는 A와 B가 서로 상쇄되고, 마지막 C가 “부전승”으로 후보가 되어 C:1로 끝납니다. 논문은 표를 다른 순서로 처리했다면 A나 B가 이겼을 수도 있다고 덧붙입니다. 과반이 없을 때 남는 후보는 입력 순서에 달린 값이라 그 자체로는 의미가 없습니다.

두 번째 패스는 언제 생략할 수 있나

과반이 있다고 확인해야 한다면 데이터를 한 번 더 훑어 후보의 실제 개수를 세야 합니다. Moore의 설명 페이지도 이 점을 분명히 적습니다. 과반이 있다는 사실을 알고 있거나 그렇게 가정할 수 있는 경우에만 이 패스를 뺄 수 있습니다.

논문이 드는 예는 SIFT 항공기 제어 시스템입니다. 중복 프로세서와 소프트웨어 투표로 신뢰성을 얻는 시스템인데, 개별 프로세서의 고장률이 충분히 낮아서 한 비행 동안 정상 프로세서의 과반이 매 투표에 합의한다고 가정할 수 있다고 합니다. 이렇게 과반을 가정할 수 있으면 세기 단계를 없앨 수 있고, 논문은 그러면 표를 모아 두었다가 일괄 처리하는 대신 실시간으로 처리할 수도 있다고 적습니다.

그 외에 논문의 Fortran 구현에는 작은 개선이 하나 들어 있습니다.

  • 짝짓기 단계가 끝났을 때 카운터가 0이면 과반이 없다고 바로 답합니다.
  • 카운터 k가 n/2보다 크면 세기 단계 없이 cand를 과반으로 발표합니다. cand의 표가 적어도 k개 있기 때문입니다.

논문은 같은 검사를 첫 루프에서 카운터를 늘릴 때마다 하면 두 번째 패스를 피하는 경우가 생기지만, 그게 실행 시간을 줄이는지는 표의 분포에 달렸다고 말합니다. 그리고 첫 단계를 고쳐서 두 번째 단계를 아예 없애는 방법은 찾지 못했다고 밝힙니다.

Python으로 옮긴 예

아래는 논문의 Fortran 서브루틴과 같은 순서로 동작하도록 이 글에서 옮긴 코드입니다. Fortran 버전이 BOOLE과 CAND 두 값으로 결과를 돌려주듯, 과반 여부와 후보를 함께 반환합니다. 후보를 None으로 표시하지 않고 따로 플래그를 둔 이유는 None 자체가 입력값일 수 있기 때문입니다.

def majority(xs):
    """(과반 원소가 있는지, 그 원소)를 반환한다. xs는 두 번 읽는다."""
    n = len(xs)
    cand, k = None, 0
    # 짝짓기 단계
    for x in xs:
        if k == 0:
            cand, k = x, 1
        elif x == cand:
            k += 1
        else:
            k -= 1
    if k == 0:
        return False, None
    if k > n // 2:
        return True, cand
    # 세기 단계: 후보가 실제로 절반보다 많은지 센다
    count = 0
    for x in xs:
        if x == cand:
            count += 1
            if count > n // 2:
                return True, cand
    return False, None


print(majority(list("AAACCBBCCCBCC")))  # (True, 'C')
print(majority(list("AAAACBBCCCBCC")))  # (False, None)
print(majority(list("AAABBBC")))        # (False, None)

count > n // 2는 정수 범위에서 “n/2보다 크다”와 같은 조건입니다. n이 홀수 13이면 n // 2는 6이고, 7 이상이어야 통과합니다. 세기 단계에서 카운트가 n/2를 넘자마자 끝내는 것도 논문의 Fortran 코드와 같습니다.

몇 가지를 덧붙입니다. 모두 이 코드에 대한 이 글의 설명입니다.

  • 데이터를 두 번 읽고, 시작할 때 len(xs)를 부릅니다. 그래서 이 함수에는 리스트처럼 길이를 알 수 있고 여러 번 순회할 수 있는 컬렉션을 넘겨야 합니다. 한 번만 소비할 수 있는 이터레이터나 스트림은 과반이 보장되더라도 len()에서 바로 실패합니다.
  • == 비교만 씁니다. 정렬할 수 없는 값이나 해시할 수 없는 값에도 그대로 쓸 수 있습니다. 대신 ==가 일관되게 동작해야 합니다. 같은 두 값을 비교했을 때 결과가 매번 다르거나, 어떤 값이 자기 자신과 같지 않다고 나오면 위의 증명이 기대는 “같은 표끼리 무리를 이룬다”는 전제가 깨집니다.
  • 입력 외에 쓰는 추가 공간은 O(1)입니다. 후보와 카운터, n, count, 반복 변수처럼 입력 크기와 상관없이 정해진 개수의 변수만 씁니다.

비용과 접근 방식

논문 기준으로 정리하면 이렇습니다.

  • 비교 횟수: 짝짓기 n번, 세기 많아야 n번, 합쳐 많아야 2n번입니다. 확률이나 평균이 아니라 모든 입력에 대한 상한입니다.
  • 비교 종류: 같은지 다른지만 봅니다. 값에 순서가 없어도 됩니다.
  • 접근 순서: 배열 원소를 앞에서부터 차례로 읽습니다. 논문은 그래서 표가 너무 많아 자기 테이프에서 읽어야 할 때도 효율적으로 쓸 수 있고, 첫 단계 뒤에 테이프를 한 번 되감으면 된다고 설명합니다.

출판에 얽힌 사연

논문 말미의 “Postscript 1991” 절은 이 알고리즘의 출판 과정을 직접 설명합니다. 알고리즘은 1980년, 두 저자가 SRI International에 있을 때 고안되었습니다. 고장 허용(fault tolerance)을 연구하던 동료가 Boyer–Moore 정리 증명기의 논리 안에서 리스트의 과반 원소를 우아하게 정의해 달라고 요청했고, 그 답이 이 알고리즘의 재귀적 표현이었다고 합니다. 1980년 말에 Fortran 버전을 쓰고 정확성을 기계적으로 증명했으며, 1981년 2월에 논문을 써서 투고했습니다.

1981년 두 저자는 텍사스 대학교로 옮겼습니다. 논문이 인용한 Jay Misra의 개인 서신(1990)에 따르면, Misra는 이 알고리즘 발표를 듣고 n/k번보다 많이 나오는 원소를 찾도록 일반화하는 방법을 고민했고, 이후 David Gries와 함께 1982년 “Finding Repeated Elements”(Science of Computer Programming 2권)를 발표했습니다. 이 논문은 Boyer와 Moore의 기술 보고서를 “투고 중”으로 인용했습니다.

정작 Boyer와 Moore의 논문은 Fortran과 기계 검증에 무게를 둔다는 이유로 거듭 거절되었고, 알고리즘 중심으로 다시 쓴 판은 Misra와 Gries의 논문에 이미 포함된 내용이라는 이유로 거절되었습니다. 결국 원래 논문은 1991년 Woody Bledsoe 기념 논문집에 실렸습니다. 저자들은 새 알고리즘이 기계로 검사한 정확성 증명과 함께 처음 발표된 사례라는 점에서 이 논문을 형식 검증과 자동 추론 분야의 작은 이정표로 여긴다고 적었습니다.

쓰기 전에 확인할 것

  • 과반의 뜻: 정확히 n/2보다 많아야 합니다. 절반과 같은 경우는 과반이 아닙니다.
  • 두 번째 패스: 과반이 보장되지 않은 입력이라면 후보의 개수를 다시 세야 합니다. 첫 패스의 결과만으로는 과반이 없는 입력에서도 후보가 나옵니다.
  • 카운터의 의미: 마지막 카운터는 후보의 득표수가 아니라 상쇄되고 남은 표의 수입니다.
  • 비교: 같음 비교만 쓰므로 순서가 없는 값에도 쓸 수 있지만, 그 비교가 일관되어야 합니다.
  • 적용 범위: 이 알고리즘은 절반보다 많은 원소 하나만 다룹니다. n/k번보다 많은 원소를 찾는 문제는 위의 Misra–Gries 논문이 다루며, 이 글의 범위 밖입니다.

참고 자료

모두 2026-10-18에 확인했습니다.

블로그로