وسيط مصفوفتين مرتبتين
حلّ مسألة وسيط مصفوفتين مرتبتين في O(log(min(m,n))) باستخدام البحث الثنائي عن حدّ التقسيم في المصفوفة الأقصر.
وسيط مصفوفتين مرتبتين درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
وسيط مصفوفتين مرتبتين
تُعد مسألة وسيط مصفوفتين مرتبتين (LeetCode 4) مسألة كلاسيكية صعبة. بالنظر إلى مصفوفتين مرتبتين nums1 (طولهما m) وnums2 (طولهما n)، اعثر على وسيط التسلسل المرتب الناتج عن دمجهما في زمن O(log(min(m,n))). يدمج النهج الساذج المصفوفتين في O(m+n)، لكن الحل الأمثل يستخدم البحث الثنائي على حدود التقسيم. وهذه من أكثر المسائل الصعبة شيوعًا في مقابلات شركات التقنية الكبرى.
# Examples:
nums1 = [1, 3]
nums2 = [2]
# Combined sorted: [1, 2, 3] → median = 2.0
nums1b = [1, 2]
nums2b = [3, 4]
# Combined sorted: [1, 2, 3, 4] → median = (2+3)/2 = 2.5
print('Example 1 median:', 2.0)
print('Example 2 median:', 2.5)
print('Total length:', len(nums1)+len(nums2), 'and', len(nums1b)+len(nums2b))نهج الدمج الساذج
أبسط نهج بتعقيد O(m+n): ادمج المصفوفتين المرتبتين، ثم اعثر على الوسيط. يستغرق دمج مصفوفتين مرتبتين O(m+n). وسيط المصفوفة ذات الطول L هو arr[L//2] إذا كان L فرديًا، أو (arr[L//2-1] + arr[L//2]) / 2 إذا كان L زوجيًا. هذا النهج صحيح، لكنه لا يحقق المتطلب O(log(min(m,n))). اعرضه دائمًا أولًا في المقابلة لوضع خط أساس، ثم حسّن الحل.
def find_median_naive(nums1, nums2):
# Merge two sorted arrays
merged = []
i = j = 0
while i < len(nums1) and j < len(nums2):
if nums1[i] <= nums2[j]:
merged.append(nums1[i]); i += 1
else:
merged.append(nums2[j]); j += 1
merged += nums1[i:] + nums2[j:]
L = len(merged)
if L % 2 == 1:
return float(merged[L // 2])
return (merged[L//2 - 1] + merged[L//2]) / 2.0
print(find_median_naive([1,3],[2])) # 2.0
print(find_median_naive([1,2],[3,4])) # 2.5فكرة التقسيم
الفكرة الأساسية: يقسم الوسيط المصفوفة المدمجة إلى نصفين متساويين. نحتاج إلى إيجاد تقسيم لـ nums1 وتقسيم لـ nums2 بحيث: (1) يكون الحجم الإجمالي للنصفين الأيسرَين مساويًا للحجم الإجمالي للنصفين الأيمنَين. (2) تكون جميع العناصر في النصفين الأيسرَين ≤ جميع العناصر في النصفين الأيمنَين. إذا أجرينا بحثًا ثنائيًا عن موضع التقسيم الصحيح في nums1، فإن التقسيم في nums2 يتحدد تلقائيًا وفق قيد الطول الإجمالي.
# Partition concept visualised:
# nums1: [1, 3] | [5, 7] (partition after index 1)
# nums2: [2, 4] | [6, 8] (partition after index 1)
# Combined left: [1, 3, 2, 4] = 4 elements
# Combined right: [5, 7, 6, 8] = 4 elements
# Valid if max(left) <= min(right): max(3,4)=4 <= min(5,6)=5 ✓
# Median = (max_left + min_right) / 2 = (4+5)/2 = 4.5
nums1, nums2 = [1,3,5,7], [2,4,6,8]
merged = sorted(nums1+nums2)
print('Merged:', merged)
L = len(merged)
print('Median:', (merged[L//2-1]+merged[L//2])/2 if L%2==0 else merged[L//2])البحث الثنائي عن حد التقسيم
أجرِ البحث الثنائي على فهرس التقسيم i في nums1، وهي المصفوفة الأقصر. ويتحدد فهرس التقسيم j في nums2 وفق j = (m+n+1)//2 - i، مما يضمن احتواء النصفين الأيسرَين على (m+n+1)//2 عنصرًا. يكون التقسيم صحيحًا عندما يتحقق nums1[i-1] ≤ nums2[j] وnums2[j-1] ≤ nums1[i]. ويعدّل البحث الثنائي i بالزيادة أو النقصان للعثور على هذا التوازن.
def find_median_sorted_arrays(nums1, nums2):
# Ensure nums1 is the shorter array
if len(nums1) > len(nums2):
return find_median_sorted_arrays(nums2, nums1)
m, n = len(nums1), len(nums2)
lo, hi = 0, m
while lo <= hi:
i = (lo + hi) // 2 # partition index in nums1
j = (m + n + 1) // 2 - i # partition index in nums2
# Boundary values with sentinels
max_left1 = float('-inf') if i == 0 else nums1[i-1]
min_right1 = float('inf') if i == m else nums1[i]
max_left2 = float('-inf') if j == 0 else nums2[j-1]
min_right2 = float('inf') if j == n else nums2[j]
if max_left1 <= min_right2 and max_left2 <= min_right1:
# Found the correct partition
if (m + n) % 2 == 1:
return float(max(max_left1, max_left2))
return (max(max_left1, max_left2) + min(min_right1, min_right2)) / 2.0
elif max_left1 > min_right2:
hi = i - 1 # i is too large, move left
else:
lo = i + 1 # i is too small, move right
return 0.0
print(find_median_sorted_arrays([1,3],[2])) # 2.0
print(find_median_sorted_arrays([1,2],[3,4])) # 2.5تتبّع البحث الثنائي
تتبّع nums1=[1,3], nums2=[2]: m=2، n=1، total=3، lo=0، hi=2. i=(0+2)//2=1، j=(2+1+1)//2-1=1. max_left1=nums1[0]=1، min_right1=nums1[1]=3، max_left2=nums2[0]=2، min_right2=inf (j=1=n). التحقق: 1≤inf و2≤3 ✓. المجموع فردي: أعد max(1,2)=2.0. ✓ عثرت الخوارزمية على التقسيم في الخطوة الأولى لأن أحجام المصفوفتين صغيرة.
def find_median_traced(nums1, nums2):
if len(nums1) > len(nums2):
return find_median_traced(nums2, nums1)
m, n = len(nums1), len(nums2)
lo, hi = 0, m
step = 0
while lo <= hi:
step += 1
i = (lo + hi) // 2
j = (m + n + 1) // 2 - i
ml1 = float('-inf') if i==0 else nums1[i-1]
mr1 = float('inf') if i==m else nums1[i]
ml2 = float('-inf') if j==0 else nums2[j-1]
mr2 = float('inf') if j==n else nums2[j]
print(f'Step {step}: i={i},j={j}, ml1={ml1},mr1={mr1},ml2={ml2},mr2={mr2}')
if ml1<=mr2 and ml2<=mr1:
if (m+n)%2==1: return float(max(ml1,ml2))
return (max(ml1,ml2)+min(mr1,mr2))/2.0
elif ml1>mr2: hi=i-1
else: lo=i+1
return 0.0
print(find_median_traced([1,3],[2]))لماذا نبحث ثنائيًا في المصفوفة الأقصر
نجري البحث الثنائي في المصفوفة الأقصر لتحقيق O(log(min(m,n))) بدلًا من O(log(m+n)). ويتحدد تقسيم المصفوفة الأطول بالكامل من خلال تقسيم المصفوفة الأقصر. ويضمن تبديل المدخلين عند len(nums1) > len(nums2) أن تكون المصفوفة الأقصر دائمًا هي نطاق البحث. والثابت هو أنه عندما يُشتق j من i والطول الإجمالي، يكون j دائمًا فهرس تقسيم صحيحًا لـ nums2.
# Prove j is always valid:
# Total elements in left halves = (m+n+1)//2
# Left from nums1: i elements (0 <= i <= m)
# Left from nums2: j = (m+n+1)//2 - i elements
# j must be in [0, n]:
# j >= 0: i <= (m+n+1)//2 <= (m+n+1)//2 ≤ ... always true for valid lo/hi
# j <= n: i >= (m+n+1)//2 - n = (m-n+1)//2 >= 0 (since m <= n)
m, n = 3, 5 # m <= n
half = (m+n+1)//2
for i in range(m+1):
j = half - i
valid = 0 <= j <= n
print(f'i={i}: j={j}, valid={valid}')معالجة الأطوال الإجمالية الزوجية والفردية
عندما يكون الطول المدمج فرديًا: يكون الوسيط هو القيمة العظمى للنصفين الأيسرَين (max(max_left1, max_left2)). وعندما يكون زوجيًا: يكون الوسيط هو متوسط القيمة العظمى للنصفين الأيسرَين والقيمة الصغرى للنصفين الأيمنَين. وتعمل صيغة (m+n+1)//2 لحجم النصف الأيسر في الحالتين: ففي الطول الإجمالي الزوجي تعطي n//2، مع عنصر إضافي واحد في اليسار، ثم نحسب المتوسط مع min_right للحصول على الوسيط الزوجي.
def median_demo(a, b):
merged = sorted(a + b)
L = len(merged)
expected = merged[L//2] if L%2==1 else (merged[L//2-1]+merged[L//2])/2
computed = find_median_sorted_arrays(a[:], b[:])
print(f'a={a}, b={b}: merged={merged}, median={expected}, computed={computed}')
assert abs(expected - computed) < 1e-9
def find_median_sorted_arrays(nums1, nums2):
if len(nums1)>len(nums2): return find_median_sorted_arrays(nums2,nums1)
m,n=len(nums1),len(nums2); lo,hi=0,m
while lo<=hi:
i=(lo+hi)//2; j=(m+n+1)//2-i
ml1=float('-inf') if i==0 else nums1[i-1]; mr1=float('inf') if i==m else nums1[i]
ml2=float('-inf') if j==0 else nums2[j-1]; mr2=float('inf') if j==n else nums2[j]
if ml1<=mr2 and ml2<=mr1:
if (m+n)%2==1: return float(max(ml1,ml2))
return (max(ml1,ml2)+min(mr1,mr2))/2.0
elif ml1>mr2: hi=i-1
else: lo=i+1
return 0.0
median_demo([1,3],[2])
median_demo([1,2],[3,4])
median_demo([],[1])
median_demo([2],[]) # single arrayالحالات الطرفية
الحالات الطرفية المهمة: (1) إحدى المصفوفتين فارغة؛ يكون الوسيط هو وسيط المصفوفة غير الفارغة. (2) جميع عناصر إحدى المصفوفتين أصغر من عناصر الأخرى؛ يقع التقسيم عند أحد الطرفين. (3) وجود عناصر مكررة؛ تتعامل الخوارزمية معها تلقائيًا. (4) طول كلتا المصفوفتين يساوي 1؛ يكون الوسيط هو وسيط عنصرين بسيط. اختبر هذه الحالات دائمًا بعد كتابة الشيفرة. وتتعامل قيمتا الحارس -∞ و+∞ مع التقسيمات الحدّية (i=0 أو i=m) بسلاسة.
def fmsa(a,b):
if len(a)>len(b): return fmsa(b,a)
m,n=len(a),len(b); lo,hi=0,m
while lo<=hi:
i=(lo+hi)//2; j=(m+n+1)//2-i
ml1=float('-inf') if i==0 else a[i-1]; mr1=float('inf') if i==m else a[i]
ml2=float('-inf') if j==0 else b[j-1]; mr2=float('inf') if j==n else b[j]
if ml1<=mr2 and ml2<=mr1:
if (m+n)%2==1: return float(max(ml1,ml2))
return (max(ml1,ml2)+min(mr1,mr2))/2.0
elif ml1>mr2: hi=i-1
else: lo=i+1
# Edge cases
print(fmsa([], [1])) # 1.0
print(fmsa([2], [])) # 2.0
print(fmsa([1,2], [3,4])) # 2.5
print(fmsa([3,4], [1,2])) # 2.5
print(fmsa([1,1,1], [1,1])) # 1.0 (duplicates)
print(fmsa([10,20,30],[5,15,25,35])) # 17.5تعميم: العنصر k الأصغر في مصفوفتين
تتعمم مسألة الوسيط إلى العثور على العنصر k الأصغر بين مصفوفتين مرتبتين. في كل خطوة، قارن العنصر الواقع عند الموضع k//2 في كل مصفوفة. احذف النصف الأصغر: فجميع عناصره وعددها k//2 أصغر من العنصر k الأصغر، ولذلك يمكننا تجاهلها. خفّض k بمقدار k//2 ثم أجرِ الاستدعاء التكراري. الحالات الأساسية: أن تكون إحدى المصفوفتين فارغة (أعد العنصر k الأصغر من الجزء المتبقي)، أو أن تكون k=1 (أعد الأصغر بين أول عنصرين). الزمن: O(log k) = O(log(m+n)).
def kth_smallest(nums1, nums2, k):
if not nums1: return nums2[k-1]
if not nums2: return nums1[k-1]
if k == 1: return min(nums1[0], nums2[0])
# Compare k//2-th elements
half = k // 2
i = min(half, len(nums1)) - 1 # index in nums1
j = min(half, len(nums2)) - 1 # index in nums2
if nums1[i] <= nums2[j]:
# Eliminate first (i+1) elements of nums1
return kth_smallest(nums1[i+1:], nums2, k - (i+1))
else:
return kth_smallest(nums1, nums2[j+1:], k - (j+1))
nums1, nums2 = [1,3,5,7], [2,4,6,8]
for k in range(1, 9):
print(f'k={k}: {kth_smallest(nums1[:], nums2[:], k)}')مقارنة جميع الأساليب
المقارنة النهائية: دمج المصفوفات: زمن قدره O(m+n)، ومساحة قدرها O(m+n). البحث الثنائي عن موضع التقسيم: زمن قدره O(log(min(m,n)))، ومساحة قدرها O(1). الاستدعاء التكراري لإيجاد العنصر k الأصغر: زمن قدره O(log(m+n))، ومكدس استدعاءات بمساحة O(log k). طريقة البحث الثنائي عن موضع التقسيم هي ما يتوقعه المحاورون لهذا السؤال. إنها أصعب مسألة شائعة في LeetCode من حيث الشرح الواضح — تدرّبوا على منطق التقسيم وفحوص الحدود الأربعة حتى تصبح تلقائية.
# Performance comparison
import time, random
def merge_median(a, b):
merged = sorted(a+b)
L=len(merged)
return merged[L//2] if L%2==1 else (merged[L//2-1]+merged[L//2])/2
def binary_median(a, b):
if len(a)>len(b): return binary_median(b,a)
m,n=len(a),len(b);lo,hi=0,m
while lo<=hi:
i=(lo+hi)//2;j=(m+n+1)//2-i
ml1=float('-inf') if i==0 else a[i-1];mr1=float('inf') if i==m else a[i]
ml2=float('-inf') if j==0 else b[j-1];mr2=float('inf') if j==n else b[j]
if ml1<=mr2 and ml2<=mr1:
if (m+n)%2==1: return float(max(ml1,ml2))
return (max(ml1,ml2)+min(mr1,mr2))/2.0
elif ml1>mr2: hi=i-1
else: lo=i+1
for size in [100, 10000]:
a = sorted(random.sample(range(size*2), size))
b = sorted(random.sample(range(size*2), size))
t1=time.time(); [merge_median(a,b) for _ in range(1000)]; t1=time.time()-t1
t2=time.time(); [binary_median(a,b) for _ in range(1000)]; t2=time.time()-t2
print(f'n={size}: merge={t1:.4f}s, binary={t2:.4f}s, speedup={t1/t2:.1f}x')استراتيجية التواصل في المقابلة
لحل هذه المسألة الصعبة في مقابلة: (1) اذكروا فورًا طريقة الدمج الساذجة ذات O(m+n) — فهذا يوضح كفاءتكم. (2) اشرحوا الهدف ذي التعقيد O(log(min(m,n))) وفكرة التقسيم. (3) استعرضوا ثابت التقسيم: max_left1 ≤ min_right2 و max_left2 ≤ min_right1. (4) تعاملوا مع القيم الحارسة بوضوح. (5) اذكروا صيغة الوسيط للحالتين الفردية والزوجية. (6) اختبروا الحل بمثال أو مثالين. يوضح هذا الإطار المؤلف من 5 خطوات قدرتكم على حل المشكلات بطريقة منهجية، حتى في مسألة لا يتمكن سوى عدد قليل من المرشحين من حلها بإتقان تحت الضغط.
# Clean final solution for interviews:
def findMedianSortedArrays(nums1, nums2):
if len(nums1) > len(nums2):
return findMedianSortedArrays(nums2, nums1)
m, n = len(nums1), len(nums2)
lo, hi = 0, m
while lo <= hi:
i = (lo + hi) // 2
j = (m + n + 1) // 2 - i
max_l1 = nums1[i-1] if i > 0 else float('-inf')
min_r1 = nums1[i] if i < m else float('inf')
max_l2 = nums2[j-1] if j > 0 else float('-inf')
min_r2 = nums2[j] if j < n else float('inf')
if max_l1 <= min_r2 and max_l2 <= min_r1:
if (m + n) % 2:
return float(max(max_l1, max_l2))
return (max(max_l1, max_l2) + min(min_r1, min_r2)) / 2.0
elif max_l1 > min_r2: hi = i - 1
else: lo = i + 1
# Time: O(log(min(m,n))), Space: O(1)
print(findMedianSortedArrays([1,3],[2])) # 2.0
print(findMedianSortedArrays([1,2],[3,4])) # 2.5اختبار سريع
اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.
مراجعة الدرس
تعلّمتم في هذا الدرس أن وسيط مصفوفتين مرتبتين يمكن إيجاده في O(log(min(m,n))) باستخدام البحث الثنائي عن حد التقسيم الصحيح في المصفوفة الأقصر، وأن التقسيم يكون صالحًا عندما يتحقق max_left1 ≤ min_right2 و max_left2 ≤ min_right1، مع استخدام قيم حارسة للتعامل مع حالات الحدود، وأن التعميم لإيجاد العنصر k الأصغر يستخدم أسلوبًا تكراريًا لحذف نصف العناصر في كل مرة بزمن O(log k). تهانينا على إكمال دروس التقسيم والسيطرة — أصبح لديكم الآن مجموعة أدوات شاملة لمقابلات البرمجة!
الأسئلة الشائعة
هل درس «وسيط مصفوفتين مرتبتين» مجاني؟
نعم — نص درس «وسيط مصفوفتين مرتبتين» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «وسيط مصفوفتين مرتبتين»؟
حلّ مسألة وسيط مصفوفتين مرتبتين في O(log(min(m,n))) باستخدام البحث الثنائي عن حدّ التقسيم في المصفوفة الأقصر. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «وسيط مصفوفتين مرتبتين»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- قالب التقسيم والغزو
- عدّ الانقلابات باستخدام دمج معدّل
- العنصر الغالب: تصويت Boyer-Moore
- وسيط مصفوفتين مرتبتين