0Pricing
DSA Interview Prep · درس

أقصى قيمة في النافذة المنزلقة باستخدام deque رتيب

حافظ على deque تنازلي من الفهارس للإجابة عن استعلامات القيمة القصوى في النافذة في O(1) لكل عنصر، وحلّ مسألة sliding-window-maximum في O(n).

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

مسألة الحد الأقصى للنافذة المنزلقة

تعطي مسألة Sliding Window Maximum (LeetCode 239) مصفوفة وحجم نافذة k. ومع انزلاق النافذة من اليسار إلى اليمين موضعًا واحدًا في كل مرة، أخرج أكبر عنصر في كل نافذة. يحسب الحل بالقوة الغاشمة القيمة العظمى لكل نافذة مكوّنة من k عناصر في O(k)، مما يعطي تعقيدًا إجماليًا قدره O(nk)، وهو بطيء جدًا عندما تكون k كبيرة.

يحقق الحل باستخدام deque رتيب (طابور مزدوج النهاية) تعقيدًا إجماليًا قدره O(n)، وذلك بالحفاظ على deque تنازلي من الفهارس. وتحتوي المقدمة دائمًا على فهرس أكبر عنصر في النافذة الحالية، مما يوفر استعلامات عن القيمة العظمى في O(1)، مع السماح بالعمليات من المقدمة والمؤخرة.

from collections import deque

# Brute force O(nk) for comparison
def sliding_max_brute(nums, k):
    return [max(nums[i:i+k]) for i in range(len(nums) - k + 1)]

nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
print('Input:', nums, 'k=', k)
print('Expected: [3, 3, 5, 5, 6, 7]')
print('Brute:   ', sliding_max_brute(nums, k))

deque رتيب: الفكرة الأساسية

حافظ على deque تنازلي رتيب يخزّن الفهارس (لا القيم). الثابت هو: nums[deque[0]] >= nums[deque[1]] >= ... >= nums[deque[-1]]. قبل إضافة الفهرس i:

  • أزل الفهارس منتهية الصلاحية من المقدمة: إذا كان deque[0] <= i - k، فهذا يعني أن الفهرس خرج من النافذة.
  • أزل الفهارس ذات القيم الأصغر من المؤخرة: ما دام nums[deque[-1]] <= nums[i]، فلن تتمكن تلك الفهارس من أن تكون القيمة العظمى لأي نافذة مستقبلية (فهي تقع إلى اليسار وقيمها أصغر)، لذا تخلّص منها.

بعد هذه العمليات، أضف i إلى المؤخرة. وتُعطي المقدمة دائمًا القيمة العظمى للنافذة الحالية.

from collections import deque

def sliding_window_max(nums, k):
    dq = deque()  # stores indices; values are decreasing
    result = []

    for i, n in enumerate(nums):
        # 1. Remove indices outside the current window
        while dq and dq[0] <= i - k:
            dq.popleft()

        # 2. Remove indices with smaller values from the back
        while dq and nums[dq[-1]] <= n:
            dq.pop()

        dq.append(i)

        # 3. Record max when first full window is complete
        if i >= k - 1:
            result.append(nums[dq[0]])   # front = max of current window

    return result

nums = [1, 3, -1, -3, 5, 3, 6, 7]
print(sliding_window_max(nums, 3))  # [3, 3, 5, 5, 6, 7]

تتبّع deque خطوة بخطوة

لنتتبّع المصفوفة [1, 3, -1, -3, 5, 3, 6, 7] مع k=3:

  • i=0 (1): dq=[0]
  • i=1 (3): أخرج 0 (1<3)، dq=[1]
  • i=2 (-1): -1<3 لذا نُبقيه، dq=[1,2]. النافذة [1,3,-1]، max=nums[1]=3
  • i=3 (-3): -3<-1، dq=[1,2,3]. افحص المقدمة: 1 > 3-3=0، وهي صالحة. القيمة العظمى للنافذة=3
  • i=4 (5): أخرج 3،2،1 (كلها أصغر)، dq=[4]. المقدمة 4 > 4-3=1، وهي صالحة. القيمة العظمى=5
  • i=5 (3): 3<5، dq=[4,5]. المقدمة 4 > 5-3=2، وهي صالحة. القيمة العظمى=5
  • i=6 (6): أخرج 5،4 (كلاهما أصغر)، dq=[6]. القيمة العظمى=6
  • i=7 (7): أخرج 6، dq=[7]. القيمة العظمى=7
from collections import deque

def sliding_window_max_trace(nums, k):
    dq = deque()
    result = []
    for i, n in enumerate(nums):
        while dq and dq[0] <= i - k:
            print(f'  Remove expired index {dq[0]} from front')
            dq.popleft()
        while dq and nums[dq[-1]] <= n:
            print(f'  Remove smaller index {dq[-1]} (val={nums[dq[-1]]}) from back')
            dq.pop()
        dq.append(i)
        print(f'i={i} n={n}: dq={list(dq)} vals={[nums[j] for j in dq]}')
        if i >= k - 1:
            win_max = nums[dq[0]]
            result.append(win_max)
            print(f'  Window {nums[max(0,i-k+1):i+1]} -> max={win_max}')
    return result

nums = [1, 3, -1, -3, 5, 3, 6, 7]
result = sliding_window_max_trace(nums, 3)
print('Result:', result)

لماذا يُضاف كل عنصر ويُزال مرة واحدة على الأكثر

يأتي ضمان O(n) من التحليل المُطفأ نفسه المستخدم مع المكدس الرتيب: يُضاف كل فهرس إلى deque مرة واحدة بالضبط، ويُزال — إما من المقدمة عند انتهاء صلاحيته أو من المؤخرة عند استبداله — مرة واحدة على الأكثر. لذلك لا يتجاوز العدد الإجمالي لعمليات deque خلال الحلقة كلها 2n.

لا تزيد حلقات while الداخلية التعقيد الإجمالي؛ فأي عملية إخراج تُجرى فيها تكون تكلفتها مدفوعة بعملية الإضافة السابقة. وهذا هو المنطق نفسه المستخدم مع المكدس الرتيب، لكنه ممتد إلى deque الذي يتيح الإزالة من الطرفين.

from collections import deque

def sliding_window_max_instrumented(nums, k):
    dq = deque()
    result = []
    front_pops = back_pops = pushes = 0

    for i, n in enumerate(nums):
        while dq and dq[0] <= i - k:
            dq.popleft(); front_pops += 1
        while dq and nums[dq[-1]] <= n:
            dq.pop(); back_pops += 1
        dq.append(i); pushes += 1
        if i >= k - 1:
            result.append(nums[dq[0]])

    print(f'n={len(nums)}: pushes={pushes}, front_pops={front_pops}, back_pops={back_pops}')
    print(f'Total deque ops = {pushes + front_pops + back_pops} <= 3n = {3*len(nums)}')
    return result

import random; random.seed(0)
nums = [random.randint(-100, 100) for _ in range(20)]
sliding_window_max_instrumented(nums, 5)

الحد الأدنى للنافذة المنزلقة

الحد الأدنى للنافذة المنزلقة هو النظير المتماثل: حافظ على deque متزايد رتيب (أخرج من المؤخرة عندما يكون العنصر الجديد أصغر من العنصر الموجود في المؤخرة). وتحتوي المقدمة دائمًا على القيمة الصغرى للنافذة الحالية. أما جميع الخطوات الأخرى فمطابقة لنسخة الحد الأقصى — ما عليك سوى عكس اتجاه المقارنة.

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

from collections import deque

def sliding_window_min(nums, k):
    dq = deque()  # increasing monotonic deque
    result = []

    for i, n in enumerate(nums):
        while dq and dq[0] <= i - k:
            dq.popleft()               # expired
        while dq and nums[dq[-1]] >= n:
            dq.pop()                   # pop larger values from back
        dq.append(i)
        if i >= k - 1:
            result.append(nums[dq[0]])  # front = min
    return result

nums = [1, 3, -1, -3, 5, 3, 6, 7]
print('Max k=3:', sliding_window_min.__name__, '->', end=' ')
print(sliding_window_min(nums, 3))   # [-1, -3, -3, -3, 3, 3]

from collections import deque
def sliding_window_max(nums, k):
    dq = deque(); result = []
    for i, n in enumerate(nums):
        while dq and dq[0] <= i-k: dq.popleft()
        while dq and nums[dq[-1]] <= n: dq.pop()
        dq.append(i)
        if i >= k-1: result.append(nums[dq[0]])
    return result

print('Max k=3:', sliding_window_max(nums, 3))   # [3,3,5,5,6,7]

Jump Game VI: برمجة ديناميكية باستخدام deque رتيب

تُعد Jump Game VI (LeetCode 1696) مثالًا كلاسيكيًا يجمع بين البرمجة الديناميكية وdeque الرتيب. مع إعطائك مصفوفة وحجم قفزة أقصى k، وبدءًا من الفهرس 0، تقفز في كل خطوة من 1 إلى k من الخطوات إلى الأمام، مع إضافة نقاط الخلية الهدف. أوجد أكبر مجموع للنقاط. علاقة التكرار في البرمجة الديناميكية هي dp[i] = nums[i] + max(dp[i-k], ..., dp[i-1]). ويعطي حساب الحد الأقصى لنافذة منزلقة على مصفوفة DP تعقيدًا إجماليًا قدره O(n).

يظهر هذا النمط — علاقة تكرار في البرمجة الديناميكية تعتمد فيها كل خلية على القيمة العظمى لنافذة ثابتة الحجم من الخلايا السابقة — كثيرًا، ويستدعي دائمًا استخدام deque رتيب.

from collections import deque

def max_result(nums, k):
    n = len(nums)
    dp = [0] * n
    dp[0] = nums[0]
    dq = deque([0])   # indices of max dp values in current window

    for i in range(1, n):
        # Remove expired indices
        while dq and dq[0] < i - k:
            dq.popleft()
        # dp[i] = nums[i] + max dp in window [i-k, i-1]
        dp[i] = nums[i] + dp[dq[0]]
        # Maintain decreasing deque on dp values
        while dq and dp[dq[-1]] <= dp[i]:
            dq.pop()
        dq.append(i)

    return dp[n - 1]

print(max_result([1,-1,-2,4,-7,3], 2))    # 7: path 1->4->3
print(max_result([10,-5,-2,4,0,3], 3))    # 17: path 10->4->3
print(max_result([1,-5,-20,4,-1,3,-6,-3], 2))  # 0

الحد الأقصى للنافذة المنزلقة: بديل شجرة المقاطع

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

في المقابلات، فضّل دائمًا deque الرتيب بتعقيد O(n) على شجرة المقاطع بتعقيد O(n log n) عندما يكون حجم النافذة ثابتًا. واذكر المفاضلة: لا يستطيع deque التعامل مع أحجام النوافذ العشوائية أو التحديثات، بينما تستطيع أشجار المقاطع ذلك.

# Sparse table for static RMQ (range maximum query)
import math

def build_sparse_table(arr):
    n = len(arr)
    LOG = int(math.log2(n)) + 1 if n else 1
    table = [[0]*n for _ in range(LOG)]
    table[0] = arr[:]
    j = 1
    while (1 << j) <= n:
        for i in range(n - (1 << j) + 1):
            table[j][i] = max(table[j-1][i], table[j-1][i + (1 << (j-1))])
        j += 1
    return table

def query(table, l, r):
    k = int(math.log2(r - l + 1))
    return max(table[k][l], table[k][r - (1 << k) + 1])

arr = [1, 3, -1, -3, 5, 3, 6, 7]
table = build_sparse_table(arr)
k = 3
result = [query(table, i, i + k - 1) for i in range(len(arr) - k + 1)]
print('Sparse table result:', result)  # [3, 3, 5, 5, 6, 7]

أطول مقطع فرعي من الواحدات بعد حذف عنصر واحد

LeetCode 1493: مع إعطائك مصفوفة ثنائية، أوجد طول أطول مقطع فرعي من العناصر 1 بعد حذف عنصر واحد بالضبط (يمكن أن يكون هذا العنصر 0 أو 1). هذه مسألة نافذة منزلقة. حافظ على نافذة تحتوي على 0 واحد على الأكثر. وعندما تحتوي النافذة على أكثر من 0 واحد، صغّرها من اليسار.

يستخدم هذا النمط نافذة منزلقة متغيرة الحجم، وليس deque. ومع ذلك، يجمعه مع تقنية إيجاد النافذة الأكبر: بعد العثور على جميع النوافذ الصالحة، يكون الطول الأقصى هو الإجابة. ويعني «حذف عنصر واحد» أننا نسمح بوجود 0 واحد بالضبط في نافذة العناصر 1.

def longest_subarray(nums):
    left = 0
    zeros = 0
    max_len = 0

    for right in range(len(nums)):
        if nums[right] == 0:
            zeros += 1
        while zeros > 1:
            if nums[left] == 0:
                zeros -= 1
            left += 1
        # Window [left, right] has at most 1 zero
        # After deleting one element, length = right - left (not +1, since we delete one)
        max_len = max(max_len, right - left)

    return max_len

print(longest_subarray([1,1,0,1]))       # 3: delete the 0
print(longest_subarray([0,1,1,1,0,1,1,0,1]))  # 5
print(longest_subarray([1,1,1]))          # 2: must delete one 1

مقارنة بين deque والطابور والمكدس

يُعد فهم الوقت المناسب لاستخدام كل حاوية أمرًا أساسيًا في المقابلات:

  • المكدس (list): بنظام LIFO، مع الوصول من طرف واحد. استخدمه في DFS وتحليل التعابير ومسائل المكدس الرتيب.
  • الطابور (deque مع appendleft/popleft): بنظام FIFO، مع الإضافة من طرف والإخراج من الطرف الآخر. استخدمه في BFS وجدولة المهام.
  • deque: يمكن الوصول إلى طرفيه في O(1). استخدمه للنوافذ المنزلقة ذات العناصر منتهية الصلاحية (إزالة من المقدمة) وللحفاظ على الثابت الرتيب (إزالة من المؤخرة). ويُعد الحد الأقصى للنافذة المنزلقة المسألة الأساسية لاستخدام deque.

توفر collections.deque الأداة اللازمة للسلوكيات الثلاثة. استخدم append/pop لسلوك المكدس، وappend/popleft أو appendleft/pop لسلوك الطابور أو deque.

from collections import deque

# deque as stack
stack = deque()
stack.append(1); stack.append(2); stack.append(3)
print('Stack pop:', stack.pop())  # 3 (LIFO)

# deque as queue
queue = deque()
queue.append(1); queue.append(2); queue.append(3)
print('Queue pop:', queue.popleft())  # 1 (FIFO)

# deque as sliding window with front expiry + back monotonic
dq = deque()
nums = [3, 1, 4, 1, 5, 9, 2, 6]
k = 3
for i, n in enumerate(nums):
    while dq and dq[0] <= i - k: dq.popleft()   # expire front
    while dq and nums[dq[-1]] <= n: dq.pop()     # maintain back
    dq.append(i)
    if i >= k - 1:
        print(f'Window {nums[max(0,i-k+1):i+1]}: max={nums[dq[0]]}')

أقصر مقطع فرعي مجموعُه لا يقل عن K: deque ومجاميع بادئة

تُعد مسألة Shortest Subarray with Sum at Least K (LeetCode 862) مسألة متقدمة تجمع بين مجاميع البادئة وdeque رتيب. أنشئ مجاميع البادئة، ثم استخدم deque للعثور، لكل نهاية يمنى، على مجموع البادئة الواقع في أقصى اليسار الذي يحقق prefix[right] - prefix[left] >= k. ويحافظ deque على مجاميع بادئة متزايدة (يُخرج من المؤخرة للحفاظ على الترتيب المتزايد)، ويُخرج من المقدمة لجمع الإجابات الصالحة.

هذه واحدة من أصعب مسائل النوافذ المنزلقة لأنها تتضمن أعدادًا سالبة (مما يستبعد استخدام مؤشرين بسيطين)، ولأنها تتطلب من deque أن يعمل في الوقت نفسه كبنية رتيبة وآلية لانتهاء الصلاحية.

from collections import deque

def shortest_subarray(nums, k):
    n = len(nums)
    prefix = [0] * (n + 1)
    for i in range(n):
        prefix[i + 1] = prefix[i] + nums[i]

    dq = deque()    # monotonic increasing deque of indices into prefix
    result = float('inf')

    for right in range(n + 1):
        # Pop from front: valid subarrays ending at `right`
        while dq and prefix[right] - prefix[dq[0]] >= k:
            result = min(result, right - dq.popleft())
        # Pop from back: maintain increasing deque
        while dq and prefix[dq[-1]] >= prefix[right]:
            dq.pop()
        dq.append(right)

    return result if result != float('inf') else -1

print(shortest_subarray([1], 1))               # 1
print(shortest_subarray([1, 2], 4))            # -1
print(shortest_subarray([2, -1, 2], 3))        # 3
print(shortest_subarray([84,-37,32,40,95], 167))  # 3

استراتيجية المقابلة لمسائل deque

تعرّف على مسألة deque رتيب من خلال المؤشرات التالية: (1) تحتاج إلى القيمة العظمى أو الصغرى لنافذة منزلقة ثابتة الحجم، أو (2) تحتاج إلى علاقة تكرار في البرمجة الديناميكية dp[i] = f(nums[i], max(dp[i-k..i-1]))، أو (3) تحتاج إلى أقرب فهرس صالح يحقق شرطًا رتيبًا.

في المقابلات، اكتب حل deque بطريقة واضحة: استورد deque، وحافظ على الثابتين (انتهاء صلاحية المقدمة، والترتيب الرتيب للمؤخرة)، وأعِد النتائج بدءًا من الفهرس k-1. اذكر دائمًا التعقيد الزمني O(n) والمساحة O(k) لـ deque (إذ يخزّن في الوقت نفسه ما لا يزيد على k من الفهارس)، وقارن ذلك بالحل بالقوة الغاشمة ذي التعقيد O(nk) لإظهار التحسّن.

from collections import deque

# Clean, interview-ready template
def sliding_window_max_template(nums, k):
    if not nums or k == 0:
        return []

    dq = deque()   # monotonic decreasing, stores indices
    result = []

    for i in range(len(nums)):
        # Invariant 1: remove expired indices (outside window)
        while dq and dq[0] < i - k + 1:
            dq.popleft()

        # Invariant 2: remove indices with smaller values (useless)
        while dq and nums[dq[-1]] < nums[i]:
            dq.pop()

        dq.append(i)

        # Record result once first full window is established
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result

# Complexity: O(n) time, O(k) space
print(sliding_window_max_template([1,3,-1,-3,5,3,6,7], 3))
print(sliding_window_max_template([1], 1))
print(sliding_window_max_template([], 3))

اختبار سريع

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

مراجعة الدرس

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

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

هل درس «أقصى قيمة في النافذة المنزلقة باستخدام deque رتيب» مجاني؟

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

ماذا ستتعلم في «أقصى قيمة في النافذة المنزلقة باستخدام deque رتيب»؟

حافظ على deque تنازلي من الفهارس للإجابة عن استعلامات القيمة القصوى في النافذة في O(1) لكل عنصر، وحلّ مسألة sliding-window-maximum في O(n). تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «أقصى قيمة في النافذة المنزلقة باستخدام deque رتيب»؟

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

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

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

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

  1. المكدس الرتيب: تصاعدي مقابل تنازلي
  2. أكبر مستطيل في المدرج التكراري
  3. أقصى قيمة في النافذة المنزلقة باستخدام deque رتيب
  4. احتجاز مياه الأمطار: المكدس والمؤشران
← العودة إلى DSA Interview Prep