0Pricing
Coding Interview Prep · درس

الرقم الوحيد وخصائص XOR

استخدم خاصية المعكوس الذاتي لـ XOR للعثور على العنصر الذي يظهر مرة واحدة في قائمة تظهر جميع عناصرها الأخرى مرتين، ثم وسّع الحل إلى single-number-II وsingle-number-III.

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

مشكلة الرقم الوحيد

تطلب مشكلة الرقم الوحيد (LeetCode 136) ما يلي: بالنظر إلى مصفوفة يظهر فيها كل عنصر مرتين بالضبط باستثناء عنصر واحد، أوجد العنصر الذي يظهر مرة واحدة فقط. يستبعد القيد الذي يتطلب زمنًا قدره O(n) ومساحة قدرها O(1) استخدام جداول التجزئة (بمساحة O(n)) والفرز (بزمن O(n log n) أو بمساحة O(n) التي قد تتطلبها عملية الفرز).

يستخدم الحل الأنيق عملية XOR. نفّذ XOR على جميع العناصر معًا. بما أن العناصر المتطابقة تلغي بعضها (a ^ a = 0)، وبما أن XOR تبديلية وتجميعية، تختفي جميع العناصر المزدوجة ولا يبقى سوى العنصر الوحيد. يُعد هذا من أكثر حلول O(n)/O(1) إرضاءً في البرمجة التنافسية.

def single_number(nums):
    result = 0
    for n in nums:
        result ^= n
    return result

# All pairs cancel, leaving the lone element
print(single_number([2, 2, 1]))              # 1
print(single_number([4, 1, 2, 1, 2]))        # 4
print(single_number([1]))                    # 1
print(single_number([7, 3, 5, 3, 7]))        # 5

# Even more concise with functools.reduce
from functools import reduce
from operator import xor
print(reduce(xor, [2, 2, 1]))  # 1

لماذا تعمل XOR: ثلاث خصائص أساسية

تنبع قوة XOR من تضافر ثلاث خصائص جبرية:

  • المعكوس الذاتي: a ^ a = 0 — القيم المتطابقة تلغي بعضها
  • العنصر المحايد: a ^ 0 = a — إجراء XOR مع الصفر لا يغيّر القيم
  • التبديلية والتجميعية: لا يهم الترتيب ولا طريقة التجميع

تعني هذه الخصائص الثلاث مجتمعةً أن تطبيق XOR على متعدد مجموعات يختزل جميع العناصر التي تظهر عددًا زوجيًا من المرات إلى 0، ولا يُبقي إلا العناصر التي تظهر عددًا فرديًا من المرات. في Single Number I، يظهر عنصر واحد بالضبط مرة واحدة (أي عددًا فرديًا من المرات)، ولذلك فهو ناتج XOR.

# Demonstrating the three XOR properties
print('Self-inverse: a ^ a = 0')
for a in [5, 13, 255, 0]:
    print(f'  {a} ^ {a} = {a ^ a}')

print('Identity: a ^ 0 = a')
for a in [5, 13, 0, 1024]:
    print(f'  {a} ^ 0 = {a ^ 0}')

print('Commutativity and Associativity:')
a, b, c = 3, 5, 7
print(f'  a^b^c = {a^b^c}')
print(f'  c^a^b = {c^a^b}')  # same result
print(f'  (a^b)^c = {(a^b)^c}')
print(f'  a^(b^c) = {a^(b^c)}')  # same result

تتبّع حل مشكلة الرقم الوحيد

لنتتبّع [4, 1, 2, 1, 2] خطوة بخطوة لنرى الإلغاء أثناء حدوثه. ننفّذ XOR على جميع العناصر: 4 ^ 1 ^ 2 ^ 1 ^ 2. وبما أن XOR تبديلية، يمكن إعادة الترتيب على النحو التالي: (1 ^ 1) ^ (2 ^ 2) ^ 4 = 0 ^ 0 ^ 4 = 4. تلغي الأزواج بعضها ولا يبقى سوى 4.

في الخوارزمية الفعلية، لا نعيد الترتيب، بل ننفّذ XOR من اليسار إلى اليمين. لكن النتيجة النهائية واحدة، لأن التبديلية والتجميعية تضمنان عدم تأثير الترتيب في الناتج. يمكنكم تجميع الأزواج ذهنيًا بأي طريقة، وستلغي بعضها جميعًا.

nums = [4, 1, 2, 1, 2]
result = 0
print(f'Start: result = {result} ({bin(result)})')
for n in nums:
    prev = result
    result ^= n
    print(f'XOR {n:2d}: {bin(prev):8s} ^ {bin(n):6s} = {bin(result):8s} = {result}')
print(f'Final: {result}')  # 4

# Alternative: show pair cancellation
print('\nMath view:')
print('4 ^ 1 ^ 2 ^ 1 ^ 2')
print('= 4 ^ (1^1) ^ (2^2)')
print('= 4 ^  0   ^  0')
print('= 4')

Single Number II: ظهور كل عنصر ثلاث مرات

في Single Number II (LeetCode 137)، يظهر كل عنصر ثلاث مرات باستثناء عنصر واحد يظهر مرة واحدة. لا تكفي XOR وحدها — فلم تعد الأزواج تلغي بعضها ضمن مجموعات من ثلاثة. بدلًا من ذلك، نعدّ عدد مرات ظهور كل بت عبر جميع الأعداد. إذا ظهر بت في العنصر المطلوب، فإنه يساهم بمقدار 1، أما في العناصر الثلاثية فإنه يساهم بمقدار 3. نحسب باقي القسمة على 3 للعدد لكل بت لعزل بتات العنصر المطلوب.

يمكننا محاكاة ذلك باستخدام متغيرين صحيحين هما ones وtwos يعملان كعداد على مستوى البتات بترديد modulo 3. هذا أسلوب منطق رقمي: يحتفظ ones بالبتات التي شوهدت عددًا فرديًا من المرات وفق modulo 2، بينما يحتفظ twos بالبتات التي شوهدت مرتين وفق modulo 3.

def single_number_II(nums):
    ones, twos = 0, 0
    for n in nums:
        ones = (ones ^ n) & ~twos   # bits seen 1 mod 3 times
        twos = (twos ^ n) & ~ones   # bits seen 2 mod 3 times
    return ones  # bits seen exactly once

print(single_number_II([2, 2, 3, 2]))    # 3
print(single_number_II([0, 1, 0, 1, 0, 1, 99]))  # 99

# Simpler but O(32) bit-by-bit approach
def single_number_II_simple(nums):
    result = 0
    for bit in range(32):
        total = sum((n >> bit) & 1 for n in nums)
        if total % 3 == 1:
            result |= (1 << bit)
    return result

print(single_number_II_simple([2, 2, 3, 2]))  # 3

Single Number III: ظهور عنصرين مرة واحدة

في Single Number III (LeetCode 260)، يظهر عنصران مرة واحدة لكل منهما، بينما تظهر جميع العناصر الأخرى مرتين. نفّذ XOR على جميع العناصر لتحصل على a ^ b (أي XOR للعنصرين الفريدين). بما أن a ≠ b، فهناك بت واحد على الأقل قيمته 1 في a ^ b — أوجد أدنى بت مفعّل في a ^ b باستخدام diff = xor_all & (-xor_all).

تكون قيمة هذا البت 1 في أحد العنصرين a أو b فقط. قسّم جميع الأعداد إلى مجموعتين وفقًا لكون هذا البت مفعّلًا أم لا. نفّذ XOR على كل مجموعة على حدة؛ فتلغي العناصر المزدوجة بعضها، ويبقى a في إحدى المجموعتين وb في المجموعة الأخرى.

def single_number_III(nums):
    xor_all = 0
    for n in nums:
        xor_all ^= n              # xor_all = a ^ b

    diff = xor_all & (-xor_all)  # isolate lowest differing bit

    a = 0
    for n in nums:
        if n & diff:              # group 1: has the diff bit set
            a ^= n
    b = xor_all ^ a              # a ^ b ^ a = b
    return [a, b]

print(sorted(single_number_III([1, 2, 1, 3, 2, 5])))   # [3, 5]
print(sorted(single_number_III([-1, 0])))               # [-1, 0]
print(sorted(single_number_III([0, 1])))                # [0, 1]

إيجاد العدد المفقود باستخدام XOR

تتمثل مشكلة العدد المفقود (LeetCode 268) في أنه بالنظر إلى مصفوفة تحتوي على n عددًا مميزًا من 0 إلى n، عليكم إيجاد العدد المفقود. نفّذ XOR على جميع أعداد المصفوفة مع جميع الأعداد من 0 إلى n. ستلغي الأزواج بعضها، ولا يبقى سوى العدد المفقود. يحقق هذا زمنًا قدره O(n) ومساحة قدرها O(1).

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

def missing_number_xor(nums):
    n = len(nums)
    result = n              # start with n (the last expected value)
    for i, num in enumerate(nums):
        result ^= i ^ num   # XOR with both index and value
    return result

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

for nums, expected in [([3,0,1], 2), ([0,1], 2), ([9,6,4,2,3,5,7,0,1], 8)]:
    xor_ans = missing_number_xor(nums)
    sum_ans = missing_number_sum(nums)
    print(f'nums={nums}: XOR={xor_ans}, Sum={sum_ans}, expected={expected}')

تبديل القيم باستخدام XOR دون متغير مؤقت

تتيح XOR تبديل قيم متغيرين دون استخدام متغير مؤقت. تكمن الفكرة في أن a ^ b ^ a = b وa ^ b ^ b = a. طبّقوا ثلاث عمليات إسناد باستخدام XOR بالتسلسل: a ^= b، ثم b ^= a، ثم a ^= b. بعد العمليات الثلاث، سيحتوي a على قيمة b الأصلية، وسيحتوي b على قيمة a الأصلية.

ملاحظة مهمة: تفشل هذه الحيلة إذا كان a وb يشيران إلى موقع الذاكرة نفسه (أي إذا كانا المتغير نفسه). في هذه الحالة، تؤدي a ^= a إلى ضبط a على 0، فتُفقد القيمة. في Python، يُعد فك الإسناد المتعدد (a, b = b, a) أكثر أمانًا ووضوحًا. ويُستخدم تبديل XOR أساسًا في سياقات C والأنظمة المضمنة التي لا تتوفر فيها ذاكرة إضافية.

# XOR swap
a, b = 17, 42
print(f'Before: a={a}, b={b}')
a ^= b   # a = 17 ^ 42
b ^= a   # b = 42 ^ (17 ^ 42) = 17
a ^= b   # a = (17 ^ 42) ^ 17 = 42
print(f'After:  a={a}, b={b}')   # a=42, b=17

# The caveat: same variable/reference => broken
c = 99
# If a and b pointed to same value:
c ^= c   # c = 0  (destroyed!)
print(f'Same-variable XOR swap: c={c}')  # 0, not 99

# Pythonic swap: always prefer this
a, b = 17, 42
a, b = b, a   # safe, clear, handles aliases
print(f'Pythonic: a={a}, b={b}')

XOR في التجزئة والمجاميع الاختبارية

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

تُستخدم XOR أيضًا في تكافؤ RAID-5: بالنسبة إلى ثلاثة محركات، خزّنوا XOR لبيانات محركين على المحرك الثالث. إذا تعطل أحد المحركات، نفّذوا XOR على المحركين المتبقيين لإعادة بناء البيانات المفقودة. هذا هو منطق مشكلة الرقم الوحيد معكوسًا تمامًا — إذ إن محرك التكافؤ هو «العنصر الوحيد» الذي يرمّز إلى ما يُلغى عند تنفيذ XOR على المحركات الثلاثة.

# Simple XOR checksum
def xor_checksum(data):
    result = 0
    for byte in data:
        result ^= byte
    return result

data = [0x48, 0x65, 0x6C, 0x6C, 0x6F]  # 'Hello' in ASCII
checksum = xor_checksum(data)
print(f'Checksum: {hex(checksum)}')

# Detect corruption
corrupted = data[:]
corrupted[2] ^= 0xFF   # flip all bits of 3rd byte
new_checksum = xor_checksum(corrupted)
print(f'Original checksum: {hex(checksum)}')
print(f'Corrupted checksum: {hex(new_checksum)}')
print(f'Error detected: {checksum != new_checksum}')

# RAID-5 parity recovery
d1 = [1, 0, 1, 1]
d2 = [0, 1, 1, 0]
parity = [d1[i] ^ d2[i] for i in range(4)]
recovered = [parity[i] ^ d2[i] for i in range(4)]  # recover d1
print(f'd1={d1}, parity={parity}, recovered={recovered}')

XOR ومسائل المجموعات الجزئية

تظهر XOR في مسائل المجموعات الجزئية عندما تحتاجون إلى حساب XOR لجميع المجموعات الجزئية. الفكرة الأساسية هي أنه بالنسبة إلى n عناصر، يظهر كل عنصر في 2^(n-1) مجموعة جزئية بالضبط. إذا كان n > 1، يظهر كل عنصر في عدد زوجي من المجموعات الجزئية، ولذلك تلغى مساهمته في XOR. يكون XOR لجميع قيم XOR للمجموعات الجزئية مساويًا لـ 0 عندما يكون n > 1.

عندما يكون n == 1، تكون المجموعة الجزئية الوحيدة غير الفارغة هي العنصر نفسه، ولذلك يكون XOR لجميع المجموعات الجزئية هو ذلك العنصر. يُختبر هذا النوع من الاستدلال — باستخدام خصائص XOR والعد — في مسائل متقدمة لمعالجة البتات.

from itertools import combinations
from functools import reduce
from operator import xor

def xor_of_all_subsets(arr):
    n = len(arr)
    total_xor = 0
    for r in range(1, n + 1):
        for subset in combinations(arr, r):
            subset_xor = reduce(xor, subset)
            total_xor ^= subset_xor
    return total_xor

# For n > 1, each element appears 2^(n-1) times (even) => cancels
# Result is always 0 for n > 1
for arr in [[1,2,3], [5,7], [1], [1,2,3,4]]:
    result = xor_of_all_subsets(arr)
    predicted = arr[0] if len(arr) == 1 else 0
    print(f'arr={arr}: XOR of all subsets = {result}, predicted = {predicted}')

نمط المقابلات: استخدام XOR لإيجاد العناصر الفريدة

تعرّفوا على نمط استخدام XOR لإيجاد العناصر الفريدة عندما تنص المسألة على ما يلي: 'يظهر كل عنصر k مرات باستثناء عنصر واحد يظهر m مرات، حيث إن m mod k != 0'. في حالة k=2 وm=1 (Single Number I)، نفّذوا XOR على جميع العناصر. وفي حالة k=3 وm=1 (Single Number II)، عدّوا البتات مع أخذ باقي القسمة على 3. وفي حالة k=2 وm=1 مع وجود عنصرين فريدين (Single Number III)، نفّذوا XOR ثم اقسموا العناصر وفقًا لأدنى بت مختلف.

يتمثل الأسلوب العام لأي قيمة k في عدّ إجمالي مرات ظهور كل بت ثم أخذ باقي القسمة على k. إذا كان العدد غير صفري، فإن ذلك البت ينتمي إلى العنصر الفريد. ينتج عن ذلك خوارزمية بزمن O(32n) = O(n) ومساحة O(1) لأي قيمة k.

def single_number_k_times(nums, k):
    '''Find the element that appears m times when all others appear k times.'''
    # Count each bit's occurrence and take mod k
    result = 0
    for bit in range(32):
        total = sum((n >> bit) & 1 for n in nums)
        if total % k != 0:
            result |= (1 << bit)
    # Handle negative 32-bit numbers
    if result >= (1 << 31):
        result -= (1 << 32)
    return result

# k=2, element appears once
print(single_number_k_times([2,2,1], 2))         # 1
# k=3, element appears once
print(single_number_k_times([2,2,3,2], 3))       # 3
# k=4, element appears once
print(single_number_k_times([1,1,1,1,7,2,2,2,2], 4))  # 7

مسائل XOR الشائعة في المقابلات

بالإضافة إلى عائلة مسائل الرقم الوحيد، تظهر XOR في هذه المسائل الشائعة في المقابلات:

  • إيجاد الفرق (LC 389): نفّذوا XOR على جميع محارف السلسلتين؛ فيبقى المحرف الإضافي
  • مسافة هامِنغ (LC 461): نفّذوا XOR على عددين، ثم عدّوا البتات التي قيمتها 1 في الناتج
  • إجمالي مسافة هامِنغ (LC 477): عدّوا الأصفار والآحاد في كل موضع بت عبر جميع الأزواج
  • استعلامات XOR لمصفوفة فرعية (LC 1310): استخدموا مصفوفة XOR التراكمية للاستعلامات عن النطاقات

في كل حالة، تلغي خاصية الإلغاء في XOR التكرار وتحوّل الحل بالقوة الغاشمة ذي O(n²) إلى حل ذي O(n).

# Find the difference between two strings
def find_the_difference(s, t):
    result = 0
    for c in s + t:
        result ^= ord(c)
    return chr(result)

print(find_the_difference('abcd', 'abcde'))  # 'e'

# Hamming distance: count differing bits
def hamming_distance(x, y):
    diff = x ^ y
    count = 0
    while diff:
        count += diff & 1
        diff >>= 1
    return count
    # or: bin(x ^ y).count('1')

print(hamming_distance(1, 4))   # 2: 001 vs 100 differ in bits 0 and 2
print(hamming_distance(3, 1))   # 1: 011 vs 001 differ in bit 1

# Prefix XOR for range queries
def xor_queries(arr, queries):
    prefix = [0] * (len(arr) + 1)
    for i, v in enumerate(arr):
        prefix[i+1] = prefix[i] ^ v
    return [prefix[r+1] ^ prefix[l] for l, r in queries]

print(xor_queries([1,3,4,8], [[0,1],[1,2],[0,3],[3,3]]))

تحقق سريع

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

ملخص الدرس

تعلّمتم في هذا الدرس أن خاصية المعكوس الذاتي لـ XOR (a ^ a = 0) تؤدي إلى إلغاء العناصر المزدوجة، فلا يبقى سوى العنصر الفريد عند تنفيذ XOR على جميع الأعداد معًا، وأن Single Number II تستخدم عدّ البتات مع أخذ باقي القسمة على 3، بينما تقسّم Single Number III العناصر وفقًا لأدنى بت مختلف، وأن XOR تحل أيضًا مسائل العدد المفقود، وإيجاد الفرق، ومسافة هامِنغ، واستعلامات XOR عن النطاقات. بعد ذلك، سنستكشف أقنعة البتات لضبط البتات الفردية ومسحها وتبديلها والتحقق منها.

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

هل درس «الرقم الوحيد وخصائص XOR» مجاني؟

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

ماذا ستتعلم في «الرقم الوحيد وخصائص XOR»؟

استخدم خاصية المعكوس الذاتي لـ XOR للعثور على العنصر الذي يظهر مرة واحدة في قائمة تظهر جميع عناصرها الأخرى مرتين، ثم وسّع الحل إلى single-number-II وsingle-number-III. تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «الرقم الوحيد وخصائص XOR»؟

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

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

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

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

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