0Pricing
Coding Interview Prep · Lekcja

Element większościowy: głosowanie Boyera-Moore’a

Znajdować element występujący więcej niż n/2 razy za pomocą liniowego algorytmu głosowania Boyera-Moore’a o zużyciu pamięci O(1) oraz dowodzić jego poprawności

Element większościowy: głosowanie Boyera-Moore’a to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Problem elementu większościowego

Element większościowy (LeetCode 169): znajdź element, który występuje w tablicy długości n więcej niż n/2 razy. Zgodnie z treścią zadania element większościowy zawsze istnieje. Dla [3, 2, 3] odpowiedzią jest 3. Dla [2, 2, 1, 1, 1, 2, 2] odpowiedzią jest 2 (występuje 4 razy na 7 elementów). Podejścia obejmują sortowanie w czasie O(n log n) oraz elegancki algorytm głosowania Boyera-Moore’a o czasie O(n) i złożoności pamięciowej O(1).

# 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())})')

Podejścia przed algorytmem Boyera-Moore’a

Trzy podejścia poprzedzające optymalne: (1) Sortowanie: posortuj tablicę; element środkowy zawsze jest większościowy (ponieważ występuje >n/2 razy). Złożoność czasowa wynosi O(n log n), a pamięciowa O(1). (2) Mapa haszująca: zlicz częstości wystąpień i zwróć element, którego licznik jest większy niż n/2. Czas wynosi O(n), a pamięć O(n). (3) Losowe próbkowanie: wybierz losowy element i sprawdź, czy występuje >n/2 razy; oczekiwana liczba prób wynosi O(1) (element większościowy jest wybierany z prawdopodobieństwem >1/2). Algorytm Boyera-Moore’a deterministycznie osiąga czas O(n) i pamięć 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))   # 2

Algorytm głosowania Boyera-Moore’a

Algorytm głosowania Boyera-Moore’a przechowuje candidate i count. Przejdź przez tablicę: jeśli count == 0, ustaw bieżący element jako nowy candidate. Jeśli bieżący element jest równy candidate, zwiększ count. W przeciwnym razie zmniejsz count. Na końcu candidate jest elementem większościowym. Działa to, ponieważ element większościowy występuje częściej niż wszystkie pozostałe elementy łącznie — nie można go całkowicie wyeliminować w głosowaniu.

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

Intuicja stojąca za algorytmem

Intuicja: wyobraźmy sobie, że każde wystąpienie elementu „anuluje” jedno wystąpienie innego elementu. Element większościowy (count > n/2) ma więcej wystąpień niż wszystkie pozostałe elementy łącznie, więc może anulować wszystkie elementy niebędące większościowymi i nadal zachować część swoich wystąpień. Zmienna count śledzi przewagę netto bieżącego candidate. Gdy count osiąga 0, bieżący candidate został anulowany przez taką samą liczbę elementów przeciwnych — element, który pojawi się następnie, zostaje nowym candidate.

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

Dowód poprawności

Dowód: niech m będzie elementem większościowym, którego liczność wynosi k > n/2. Czy po zakończeniu algorytmu kandydatem może być element niebędący większościowym? Aby tak się stało, m musi zostać całkowicie anulowany. Każde anulowanie m wymaga jednego wystąpienia pewnego innego elementu. Aby anulować wszystkie k wystąpień m, potrzeba co najmniej k wystąpień elementów innych niż m. Jednak k > n/2, a łączna liczba elementów innych niż m wynosi n-k < n/2 < k. Sprzeczność — m nie może zostać całkowicie anulowany.

# 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]])

Element większościowy II: więcej niż n/3

Element większościowy II (LeetCode 229): znajdź wszystkie elementy występujące więcej niż n/3 razy. Co najwyżej 2 elementy mogą spełniać ten warunek (ponieważ 3 × n/3 = n). Rozszerz algorytm Boyera-Moore’a tak, aby przechowywał dwóch kandydatów i dwa liczniki. Gdy nowy element nie pasuje do żadnego kandydata, a oba liczniki są dodatnie, zmniejsz oba liczniki. Końcowe przejście weryfikujące potwierdza, którzy kandydaci rzeczywiście występują częściej niż n/3 razy.

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]

Uogólniony algorytm Boyera-Moore’a: większość n/k

Algorytm Boyera-Moore’a można uogólnić tak, aby znajdował wszystkie elementy występujące więcej niż n/k razy, używając k-1 kandydatów. Co najwyżej k-1 elementów może spełniać ten warunek. Należy przechowywać k-1 par (candidate, count). Gdy żaden kandydat nie pasuje, a wszystkie liczniki są dodatnie, zmniejsz wszystkie liczniki o 1. Ten uogólniony algorytm działa w czasie O(n) i wykorzystuje O(k) pamięci. Na rozmowach kwalifikacyjnych zwykle wystarczy znajomość rozszerzenia do dwóch kandydatów (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)

Element większościowy metodą dziel i zwyciężaj

Podejście D&C: podziel tablicę na połowy. Element większościowy całej tablicy musi być większościowy w co najmniej jednej połowie (jeśli nie jest większościowy w żadnej z nich, nie może występować więcej niż n/2 razy łącznie). Rekurencyjnie znajdź element większościowy każdej połowy. Jeśli obie połowy wskazują ten sam element, jest to odpowiedź. W przeciwnym razie zlicz wystąpienia obu kandydatów w całej tablicy i zwróć tego, który występuje częściej. Rekurencja: 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]))  # 2

Boyer-Moore a inne metody

Porównanie metod dla problemu elementu większościowego: Sortowanie: czas O(n log n), pamięć O(1), modyfikuje dane. Mapa haszująca: czas O(n), pamięć O(n), nie modyfikuje danych. D&C: czas O(n log n), pamięć stosu wywołań O(log n). Boyer-Moore: czas O(n), pamięć O(1), jedno przejście, nie modyfikuje danych. Boyer-Moore jest dla tego problemu ściśle lepszy. Na rozmowie kwalifikacyjnej po krótkim wspomnieniu o prostszym podejściu z mapą haszującą zawsze należy zacząć od Boyera-Moore’a.

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}')

Gdy nie ma gwarancji istnienia elementu większościowego

Boyer-Moore zawsze zwraca candidate, ale jeśli żaden element nie jest większościowy, candidate może nim nie być. Jeśli zadanie nie gwarantuje istnienia elementu większościowego, należy przeprowadzić weryfikację: po zastosowaniu algorytmu Boyera-Moore’a zlicz wystąpienia candidate. Jeśli count > n/2, jest to element większościowy. W przeciwnym razie zwróć -1 lub None. Ta weryfikacja dodaje kolejne przejście O(n), ale cały algorytm nadal działa w czasie O(n) i wykorzystuje O(1) pamięci.

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)

Przebieg rozwiązania podczas rozmowy kwalifikacyjnej

Podejście do problemu elementu większościowego podczas rozmowy kwalifikacyjnej: (1) Wspomnij o sortowaniu (O(n log n), O(1)) oraz mapie haszującej (O(n), O(n)) jako początkowych podejściach. (2) Przedstaw algorytm Boyera-Moore’a jako optymalne rozwiązanie O(n) O(1). (3) Wyjaśnij intuicję anulowania: element większościowy nie może zostać anulowany, ponieważ występuje częściej niż wszystkie pozostałe elementy łącznie. (4) Zapisz przejrzysty kod w 5 wierszach. (5) Uwzględnij przypadek brzegowy: jeśli istnienie elementu większościowego nie jest gwarantowane, dodaj przejście weryfikujące. Taka struktura pokazuje systematyczne myślenie pod presją czasu.

# 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)')

Szybki test

Sprawdź swoje zrozumienie zagadnień Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.

Podsumowanie lekcji

W tej lekcji poznali Państwo: algorytm głosowania Boyera-Moore’a znajduje element większościowy w czasie O(n) i przy pamięci O(1), używając candidate i count, które anulują elementy niebędące większościowymi, algorytm można rozszerzyć do problemu n/3 za pomocą dwóch kandydatów, a gdy istnienie elementu większościowego nie jest gwarantowane, wymagane jest przejście weryfikujące, oraz dowód opiera się na fakcie, że element większościowy ma więcej wystąpień niż wszystkie pozostałe elementy łącznie, co uniemożliwia jego całkowite anulowanie. Następnie zajmiemy się medianą dwóch posortowanych tablic za pomocą wyszukiwania binarnego na granicy podziału.

Często zadawane pytania

Czy lekcja „Element większościowy: głosowanie Boyera-Moore’a” jest bezpłatna?

Tak — pełny tekst „Element większościowy: głosowanie Boyera-Moore’a” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Element większościowy: głosowanie Boyera-Moore’a”?

Znajdować element występujący więcej niż n/2 razy za pomocą liniowego algorytmu głosowania Boyera-Moore’a o zużyciu pamięci O(1) oraz dowodzić jego poprawności Ćwiczysz Coding Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Coding Interview Prep?

Nie wymagamy żadnego doświadczenia. Coding Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Element większościowy: głosowanie Boyera-Moore’a”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Coding Interview Prep?

Tak. Każda lekcja Coding Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Schemat dziel i zwyciężaj
  2. Zliczanie inwersji za pomocą zmodyfikowanego sortowania przez scalanie
  3. Element większościowy: głosowanie Boyera-Moore’a
  4. Mediana dwóch posortowanych tablic
← Powrót do Coding Interview Prep