0Pricing
Coding Interview Prep · บทเรียน

สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore

ค้นหาสมาชิกที่ปรากฏมากกว่า n/2 ครั้งด้วยอัลกอริทึมการลงคะแนนแบบ Boyer-Moore ซึ่งใช้เวลาเชิงเส้นและพื้นที่ O(1) พร้อมพิสูจน์ความถูกต้อง

สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

ปัญหาสมาชิกเสียงข้างมาก

สมาชิกเสียงข้างมาก (LeetCode 169): ค้นหาสมาชิกที่ปรากฏมากกว่า n/2 ครั้งในอาร์เรย์ความยาว n โดยโจทย์รับประกันว่าสมาชิกเสียงข้างมากมีอยู่เสมอ สำหรับ [3, 2, 3] คำตอบคือ 3 ส่วน [2, 2, 1, 1, 1, 2, 2] คำตอบคือ 2 (ปรากฏ 4 ครั้งจากทั้งหมด 7 ครั้ง) แนวทางมีตั้งแต่การเรียงลำดับที่ใช้เวลา O(n log n) ไปจนถึงอัลกอริทึมการลงคะแนนแบบ Boyer-Moore ที่สง่างามและใช้เวลา O(n) กับพื้นที่ 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())})')

แนวทางก่อนใช้ Boyer-Moore

มีสามแนวทางก่อนถึงแนวทางที่เหมาะสมที่สุด: (1) การเรียงลำดับ: เรียงลำดับอาร์เรย์ แล้วสมาชิกตรงกลางจะเป็นสมาชิกเสียงข้างมากเสมอ (เนื่องจากปรากฏมากกว่า >n/2 ครั้ง) ใช้เวลา O(n log n) และพื้นที่ O(1) (2) แผนที่แฮช: นับความถี่แล้วคืนสมาชิกที่มี count > n/2 ใช้เวลา O(n) และพื้นที่ O(n) (3) การสุ่มตัวอย่าง: เลือกสมาชิกแบบสุ่มและตรวจสอบว่าปรากฏมากกว่า >n/2 ครั้ง โดยคาดว่าจะใช้การทดลอง O(1) ครั้ง (สมาชิกเสียงข้างมากถูกเลือกด้วยความน่าจะเป็น >1/2) ส่วน Boyer-Moore ใช้เวลา O(n) และพื้นที่ 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

อัลกอริทึมการลงคะแนนแบบ Boyer-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

สัญชาตญาณเบื้องหลังอัลกอริทึม

แนวคิดคือ ลองจินตนาการว่าสมาชิกแต่ละตัวสามารถยกเลิกการปรากฏหนึ่งครั้งของสมาชิกตัวอื่นได้ สมาชิกเสียงข้างมาก (มีจำนวนมากกว่า 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 เป็นสมาชิกเสียงข้างมากที่มีจำนวน k > n/2 เมื่ออัลกอริทึมจบลง สมาชิกที่ไม่ใช่เสียงข้างมากจะเป็นตัวเลือกได้หรือไม่ หากเป็นเช่นนั้น m ต้องถูกยกเลิกจนหมด การยกเลิก m แต่ละครั้งต้องใช้การปรากฏหนึ่งครั้งของสมาชิกอื่น หากต้องการยกเลิกการปรากฏของ m ทั้ง k ครั้ง จะต้องมีสมาชิกอื่นที่ไม่ใช่ m อย่างน้อย 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 สามารถปรับใช้เพื่อค้นหาสมาชิกทั้งหมดที่ปรากฏมากกว่า n/k ครั้ง โดยใช้ตัวเลือก k-1 ตัว จะมีสมาชิกที่ตรงเงื่อนไขได้มากที่สุด k-1 ตัว ให้ติดตามคู่ (ตัวเลือก, count) จำนวน k-1 คู่ เมื่อไม่มีคู่ใดตรงกันและตัวนับทั้งหมดเป็นบวก ให้ลดตัวนับทั้งหมดลง 1 อัลกอริทึมแบบทั่วไปนี้ใช้เวลา O(n) และพื้นที่ 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]))  # 2

Boyer-Moore เทียบกับวิธีอื่น

การเปรียบเทียบวิธีการสำหรับสมาชิกเสียงข้างมาก: การเรียงลำดับ: ใช้เวลา O(n log n) พื้นที่ O(1) และทำลายข้อมูลเดิม แผนที่แฮช: ใช้เวลา O(n) พื้นที่ O(n) และไม่ทำลายข้อมูลเดิม การแบ่งแยกและพิชิต: ใช้เวลา O(n log n) และใช้พื้นที่ O(log n) สำหรับสแตกการเรียก Boyer-Moore: ใช้เวลา O(n) พื้นที่ 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 หรือค่าไม่มี การตรวจสอบนี้เพิ่มการประมวลผลอีก O(n) รอบ แต่ความซับซ้อนโดยรวมยังเป็นเวลา O(n) และพื้นที่ 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) แนะนำ Boyer-Moore เป็นคำตอบที่เหมาะสมที่สุดด้วยเวลา O(n) และพื้นที่ O(1) (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 ค้นหาสมาชิกเสียงข้างมากได้ในเวลา O(n) และพื้นที่ O(1) โดยใช้ตัวเลือกกับ count เพื่อหักล้างสมาชิกที่ไม่ใช่เสียงข้างมาก อัลกอริทึมนี้ขยายไปยังกรณีสมาชิกเสียงข้างมาก n/3 ได้ด้วยตัวเลือกสองตัว และต้องมีรอบการตรวจสอบเมื่อไม่ได้รับประกันว่ามีสมาชิกเสียงข้างมาก และ การพิสูจน์อาศัยข้อเท็จจริงที่ว่าสมาชิกเสียงข้างมากมีจำนวนการปรากฏมากกว่าสมาชิกอื่นทั้งหมดรวมกัน จึงไม่สามารถถูกยกเลิกจนหมดได้ ต่อไปเราจะจัดการปัญหามัธยฐานของอาร์เรย์เรียงลำดับสองชุดด้วยการค้นหาแบบทวิภาคบนขอบเขตจุดแบ่ง

คำถามที่พบบ่อย

บทเรียน “สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore”

ค้นหาสมาชิกที่ปรากฏมากกว่า n/2 ครั้งด้วยอัลกอริทึมการลงคะแนนแบบ Boyer-Moore ซึ่งใช้เวลาเชิงเส้นและพื้นที่ O(1) พร้อมพิสูจน์ความถูกต้อง คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม

ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. แม่แบบการแบ่งและพิชิต
  2. นับจำนวนคู่กลับลำดับด้วยการเรียงลำดับแบบผสานที่ดัดแปลง
  3. สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore
  4. มัธยฐานของอาร์เรย์เรียงลำดับสองชุด
← กลับไปที่ Coding Interview Prep