0Pricing
DSA Interview Prep · درس

عدّ الانقلابات باستخدام دمج معدّل

احسب عدد الانقلابات في مصفوفة — أي الأزواج التي تحقق a[i] > a[j] وi < j — عبر عدّ الانقلابات العابرة للتقسيم أثناء خطوة الدمج.

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

ما المقصود بالانقلاب؟

الانقلاب في المصفوفة هو زوج من الفهارس (i, j) حيث i < j لكن a[i] > a[j]؛ أي يظهر عنصر أكبر قبل عنصر أصغر. على سبيل المثال، في [3, 1, 2]، الانقلابان هما (3,1) و(3,2)، ولذلك يوجد انقلابان. تحتوي المصفوفة المرتبة على 0 من الانقلابات. أما المصفوفة المرتبة عكسيًا المكونة من n عنصرًا فتحتوي على n(n-1)/2 من الانقلابات. ويقيس عدّ الانقلابات مدى بُعد المصفوفة عن الترتيب التصاعدي.

arr = [3, 1, 2]
# Inversions: pairs (i,j) where i<j and arr[i]>arr[j]
inversions = []
for i in range(len(arr)):
    for j in range(i+1, len(arr)):
        if arr[i] > arr[j]:
            inversions.append((arr[i], arr[j]))
print('Inversions in', arr, ':', inversions)
print('Count:', len(inversions))  # 2

# Maximum inversions in n-element array:
import math
n = 5
print(f'Max inversions for n={n}: {n*(n-1)//2}')  # 10 for [5,4,3,2,1]

الأسلوب الساذج O(n²)

يفحص الأسلوب بالقوة الغاشمة جميع الأزواج (i, j) التي تحقق i < j، ويعدّ الأزواج التي يتحقق فيها a[i] > a[j]. يحتاج هذا الأسلوب إلى زمن O(n²) ومساحة O(1). وعندما يكون n = 10⁵، فهذا يعني إجراء 5 × 10⁹ مقارنة، وهو أبطأ من اللازم. أما أسلوب التقسيم والحل باستخدام نسخة معدّلة من فرز الدمج، فيحل المسألة بزمن O(n log n). والفكرة الأساسية هي أنه يمكننا أثناء خطوة الدمج في فرز الدمج عدّ الانقلابات العابرة بين النصفين بكفاءة.

def count_inversions_brute(arr):
    n = len(arr)
    count = 0
    for i in range(n):
        for j in range(i + 1, n):
            if arr[i] > arr[j]:
                count += 1
    return count

print(count_inversions_brute([3, 1, 2]))   # 2
print(count_inversions_brute([5, 4, 3, 2, 1]))  # 10
print(count_inversions_brute([1, 2, 3, 4, 5]))  # 0
print(count_inversions_brute([2, 4, 1, 3, 5]))  # 3

الفكرة الأساسية في فرز الدمج

أثناء دمج نصفين مرتبين L وR، إذا اخترنا العنصر R[j] بدلًا من L[i] لأن R[j] < L[i]، فإن جميع العناصر المتبقية في L بدءًا من الفهرس i ستكون أيضًا أكبر من R[j]. والسبب هو أن L مرتبة. لذلك، في كل مرة نأخذ فيها عنصرًا من النصف الأيمن، نعدّ len(L) - i من الانقلابات العابرة بين النصفين. ويُجرى هذا العد مجانًا، إذ يحدث أثناء الدمج الاعتيادي.

# During merge of [1, 3, 5] and [2, 4, 6]:
# Compare L[0]=1 vs R[0]=2: take L[0]=1, no inversions
# Compare L[1]=3 vs R[0]=2: take R[0]=2, inversions += len(L)-1 = 2 (3>2, 5>2)
# Compare L[1]=3 vs R[1]=4: take L[1]=3, no inversions
# Compare L[2]=5 vs R[1]=4: take R[1]=4, inversions += len(L)-2 = 1 (5>4)
# Compare L[2]=5 vs R[2]=6: take L[2]=5, no inversions
# Take R[2]=6
# Total cross-inversions = 2 + 1 = 3
print('Cross-inversions identified during merge: 3')

تنفيذ فرز الدمج المعدّل

عدّل فرز الدمج ليعيد كلًا من المصفوفة المرتبة وعدد الانقلابات. يساوي إجمالي الانقلابات: انقلابات النصف الأيسر + انقلابات النصف الأيمن + الانقلابات العابرة التي عُثر عليها أثناء الدمج. تعيد الحالة الأساسية عنصرًا واحدًا و0 من الانقلابات. وتعدّ دالة الدمج الانقلابات أثناء الدمج. إجمالي الزمن: O(n log n).

def count_inversions(arr):
    def merge_sort_count(arr):
        if len(arr) <= 1:
            return arr, 0
        mid = len(arr) // 2
        left,  left_count  = merge_sort_count(arr[:mid])
        right, right_count = merge_sort_count(arr[mid:])
        merged, cross_count = merge_count(left, right)
        return merged, left_count + right_count + cross_count
    
    def merge_count(left, right):
        result, count = [], 0
        i = j = 0
        while i < len(left) and j < len(right):
            if left[i] <= right[j]:
                result.append(left[i]); i += 1
            else:
                result.append(right[j]); j += 1
                count += len(left) - i  # all remaining in left are inversions
        result += left[i:] + right[j:]
        return result, count
    
    _, total = merge_sort_count(arr)
    return total

print(count_inversions([3, 1, 2]))        # 2
print(count_inversions([5, 4, 3, 2, 1])) # 10
print(count_inversions([2, 4, 1, 3, 5])) # 3

تتبّع الخوارزمية

تتبّع [2, 4, 1, 3]: قسّمها إلى [2, 4] و[1, 3]. فرز الجزء الأيسر: [2, 4] → [2,4] مرتبة، و0 من الانقلابات. فرز الجزء الأيمن: [1, 3] → [1,3] مرتبة، و0 من الانقلابات. ادمج [2,4] و[1,3]: خذ 1، ثم أضف 2 إلى العدّ بسبب 2>1 و4>1، ثم خذ 2 دون إضافة، ثم خذ 3 وأضف 1 بسبب 4>3، ثم خذ 4. الانقلابات العابرة = 3. الإجمالي = 0+0+3 = 3. للتحقق: الأزواج (2,1) و(4,1) و(4,3) = 3 انقلابات. ✓

def count_with_trace(arr):
    def ms(arr, depth=0):
        indent = '  ' * depth
        if len(arr) <= 1: return arr, 0
        mid = len(arr) // 2
        L, lc = ms(arr[:mid], depth+1)
        R, rc = ms(arr[mid:], depth+1)
        merged, cc = merge_c(L, R)
        print(f'{indent}merge({L},{R}) → cross={cc}')
        return merged, lc + rc + cc
    
    def merge_c(L, R):
        res, c, i, j = [], 0, 0, 0
        while i < len(L) and j < len(R):
            if L[i] <= R[j]: res.append(L[i]); i += 1
            else: res.append(R[j]); j += 1; c += len(L) - i
        return res + L[i:] + R[j:], c
    
    _, total = ms(arr)
    return total

print('Total inversions:', count_with_trace([2, 4, 1, 3]))

لماذا تُحتسب الانقلابات العابرة بشكل صحيح

الصحة: ينتمي كل زوج انقلاب (a[i], a[j]) حيث i < j إلى فئة واحدة بالضبط من ثلاث فئات: (1) كلا العنصرين في النصف الأيسر، ويُحتسبان في الاستدعاء التكراري الأيسر. (2) كلا العنصرين في النصف الأيمن، ويُحتسبان في الاستدعاء التكراري الأيمن. (3) عنصر من النصف الأيسر أكبر من عنصر من النصف الأيمن، ويُحتسب الزوج أثناء الدمج بوصفه انقلابًا عابرًا. هذه الفئات متباينة وشاملة، ولذلك لا يُحتسب أي انقلاب مرتين ولا يُغفل أي انقلاب. وتُعد حجة التقسيم هذه البرهان القياسي على صحة التقسيم والحل.

# Verification: compare with brute force on random arrays
import random

def count_brute(arr):
    n = len(arr)
    return sum(1 for i in range(n) for j in range(i+1,n) if arr[i]>arr[j])

def count_dc(arr):
    def ms(a):
        if len(a)<=1: return a, 0
        m=len(a)//2
        L,lc=ms(a[:m]); R,rc=ms(a[m:])
        res,c,i,j=[],0,0,0
        while i<len(L) and j<len(R):
            if L[i]<=R[j]: res.append(L[i]);i+=1
            else: res.append(R[j]);j+=1;c+=len(L)-i
        return res+L[i:]+R[j:],(lc+rc+c)
    return ms(arr)[1]

for _ in range(100):
    arr = random.choices(range(20), k=random.randint(1,10))
    assert count_dc(arr[:]) == count_brute(arr), 'MISMATCH!'
print('All 100 random tests passed!')

تطبيقات عدّ الانقلابات

تقيس الانقلابات مدى ترتيب المصفوفة. ومن تطبيقاتها: (1) ارتباط الترتيب: مسافة Kendall tau بين قائمتين مرتبتين تساوي عدد الانقلابات. (2) كفاءة فرز الإدراج: يجري فرز الإدراج عددًا من عمليات التبديل يساوي تمامًا عدد الانقلابات. (3) تحليل فرز الفقاعات: تقلل كل جولة من جولات فرز الفقاعات عدد الانقلابات، ويساوي عدد الجولات المطلوبة عدد الانقلابات. (4) قابلية حل الألغاز: يكون لغز 8-puzzle أو 15-puzzle قابلًا للحل إذا وفقط إذا كانت زوجية عدد الانقلابات ذات تكافؤ محدد.

# Kendall tau: number of inversions between two rankings
# Useful for comparing search result rankings or recommendation systems

def kendall_tau(rank1, rank2):
    '''Count inversions where rank1 and rank2 disagree on relative order.'''
    # Map rank2 positions to create a comparison sequence
    pos = {v: i for i, v in enumerate(rank2)}
    # Convert rank1 to position-in-rank2 ordering
    arr = [pos[v] for v in rank1]
    return count_inversions(arr)

def count_inversions(arr):
    def ms(a):
        if len(a)<=1: return a,0
        m=len(a)//2; L,lc=ms(a[:m]); R,rc=ms(a[m:])
        res,c,i,j=[],0,0,0
        while i<len(L) and j<len(R):
            if L[i]<=R[j]: res.append(L[i]);i+=1
            else: res.append(R[j]);j+=1;c+=len(L)-i
        return res+L[i:]+R[j:],(lc+rc+c)
    return ms(arr[:])[1]

print(kendall_tau([1,2,3],[3,1,2]))  # measures disagreement

ذات صلة: عدّ الأعداد الأصغر بعد كل عنصر

تطلب مسألة Count Smaller Numbers After Self (LeetCode 315) معرفة عدد العناصر الأصغر الموجودة إلى يمين كل عنصر. وهذا يمثل عدًّا للانقلابات لكل عنصر على حدة. يمكن حلها باستخدام نسخة فرز الدمج المعدّلة نفسها، مع تتبّع الفهارس الأصلية التي يجري احتسابها. ويمكن بديلًا استخدام Binary Indexed Tree (Fenwick Tree)، أو فرز الدمج مع تتبّع الفهارس. يعمل أسلوب التقسيم والحل بزمن O(n log n).

def count_smaller(nums):
    n = len(nums)
    result = [0] * n
    indexed = list(enumerate(nums))
    
    def merge_sort(arr):
        if len(arr) <= 1: return arr
        mid = len(arr) // 2
        left  = merge_sort(arr[:mid])
        right = merge_sort(arr[mid:])
        return merge(left, right)
    
    def merge(left, right):
        merged = []
        i = j = 0
        while i < len(left) and j < len(right):
            if left[i][1] <= right[j][1]:
                # left[i] is placed; j elements from right are smaller and to the right
                result[left[i][0]] += j
                merged.append(left[i]); i += 1
            else:
                merged.append(right[j]); j += 1
        while i < len(left):
            result[left[i][0]] += j  # all of right is smaller
            merged.append(left[i]); i += 1
        return merged + right[j:]
    
    merge_sort(indexed)
    return result

print(count_smaller([5, 2, 6, 1]))  # [2, 1, 1, 0]

الأزواج المعكوسة

تعدّ مسألة Reverse Pairs (LeetCode 493) الأزواج (i, j) التي تحقق i < j وnums[i] > 2 × nums[j]. يستخدم عدّ الانقلابات المعتاد الشرط nums[i] > nums[j]. أما هنا، فتتغير العتبة إلى 2 × nums[j]. عدّل فرز الدمج: عدّ الأزواج العابرة قبل الدمج، باستخدام مؤشرين للعدّ ما دام النصف الأيسر يحتوي على عناصر صالحة، ثم أجرِ الدمج بصورة طبيعية. إجمالي الزمن O(n log n).

def reverse_pairs(nums):
    def merge_sort_count(arr):
        if len(arr) <= 1: return arr, 0
        mid = len(arr) // 2
        L, lc = merge_sort_count(arr[:mid])
        R, rc = merge_sort_count(arr[mid:])
        # Count cross pairs: L[i] > 2*R[j]
        j = 0
        cross = 0
        for l_val in L:
            while j < len(R) and l_val > 2 * R[j]:
                j += 1
            cross += j
        # Normal merge (separate from count)
        merged = []
        i = jj = 0
        while i < len(L) and jj < len(R):
            if L[i] <= R[jj]: merged.append(L[i]); i += 1
            else: merged.append(R[jj]); jj += 1
        merged += L[i:] + R[jj:]
        return merged, lc + rc + cross
    
    return merge_sort_count(nums)[1]

print(reverse_pairs([1, 3, 2, 3, 1]))  # 2
print(reverse_pairs([2, 4, 3, 5, 1])) # 3

عدد الانعكاسات العام مقابل المحلي

الانعكاسات العامة والمحلية (LeetCode 775): بالنظر إلى تبديل للعناصر من 0 إلى n-1، حدّد ما إذا كان عدد الانعكاسات العامة (جميع الأزواج i<j التي تحقق a[i]>a[j]) يساوي عدد الانعكاسات المحلية (الأزواج المتجاورة). الفكرة الأساسية هي أن كل انعكاس محلي هو أيضًا انعكاس عام، ولذلك فإن العام ≥ المحلي. ويتساويان فقط عند عدم وجود انعكاسات بين عناصر غير متجاورة، أي عندما لا يبتعد أي عنصر أكثر من موضع واحد عن فهرسه في الترتيب الصحيح. ويختزل ذلك إلى التحقق من abs(a[i] - i) ≤ 1 لكل i.

def is_ideal_permutation(A):
    '''Global inversions == local inversions
    iff no element is more than 1 position from its sorted index.'''
    return all(abs(a - i) <= 1 for i, a in enumerate(A))

print(is_ideal_permutation([1, 0, 2]))  # True
print(is_ideal_permutation([1, 2, 0]))  # False (A[0]=1 is far from 2, A[2]=0 is far)

# Verification with inversion counts
print(count_inversions([1, 0, 2]))  # 1 (global)
local1 = sum(1 for i in range(len([1,0,2])-1) if [1,0,2][i]>[1,0,2][i+1])
print('local:', local1)  # 1 (equal)

def count_inversions(arr):
    def ms(a):
        if len(a)<=1: return a,0
        m=len(a)//2; L,lc=ms(a[:m]); R,rc=ms(a[m:])
        res,c,i,j=[],0,0,0
        while i<len(L) and j<len(R):
            if L[i]<=R[j]: res.append(L[i]);i+=1
            else: res.append(R[j]);j+=1;c+=len(L)-i
        return res+L[i:]+R[j:],(lc+rc+c)
    return ms(arr[:])[1]

ملخص تعقيد حساب الانعكاسات

الملخص: يستغرق حساب الانعكاسات بالقوة الغاشمة O(n²). ويحقق فرز الدمج المعدّل تعقيدًا قدره O(n log n) من خلال حساب الانعكاسات العابرة بين جزأي التقسيم أثناء خطوة الدمج. وتبلغ التكلفة الإضافية O(1) لكل مقارنة (بإضافة len(left) - i)، ولذلك فإن إجمالي الحمل هو O(n) لكل مستوى من مستويات الدمج، وهو نفس الحمل في فرز الدمج القياسي. وتبلغ المساحة O(n) للمصفوفات المساعدة. وهذا مثال نموذجي على استخدام التقسيم والسيطرة (D&C) لحساب إحصاءات الترتيب في زمن O(n log n).

import time, random

def time_method(func, arr):
    start = time.time()
    result = func(arr[:])
    return result, time.time() - start

def count_brute(arr):
    return sum(1 for i in range(len(arr)) for j in range(i+1,len(arr)) if arr[i]>arr[j])

def count_dc(arr):
    def ms(a):
        if len(a)<=1: return a,0
        m=len(a)//2;L,lc=ms(a[:m]);R,rc=ms(a[m:])
        res,c,i,j=[],0,0,0
        while i<len(L) and j<len(R):
            if L[i]<=R[j]: res.append(L[i]);i+=1
            else: res.append(R[j]);j+=1;c+=len(L)-i
        return res+L[i:]+R[j:],(lc+rc+c)
    return ms(arr[:])[1]

arr = random.sample(range(1000), 1000)
r1, t1 = time_method(count_brute, arr)
r2, t2 = time_method(count_dc, arr)
print(f'Brute: {r1} in {t1:.4f}s')
print(f'D&C:   {r2} in {t2:.4f}s')
print(f'Speedup: {t1/t2:.1f}x')

تحقق سريع

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

مراجعة الدرس

لقد تعلمت في هذا الدرس أن: الانعكاسات تقيس مدى عدم ترتيب المصفوفة، مع تعقيد O(n²) للقوة الغاشمة وO(n log n) للتقسيم والسيطرة (D&C)، وأن فرز الدمج المعدّل يحسب الانعكاسات العابرة بين النصفين بإضافة len(left)-i في كل مرة يُختار فيها عنصر من الجزء الأيمن بدلًا من عنصر من الجزء الأيسر، وأن صحة الخوارزمية تعتمد على التقسيم إلى انعكاسات داخل الجزء الأيسر، وداخل الجزء الأيمن، وعابرة بين الجزأين؛ وهذه الفئات متباينة ويغطي مجموعها جميع الانعكاسات. في الدرس التالي سنستكشف خوارزمية التصويت Boyer-Moore للعثور على عنصر الأغلبية.

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

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

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

ماذا ستتعلم في «عدّ الانقلابات باستخدام دمج معدّل»؟

احسب عدد الانقلابات في مصفوفة — أي الأزواج التي تحقق a[i] > a[j] وi < j — عبر عدّ الانقلابات العابرة للتقسيم أثناء خطوة الدمج. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

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

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

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

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

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

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