فرز الدمج: قسّم وفرز وادمج
نفّذ فرز الدمج بصورة ذاتية، وتتبع شجرة التقسيم والتغلب، واشرح سبب ضمانه O(n log n) في جميع الحالات
فرز الدمج: قسّم وفرز وادمج درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
حدس خوارزمية فرق تسد
فرز الدمج خوارزمية كلاسيكية من خوارزميات فرق تسد: اقسم المصفوفة إلى نصفين، ثم رتّب كل نصف تكراريًا، وبعد ذلك ادمج النصفين المرتبين في ناتج مرتب واحد. تكمن الفكرة في أن دمج مصفوفتين مرتبتين يستغرق O(n)، وهو أقل بكثير من فرزهما من الصفر. ينتج عن هذا التفكيك شجرة استدعاء تكراري تحتوي على log n من المستويات، ويتطلب كل مستوى عمل دمج بمقدار O(n)، مما يعطي الحد الأمثل لفرز المقارنة، وهو O(n log n).
# High-level merge sort structure
def merge_sort(arr):
# Base case: 0 or 1 element already sorted
if len(arr) <= 1:
return arr
# Divide
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # sort left half
right = merge_sort(arr[mid:]) # sort right half
# Conquer (merge)
return merge(left, right)
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
# [3, 9, 10, 27, 38, 43, 82]شرح خطوة الدمج
لدمج مصفوفتين مرتبتين، حافظ على مؤشرين، أحدهما لكل نصف. قارن العنصرين الموجودين في المقدمة؛ وانسخ الأصغر إلى الناتج، ثم حرّك المؤشر المقابل. وعندما ينفد أحد النصفين، انسخ بقية النصف الآخر مباشرة. يعمل ذلك في O(n) من الوقت وO(n) من المساحة لمصفوفة الناتج. خطوة الدمج هي جوهر خوارزمية فرز الدمج، لذا احرص على فهمها بعمق.
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # <= preserves stability
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# Append remaining elements
result.extend(left[i:])
result.extend(right[j:])
return result
print(merge([1,3,5,7], [2,4,6,8]))
# [1, 2, 3, 4, 5, 6, 7, 8]التنفيذ الكامل لفرز الدمج
بجمع عمليتي التقسيم والدمج: تستمر الاستدعاءات التكرارية في تنصيف المسألة حتى تبقى عناصر مفردة (وهي مرتبة بصورة بديهية)، ثم تجمع استدعاءات الدمج هذه العناصر من جديد. يدمج كل مستوى من شجرة الاستدعاء العناصر n نفسها إجمالًا (موزعة على عمليات دمج متعددة). يبلغ عمق التكرار log₂(n)، ما يعطي زمنًا إجماليًا قدره O(n log n) ومساحة إضافية قدرها O(n) لمصفوفات ناتج الدمج، إضافة إلى عمق قدره O(log n) لمكدس الاستدعاءات.
def merge_sort_full(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort_full(arr[:mid])
right = merge_sort_full(arr[mid:])
# Merge the two sorted halves
merged = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: merged.append(left[i]); i += 1
else: merged.append(right[j]); j += 1
merged.extend(left[i:] + right[j:])
return merged
print(merge_sort_full([5,2,4,6,1,3,2,6]))
# [1, 2, 2, 3, 4, 5, 6, 6]شجرة الاستدعاءات التكرارية لفرز الدمج
تصوّر شجرة الاستدعاءات التكرارية لفرز الدمج عندما تكون n=8: يضم المستوى 0 مصفوفة واحدة من 8 عناصر؛ ويضم المستوى 1 مصفوفتين من 4 عناصر؛ ويضم المستوى 2 أربع مصفوفات من عنصرين؛ ويضم المستوى 3 ثمانية عناصر مفردة (حالات الأساس). عند الصعود مجددًا، يدمج الانتقال من المستوى 3 إلى 2 عددًا إجماليًا قدره 8 عناصر، وكذلك الانتقال من المستوى 2 إلى 1، ومن المستوى 1 إلى 0. أي إن 3 مستويات × 8 عناصر = 24 عملية ≈ 8 × log₂(8) = 24. وهذا يؤكد التعقيد O(n log n).
# Trace the tree depth
level_work = []
def merge_sort_traced(arr, depth=0):
if depth >= len(level_work):
level_work.append(0)
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort_traced(arr[:mid], depth+1)
right = merge_sort_traced(arr[mid:], depth+1)
level_work[depth] += len(arr) # track merge work
merged = sorted(left + right) # simplified merge
return merged
merge_sort_traced(list(range(8, 0, -1)))
for d, work in enumerate(level_work):
print(f'Level {d}: {work} elements merged')فرز الدمج في المكان نفسه
يخصّص فرز الدمج التكراري القياسي مساحة إضافية قدرها O(n) لناتج الدمج. يوجد فرز دمج في المكان نفسه، لكنه معقد وله ثوابت كبيرة، ولذلك نادرًا ما يُطرح في المقابلات. والسؤال الشائع في المقابلات هو: «هل يمكنك تنفيذ فرز الدمج باستخدام مساحة إضافية قدرها O(1)؟» والإجابة الصحيحة: «نعم من الناحية النظرية، لكن التطبيقات العملية إما أن تتنازل عن مساحة قدرها O(n) أو تضيف تعقيدًا؛ إذ يستخدم Timsort في Python مساحة قدرها O(n) لإجراء الدمج.»
# Bottom-up merge sort: iterative, avoids recursion stack
def merge_sort_bottomup(arr):
n = len(arr)
width = 1
while width < n:
for i in range(0, n, 2 * width):
left = arr[i:i+width]
right = arr[i+width:i+2*width]
# Merge and put back
merged = []
a, b = 0, 0
while a < len(left) and b < len(right):
if left[a] <= right[b]: merged.append(left[a]); a+=1
else: merged.append(right[b]); b+=1
merged += left[a:] + right[b:]
arr[i:i+len(merged)] = merged
width *= 2
return arr
print(merge_sort_bottomup([5,2,4,6,1,3]))
# [1, 2, 3, 4, 5, 6]فرز الدمج مستقر
فرز الدمج مستقر: تظهر العناصر المتساوية من النصف الأيسر دائمًا قبل العناصر المتساوية من النصف الأيمن في الناتج المدمج. ويضمن ذلك استخدام <= (بدلًا من <) عند تفضيل العنصر الأيسر. وتهم الاستقرارية عند الفرز باستخدام مفاتيح متعددة. تستخدم الدالتان المدمجتان في Python، sorted() وlist.sort()، خوارزمية Timsort، وهي مستقرة أيضًا وتعقيدها O(n log n)، ما يجعلهما الخيار الآمن في جميع تطبيقات الإنتاج.
# Demonstrating stability: sort (value, original_index) pairs
items = [(3,'A'), (1,'B'), (3,'C'), (2,'D')]
# Sort by value only
result = merge_sort_full(items) # won't work directly
# Use Python's stable sort:
result = sorted(items, key=lambda x: x[0])
print(result)
# [(1,'B'),(2,'D'),(3,'A'),(3,'C')]
# 'A' comes before 'C' for value=3 (stable order)دمج k من المصفوفات المرتبة
يمكن دمج k من المصفوفات المرتبة التي تضم n عنصرًا إجمالًا بدمج الأزواج مرارًا (على غرار أدوار البطولة)، وذلك في زمن قدره O(n log k). يعالج كل مستوى من مستويات الدمج n عنصرًا، ويوجد log k من المستويات. وبدلًا من ذلك، يمكن استخدام كومة دنيا بحجم k: أضف أصغر عنصر متبقٍّ من كل مصفوفة، ثم أخرج العنصر الأصغر وأضف العنصر التالي من تلك المصفوفة. ويحقق أسلوب الكومة أيضًا O(n log k)، لكنه أكثر كفاءة من حيث الذاكرة عندما تكون k كبيرة جدًا.
import heapq
def merge_k_sorted(arrays):
result = []
heap = []
# Push first element from each array with array index
for i, arr in enumerate(arrays):
if arr:
heapq.heappush(heap, (arr[0], i, 0))
while heap:
val, arr_i, elem_i = heapq.heappop(heap)
result.append(val)
if elem_i + 1 < len(arrays[arr_i]):
next_val = arrays[arr_i][elem_i + 1]
heapq.heappush(heap, (next_val, arr_i, elem_i+1))
return result
arrs = [[1,4,7],[2,5,8],[3,6,9]]
print(merge_k_sorted(arrs)) # [1,2,3,4,5,6,7,8,9]عدّ الانقلابات باستخدام فرز الدمج
يتطلب عدّ الانقلابات (الأزواج التي يكون فيها a[i] > a[j] و i < j) زمنًا قدره O(n log n) باستخدام نسخة معدّلة من فرز الدمج. أثناء خطوة الدمج، عندما يكون عنصر من المصفوفة الفرعية اليمنى أصغر من عنصر من المصفوفة الفرعية اليسرى، فإنه يشكّل انقلابًا مع كل عنصر متبقٍّ في المصفوفة الفرعية اليسرى. أضف len(left) - i إلى العدد في تلك اللحظة.
def count_inversions(arr):
if len(arr) <= 1:
return arr, 0
mid = len(arr) // 2
left, l_inv = count_inversions(arr[:mid])
right, r_inv = count_inversions(arr[mid:])
merged = []
inversions = l_inv + r_inv
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i]); i += 1
else:
merged.append(right[j]); j += 1
inversions += len(left) - i # all remaining left elements > right[j]
merged.extend(left[i:] + right[j:])
return merged, inversions
_, inv = count_inversions([3, 1, 2])
print(inv) # 2: (3,1) and (3,2)فرز الدمج مقابل الفرز السريع
يضمن فرز الدمج تعقيد O(n log n) في جميع الحالات، وهو مستقر، كما أنه الخيار الأفضل للقوائم المرتبطة والفرز الخارجي. أما الفرز السريع فتعقيده المتوسط O(n log n)، لكن أسوأ حالاته O(n²)، ويعمل في المكان نفسه (مع مساحة مكدس قدرها O(log n))، وغالبًا ما يكون أسرع عمليًا بفضل كفاءة الذاكرة المخبئية عند التعامل مع المصفوفات. ويستخدم فرز Python المدمج خوارزمية Timsort (وهي من أنواع فرز الدمج)، ولذلك فهو الخيار الافتراضي الصحيح دائمًا.
# Head-to-head complexity comparison:
# Algorithm | Best | Avg | Worst | Space | Stable
# Bubble sort | O(n) | O(n^2) | O(n^2) | O(1) | Yes
# Insertion sort| O(n) | O(n^2) | O(n^2) | O(1) | Yes
# Merge sort | O(nlogn)| O(nlogn)| O(nlogn)| O(n) | Yes
# Quick sort | O(nlogn)| O(nlogn)| O(n^2) | O(logn)| No
# Heap sort | O(nlogn)| O(nlogn)| O(nlogn)| O(1) | No
print('Merge sort: stable, O(n log n) guaranteed, O(n) space')الفرز الخارجي: فرز الدمج على نطاق واسع
فرز الدمج هو الخوارزمية المستخدمة في الفرز الخارجي (فرز بيانات أكبر من أن تتسع لها ذاكرة الوصول العشوائي). تُقرأ البيانات على هيئة أجزاء، ويُفرز كل جزء في الذاكرة، ثم تُدمج الأجزاء من القرص. تقرأ خطوة الدمج عنصرًا واحدًا في كل مرة من كل سلسلة مرتبة، ولا تحتفظ في الذاكرة في الوقت نفسه إلا بعدد O(k) من العناصر (عنصر واحد لكل سلسلة). ولهذا يُستخدم فرز الدمج في قواعد البيانات وHadoop MapReduce وخوارزميات الفرز الكلاسيكية باستخدام الأشرطة.
# Simulated external sort: sort in chunks then merge
def external_sort(data, chunk_size):
chunks = []
for i in range(0, len(data), chunk_size):
chunk = sorted(data[i:i+chunk_size]) # sort in-memory
chunks.append(chunk)
print(f'Created {len(chunks)} sorted chunks')
# Merge all chunks
import heapq
heap = [(c[0], i, 0) for i, c in enumerate(chunks) if c]
heapq.heapify(heap)
result = []
while heap:
val, ci, ei = heapq.heappop(heap)
result.append(val)
if ei + 1 < len(chunks[ci]):
heapq.heappush(heap, (chunks[ci][ei+1], ci, ei+1))
return result
print(external_sort(list(range(20,0,-1)), 5)[:10])ملخص فرز الدمج ونصائح للمقابلات
في المقابلات، يبرهن تنفيذ فرز الدمج بطريقة واضحة على فهمك للتكرار، وخطوة الدمج، وتقنية التقسيم والغزو. ومن الأسئلة اللاحقة الشائعة:
- لماذا التعقيد O(n log n) وليس O(n²)؟ (log n من المستويات × n من العمل في كل مستوى)
- هل هو مستقر؟ (نعم، استخدم <= في الدمج)
- ما مقدار المساحة المطلوبة؟ (مساحة إضافية O(n) + مكدس O(log n))
- هل يمكنك تنفيذه تكراريًا؟ (نعم، باستخدام فرز الدمج من الأسفل إلى الأعلى)
- كيف ستستخدمه مع قائمة مرتبطة؟ (أسهل من استخدامه مع مصفوفة، إذ لا توجد تكلفة تقطيع O(n)؛ استخدم مؤشري البطيء والسريع للعثور على نقطة المنتصف)
# One-shot merge sort for interview clarity:
def ms(a):
if len(a) <= 1: return a
m = len(a) // 2
l, r, res, i, j = ms(a[:m]), ms(a[m:]), [], 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
return res + l[i:] + r[j:]
print(ms([5,2,4,6,1,3])) # [1,2,3,4,5,6]اختبار سريع
اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
تعلّمت في هذا الدرس أن فرز الدمج يقسّم المصفوفة عند نقطة المنتصف، ويفرز كل نصف تكراريًا، ثم يدمج النصفين المرتبين في O(n)، ما ينتج عنه زمن إجمالي قدره O(n log n) عبر مستويات التكرار البالغ عددها log n، وأن خطوة الدمج تستخدم <= لاختيار العنصر الأيسر عند التعادل، وبذلك تضمن الاستقرارية، وأن فرز الدمج هو الخوارزمية المفضلة للقوائم المرتبطة والفرز الخارجي وعندما تكون الاستقرارية مطلوبة، بينما يُفضّل الفرز السريع للمصفوفات الموجودة في الذاكرة عندما تكون المساحة محدودة. سننفّذ بعد ذلك الفرز السريع ونستكشف استراتيجيات اختيار المحور.
الأسئلة الشائعة
هل درس «فرز الدمج: قسّم وفرز وادمج» مجاني؟
نعم — نص درس «فرز الدمج: قسّم وفرز وادمج» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «فرز الدمج: قسّم وفرز وادمج»؟
نفّذ فرز الدمج بصورة ذاتية، وتتبع شجرة التقسيم والتغلب، واشرح سبب ضمانه O(n log n) في جميع الحالات تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «فرز الدمج: قسّم وفرز وادمج»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- فرز الفقاعات وفرز الإدراج
- فرز الدمج: قسّم وفرز وادمج
- الفرز السريع واختيار المحور
- فرز بلا مقارنة ودالة sort() في Python