과반수 원소: Boyer-Moore 투표
선형 시간 및 O(1) 공간의 Boyer-Moore 투표 알고리즘으로 n/2번보다 많이 나타나는 원소를 찾고, 알고리즘의 정확성을 증명합니다.
과반수 원소: Boyer-Moore 투표은(는) CoddyKit의 무료 Coding Interview Prep 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Coding Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
다수 원소 문제
다수 원소 (LeetCode 169): 길이가 n인 배열에서 n/2번보다 많이 나타나는 원소를 찾습니다. 문제의 조건에 따라 다수 원소는 항상 존재합니다. [3, 2, 3]에서는 답이 3입니다. [2, 2, 1, 1, 1, 2, 2]에서는 답이 2입니다(7개 중 4번 나타남). 접근법으로는 O(n log n)의 정렬부터 우아한 O(n) O(1) Boyer-Moore 투표 알고리즘까지 다양합니다.
# The majority element appears MORE than n/2 times
# So it appears more than all other elements COMBINED
examples = [
[3, 2, 3], # 3 appears 2/3 times > 1/2
[2, 2, 1, 1, 1, 2, 2], # 2 appears 4/7 times > 3.5
[1], # trivially 1
[1, 1, 2, 1], # 1 appears 3/4 times
]
for e in examples:
from collections import Counter
c = Counter(e)
print(f'Array: {e} → majority: {max(c, key=c.get)} (count {max(c.values())})')Boyer-Moore 이전의 접근법
최적의 방법에 앞서 살펴볼 세 가지 접근법은 다음과 같습니다. (1) 정렬: 배열을 sort하면 중앙 원소가 항상 다수 원소입니다(다수 원소가 n/2번보다 많이 나타나기 때문입니다). O(n log n), 공간 O(1)입니다. (2) 해시 맵: 빈도를 세고 count가 n/2보다 큰 원소를 반환합니다. O(n) time, 공간 O(n)입니다. (3) 무작위 표본 추출: 무작위 원소 하나를 선택하고 n/2번보다 많이 나타나는지 확인합니다. 기대 시행 횟수는 O(1)입니다(다수 원소가 선택될 확률이 1/2보다 큽니다). Boyer-Moore는 결정적으로 O(n) time과 O(1) 공간을 달성합니다.
from collections import Counter
def majority_sort(nums):
nums.sort()
return nums[len(nums) // 2] # middle is always majority
def majority_hashmap(nums):
count = Counter(nums)
return max(count, key=count.get)
def majority_random(nums):
import random
n = len(nums)
while True:
candidate = random.choice(nums)
if nums.count(candidate) > n // 2:
return candidate
nums = [2, 2, 1, 1, 1, 2, 2]
print(majority_sort(nums[:])) # 2
print(majority_hashmap(nums)) # 2Boyer-Moore 투표 알고리즘
Boyer-Moore 투표 알고리즘은 candidate와 count를 유지합니다. 배열을 순회하면서 count == 0이면 현재 원소를 새로운 후보로 설정합니다. 현재 원소가 후보와 같으면 count를 증가시킵니다. 그렇지 않으면 count를 감소시킵니다. 마지막에는 후보가 다수 원소가 됩니다. 다수 원소는 다른 모든 원소를 합친 것보다 더 많이 나타나므로 완전히 투표에서 밀려날 수 없기 때문에 이 방법이 성립합니다.
def majority_element(nums):
candidate = None
count = 0
for num in nums:
if count == 0:
candidate = num # new candidate
if num == candidate:
count += 1
else:
count -= 1
return candidate
print(majority_element([3, 2, 3])) # 3
print(majority_element([2, 2, 1, 1, 1, 2, 2])) # 2
print(majority_element([1])) # 1알고리즘의 직관
직관적으로 각 원소가 다른 원소 하나의 출현을 ‘상쇄’한다고 생각해 보십시오. 다수 원소(count > n/2)는 다른 모든 원소의 출현 횟수를 합친 것보다 더 많이 나타나므로, 다수가 아닌 모든 원소를 상쇄하고도 출현이 남습니다. count 변수는 현재 후보의 순수 우세를 추적합니다. count가 0이 되면 현재 후보는 그만큼의 반대 원소에 의해 상쇄된 것입니다. 다음에 등장하는 원소가 새로운 후보가 됩니다.
def bm_trace(nums):
candidate = count = 0
for i, num in enumerate(nums):
if count == 0:
candidate = num
old_count = count
if num == candidate: count += 1
else: count -= 1
print(f'num={num}: candidate={candidate}, count: {old_count}→{count}')
return candidate
bm_trace([2, 2, 1, 1, 1, 2, 2])
# 2→c=1, 2→c=2, 1→c=1, 1→c=0, 1→new cand=1 c=1, 2→c=0, 2→new cand=2 c=1정확성 증명
증명해 보겠습니다. m을 count가 k > n/2인 다수 원소라고 하겠습니다. 알고리즘이 끝났을 때 다수가 아닌 원소가 후보가 될 수 있을까요? 그러려면 m이 완전히 상쇄되어야 합니다. m을 한 번 상쇄할 때마다 다른 원소 하나의 출현이 필요합니다. m의 k번 출현을 모두 상쇄하려면 다수가 아닌 원소가 최소 k번 나타나야 합니다. 그러나 k > n/2이고, m이 아닌 원소의 전체 개수는 n-k < n/2 < k입니다. 이는 모순이므로 m은 완전히 상쇄될 수 없습니다.
# Proof by contradiction visualised:
# Array: [M, M, M, A, B, A, B] (M is majority, 4/7 times)
# Cancellations: M-A, M-B, M-A, M-B would need 4 non-M elements
# But there are only 4 non-M elements and 4 M's > n/2 = 3.5
# So M can survive: after cancellations, at least 1 M remains uncancelled
def verify_bm(tests):
for nums in tests:
result = majority_element(nums)
brute = max(set(nums), key=nums.count)
assert result == brute, f'Mismatch: {nums} → BM={result}, Brute={brute}'
print('All tests passed!')
def majority_element(nums):
c = cnt = 0
for n in nums:
if cnt == 0: c = n
cnt += 1 if n == c else -1
return c
verify_bm([[1],[3,2,3],[1,1,2,1],[2,2,1,1,1,2,2]])다수 원소 II: n/3 초과
다수 원소 II (LeetCode 229): n/3번보다 많이 나타나는 모든 원소를 찾습니다. 이 조건을 만족할 수 있는 원소는 최대 2개입니다(3 × n/3 = n이기 때문입니다). 두 개의 후보와 두 개의 개수로 Boyer-Moore를 확장합니다. 새로운 원소가 어느 후보와도 일치하지 않고 두 개수가 모두 양수이면 두 개수를 모두 감소시킵니다. 마지막 검증 순회에서 실제로 n/3을 초과하는 후보를 확인합니다.
def majority_element_ii(nums):
cand1 = cand2 = None
count1 = count2 = 0
for num in nums:
if num == cand1: count1 += 1
elif num == cand2: count2 += 1
elif count1 == 0: cand1, count1 = num, 1
elif count2 == 0: cand2, count2 = num, 1
else:
count1 -= 1
count2 -= 1
# Verify: candidates must exceed n/3
n = len(nums)
return [c for c in [cand1, cand2]
if c is not None and nums.count(c) > n // 3]
print(majority_element_ii([3, 2, 3])) # [3]
print(majority_element_ii([1, 2])) # [1, 2]
print(majority_element_ii([1, 1, 1, 3, 3, 2, 2, 2])) # [1, 2]일반화된 Boyer-Moore: n/k 다수 원소
Boyer-Moore는 k-1개의 후보를 사용해 n/k번보다 많이 나타나는 모든 원소를 찾도록 일반화할 수 있습니다. 이 조건을 만족할 수 있는 원소는 최대 k-1개입니다. k-1개의 (후보, 개수) 쌍을 유지합니다. 일치하는 후보가 없고 모든 개수가 양수이면 모든 개수를 1씩 감소시킵니다. 이 일반화된 알고리즘은 O(n) time과 O(k) 공간으로 실행됩니다. 면접에서는 보통 두 후보를 사용하는 (n/3) 확장만 알고 있어도 충분합니다.
def majority_nk(nums, k):
'''Find all elements appearing more than n/k times.'''
counts = {} # candidate -> count
for num in nums:
counts[num] = counts.get(num, 0) + 1
if len(counts) >= k:
# Remove all candidates by decrementing
new_counts = {c: cnt-1 for c, cnt in counts.items() if cnt > 1}
counts = new_counts
# Verify
threshold = len(nums) // k
return [c for c in counts if nums.count(c) > threshold]
print(majority_nk([1,2,3,1,2,1,2,1], 3)) # [1, 2] (both > 8/3 ≈ 2.67)
print(majority_nk([1,1,1,2,2,3,3,3], 4)) # [1, 3] (both > 8/4 = 2)분할 정복 다수 원소
분할 정복 접근법은 배열을 두 절반으로 나눕니다. 전체 배열의 다수 원소는 적어도 한 절반에서는 다수여야 합니다(어느 절반에서도 다수가 아니라면 전체에서 n/2번보다 많이 나타날 수 없습니다). 각 절반의 다수 원소를 재귀적으로 찾습니다. 두 절반의 결과가 같으면 그것이 답입니다. 다르면 전체 배열에서 두 후보의 출현 횟수를 세고 더 많이 나타나는 후보를 반환합니다. 점화식은 T(n) = 2T(n/2) + O(n) → O(n log n)입니다.
def majority_dc(nums, lo=None, hi=None):
if lo is None: lo, hi = 0, len(nums) - 1
if lo == hi: return nums[lo]
mid = (lo + hi) // 2
left_maj = majority_dc(nums, lo, mid)
right_maj = majority_dc(nums, mid + 1, hi)
if left_maj == right_maj:
return left_maj
# Count both candidates across the sub-range
left_count = sum(1 for i in range(lo, hi+1) if nums[i] == left_maj)
right_count = sum(1 for i in range(lo, hi+1) if nums[i] == right_maj)
return left_maj if left_count > right_count else right_maj
print(majority_dc([3, 2, 3])) # 3
print(majority_dc([2, 2, 1, 1, 1, 2, 2])) # 2Boyer-Moore와 다른 방법 비교
다수 원소 문제의 방법을 비교해 보겠습니다. 정렬: O(n log n) time, 공간 O(1), 원본을 변경합니다. 해시 맵: O(n) time, 공간 O(n), 원본을 변경하지 않습니다. 분할 정복: O(n log n) time, 호출 스택 공간 O(log n)입니다. Boyer-Moore: O(n) time, 공간 O(1), 한 번 순회하며 원본을 변경하지 않습니다. 이 문제에서는 Boyer-Moore가 다른 방법보다 명백히 우수합니다. 면접에서는 더 쉬운 해시 맵 접근법을 간단히 언급한 뒤 항상 Boyer-Moore부터 제시하십시오.
import time, random
nums = [random.randint(1, 100) for _ in range(500000)]
# Make element 42 the majority
nums = [42] * 300000 + nums[:200000]
random.shuffle(nums)
start = time.time()
from collections import Counter
hm = Counter(nums).most_common(1)[0][0]
print(f'HashMap: {hm} in {time.time()-start:.4f}s')
def bm(nums):
c = cnt = 0
for n in nums:
if cnt == 0: c = n
cnt += 1 if n == c else -1
return c
start = time.time()
result = bm(nums)
print(f'Boyer-Moore: {result} in {time.time()-start:.4f}s')
print(f'Both correct: {hm == result}')다수 원소가 보장되지 않는 경우
Boyer-Moore는 항상 후보를 반환하지만, 다수 원소가 존재하지 않으면 그 후보가 다수 원소가 아닐 수 있습니다. 문제에서 다수 원소를 보장하지 않는다면 검증해야 합니다. Boyer-Moore를 실행한 뒤 후보의 출현 횟수를 셉니다. count가 > n/2이면 해당 원소가 다수 원소입니다. 그렇지 않으면 -1 또는 None을 반환합니다. 이 검증으로 O(n) 순회가 한 번 더 필요하지만 전체 알고리즘은 O(n) time과 O(1) 공간을 유지합니다.
def majority_element_safe(nums):
'''Returns majority element or None if it doesn't exist.'''
# Phase 1: find candidate
candidate = count = 0
for num in nums:
if count == 0:
candidate = num
count += 1 if num == candidate else -1
# Phase 2: verify
if nums.count(candidate) > len(nums) // 2:
return candidate
return None
print(majority_element_safe([3, 2, 3])) # 3 (majority exists)
print(majority_element_safe([1, 2, 3])) # None (no majority)
print(majority_element_safe([1, 2, 1, 2])) # None (tie, neither > n/2)면접 풀이 과정
다수 원소 문제의 면접 접근법은 다음과 같습니다. (1) 초기 접근법으로 정렬(O(n log n), O(1))과 해시 맵(O(n), O(n))을 언급합니다. (2) 최적인 O(n) O(1) 해법으로 Boyer-Moore를 소개합니다. (3) 상쇄 직관을 설명합니다. 다수 원소는 다른 모든 원소를 합친 것보다 더 많이 나타나므로 상쇄될 수 없습니다. (4) 5줄로 깔끔하게 구현합니다. (5) 경계 사례를 처리합니다. 다수 원소가 보장되지 않는다면 검증 순회를 추가합니다. 이 구조는 시간 압박 속에서도 체계적으로 사고하는 모습을 보여 줍니다.
# Clean 5-line Boyer-Moore for interviews
def majority_element(nums):
c, cnt = nums[0], 1
for n in nums[1:]:
cnt += (1 if n == c else -1)
if cnt == 0: c, cnt = n, 1
return c
# Verification (if majority not guaranteed)
def majority_with_check(nums):
c = majority_element(nums)
return c if nums.count(c) > len(nums) // 2 else -1
print(majority_element([3, 2, 3])) # 3
print(majority_element([2, 2, 1, 1, 1, 2, 2])) # 2
print('Time: O(n), Space: O(1)')빠른 확인
이 단원에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념에 대한 이해도를 확인해 보십시오.
단원 복습
이 단원에서는 다음을 배웠습니다. Boyer-Moore 투표는 후보와 count를 사용해 다수가 아닌 원소를 상쇄하면서 O(n) time과 O(1) 공간으로 다수 원소를 찾습니다. 또한 이 알고리즘은 두 후보를 사용해 n/3 다수 원소 문제로 확장할 수 있으며, 다수 원소가 보장되지 않을 때는 검증 순회가 필요합니다. 그리고 증명은 다수 원소의 출현 횟수가 다른 모든 원소의 출현 횟수를 합친 것보다 많으므로 완전히 상쇄될 수 없다는 사실에 기반합니다. 다음에는 분할 경계에서 이진 탐색을 수행해 정렬된 두 배열의 중앙값을 찾는 문제를 다룹니다.
자주 묻는 질문
“과반수 원소: Boyer-Moore 투표” 강의는 무료인가요?
네 — “과반수 원소: Boyer-Moore 투표” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Coding Interview Prep 강의 전체를 잠금 해제할 수 있습니다. Coding Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“과반수 원소: Boyer-Moore 투표”에서 뭘 배우나요?
선형 시간 및 O(1) 공간의 Boyer-Moore 투표 알고리즘으로 n/2번보다 많이 나타나는 원소를 찾고, 알고리즘의 정확성을 증명합니다. 브라우저에서 직접 실행하는 실습 코드로 Coding Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Coding Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Coding Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“과반수 원소: Boyer-Moore 투표” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Coding Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Coding Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 분할 정복 템플릿
- 수정된 병합 정렬로 역전쌍 세기
- 과반수 원소: Boyer-Moore 투표
- 정렬된 두 배열의 중앙값