DSA Interview Prep · पाठ

संशोधित मर्ज सॉर्ट से इनवर्ज़न गिनना

किसी ऐरे में इनवर्ज़न की संख्या गिनिए — ऐसे युग्म जहाँ a[i] > a[j] और i < j — तथा मर्ज चरण के दौरान विभाजनों के बीच के इनवर्ज़न गिनिए।

पाठ 2, कुल 4 में से13 चरण

संशोधित मर्ज सॉर्ट से इनवर्ज़न गिनना, CoddyKit पर DSA Interview Prep का एक निःशुल्क पाठ है। यह 4 में से 2वाँ पाठ है। इस अध्ययन पथ के 3 तक कोई भी पाठ पूरा पढ़ना निःशुल्क है — इसके बाद CoddyKit PRO हर पाठ अनलॉक करता है, साथ ही अंतर्निर्मित कोड संपादक और चौबीसों घंटे एआई शिक्षक के साथ व्यावहारिक अभ्यास भी उपलब्ध कराता है। यह DSA Interview Prep सीखने के मार्ग का हिस्सा है और आपकी प्रगति वेब तथा CoddyKit ऐप पर सिंक होती रहती है। DSA Interview Prep पाठ्यक्रम में कुल 4 पाठ शामिल हैं।

उलटफेर क्या है

सारणी में उलटफेर सूचकांकों की ऐसी जोड़ी (i, j) होती है जिसमें i < j लेकिन a[i] > a[j] — अर्थात बड़ी संख्या छोटी संख्या से पहले आती है। उदाहरण के लिए, [3, 1, 2] में उलटफेर (3,1) और (3,2) हैं, इसलिए कुल 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⁹ तुलनाएँ है — जो बहुत धीमा है। संशोधित merge क्रमबद्धीकरण का उपयोग करने वाला विभाजन और विजय दृष्टिकोण इसे O(n log n) में हल करता है। मुख्य अंतर्दृष्टि यह है कि merge क्रमबद्धीकरण के merge चरण के दौरान हम विभाजन-पार उलटफेरों की कुशलता से गिनती कर सकते हैं।

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

merge क्रमबद्धीकरण की मुख्य अंतर्दृष्टि

दो क्रमबद्ध आधे हिस्सों L और R के merge के दौरान, यदि हम L[i] के बजाय R[j] चुनते हैं, क्योंकि R[j] < L[i], तो L में i से आगे के सभी शेष तत्व भी R[j] से बड़े होंगे। ऐसा इसलिए है क्योंकि L क्रमबद्ध है। इसलिए जब भी हम दाएँ आधे हिस्से से कोई तत्व लेते हैं, तो len(L) - i विभाजन-पार उलटफेर गिनते हैं। यह गिनती अतिरिक्त काम के बिना होती है — सामान्य merge के दौरान ही हो जाती है।

# 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')

संशोधित merge क्रमबद्धीकरण का कार्यान्वयन

merge क्रमबद्धीकरण को इस प्रकार संशोधित कीजिए कि वह क्रमबद्ध सारणी और उलटफेरों की गिनती, दोनों लौटाए। कुल उलटफेर = बाएँ आधे के उलटफेर + दाएँ आधे के उलटफेर + merge के दौरान मिले विभाजन-पार उलटफेर। आधार-प्रकरण एकल तत्व और 0 उलटफेर लौटाता है। merge फलन तत्वों को मिलाते समय उलटफेरों की गिनती करता है। कुल समय: 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] को merge कीजिए: 1 लीजिए, 2>1 और 4>1 के लिए गिनती में 2 जोड़िए; 2 लीजिए, कोई गिनती नहीं; 3 लीजिए, 4>3 के लिए गिनती में 1 जोड़िए; फिर 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]))

विभाजन-पार उलटफेर सही ढंग से क्यों मिलते हैं

शुद्धता: i < j वाली कोई भी उलटफेर जोड़ी (a[i], a[j]) ठीक तीन में से किसी एक श्रेणी में आती है: (1) दोनों बाएँ आधे में — बाईं पुनरावर्ती कॉल द्वारा गिनी जाती है। (2) दोनों दाएँ आधे में — दाईं पुनरावर्ती कॉल द्वारा गिनी जाती है। (3) बाएँ आधे का तत्व > दाएँ आधे का तत्व — merge के दौरान विभाजन-पार उलटफेर के रूप में गिना जाता है। ये श्रेणियाँ परस्पर अलग और पूर्ण हैं, इसलिए किसी उलटफेर की दोबारा गिनती नहीं होती और कोई उलटफेर छूटता भी नहीं। यह विभाजन तर्क विभाजन और विजय की शुद्धता का मानक प्रमाण है।

# 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) रैंक सहसंबंध: दो क्रमबद्ध सूचियों के बीच केन्डल टाउ दूरी उलटफेरों की संख्या होती है। (2) सम्मिलन क्रमबद्धीकरण की दक्षता: सम्मिलन क्रमबद्धीकरण उतने ही अदला-बदली करता है जितने उलटफेर होते हैं। (3) बबल क्रमबद्धीकरण का विश्लेषण: बबल क्रमबद्धीकरण का प्रत्येक चरण उलटफेरों को घटाता है; आवश्यक चरणों की संख्या उलटफेरों की संख्या के बराबर होती है। (4) पहेली का हल योग्य होना: 8-पहेली या 15-पहेली तभी और केवल तभी हल की जा सकती है जब उलटफेरों की संख्या की समता या विषमता विशेष हो।

# 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

संबंधित: स्वयं के बाद छोटी संख्याओं की गिनती

स्वयं के बाद छोटी संख्याओं की गिनती (LeetCode 315) प्रत्येक तत्व के लिए पूछता है: उसके दाईं ओर कितने तत्व उससे छोटे हैं? यह प्रत्येक तत्व के लिए उलटफेरों की गिनती है। इसे उसी संशोधित merge क्रमबद्धीकरण से हल किया जा सकता है, लेकिन इसमें यह भी दर्ज करना होता है कि किन मूल सूचकांकों की गिनती की जा रही है। दूसरा विकल्प द्विआधारी अनुक्रमित वृक्ष (फेनविक वृक्ष) या सूचकांक दर्ज करने वाले merge क्रमबद्धीकरण का उपयोग करना है। विभाजन और विजय दृष्टिकोण 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]

उलटे युग्म

उलटे युग्म (LeetCode 493) उन जोड़ियों (i, j) की गिनती करता है जिनमें i < j और nums[i] > 2 × nums[j] हो। मानक उलटफेर गिनती में nums[i] > nums[j] होता है। यहाँ सीमा बदलकर 2 × nums[j] हो जाती है। merge क्रमबद्धीकरण को संशोधित कीजिए: merge करने से पहले विभाजनों के बीच की गिनती कीजिए, इसके लिए दो-सूचक तकनीक का उपयोग करते हुए तब तक गिनिए जब तक बाएँ आधे में मान्य तत्व मौजूद हों; फिर सामान्य रूप से merge कीजिए। कुल समय 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]) स्थानीय उलट-युग्मों (आसन्न युग्मों) की संख्या के बराबर है या नहीं। मुख्य अंतर्दृष्टि यह है कि प्रत्येक स्थानीय उलट-युग्म वैश्विक भी होता है, इसलिए वैश्विक संख्या ≥ स्थानीय संख्या। दोनों तभी बराबर होंगे जब कोई गैर-आसन्न उलट-युग्म न हो—अर्थात कोई भी तत्व अपनी क्रमबद्ध अनुक्रमणिका से 1 से अधिक स्थान दूर न हो। इसे सभी i के लिए abs(a[i] - i) ≤ 1 जाँचकर हल किया जा सकता है।

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) है। रैखिक-लघुगणकीय समय में क्रम-सांख्यिकी की गणना के लिए विभाजित करो और जीतो पद्धति का उपयोग करने का यह एक आदर्श उदाहरण है।

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')

त्वरित जाँच

इस पाठ में पढ़ी गई डेटा संरचनाओं और एल्गोरिदम—कोडिंग साक्षात्कार की तैयारी—से जुड़ी अवधारणाओं की अपनी समझ जाँचिए।

पाठ का पुनरावलोकन

इस पाठ में आपने सीखा: उलट-युग्म यह मापते हैं कि कोई सारणी कितनी अव्यवस्थित है; बलपूर्वक विधि की जटिलता O(n²) और विभाजित करो और जीतो विधि की O(n log n) होती है, संशोधित मर्ज सॉर्ट हर बार किसी दाएँ तत्व को बाएँ तत्व पर चुनने पर len(left)-i जोड़कर दोनों हिस्सों के बीच के उलट-युग्मों की गणना करता है, और शुद्धता इस विभाजन पर निर्भर करती है: बाएँ-बाएँ, दाएँ-दाएँ और दोनों हिस्सों के बीच के उलट-युग्म परस्पर असंबद्ध होते हैं और मिलकर सभी उलट-युग्मों को समेटते हैं। आगे हम बहुसंख्यक तत्व खोजने के लिए बॉयर-मूर मतदान एल्गोरिदम का अध्ययन करेंगे।

शुरुआत निःशुल्क

एआई शिक्षक के साथ Python सीखें — निःशुल्क

अपने ब्राउज़र में वास्तविक कोड लिखें और चलाएँ, चौबीसों घंटे एआई शिक्षक से तुरंत सहायता पाएँ, और वेब या ऐप पर वहीं से शुरू करें जहाँ आपने छोड़ा था।

पाठ्यक्रम
30
पाठ
120

अक्सर पूछे जाने वाले प्रश्न

क्या “संशोधित मर्ज सॉर्ट से इनवर्ज़न गिनना” पाठ निःशुल्क है?

हाँ — DSA Interview Prep अध्ययन पथ के 3 तक कोई भी पाठ, जिसमें “संशोधित मर्ज सॉर्ट से इनवर्ज़न गिनना” भी शामिल है, यहाँ वेब पर पूरा पढ़ना निःशुल्क है। इसके बाद CoddyKit PRO हर पाठ अनलॉक करता है, साथ ही अंतर्निर्मित कोड संपादक और चौबीसों घंटे एआई शिक्षक के साथ इंटरैक्टिव अभ्यास भी उपलब्ध कराता है। DSA Interview Prep पाठ्यक्रम में कुल 4 पाठ शामिल हैं।

“संशोधित मर्ज सॉर्ट से इनवर्ज़न गिनना” में मैं क्या सीखूँगा?

किसी ऐरे में इनवर्ज़न की संख्या गिनिए — ऐसे युग्म जहाँ a[i] > a[j] और i < j — तथा मर्ज चरण के दौरान विभाजनों के बीच के इनवर्ज़न गिनिए। आप ब्राउज़र में सीधे चलाए जाने वाले व्यावहारिक कोड के साथ DSA Interview Prep का अभ्यास करते हैं, और पाठ पूरा करते समय 24/7 एआई ट्यूटर आपके प्रश्नों के उत्तर देता है।

क्या DSA Interview Prep शुरू करने के लिए मुझे किसी अनुभव की आवश्यकता है?

पहले के अनुभव की आवश्यकता नहीं है। CoddyKit पर DSA Interview Prep शुरुआती से लेकर उन्नत शिक्षार्थियों तक सभी के लिए व्यवस्थित किया गया है, इसलिए आप यहीं से या शुरुआत से सीखना शुरू कर सकते हैं और अपनी गति से आगे बढ़ सकते हैं। यह 4 में से 2वाँ पाठ है।

“संशोधित मर्ज सॉर्ट से इनवर्ज़न गिनना” पाठ पूरा करने में कितना समय लगता है?

CoddyKit का अधिकांश पाठ लगभग 5–10 मिनट में पूरा हो जाता है। हर पाठ छोटा और संवादात्मक है, इसलिए आप लगातार प्रगति करते हैं और वेब या ऐप पर वहीं से सीखना जारी रख सकते हैं जहाँ आपने छोड़ा था।

क्या मैं इस DSA Interview Prep पाठ में कोड लिख और चला सकता हूँ?

हाँ। हर DSA Interview Prep पाठ में एक अंतर्निर्मित कोड संपादक शामिल है, जिससे आप सीधे अपने ब्राउज़र में वास्तविक कोड लिख और चला सकते हैं और तुरंत एआई प्रतिक्रिया पा सकते हैं—स्थानीय सेटअप की आवश्यकता नहीं है।

इस पाठ्यक्रम के सभी पाठ

  1. विभाजित करें और जीतें का खाका
  2. संशोधित मर्ज सॉर्ट से इनवर्ज़न गिनना
  3. बहुसंख्यक तत्व: Boyer-Moore मतदान
  4. दो क्रमबद्ध ऐरे का माध्यिका
← DSA Interview Prep पर वापस जाएँ