0Pricing
DSA Interview Prep · درس

عدّ البتات والرقم المفقود وعكس البتات

احسب أعداد البتات للأعداد من 0 إلى n باستخدام DP وحيلة أقل بت مضبوط، واعثر على رقم مفقود باستخدام XOR، واعكس بتات عدد صحيح من 32 بت.

عدّ البتات والرقم المفقود وعكس البتات درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

نظرة عامة على مسألة عدّ البتات

تطلب مسألة Counting Bits (LeetCode 338)، عند إعطاء n، إرجاع مصفوفة ans بحجم n+1 بحيث يكون ans[i] هو عدد البتات 1 في i. أما النهج الساذج فزمنه O(n log n)، إذ يحسب البتات في كل عدد على حدة. ويحقق نهج DP زمنًا قدره O(n) من خلال استغلال العلاقة بين i ونصفه أو بأقل بت مضبوط فيه.

تدعم ملاحظتان أساسيتان نهج DP: (1) تؤدي i >> 1 إلى إسقاط أقل بت، ولذلك bits[i] = bits[i >> 1] + (i & 1). (2) لمسح أقل بت مضبوط: bits[i] = bits[i & (i-1)] + 1. يحقق كلا النهجين زمنًا قدره O(n) ومساحة قدرها O(n) لمصفوفة الناتج.

def count_bits_v1(n):
    # O(n log n): naive individual count
    return [bin(i).count('1') for i in range(n + 1)]

def count_bits_dp(n):
    # O(n): DP using right shift
    dp = [0] * (n + 1)
    for i in range(1, n + 1):
        dp[i] = dp[i >> 1] + (i & 1)   # i >> 1 drops last bit
    return dp

def count_bits_dp2(n):
    # O(n): DP using lowest-set-bit trick
    dp = [0] * (n + 1)
    for i in range(1, n + 1):
        dp[i] = dp[i & (i - 1)] + 1   # i & (i-1) clears lowest set bit
    return dp

n = 10
print('Naive:', count_bits_v1(n))
print('DP v1:', count_bits_dp(n))
print('DP v2:', count_bits_dp2(n))

لماذا تعمل علاقات التكرار في DP

بالنسبة إلى علاقة التكرار بالإزاحة إلى اليمين dp[i] = dp[i >> 1] + (i & 1)، فإن القسمة على 2 (الإزاحة إلى اليمين) تزيل البت الأخير. فإذا كان البت الأخير يساوي 1، يزداد العدد بمقدار 1، وإذا كان يساوي 0 فلا يتغير. لذلك bits[i] = bits[i // 2] + (i mod 2).

وبالنسبة إلى علاقة التكرار باستخدام أقل بت مضبوط dp[i] = dp[i & (i-1)] + 1، فإن i & (i-1) تمسح أقصى بت 1 إلى اليمين، ولذلك تحتوي على بت مضبوط واحد أقل من i. ومن ثم يكون العدد المطلوب هو عدد البتات في القيمة الناتجة بعد تقليلها، مضافًا إليه 1. تعالج كلتا علاقتي التكرار قيم i بترتيب تصاعدي، لذا تُحل المسائل الأصغر دائمًا أولًا.

# Trace both recurrences for i = 0..8
print('i | i>>1 | i&1 | dp[i>>1]+(i&1) | i&(i-1) | 1+dp[i&(i-1)]')
print('-' * 60)
dp = [0] * 9
for i in range(1, 9):
    # Right shift method
    v1 = dp[i >> 1] + (i & 1)
    # Lowest set bit method
    v2 = dp[i & (i - 1)] + 1
    dp[i] = v1   # either works
    print(f'{i:2d} ({bin(i)[2:]:4s}) | {i>>1:2d} | {i&1} | {v1}               | {i&(i-1):2d}      | {v2}')
print('\nFinal dp:', dp)

العدد المفقود: نهجا XOR والمجموع

تعطي مسألة Missing Number (LeetCode 268) مصفوفة تحتوي على n عددًا مميزًا من [0, n]، مع وجود عدد واحد مفقود تمامًا. في نهج XOR، نُجري XOR على جميع الفهارس من 0 إلى n وعلى جميع القيم في المصفوفة. تلغي الأزواج بعضها، ويبقى العدد المفقود. أما نهج المجموع فيستخدم expected = n*(n+1)//2 ثم يعيد expected - sum(nums).

كلا النهجين يعملان بزمن O(n) ومساحة O(1). ويكون نهج XOR أكثر متانة في اللغات التي تستخدم أعدادًا صحيحة بعرض ثابت، لأنه يتجنب احتمال تجاوز السعة. أما في Python، فيعمل النهجان جيدًا لأن الأعداد الصحيحة ذات دقة اعتباطية.

def missing_xor(nums):
    n = len(nums)
    result = n
    for i, val in enumerate(nums):
        result ^= i ^ val   # each index i cancels its matching value
    return result

def missing_sum(nums):
    n = len(nums)
    return n * (n + 1) // 2 - sum(nums)

test_cases = [
    [3, 0, 1],           # missing 2
    [0, 1],              # missing 2
    [9,6,4,2,3,5,7,0,1], # missing 8
    [0],                 # missing 1
]
for nums in test_cases:
    print(f'{nums} => XOR={missing_xor(nums)}, Sum={missing_sum(nums)}')

عكس بتات عدد صحيح من 32 بتًا

تطلب مسألة Reverse Bits (LeetCode 190) عكس التمثيل الثنائي لعدد صحيح غير موقّع مكوّن من 32 بتًا. يعتمد النهج التكراري على معالجة كل بت من البتات الـ32 من اليمين إلى اليسار في الإدخال، ووضعها من اليسار إلى اليمين في الناتج. في كل تكرار، استخرجوا البت الأيمن باستخدام n & 1، ثم أزيحوا الناتج إلى اليسار لإفساح المجال، وأجروا OR لإدخال البت، ثم أزيحوا n إلى اليمين.

بعد 32 تكرارًا، يحتوي العدد الناتج على جميع بتات n بترتيب معكوس. وهذا يحقق O(32) = O(1) لكل استدعاء، أو O(1) بالتكلفة الموزعة عند استخدام التخزين المؤقت لاستدعاءات متكررة على أجزاء من 8 بتات.

def reverse_bits(n):
    result = 0
    for _ in range(32):
        result = (result << 1) | (n & 1)  # shift result left, OR in rightmost bit
        n >>= 1                            # move to next bit
    return result

# Test with known values
print(reverse_bits(0b00000010100101000001111010011100))  # 964176192
print(reverse_bits(0b11111111111111111111111111111101))  # 3221225471
print(reverse_bits(0))   # 0
print(reverse_bits(1))   # 2147483648 (bit 0 goes to bit 31)
print(reverse_bits(0b10000000000000000000000000000000))  # 1

عكس البتات: التقسيم والتغلب

يعكس نهج أسرع بزمن O(log 32) = O(1) البتات باستخدام التبديل بالتقسيم والتغلب. يبدأ بتبديل البتات المتجاورة، ثم تبديل مجموعات البتات المتجاورة ذات الحجم 2، ثم المجموعات ذات الحجم 4، وهكذا. تستخدم كل مرحلة من مراحل التبديل أقنعة لفصل المجموعات المتناوبة، ثم تستخدم الإزاحة لتداخلها. وبعد 5 عمليات تبديل، تنعكس جميع البتات الـ32.

يستخدم هذا النهج عددًا ثابتًا من العمليات O(1) بغض النظر عن الإدخال، ويُستخدم في تطبيقات العتاد. والأقنعة ثوابت: 0x55555555 (نمط 01 متناوب)، و0x33333333 (نمط 0011 متناوب)، و0x0f0f0f0f (نمط 00001111 متناوب)، وغير ذلك.

def reverse_bits_dc(n):
    # Treat n as 32-bit unsigned
    n &= 0xFFFFFFFF
    # Swap adjacent bits
    n = ((n & 0x55555555) << 1)  | ((n >> 1)  & 0x55555555)
    # Swap adjacent 2-bit groups
    n = ((n & 0x33333333) << 2)  | ((n >> 2)  & 0x33333333)
    # Swap adjacent 4-bit groups
    n = ((n & 0x0f0f0f0f) << 4)  | ((n >> 4)  & 0x0f0f0f0f)
    # Swap adjacent bytes
    n = ((n & 0x00ff00ff) << 8)  | ((n >> 8)  & 0x00ff00ff)
    # Swap adjacent 16-bit halves
    n = ((n & 0x0000ffff) << 16) | ((n >> 16) & 0x0000ffff)
    return n & 0xFFFFFFFF

# Verify against iterative version
def reverse_bits_iter(n):
    result = 0
    for _ in range(32):
        result = (result << 1) | (n & 1); n >>= 1
    return result

for test in [0b10110100, 0b11111111, 0, 1, 0xDEADBEEF]:
    assert reverse_bits_dc(test) == reverse_bits_iter(test)
    print(f'{test:#010x} reversed: {reverse_bits_dc(test):#010x}')

عدد البتات 1 (وزن هامنغ)

تطلب مسألة Number of 1 Bits (LeetCode 191) حساب وزن هامنغ (popcount) لعدد صحيح غير موقّع. توجد ثلاثة أساليب بمفاضلات مختلفة: الحلقة البسيطة (O(32))، وطريقة Brian Kernighan (O(k)، حيث k = عدد البتات المضبوطة)، والدالة المضمنة في Python n.bit_count() (3.10+).

تُفضَّل طريقة Brian Kernighan في المقابلات لأنها تبرهن على فهم حيلة n & (n-1). إذ تزيل كل دورة أقل بت مضبوط، ولذلك تعمل الحلقة عددًا من المرات يساوي تمامًا عدد البتات 1، ما يجعلها أسرع بكثير من فحص جميع البتات الـ32 عندما تكون الأعداد الصحيحة متناثرة.

def hamming_weight_naive(n):
    count = 0
    while n:
        count += n & 1
        n >>= 1
    return count

def hamming_weight_kernighan(n):
    count = 0
    while n:
        n &= n - 1   # clear lowest set bit
        count += 1
    return count

# Python 3.10+
# def hamming_weight_builtin(n): return n.bit_count()

for n in [0, 1, 11, 128, 255, 0xDEADBEEF]:
    naive = hamming_weight_naive(n)
    kern  = hamming_weight_kernighan(n)
    bits  = bin(n).count('1')
    print(f'{n:#012b} ({n:10d}): naive={naive}, kern={kern}, bin={bits}')

جمع البتات في نطاق متتالٍ: نهج المجموع التراكمي

تحتاجون أحيانًا إلى حساب عدد البتات 1 في نطاق [l, r] بسرعة. أنشئوا مجموعًا تراكميًا للبتات المضبوطة للأعداد من 0 إلى n: prefix[i] = prefix[i-1] + bin(i).count('1'). ثم يكون عدد البتات في النطاق [l, r] هو prefix[r] - prefix[l-1]. ويتيح ذلك تنفيذ استعلامات النطاق بزمن O(1) بعد معالجة مسبقة بزمن O(n).

يمكن تعميم ذلك على أي قيمة تجميعية تعتمد على البتات ضمن نطاق. فعلى سبيل المثال، يستخدم عدّ الأعداد في [l, r] التي تحتوي على عدد زوجي من البتات المضبوطة تقنية المجموع التراكمي نفسها، ولكن مع دالة تجميع مختلفة.

def build_bit_prefix(n):
    prefix = [0] * (n + 2)
    for i in range(1, n + 1):
        prefix[i] = prefix[i - 1] + bin(i).count('1')
    return prefix

def count_bits_range(prefix, l, r):
    return prefix[r] - prefix[l - 1]

# Build prefix for 0..15
prefix = build_bit_prefix(15)
print('Prefix sums (set bit counts up to i):')
for i in range(16):
    print(f'  i={i:2d} ({bin(i)[2:]:4s}): bits={bin(i).count("1")}, prefix={prefix[i]}')

# Range queries
print(f'\nSet bits in [5, 10]: {count_bits_range(prefix, 5, 10)}')
print(f'Set bits in [1, 15]: {count_bits_range(prefix, 1, 15)}')

عكس البتات للأعداد السالبة

في Python، الأعداد الصحيحة موقّعة وذات عرض اعتباطي. وعند عكس البتات لمسألة LeetCode، يجب التعامل مع الإدخال على أنه عدد صحيح غير موقّع من 32 بتًا. طبّقوا القناع & 0xFFFFFFFF على الإدخال قبل معالجته لضمان أخذ 32 بتًا فقط في الاعتبار. ويجب أن يكون الناتج أيضًا عددًا صحيحًا غير موقّع من 32 بتًا، أي غير سالب.

إذا أُعطيتم عددًا صحيحًا في Python قد يكون سالبًا، بمعنى تمثيل متمم اثنين، فطبّقوا أولًا & 0xFFFFFFFF للحصول على تمثيله غير الموقّع من 32 بتًا، ثم اعكسوا البتات. وتكون النتيجة دائمًا عددًا صحيحًا غير سالب بين 0 و2^32 - 1.

def reverse_bits_signed_safe(n):
    n &= 0xFFFFFFFF   # treat as 32-bit unsigned
    result = 0
    for _ in range(32):
        result = (result << 1) | (n & 1)
        n >>= 1
    return result & 0xFFFFFFFF

# Python treats -1 as all 1s in two's complement
print(f'-1 as 32-bit unsigned: {-1 & 0xFFFFFFFF:#010x}')  # 0xffffffff
print(f'Reversed: {reverse_bits_signed_safe(-1):#010x}')   # 0xffffffff (all 1s reversed = all 1s)

# -2 in 32-bit = 0xFFFFFFFE = 11...10
print(f'-2 as 32-bit unsigned: {-2 & 0xFFFFFFFF:#010x}')  # 0xfffffffe
print(f'Reversed: {reverse_bits_signed_safe(-2):#010x}')   # 0x7fffffff

البرمجة الديناميكية لمعالجة البتات: أنماط عدّ البتات

تكشف مسألة عدّ البتات نمطًا عامًا في DP لمعالجة البتات: إذا عرفتم الإجابة لإصدار أصغر من i، فيمكنكم حسابها لـ i باستخدام عملية على البتات بزمن ثابت. ويمكن تعميم هذا النمط على مسائل أخرى لعدّ البتات، مثل عدّ الأعداد التي تحتوي على k بتات مضبوطة تمامًا في [0, n] باستخدام التعداد الثنائي، أو إيجاد أعلى قوة للعدد 2 تقسم كل عدد.

ومن الملاحظات المفيدة أيضًا أن عدد البتات المضبوطة في i يتبع نمطًا متكررًا ضمن كل نطاق من نطاقات قوى العدد 2. فالنمط في [2^k, 2^(k+1) - 1] يطابق النمط في [0, 2^k - 1] بعد زيادة كل قيمة بمقدار 1، لأن البت k يكون مضبوطًا دائمًا في هذا النطاق.

# Visualise the repeating pattern
def show_bit_pattern(n):
    bits = [bin(i).count('1') for i in range(n + 1)]
    print('i  | bits | pattern')
    for i, b in enumerate(bits):
        block = i.bit_length() - 1 if i > 0 else 0
        print(f'{i:2d} ({bin(i)[2:]:4s}) | {b} | block {block}')
    return bits

bits = show_bit_pattern(15)
# Verify the pattern: bits[i] = bits[i - highest_power] + 1 for i >= 2^k
print('\nVerify pattern:')
for i in range(1, 16):
    highest_pow = 1 << (i.bit_length() - 1)
    if highest_pow < i:
        prev_i = i - highest_pow
        print(f'bits[{i}] = bits[{prev_i}] + 1 = {bits[prev_i]} + 1 = {bits[i]}')

دمج التقنيات الثلاث: تمرين تكاملي

تجمع العديد من مسائل المقابلات التقنية بين عدّ البتات ومنطق الأعداد المفقودة وعكس البتات في سؤال واحد. فعلى سبيل المثال، إذا أُعطيت مصفوفة عناصرها أعداد صحيحة من n بتًا وكان أحدها مفقودًا، فاعثروا على القيمة المفقودة. أو إذا أُعطيتم تدفقًا من أعداد البتات، فأعيدوا بناء العدد الصحيح المفقود. وتتطلب هذه المسائل تمييز التقنية الفرعية المناسبة.

تدرّبوا على بناء خريطة ذهنية: إذا ذكرت المسألة العثور على عناصر مفقودة، ففكروا في XOR أو المجموع. وإذا طلبت «عدّ البتات 1 بكفاءة»، ففكروا في طريقة Kernighan أو DP. وإذا طلبت «عكس البتات»، ففكروا في النهج التكراري أو التقسيم والتغلب. هذه هي الأدوات الأساسية الثلاث لمعالجة البتات في المقابلات التقنية.

# Integrated exercise: given bit-count array, find the missing number
# arr[i] = number of 1 bits in i, for all i in 0..n except one
# Reconstruct the missing number

def find_missing_from_bit_counts(bit_counts, n):
    # Rebuild full count array
    full = [bin(i).count('1') for i in range(n + 1)]
    # Find which index is missing by comparing
    for i, count in enumerate(bit_counts):
        if full[i] != count:
            return i - 1  # the entry before the mismatch is missing
    return n  # last element missing

# Simpler: use XOR on indices matching bit counts
# (This is simplified for illustration)
bits = [0,1,1,2,1,2,2,3,0,1]  # bit counts for 0..9 with 8 missing
# Normal: [0,1,1,2,1,2,2,3,1,2]
# Missing is index 8
full = [bin(i).count('1') for i in range(10)]
missing_idx = None
for i in range(10):
    if i >= len(bits) or bits[i] != full[i]:
        missing_idx = i
        break
print(f'Missing number: {missing_idx}')

التخزين المؤقت لعكس البتات

عند إجراء استدعاءات متكررة لعكس البتات، مثلًا في محاكاة العتاد، خزّنوا النتائج مؤقتًا لأجزاء من 8 بتات. وبما أن كل بايت لا يمكن أن يأخذ إلا 256 قيمة، احسبوا مسبقًا البايت المعكوس لكل قيمة من 0 إلى 255. ولعكس عدد صحيح من 32 بتًا، قسّموه إلى أربعة أجزاء من 8 بتات، واعكسوا كل جزء، ثم أعيدوا تجميعها بترتيب معكوس.

يقلل ذلك كل استدعاء إلى أربعة عمليات بحث في جدول وعمليات على البتات، وهو أسرع بكثير من حلقة من 32 تكرارًا عند معالجة كميات كبيرة. ويُبنى التخزين المؤقت مرة واحدة بزمن O(256 × 8)، ثم يُعاد استخدامه لجميع الاستدعاءات اللاحقة بزمن O(1).

# Build 8-bit reverse cache
def build_reverse_byte_cache():
    cache = [0] * 256
    for i in range(256):
        n, result = i, 0
        for _ in range(8):
            result = (result << 1) | (n & 1)
            n >>= 1
        cache[i] = result
    return cache

cache = build_reverse_byte_cache()

def reverse_bits_cached(n):
    return (cache[n & 0xFF] << 24 |
            cache[(n >> 8) & 0xFF] << 16 |
            cache[(n >> 16) & 0xFF] << 8 |
            cache[(n >> 24) & 0xFF])

# Test
for test in [0b10110100, 0b11111111, 0x12345678]:
    cached  = reverse_bits_cached(test)
    # Reference: iterative
    n, result = test, 0
    for _ in range(32): result = (result << 1) | (n & 1); n >>= 1
    assert cached == result
    print(f'{test:#010x} => {cached:#010x}')

اختبار سريع

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

مراجعة الدرس

تعلمتم في هذا الدرس أن: عدّ البتات يستخدم DP مع dp[i] = dp[i >> 1] + (i & 1) أو dp[i] = dp[i & (i-1)] + 1 لتحقيق زمن O(n)، وأن العدد المفقود يُحل بزمن O(n) ومساحة O(1) عبر إجراء XOR على جميع الفهارس مع جميع القيم أو باستخدام صيغة المجموع الحسابي، وأن عكس 32 بتًا يُنفَّذ تكراريًا بزمن O(32) أو باستخدام تقنية القناع بالتقسيم والتغلب. بعد ذلك سنستكشف المكدسات الرتيبة، بدءًا من ثبات الترتيب المتزايد مقابل المتناقص واستعلامات العنصر الأكبر التالي.

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

هل درس «عدّ البتات والرقم المفقود وعكس البتات» مجاني؟

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

ماذا ستتعلم في «عدّ البتات والرقم المفقود وعكس البتات»؟

احسب أعداد البتات للأعداد من 0 إلى n باستخدام DP وحيلة أقل بت مضبوط، واعثر على رقم مفقود باستخدام XOR، واعكس بتات عدد صحيح من 32 بت. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «عدّ البتات والرقم المفقود وعكس البتات»؟

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

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

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

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

  1. العوامل على مستوى البت: AND وOR وXOR وNOT والإزاحات
  2. الرقم الوحيد وخصائص XOR
  3. أقنعة البتات: الضبط والمسح والتبديل والتحقق
  4. عدّ البتات والرقم المفقود وعكس البتات
← العودة إلى DSA Interview Prep