0Pricing
Coding Interview Prep · درس

العنصر الغالب: تصويت Boyer-Moore

اعثر على العنصر الذي يظهر أكثر من n/2 مرة باستخدام خوارزمية تصويت Boyer-Moore ذات الزمن الخطي والمساحة O(1)، وأثبت صحتها.

العنصر الغالب: تصويت Boyer-Moore درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 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) خريطة التجزئة: احسب التكرارات وأعد العنصر الذي يكون عدّه > 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، فعيّن العنصر الحالي بوصفه candidate الجديد. وإذا طابق العنصر الحالي candidate، فزِد count. وإلا فأنقص count. في النهاية، يكون candidate هو عنصر الأغلبية. تنجح هذه الطريقة لأن عنصر الأغلبية يظهر أكثر من جميع العناصر الأخرى مجتمعة، ولذلك لا يمكن التصويت ضده وإخراجه بالكامل.

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 التقدّم الصافي لـ candidate الحالي. عندما يصل count إلى 0، يكون candidate الحالي قد أُلغي بعدد مساوٍ من العناصر المنافسة، ومن يظهر بعد ذلك يصبح 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

إثبات الصحة

الإثبات: ليكن m عنصر الأغلبية الذي يكون عدّه k > n/2. هل يمكن أن يكون عنصر غير منتمٍ إلى الأغلبية هو candidate في نهاية الخوارزمية؟ لكي يحدث ذلك، يجب أن يكون m قد أُلغي بالكامل. وكل عملية إلغاء لـ m تستهلك ظهورًا واحدًا لعنصر آخر. ولإلغاء مرات الظهور k الخاصة بـ m جميعها، نحتاج إلى ظهور mغاير واحد على الأقل لكل منها. لكن 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 مرة. يمكن لعنصرين كحد أقصى استيفاء هذا الشرط، لأن 3 × n/3 = n. وسّع Boyer-Moore للحفاظ على candidateين مع عدّين. عندما لا يطابق العنصر الجديد أيًّا من candidateين ويكون العدّان موجبين، أنقص العدّين معًا. ويؤكد تمرير تحقق نهائي أي candidateين يتجاوز فعليًا 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 من candidate. ويمكن لـ k-1 عنصرًا كحد أقصى استيفاء هذا الشرط. حافظ على أزواج عددها k-1، يتكون كل منها من (candidate, count). عندما لا يطابق أيٌّ منها العنصر الحالي وتكون جميع العدّادات موجبة، أنقص جميع العدّادات بمقدار 1. تعمل هذه الخوارزمية المعمّمة في زمن O(n) وبمساحة O(k). وفي المقابلات، يكفي عادةً الإلمام بامتداد candidateين لحالة 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 مرة إجمالًا. اعثر تكراريًا على عنصر الأغلبية في كل نصف. إذا اتفق النصفان، فهذه هي الإجابة. وإلا، فاحسب مرات ظهور candidateين في المصفوفة الكاملة وأعد العنصر الذي يظهر مرات أكثر. علاقة التكرار: 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 candidate دائمًا، لكنه قد لا يكون عنصر أغلبية إذا لم يوجد أي عنصر أغلبية. وإذا لم تضمن المسألة وجود عنصر أغلبية، فيجب إجراء تحقق: بعد Boyer-Moore، احسب مرات ظهور candidate. فإذا كان count > n/2، فهو عنصر الأغلبية. وإلا فأعد -1 أو None. يضيف هذا التحقق تمريرة أخرى بتعقيد 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)')

تحقق سريع

اختبر مدى فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

لقد تعلمت في هذا الدرس أن: تصويت Boyer-Moore يعثر على عنصر الأغلبية في زمن O(n) ومساحة O(1)، باستخدام candidate وcount اللذين يلغي أحدهما العناصر غير المنتمية إلى الأغلبية، وأن الخوارزمية تمتد إلى حالة أغلبية n/3 باستخدام candidateين، وتتطلب تمريرة تحقق عندما لا يكون وجود عنصر الأغلبية مضمونًا، وأن الإثبات يعتمد على حقيقة أن عنصر الأغلبية لديه ظهورات أكثر من جميع العناصر الأخرى مجتمعة، مما يجعل إلغاءه بالكامل مستحيلًا. في الدرس التالي سنتناول وسيط مصفوفتين مرتبتين باستخدام البحث الثنائي على حد التقسيم.

الأسئلة الشائعة

هل درس «العنصر الغالب: تصويت Boyer-Moore» مجاني؟

نعم — نص درس «العنصر الغالب: تصويت Boyer-Moore» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «العنصر الغالب: تصويت Boyer-Moore»؟

اعثر على العنصر الذي يظهر أكثر من n/2 مرة باستخدام خوارزمية تصويت Boyer-Moore ذات الزمن الخطي والمساحة O(1)، وأثبت صحتها. تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «العنصر الغالب: تصويت Boyer-Moore»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. قالب التقسيم والغزو
  2. عدّ الانقلابات باستخدام دمج معدّل
  3. العنصر الغالب: تصويت Boyer-Moore
  4. وسيط مصفوفتين مرتبتين
← العودة إلى Coding Interview Prep