بناء Heap وPush وPop من الصفر
نفّذ heapify-up لعملية push وheapify-down لعملية pop، ثم أنشئ heap من مصفوفة غير مرتبة في O(n) باستخدام خوارزمية Floyd
بناء Heap وPush وPop من الصفر درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
بناء الصنف MinHeap
يوضّح تنفيذ كومة من الصفر إتقان الآليات الأساسية، وقد يُطلب ذلك أحياناً في مقابلات المستوى المتقدم. يغلّف الصنف MinHeap مصفوفة ويعرض عمليات push وpop وpeek وsize. ويحافظ داخلياً على خاصية الكومة عبر استدعاء الرفع بعد push والخفض بعد pop. إن فهم هذا التنفيذ يجعل وحدة heapq في Python واضحة تماماً.
class MinHeap:
def __init__(self):
self._data = []
def push(self, val):
self._data.append(val)
self._sift_up(len(self._data) - 1)
def pop(self):
if len(self._data) == 1:
return self._data.pop()
min_val = self._data[0]
self._data[0] = self._data.pop() # move last to root
self._sift_down(0)
return min_val
def peek(self):
return self._data[0] if self._data else None
def size(self):
return len(self._data)
def _parent(self, i): return (i - 1) // 2
def _left(self, i): return 2 * i + 1
def _right(self, i): return 2 * i + 2
print('MinHeap class skeleton defined')تنفيذ الرفع
يقارن الرفع عقدةً بأبيها ويبدّلها إلى أعلى ما دامت خاصية الكومة (الأب <= الابن في الكومة الصغرى) منتهكة. تكمن الفكرة الأساسية في أن العنصر المُدرج حديثاً يوجد في النهاية، ثم يصعد إلى موضعه الصحيح. تُنفّذ حلقة while بحد أقصى floor(log n) من المرات، وهو ارتفاع الشجرة. عيّن i = parent في كل خطوة لمتابعة التحرك إلى أعلى.
class MinHeap:
def __init__(self):
self._data = []
def _parent(self, i): return (i - 1) // 2
def _left(self, i): return 2 * i + 1
def _right(self, i): return 2 * i + 2
def _sift_up(self, i):
while i > 0:
p = self._parent(i)
if self._data[p] > self._data[i]: # parent > child: swap
self._data[p], self._data[i] = self._data[i], self._data[p]
i = p
else:
break # heap property satisfied
def push(self, val):
self._data.append(val)
self._sift_up(len(self._data) - 1)
h = MinHeap()
for v in [5, 3, 8, 1, 4]:
h.push(v)
print(h._data) # valid min-heapتنفيذ الخفض
يدفع الخفض العقدة إلى الأسفل عبر تبديلها بشكل متكرر مع أصغر أبنائها (في الكومة الصغرى)، حتى لا يعود أي من الابنين أصغر منها أو تصل العقدة إلى ورقة. قارِن دائماً مع كلا الابنين، وبدّل مع الأصغر منهما للحفاظ على خاصية الكومة. تذكّر التحقق من وقوع فهارس الأبناء ضمن الحدود قبل مقارنة القيم.
def _sift_down(data, i):
n = len(data)
while True:
smallest = i
l = 2 * i + 1
r = 2 * i + 2
if l < n and data[l] < data[smallest]:
smallest = l
if r < n and data[r] < data[smallest]:
smallest = r
if smallest == i:
break # already the smallest among i, l, r
data[i], data[smallest] = data[smallest], data[i]
i = smallest
# Test: put a large value at root and sift down
heap = [10, 1, 2, 3, 4, 5, 6]
print('Before sift-down:', heap)
_sift_down(heap, 0)
print('After sift-down:', heap) # 1 should reach top, 10 sinkإكمال MinHeap باستخدام Pop
تزيل عملية pop الجذر وتعيده (وهو الأصغر في الكومة الصغرى). وللحفاظ على شكل الشجرة الثنائية الكاملة، انقل العنصر الأخير إلى موضع الجذر، ثم أجرِ له الخفض. يمنع ذلك إنشاء فجوات في المصفوفة ويحافظ على صحة التمثيل. حالة خاصة: إذا لم يتبقَّ سوى عنصر واحد، فأزله وأعده مباشرةً من دون إجراء الخفض.
class MinHeap:
def __init__(self):
self._data = []
def push(self, val):
self._data.append(val)
i = len(self._data) - 1
while i > 0:
p = (i - 1) // 2
if self._data[p] > self._data[i]:
self._data[p], self._data[i] = self._data[i], self._data[p]
i = p
else: break
def pop(self):
if not self._data: return None
if len(self._data) == 1: return self._data.pop()
result = self._data[0]
self._data[0] = self._data.pop() # last -> root
i, n = 0, len(self._data)
while True:
s, l, r = i, 2*i+1, 2*i+2
if l < n and self._data[l] < self._data[s]: s = l
if r < n and self._data[r] < self._data[s]: s = r
if s == i: break
self._data[i], self._data[s] = self._data[s], self._data[i]
i = s
return result
h = MinHeap()
for v in [5, 3, 8, 1, 4, 2]: h.push(v)
print([h.pop() for _ in range(6)]) # [1,2,3,4,5,8] sortedخوارزمية Floyd لبناء الكومة
تبني خوارزمية Floyd كومة صغرى من مصفوفة غير مرتبة في O(n)، عبر استدعاء الخفض على كل عقدة غير ورقية، بدءاً من آخر عقدة داخلية (n//2 - 1) والتحرك نحو الجذر. تكون الأوراق صالحة بالفعل بوصفها أكواماً من عنصر واحد. وينتج حد الزمن O(n) من حقيقة أن معظم العقد قريبة من أسفل الشجرة، ولا تحتاج إلا إلى الخفض لمسافة قصيرة.
def heapify(arr):
n = len(arr)
# Start from last non-leaf: index n//2 - 1
# Work backward to root (index 0)
for i in range(n // 2 - 1, -1, -1):
# Sift down node at index i
j = i
while True:
s = j
l, r = 2*j+1, 2*j+2
if l < n and arr[l] < arr[s]: s = l
if r < n and arr[r] < arr[s]: s = r
if s == j: break
arr[j], arr[s] = arr[s], arr[j]
j = s
return arr
arr = [9, 7, 5, 3, 1, 8, 2, 4, 6]
print('Before:', arr)
heapify(arr)
print('After (min-heap):', arr) # arr[0] should be 1لماذا تعمل خوارزمية Floyd في O(n)
برهان O(n): تحتوي الشجرة على n/2^(k+1) من العقد التي يبلغ ارتفاعها k. تنفّذ كل عقدة عند الارتفاع k ما لا يزيد على k من عمليات التبديل أثناء الخفض. إجمالي العمل = مجموع قيم جميع الارتفاعات k: n/2^(k+1) * k. تتقارب هذه المتسلسلة الهندسية إلى O(n). قارن ذلك بالإدراج الساذج عنصراً تلو الآخر: تستغرق كل عملية دفع O(log n)، لذا تكلّف n من عمليات الدفع O(n log n). تكون خوارزمية Floyd أفضل بصرامة عند البناء على دفعات.
import time
import random
# Compare: O(n) heapify vs O(n log n) one-by-one
n = 100000
data = list(range(n, 0, -1)) # reverse sorted = worst case for push
# Method 1: Floyd's O(n)
data1 = data[:]
start = time.time()
for i in range(n // 2 - 1, -1, -1):
j = i
while True:
s = j; l, r = 2*j+1, 2*j+2
if l < n and data1[l] < data1[s]: s = l
if r < n and data1[r] < data1[s]: s = r
if s == j: break
data1[j], data1[s] = data1[s], data1[j]; j = s
print(f'Floyd heapify: {time.time()-start:.4f}s')
# Method 2: One-by-one insertion
import heapq
start = time.time()
heap = []
for x in data: heapq.heappush(heap, x)
print(f'Push one-by-one: {time.time()-start:.4f}s')دفع عنصر إلى مجموعة موجودة
تُعد العمليتان heapq.heappushpop وheapq.heapreplace في Python عمليتين مدمجتين فعّالتين. تدفع heappushpop(heap, item) العنصر الجديد ثم تزيل الأصغر فوراً، وهي أكثر كفاءة من استدعاءين منفصلين. أما heapreplace(heap, item) فتزيل الأصغر وتدفع العنصر الجديد في مرور واحد (ويجب أن يكون العنصر الجديد أكبر من أو مساوياً للحد الأدنى القديم لضمان الصحة). تفيد هاتان العمليتان في خوارزميات المعالجة المتدفقة لأفضل k من العناصر.
import heapq
heap = [1, 3, 5, 7, 9]
heapq.heapify(heap)
# heappushpop: push 2, then pop minimum
# More efficient than push + pop separately
result = heapq.heappushpop(heap, 2)
print('heappushpop(2):', result, '| heap:', heap)
# heapreplace: pop minimum, then push new item
# New item does NOT need to be larger (different from heappushpop)
result2 = heapq.heapreplace(heap, 4)
print('heapreplace(4):', result2, '| heap:', heap)
# Use case: maintaining a fixed-size top-k heap
# heappushpop is the standard patternتنفيذ MaxHeap من الصفر
تعكس MaxHeap المقارنة: يجب أن يكون الأب أكبر من جميع الأبناء أو مساوياً لهم. اعكس المقارنة ببساطة في الرفع والخفض. وبدلاً من ذلك، يمكنك تغليف القيم في صنف يعتمد النفي أو نفي الأعداد الصحيحة كما يحدث مع heapq في Python. يوضّح التنفيذ من الصفر أن الكومتين الصغرى والكبرى بنيتان متطابقتان، ولا يتغير بينهما سوى عامل المقارنة.
class MaxHeap:
def __init__(self):
self._data = []
def push(self, val):
self._data.append(val)
i = len(self._data) - 1
while i > 0:
p = (i - 1) // 2
if self._data[p] < self._data[i]: # FLIP: parent < child = violation
self._data[p], self._data[i] = self._data[i], self._data[p]
i = p
else: break
def pop(self):
if not self._data: return None
if len(self._data) == 1: return self._data.pop()
result = self._data[0]
self._data[0] = self._data.pop()
i, n = 0, len(self._data)
while True:
g = i; l, r = 2*i+1, 2*i+2
if l < n and self._data[l] > self._data[g]: g = l # FLIP
if r < n and self._data[r] > self._data[g]: g = r # FLIP
if g == i: break
self._data[i], self._data[g] = self._data[g], self._data[i]; i = g
return result
h = MaxHeap()
for v in [5, 3, 8, 1, 4, 2]: h.push(v)
print([h.pop() for _ in range(6)]) # [8,5,4,3,2,1]حذف عنصر عشوائي من الكومة
يستغرق حذف عنصر عشوائي (ليس الجذر) من الكومة O(log n)، لكنه يتطلب معرفة فهرس العنصر. استبدل العنصر بالعنصر الأخير، وأزل الأخير، ثم أجرِ الرفع أو الخفض للعنصر البديل (إذ سينتهك اتجاه واحد فقط خاصية الكومة). تُستخدم هذه التقنية في خوارزمية Dijkstra مع الحذف الكسول، وفي طوابير الأولوية التي تدعم عمليات خفض المفتاح.
def delete_at_index(heap, i):
n = len(heap)
heap[i] = heap[n - 1]
heap.pop()
if i >= len(heap):
return # deleted the last element
# Try sift-up first
p = (i - 1) // 2
if i > 0 and heap[i] < heap[p]:
while i > 0:
p = (i - 1) // 2
if heap[p] > heap[i]:
heap[p], heap[i] = heap[i], heap[p]; i = p
else: break
else: # sift down
j = i; n2 = len(heap)
while True:
s = j; l, r = 2*j+1, 2*j+2
if l < n2 and heap[l] < heap[s]: s = l
if r < n2 and heap[r] < heap[s]: s = r
if s == j: break
heap[j], heap[s] = heap[s], heap[j]; j = s
heap = [1, 3, 2, 7, 4, 5, 6]
print('Before:', heap)
delete_at_index(heap, 2) # delete element at index 2 (value=2)
print('After:', heap) # 2 removed, heap still validالكومة في مسألة أكثر العناصر تكراراً
تستخدم مسألة Top-K Frequent Elements (LeetCode #347) كومة صغرى بحجم k. حافظ على كومة صغرى تكون كل خانة فيها على الصورة (frequency, element). عالج كل عنصر فريد: إذا احتوت الكومة على أقل من k من العناصر، فادفع العنصر إليها؛ وإلا، فإذا تجاوز تكرار العنصر الجديد الحد الأدنى في الكومة، فأزل الحد الأدنى وادفع العنصر الجديد. تحتوي الكومة النهائية على k من أكثر العناصر تكراراً في زمن O(n log k).
import heapq
from collections import Counter
def top_k_frequent(nums, k):
count = Counter(nums)
# Min-heap of (frequency, num)
heap = []
for num, freq in count.items():
heapq.heappush(heap, (freq, num))
if len(heap) > k:
heapq.heappop(heap) # remove least frequent
return [num for freq, num in heap]
print(top_k_frequent([1,1,1,2,2,3], 2)) # [1, 2]
print(top_k_frequent([4,4,4,3,3,2,1], 2)) # [4, 3]تطبيقات الكومات في الجدولة
إلى جانب البرمجة التنافسية، تدعم الكومات أنظمة الجدولة الواقعية. يستخدم مجدولو المهام في أنظمة التشغيل طابور أولوية (كومة) لتشغيل العملية الجاهزة ذات الأولوية الأعلى دائمًا. وتعالج المحاكاة المعتمدة على الأحداث الأحداث بترتيبها الزمني باستخدام كومة دنيا مرتبة حسب وقت الحدث. كما يعطي مجدولو حزم الشبكة الأولوية لحركة المرور وفق فئة جودة الخدمة. ويمنحك فهم الكومة نموذجًا ذهنيًا لهذه الأنظمة جميعًا، ويظهر بصورة طبيعية في مقابلات تصميم الأنظمة التي تتناول قوائم الانتظار والجدولة.
import heapq
# Simple event-driven simulation using a heap
events = [] # (time, event_description)
def schedule(time, event):
heapq.heappush(events, (time, event))
def process_next():
time, event = heapq.heappop(events)
print(f't={time}: {event}')
return time, event
# Schedule events out of order:
schedule(10, 'Send email')
schedule(3, 'Open app')
schedule(7, 'Process request')
schedule(1, 'Start server')
# Process in time order:
while events:
process_next()
# Output: t=1, t=3, t=7, t=10 -- always in time orderاختبار سريع
اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep من هذا الدرس.
مراجعة الدرس
في هذا الدرس تعلمت: MinHeap وMaxHeap من الصفر باستخدام sift-up وsift-down، وخوارزمية heapify لـ Floyd ذات التعقيد O(n)، ولماذا تتفوق على الإدراج عنصرًا تلو الآخر بتعقيد O(n log n)، بالإضافة إلى التطبيقات العملية التي تشمل العناصر الأكثر تكرارًا من نوع top-k والحذف حسب الفهرس. بعد ذلك سنستكشف وحدة heapq في Python وحيل إنشاء الكومة العليا.
الأسئلة الشائعة
هل درس «بناء Heap وPush وPop من الصفر» مجاني؟
نعم — نص درس «بناء Heap وPush وPop من الصفر» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «بناء Heap وPush وPop من الصفر»؟
نفّذ heapify-up لعملية push وheapify-down لعملية pop، ثم أنشئ heap من مصفوفة غير مرتبة في O(n) باستخدام خوارزمية Floyd تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «بناء Heap وPush وPop من الصفر»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- خاصية Heap وتمثيلها كمصفوفة
- بناء Heap وPush وPop من الصفر
- heapq في Python وحيل Max-Heap
- الوسيط من تدفق البيانات والدمج K-Way