0Pricing
Coding Interview Prep · درس

الوسيط من تدفق البيانات والدمج K-Way

حافظ على heapين، أحدهما max-heap للنصف الأصغر والآخر min-heap للنصف الأكبر، لتحديث الوسيط في O(log n)، وادمج k قوائم مرتبة باستخدام heap

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

مسألة الوسيط من تدفق البيانات

تطلب مسألة إيجاد الوسيط من تدفق البيانات (LeetCode #295) دعم عمليتين بكفاءة: addNum(num) لإضافة رقم، وfindMedian() لإرجاع الوسيط الحالي. وسيط القائمة ذات الطول الزوجي هو متوسط القيمتين الواقعتين في المنتصف. تعطي القائمة المرتبة بالطريقة الساذجة عملية إدراج بتعقيد O(n) وإيجاد وسيط بتعقيد O(1). أما الحل الأمثل فيستخدم كومتين لتحقيق إدراج بتعقيد O(log n) وإيجاد وسيط بتعقيد O(1).

import heapq

# Strategy: maintain two halves of the data
# max_heap: lower half (stores negated values for max behavior)
# min_heap: upper half
# Invariant: len(max_heap) == len(min_heap) or len(max_heap) == len(min_heap) + 1
# Invariant: max(max_heap) <= min(min_heap)
# Median:
#   odd count:  max_heap[0] (top of lower half)
#   even count: average of tops of both halves
print('Two-heap strategy for O(log n) insert, O(1) median')

تنفيذ MedianFinder باستخدام كومتين

حافظ على كومة عليا للنصف الأدنى وكومة دنيا للنصف الأعلى. تأكد دائمًا من أن حجم الكومة العليا يساوي حجم الكومة الدنيا أو يزيد عليه بعنصر واحد. عند إضافة رقم: أضفه إلى الكومة العليا، ثم وازن بنقل رأس الكومة العليا إلى الكومة الدنيا إذا تجاوز أصغر عنصر فيها، وأعد موازنة الأحجام عند الحاجة.

import heapq

class MedianFinder:
    def __init__(self):
        self.lo = []  # max-heap (negated) for lower half
        self.hi = []  # min-heap for upper half

    def addNum(self, num):
        heapq.heappush(self.lo, -num)   # push to lower half
        # Ensure max of lower <= min of upper
        if self.hi and -self.lo[0] > self.hi[0]:
            heapq.heappush(self.hi, -heapq.heappop(self.lo))
        # Balance sizes: lo can have at most 1 more than hi
        if len(self.lo) > len(self.hi) + 1:
            heapq.heappush(self.hi, -heapq.heappop(self.lo))
        elif len(self.hi) > len(self.lo):
            heapq.heappush(self.lo, -heapq.heappop(self.hi))

    def findMedian(self):
        if len(self.lo) > len(self.hi):
            return -self.lo[0]  # odd count: top of lower half
        return (-self.lo[0] + self.hi[0]) / 2

mf = MedianFinder()
for n in [1, 2, 3, 4, 5]: mf.addNum(n)
print(mf.findMedian())  # 3.0

تتبّع خطوات MedianFinder

يُعد فهم سبب الحفاظ على الثابت الخاص بالكومتين أمرًا بالغ الأهمية لشرح الحل في المقابلة. لنتتبّع إضافة [5, 15, 1, 3] خطوةً بخطوة. بعد كل إدراج، وازن الكومتين بحيث تحتوي الكومة العليا للنصف الأدنى على النصف الأصغر من القيم. ويضمن الثابت تحقق max(lo) <= min(hi) دائمًا، مما يجعل الوصول إلى الوسيط مباشرًا من رأس إحدى الكومتين أو كلتيهما.

import heapq

# Manual trace for [5, 15, 1, 3]:
# add 5:   lo=[-5]        hi=[]       median=5
# add 15:  lo=[-5]        hi=[15]     median=(5+15)/2=10
# add 1:   lo=[-5,-1]     hi=[15]     median=5
# add 3:   lo=[-5,-3,-1]  hi=[15]     -- lo too big
#       -> lo=[-5,-3]      hi=[1,15]  -- wait, wrong direction
# Actually:
# add 1:   push to lo -> lo=[-5,-1], then 1>lo? No, -lo[0]=5>15? No
#          lo has 2, hi has 1: balance -> move lo top to hi
#          lo=[-1], hi=[5,15]
# Median = (-lo[0] + hi[0])/2 = (1+5)/2 = 3
mf2 = MedianFinder()
for n, expected in [(5, 5.0), (15, 10.0), (1, 5.0), (3, 4.0)]:
    mf2.addNum(n)
    print(f'After adding {n}: median={mf2.findMedian()} (expected ~{expected})')

وسيط النافذة المنزلقة

تُعد مسألة وسيط النافذة المنزلقة (LeetCode #480) نسخة أصعب: أوجد وسيط كل نافذة حجمها k أثناء انزلاقها عبر المصفوفة. ويمتد أسلوب الكومتين باستخدام مجموعة للحذف الكسول للتعامل مع العناصر التي تخرج من النافذة. عند مغادرة عنصر للنافذة، علّمه في مجموعة الحذف؛ وعندما يصل إلى رأس أي من الكومتين، تخلّص منه.

import heapq

def median_sliding_window(nums, k):
    lo = []  # max-heap (negated)
    hi = []  # min-heap
    removed = {}
    result = []

    def balance():
        # Move valid tops to correct side
        while lo and removed.get(-lo[0], 0) > 0:
            removed[-lo[0]] -= 1; heapq.heappop(lo)
        while hi and removed.get(hi[0], 0) > 0:
            removed[hi[0]] -= 1; heapq.heappop(hi)

    for i, num in enumerate(nums):
        heapq.heappush(lo, -num)
        heapq.heappush(hi, -heapq.heappop(lo))
        if len(hi) > len(lo): heapq.heappush(lo, -heapq.heappop(hi))
        if i >= k:
            out = nums[i - k]
            removed[out] = removed.get(out, 0) + 1
        balance()
        if len(lo) > len(hi): heapq.heappush(hi, -heapq.heappop(lo))
        if i >= k - 1:
            if len(lo) > len(hi): result.append(float(-lo[0]))
            else: result.append((-lo[0] + hi[0]) / 2.0)
    return result

print(median_sliding_window([1,3,-1,-3,5,3,6,7], 3))  # [1,-1,-1,3,5,6]

الدمج متعدد المسارات: المشكلة

تُعد مسألة دمج K قوائم مرتبة (LeetCode #23) مسألة أساسية لها تطبيقات في الفرز الخارجي، ودمج قواعد البيانات، والأنظمة الموزعة. مع إعطائك k قوائم مرتبطة مرتبة تحتوي إجمالًا على n عقدة، ادمجها في قائمة مرتبة واحدة. تبلغ كلفة الأسلوب الساذج، الذي يدمج قائمتين في كل مرة، O(kn)، أو O(n log k) باستخدام أسلوب فرق تسد. أما أسلوب الكومة فيعالج كل عقدة مرة واحدة بالضبط، مع كلفة O(log k) لكل عقدة، أي O(n log k) إجمالًا.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# Build a linked list from a Python list
def build_list(arr):
    dummy = ListNode(0)
    curr = dummy
    for val in arr:
        curr.next = ListNode(val)
        curr = curr.next
    return dummy.next

# Convert linked list to Python list for printing
def to_list(head):
    result = []
    while head:
        result.append(head.val)
        head = head.next
    return result

print('K-way merge: O(n log k) using a min-heap of k heads')

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

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

import heapq

def merge_k_lists(lists):
    dummy = ListNode(0)
    curr = dummy
    heap = []
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))
    while heap:
        val, i, node = heapq.heappop(heap)
        curr.next = node
        curr = curr.next
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))
    return dummy.next

lists = [
    build_list([1, 4, 5]),
    build_list([1, 3, 4]),
    build_list([2, 6])
]
result = merge_k_lists(lists)
print(to_list(result))  # [1, 1, 2, 3, 4, 4, 5, 6]

أصغر نطاق يغطي K قوائم

تبحث مسألة أصغر نطاق (LeetCode #632) عن أصغر نطاق [lo, hi] بحيث يقع عنصر واحد على الأقل من كل قائمة من القوائم المرتبة وعددها k داخل النطاق. استخدم كومة دنيا مهيّأة بالعنصر الأول من كل قائمة، وتتبع القيمة العظمى الحالية. صغّر النطاق دائمًا بتقديم القائمة التي تحتوي على القيمة الصغرى الحالية. توقف عند استنفاد أي قائمة.

import heapq

def smallest_range(nums):
    heap = []
    current_max = float('-inf')
    for i, lst in enumerate(nums):
        heapq.heappush(heap, (lst[0], i, 0))
        current_max = max(current_max, lst[0])
    best = [float('-inf'), float('inf')]
    while heap:
        current_min, list_idx, elem_idx = heapq.heappop(heap)
        if current_max - current_min < best[1] - best[0]:
            best = [current_min, current_max]
        if elem_idx + 1 >= len(nums[list_idx]):
            break  # one list exhausted
        next_val = nums[list_idx][elem_idx + 1]
        heapq.heappush(heap, (next_val, list_idx, elem_idx + 1))
        current_max = max(current_max, next_val)
    return best

print(smallest_range([[4,10,15,24,26],[0,9,12,20],[5,18,22,30]]))
# [20, 24]

العنصر الأصغر رقم K في مصفوفة

تطلب مسألة العنصر الأصغر رقم K في مصفوفة مرتبة (LeetCode #378) التعامل مع مصفوفة n×n تكون كل صفوفها وأعمدتها مرتبة، وإيجاد العنصر الأصغر رقم k. اعتبر كل صف قائمة مرتبة، واستخدم الدمج متعدد المسارات مع كومة. ويمكن بديلًا إجراء بحث ثنائي على نطاق القيم. تبلغ كلفة أسلوب الكومة O(k log n)، وهو فعال عندما تكون k صغيرة؛ أما البحث الثنائي فكلفته O(n log(max-min))، ويتعامل بصورة أفضل مع قيم k الكبيرة.

import heapq

def kth_smallest_matrix(matrix, k):
    n = len(matrix)
    heap = [(matrix[0][0], 0, 0)]
    count = 0
    visited = {(0, 0)}
    while heap:
        val, r, c = heapq.heappop(heap)
        count += 1
        if count == k:
            return val
        # Push right neighbor
        if c + 1 < n and (r, c+1) not in visited:
            heapq.heappush(heap, (matrix[r][c+1], r, c+1))
            visited.add((r, c+1))
        # Push bottom neighbor
        if r + 1 < n and (r+1, c) not in visited:
            heapq.heappush(heap, (matrix[r+1][c], r+1, c))
            visited.add((r+1, c))
    return -1

matrix = [[1,5,9],[10,11,13],[12,13,15]]
print(kth_smallest_matrix(matrix, 8))  # 13

كومتان للإحصاءات الجارية

يتجاوز نمط الكومتين استخدام الوسيط. إذ يمكنك استخدامه للحفاظ على مئين جارٍ، مثل المئين الخامس والعشرين: اضبط حجم الكومة الدنيا لتحتوي على p*n عنصرًا، وحجم الكومة العليا لتحتوي على (1-p)*n عنصرًا. وكلما أُضيف عنصر، أعد الموازنة كما سبق. يظهر هذا النمط في مسائل الإحصاءات المتدفقة التي تحتاج إلى إدراج فعال واستعلامات مئين في الوقت نفسه.

import heapq

# Generalised two-heap for arbitrary quantile p
# lo contains floor(p * count) elements
# hi contains the remaining elements
class QuantileFinder:
    def __init__(self, p):
        self.p = p  # quantile (e.g., 0.5 for median)
        self.lo = []  # max-heap
        self.hi = []  # min-heap
        self.count = 0

    def add(self, num):
        self.count += 1
        heapq.heappush(self.lo, -num)
        heapq.heappush(self.hi, -heapq.heappop(self.lo))
        # Target: lo should have floor(p * count) elements
        target_lo = int(self.p * self.count)
        while len(self.lo) < target_lo:
            heapq.heappush(self.lo, -heapq.heappop(self.hi))
        while len(self.lo) > target_lo:
            heapq.heappush(self.hi, -heapq.heappop(self.lo))

    def quantile(self):
        return -self.lo[0] if self.lo else self.hi[0]

qf = QuantileFinder(0.5)  # median
for n in [1, 2, 3, 4, 5, 6]: qf.add(n)
print(qf.quantile())  # 3 (median of 1-6)

إيجاد K نقطة الأقرب إلى الأصل

تستخدم مسألة إيجاد K نقطة الأقرب إلى الأصل (LeetCode #973) كومة عليا حجمها k. أضف مربع مسافة كل نقطة، لتجنب حساب الجذر التربيعي. وعندما يتجاوز حجم الكومة k، أخرج النقطة الأبعد. وتكون النقاط k المتبقية هي الأقرب. يبلغ التعقيد O(n log k). ويستخدم بديلًا لذلك quickselect بمتوسط O(n)، لكن حل الكومة أبسط في التطبيق الصحيح والشرح أثناء المقابلة.

import heapq

def k_closest(points, k):
    heap = []  # max-heap via negation
    for x, y in points:
        dist_sq = x*x + y*y
        heapq.heappush(heap, (-dist_sq, x, y))
        if len(heap) > k:
            heapq.heappop(heap)  # remove farthest
    return [[x, y] for _, x, y in heap]

points = [[1,3], [-2,2], [5,8], [0,1], [-1,-1]]
print(k_closest(points, 2))
# Two closest to origin: [0,1] (dist=1) and [-1,-1] (dist=2)

# Verify by distances:
for x, y in points:
    print(f'({x},{y}): dist^2 = {x*x+y*y}')

تحليل الزمن والمساحة للكومتين

يحقق أسلوب الكومتين للوسيط كلفة O(log n) لكل addNum وكلفة O(1) لكل findMedian. وتبلغ المساحة O(n) لتخزين جميع العناصر. أما الدمج متعدد المسارات فزمنه O(n log k)، ومساحته O(k) للكومة. وتُعد هذه النتائج شبه مثلى؛ إذ يمكن إثبات حد سفلي قائم على المقارنة مقداره Omega(n log k) للدمج متعدد المسارات، مما يوضح أن حل الكومة مثالي تقاربيًا. اذكر هذه التعقيدات بوضوح دائمًا في المقابلات.

# Complexity summary for heap applications:
# Problem               | Time per op  | Space
# ----------------------|--------------|------
# MedianFinder.addNum   | O(log n)     | O(n)
# MedianFinder.find     | O(1)         | -
# Merge k sorted lists  | O(n log k)   | O(k)
# Kth smallest matrix   | O(k log n)   | O(n)
# K closest points      | O(n log k)   | O(k)
# Task scheduler        | O(n log 26)  | O(26)
# Kth largest stream    | O(log k)     | O(k)
# Sliding window median | O(n log k)   | O(k)

print('Heap problems: identify k (heap size) vs n (input size)')

اختبار سريع

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

مراجعة الدرس

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

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

هل درس «الوسيط من تدفق البيانات والدمج K-Way» مجاني؟

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

ماذا ستتعلم في «الوسيط من تدفق البيانات والدمج K-Way»؟

حافظ على heapين، أحدهما max-heap للنصف الأصغر والآخر min-heap للنصف الأكبر، لتحديث الوسيط في O(log n)، وادمج k قوائم مرتبة باستخدام heap تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «الوسيط من تدفق البيانات والدمج K-Way»؟

معظم دروس 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