0Pricing
Coding Interview Prep · درس

heapq في Python وحيل Max-Heap

استخدم heapq.heappush وheapq.heappop، واعكس القيم لمحاكاة max-heap، وطبّق heapq.nlargest وheapq.nsmallest لاستعلامات top-k السريعة

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

نظرة عامة على وحدة heapq في Python

توفر وحدة heapq في Python كومة دنيا مطبقة فوق قائمة Python عادية. وعلى خلاف فئة كومة مخصصة، تعمل heapq على القوائم الموجودة في مكانها. وظائف الوحدة هي: heapify لإنشاء كومة بتعقيد O(n)، وheappush لإضافة عنصر بتعقيد O(log n)، وheappop لإزالة أصغر عنصر بتعقيد O(log n)، وheappushpop / heapreplace لتحقيق كفاءة مدمجة.

import heapq

# heapq operates on plain Python lists
heap = []
heapq.heappush(heap, 5)
heapq.heappush(heap, 2)
heapq.heappush(heap, 8)
heapq.heappush(heap, 1)

print('Heap array:', heap)          # internal array (not sorted!)
print('Peek min:', heap[0])         # O(1) min access
print('Pop min:', heapq.heappop(heap))  # 1
print('Next min:', heap[0])         # 2

# heapify: turn any list into a heap in O(n)
data = [9, 4, 7, 1, 3, 6, 2]
heapq.heapify(data)
print('Heapified:', data, '| min:', data[0])

إنشاء كومة عليا بعكس إشارات القيم

توفر Python's heapq كومة دنيا فقط. ولمحاكاة كومة عليا، اعكس إشارة جميع القيم قبل إضافتها، ثم اعكس الإشارة مرة أخرى عند استخراجها. ينجح ذلك لأن الكومة ترتب حسب القيم المخزنة، وعكس الإشارة يقلب ترتيبها. تذكر دائمًا عكس الإشارة في الجانبين: اعكسها قبل الإضافة، وبعد الاستخراج. إن نسيان أي من الخطوتين خطأ شائع في المقابلات.

import heapq

max_heap = []
for val in [5, 1, 8, 3, 9, 2]:
    heapq.heappush(max_heap, -val)  # negate on push

print('Max-heap internal:', max_heap)  # all negated

# Pop in descending order:
results = []
while max_heap:
    results.append(-heapq.heappop(max_heap))  # negate on pop
print('Sorted descending:', results)  # [9, 8, 5, 3, 2, 1]

# Common pattern: top-k largest
data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
k = 3
heap = []
for x in data:
    heapq.heappush(heap, -x)
print('Top', k, ':', [-heapq.heappop(heap) for _ in range(k)])

heapq.nlargest وnsmallest

تُرجع heapq.nlargest(k, iterable) وheapq.nsmallest(k, iterable) أكبر k عنصرًا أو أصغرها. وتعقيدهما O(n log k)، وهما أكثر كفاءة من الفرز الكامل ذي التعقيد O(n log n) عندما تكون k أصغر بكثير من n. تستخدم الدالتان داخليًا كومة حجمها k. وعندما تكون k قريبة من n، تعود Python إلى الفرز الكامل. استخدم هاتين الدالتين لاستعلامات top-k لمرة واحدة من دون الحفاظ على كومة مستمرة.

import heapq

data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 7]

# Top 3 largest:
print(heapq.nlargest(3, data))   # [9, 8, 7]
# Top 3 smallest:
print(heapq.nsmallest(3, data))  # [1, 1, 2]

# With a key function:
words = ['banana', 'apple', 'cherry', 'date', 'elderberry']
print(heapq.nlargest(2, words, key=len))   # ['elderberry', 'banana']
print(heapq.nsmallest(2, words, key=len))  # ['date', 'apple']

# Note: when k ~ n, use sorted() instead:
# sorted(data)[-k:]  or  sorted(data, reverse=True)[:k]

استخدام الصفوف في الكومة للمفاتيح المعقدة

عندما تحتاج عناصر الكومة إلى مفتاح مقارنة مخصص، خزّنها في صورة صفوف (priority, data). تقارن heapq في Python الصفوف عنصرًا تلو الآخر، ولذلك تقارن الأولويات أولًا. وإذا تساوت الأولويات، تقارن العنصر الثاني، وقد يؤدي ذلك إلى أخطاء إذا كانت البيانات غير قابلة للمقارنة. والنمط الأكثر أمانًا هو تضمين عدّاد فريد لكسر التعادل، لتجنب مقارنة عناصر البيانات مباشرةً.

import heapq
import itertools

# Pattern: (priority, counter, item)
# Counter ensures unique tiebreaker, avoids comparing items
counter = itertools.count()
heap = []

def push_task(priority, task):
    heapq.heappush(heap, (priority, next(counter), task))

push_task(3, 'low priority task')
push_task(1, 'high priority task')
push_task(2, 'medium priority task')
push_task(1, 'another high priority')

while heap:
    pri, cnt, task = heapq.heappop(heap)
    print(f'P{pri}: {task}')
# Output in priority order: P1, P1, P2, P3

heapq.merge: دمج الكائنات القابلة للتكرار المرتبة

تدمج heapq.merge(*iterables)، بطريقة كسولة، عدة كائنات قابلة للتكرار ومرتبة في مخرجات مرتبة واحدة، من دون تحميل جميع البيانات إلى الذاكرة. ويعادل ذلك دمجًا متعدد المسارات باستخدام كومة دنيا حجمها k، ويُستخدم في خوارزميات الفرز الخارجي. وتُرجع الدالة مكرّرًا، لذا تُنتج العناصر واحدًا تلو الآخر، ما يجعلها مثالية لمجموعات البيانات الكبيرة أو لسيناريوهات التدفق.

import heapq

# Merge multiple sorted lists efficiently
sorted_lists = [
    [1, 5, 9],
    [2, 6, 8],
    [3, 4, 7]
]

# heapq.merge takes sorted iterables and returns a merged sorted iterator
merged = list(heapq.merge(*sorted_lists))
print('Merged:', merged)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# The k-way merge manually (educational version):
def merge_k_sorted(lists):
    heap = []
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst[0], i, 0))
    result = []
    while heap:
        val, list_idx, elem_idx = heapq.heappop(heap)
        result.append(val)
        if elem_idx + 1 < len(lists[list_idx]):
            heapq.heappush(heap, (lists[list_idx][elem_idx+1], list_idx, elem_idx+1))
    return result

print('Manual k-way:', merge_k_sorted(sorted_lists))

نمط الحذف الكسول للكومات

عندما تحتاج إلى إزالة عناصر عشوائية من كومة ولا تعرف فهرس العنصر، استخدم الحذف الكسول: علّم العناصر المحذوفة في مجموعة منفصلة، ثم تخطّها عند الاستخراج. تبلغ الكلفة O(log n) في المتوسط، ويجنبك ذلك تعقيد تتبع الفهارس. وهذا هو الأسلوب القياسي في خوارزمية Dijkstra عند وجود إدخالات مكررة، وفي محاكاة مجدولات المهام.

import heapq

class LazyHeap:
    def __init__(self):
        self._heap = []
        self._removed = set()

    def push(self, task):
        heapq.heappush(self._heap, task)

    def remove(self, task):
        self._removed.add(task)  # mark as removed

    def pop(self):
        while self._heap:
            task = heapq.heappop(self._heap)
            if task not in self._removed:
                return task
        return None

lh = LazyHeap()
for t in [5, 1, 8, 3, 2]:
    lh.push(t)
lh.remove(1)  # 'delete' 1 lazily
lh.remove(8)  # 'delete' 8 lazily
results = [lh.pop() for _ in range(3)]
print(results)  # [2, 3, 5] -- 1 and 8 skipped

العنصر الأكبر رقم K في تدفق

تحافظ مسألة العنصر الأكبر رقم K في تدفق (LeetCode #703) على كومة دنيا حجمها k. ويكون جذر الكومة دائمًا هو العنصر الأكبر رقم k الذي تمت رؤيته حتى الآن. عند وصول رقم جديد: أضِفه، وإذا تجاوز حجم الكومة k، فأخرج أصغر عنصر. ويظل الجذر هو العنصر الأكبر رقم k دائمًا، إذ يوجد بالضبط k-1 عنصرًا أكبر منه في الكومة.

import heapq

class KthLargest:
    def __init__(self, k, nums):
        self.k = k
        self.heap = []
        for num in nums:
            self.add(num)

    def add(self, val):
        heapq.heappush(self.heap, val)
        if len(self.heap) > self.k:
            heapq.heappop(self.heap)  # remove smallest
        return self.heap[0]  # kth largest = root of min-heap

# k=3, initial=[4,5,8,2]
kl = KthLargest(3, [4, 5, 8, 2])
print(kl.add(3))   # 4 (top 3: 8,5,4 -- kth=4)
print(kl.add(5))   # 5 (top 3: 8,5,5 -- kth=5)
print(kl.add(10))  # 5 (top 3: 10,8,5 -- kth=5)
print(kl.add(9))   # 8 (top 3: 10,9,8 -- kth=8)

إيجاد K أزواج ذات أصغر مجموع

تستخدم مسألة إيجاد K أزواج ذات أصغر مجاميع (LeetCode #373) كومة دنيا لتوليد الأزواج بالترتيب. ابدأ بجميع الأزواج (nums1[0], nums2[j]) لكل قيمة j. أخرج أصغر زوج، وبالنسبة إلى الزوج المستخرج (nums1[i], nums2[j])، أضف (nums1[i+1], nums2[j])، أي المرشح التالي من عمود nums2 نفسه. وهذا نمط شائع لتوليد الأزواج أو النواتج المرتبة باستخدام كومة.

import heapq

def k_smallest_pairs(nums1, nums2, k):
    if not nums1 or not nums2:
        return []
    heap = []
    # Initialize with pairs (nums1[0], nums2[j])
    for j in range(min(k, len(nums2))):
        heapq.heappush(heap, (nums1[0] + nums2[j], 0, j))
    result = []
    while heap and len(result) < k:
        total, i, j = heapq.heappop(heap)
        result.append([nums1[i], nums2[j]])
        if i + 1 < len(nums1):
            heapq.heappush(heap, (nums1[i+1] + nums2[j], i+1, j))
    return result

print(k_smallest_pairs([1,7,11], [2,4,6], 3))
# [[1,2], [1,4], [1,6]]

مجدول المهام باستخدام كومة عليا

تطلب مسألة مجدول المهام (LeetCode #621) إيجاد الحد الأدنى للوقت اللازم لجدولة n مهمة، مع فترة تبريد مقدارها n فواصل زمنية بين المهمتين نفسيهما. استخدم كومة عليا لتكرارات المهام: في كل خطوة زمنية، اختر المهمة المتاحة الأكثر تكرارًا، وأنقص عددها، ثم ضعها في فترة التبريد. عالج k=n+1 مهمة في كل دورة، أو املأ الوقت بفترات خمول. ويعطي هذا الأسلوب الجشع باستخدام كومة عليا الإجابة المثلى.

import heapq
from collections import Counter

def least_interval(tasks, n):
    freq = Counter(tasks)
    heap = [-f for f in freq.values()]  # max-heap (negated)
    heapq.heapify(heap)
    time = 0
    while heap:
        cycle = n + 1
        temp = []
        for _ in range(cycle):
            if heap:
                temp.append(heapq.heappop(heap))
        for f in temp:
            if f + 1 < 0:  # still tasks remaining
                heapq.heappush(heap, f + 1)
        # Add full cycle or remaining tasks if queue empty
        time += cycle if heap else len(temp)
    return time

print(least_interval(['A','A','A','B','B','B'], 2))  # 8
print(least_interval(['A','A','A','B','B','B'], 0))  # 6

الكومة في خوارزمية Dijkstra

يُطبَّق طابور الأولوية في خوارزمية Dijkstra باستخدام كومة دنيا. خزّن الصفوف (distance, node)، وعالج دائمًا أقرب عقدة لم تتم زيارتها أولًا. عند استخراج عقدة ذات مسافة أكبر من أقصر مسار معروف لها حاليًا، وهي إدخالة قديمة ناتجة عن الحذف الكسول، تخطَّها. يؤدي ذلك إلى الاستغناء عن عملية decrease-key، ويحافظ على بساطة التطبيق مع إبقاء التعقيد O((V + E) log V).

import heapq

def dijkstra(graph, start):
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    heap = [(0, start)]  # (distance, node)
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:   # stale entry, skip
            continue
        for v, w in graph[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    return dist

graph = {
    'A': [('B', 4), ('C', 1)],
    'B': [('D', 1)],
    'C': [('B', 2), ('D', 5)],
    'D': []
}
print(dijkstra(graph, 'A'))  # {'A':0,'B':3,'C':1,'D':4}

إعادة ترتيب سلسلة باستخدام كومة عليا

تطلب مسألة إعادة ترتيب سلسلة (LeetCode #767) إعادة ترتيب سلسلة بحيث لا يكون حرفان متجاوران متماثلين. استخدم كومة عليا من (-frequency, char). في كل خطوة، أخرج الحرف الأكثر تكرارًا. وإذا كان الحرف السابق هو نفسه الحرف الأكثر تكرارًا، فأخرج الحرف الثاني من حيث التكرار بدلًا منه. ويضمن هذا الأسلوب الجشع وضع الحرف الأكثر تقييدًا في أقرب موضع ممكن.

import heapq
from collections import Counter

def reorganize_string(s):
    freq = Counter(s)
    heap = [(-f, c) for c, f in freq.items()]
    heapq.heapify(heap)
    result = []
    prev_freq, prev_char = 0, ''
    while heap:
        freq, char = heapq.heappop(heap)
        result.append(char)
        # Push back the previous character if still remaining
        if prev_freq < 0:
            heapq.heappush(heap, (prev_freq, prev_char))
        prev_freq, prev_char = freq + 1, char  # decrement freq (less negative)
    result_str = ''.join(result)
    # Verify no adjacent duplicates
    return result_str if len(result_str) == len(s) else ''

print(reorganize_string('aab'))   # 'aba'
print(reorganize_string('aaab'))  # '' (impossible)

اختبار سريع

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

مراجعة الدرس

في هذا الدرس تعلمت: واجهة برمجة التطبيقات لوحدة Python's heapq، بما في ذلك heapify وheappush وheappop وnlargest وnsmallest وmerge، ومحاكاة الكومة العليا بعكس إشارات القيم، والأنماط الشائعة للكومات في مقابلات البرمجة، بما في ذلك top-k في التدفقات، والعنصر الأكبر رقم k في تدفق، ومجدول المهام، وخوارزمية Dijkstra. بعد ذلك سنتناول الوسيط من تدفق البيانات والدمج متعدد المسارات.

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

هل درس «heapq في Python وحيل Max-Heap» مجاني؟

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

ماذا ستتعلم في «heapq في Python وحيل Max-Heap»؟

استخدم heapq.heappush وheapq.heappop، واعكس القيم لمحاكاة max-heap، وطبّق heapq.nlargest وheapq.nsmallest لاستعلامات top-k السريعة تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «heapq في Python وحيل Max-Heap»؟

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

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

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

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

  1. خاصية Heap وتمثيلها كمصفوفة
  2. بناء Heap وPush وPop من الصفر
  3. heapq في Python وحيل Max-Heap
  4. الوسيط من تدفق البيانات والدمج K-Way
← العودة إلى Coding Interview Prep