สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore
ค้นหาสมาชิกที่ปรากฏมากกว่า n/2 ครั้งด้วยอัลกอริทึมการลงคะแนนแบบ Boyer-Moore ซึ่งใช้เวลาเชิงเส้นและพื้นที่ O(1) พร้อมพิสูจน์ความถูกต้อง
สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA 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])) # 2Boyer-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) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore”
ค้นหาสมาชิกที่ปรากฏมากกว่า n/2 ครั้งด้วยอัลกอริทึมการลงคะแนนแบบ Boyer-Moore ซึ่งใช้เวลาเชิงเส้นและพื้นที่ O(1) พร้อมพิสูจน์ความถูกต้อง คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- แม่แบบการแบ่งและพิชิต
- นับจำนวนคู่กลับลำดับด้วยการเรียงลำดับแบบผสานที่ดัดแปลง
- สมาชิกเสียงข้างมาก: การลงคะแนนแบบ Boyer-Moore
- มัธยฐานของอาร์เรย์เรียงลำดับสองชุด