0Pricing
DSA Interview Prep · درس

النافذة المنزلقة للسلاسل الجزئية

نفّذ النافذة المنزلقة متغيرة الحجم للعثور على أطول سلسلة جزئية بلا محارف مكررة وأصغر نافذة تحتوي على جميع المحارف المستهدفة

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

مفهوم النافذة المنزلقة

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

# Fixed-size window sum: O(n) after O(k) setup
def max_sum_window(nums, k):
    window_sum = sum(nums[:k])  # initial window
    best = window_sum
    for i in range(k, len(nums)):
        window_sum += nums[i]       # add new right
        window_sum -= nums[i - k]   # remove old left
        best = max(best, window_sum)
    return best

print(max_sum_window([2,1,5,1,3,2], 3))  # 9  ([5,1,3])

حجم النافذة الثابت مقابل المتغير

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

# Variable window: longest substring with at most k distinct chars
def longest_k_distinct(s, k):
    from collections import defaultdict
    freq = defaultdict(int)
    left = 0
    best = 0
    for right in range(len(s)):
        freq[s[right]] += 1
        while len(freq) > k:    # window invalid: shrink
            freq[s[left]] -= 1
            if freq[s[left]] == 0:
                del freq[s[left]]
            left += 1
        best = max(best, right - left + 1)
    return best

print(longest_k_distinct('eceba', 2))   # 3  ('ece')
print(longest_k_distinct('aa', 1))      # 2

أطول سلسلة فرعية من دون تكرار

هذه أشهر مسائل النافذة المنزلقة ذات الحجم المتغير. استخدم مجموعة لتتبع المحارف الموجودة في النافذة الحالية. وسّع النافذة نحو اليمين؛ وعند العثور على محرف مكرر، قلّصها من اليسار حتى إزالة التكرار. وتستخدم النسخة الأسرع خريطة تجزئة تخزّن أحدث فهرس لكل محرف، ما يسمح للمؤشر الأيسر بتجاوز المحرف المكرر في خطوة واحدة بدلًا من التقدم خطوة بخطوة.

def length_of_longest_substring(s):
    char_idx = {}  # char -> last seen index
    left = 0
    best = 0
    for right, c in enumerate(s):
        if c in char_idx and char_idx[c] >= left:
            left = char_idx[c] + 1  # jump past duplicate
        char_idx[c] = right
        best = max(best, right - left + 1)
    return best

print(length_of_longest_substring('abcabcbb'))  # 3 ('abc')
print(length_of_longest_substring('bbbbb'))     # 1
print(length_of_longest_substring('pwwkew'))    # 3 ('wke')

السلسلة الفرعية ذات النافذة الدنيا

بالنظر إلى السلسلتين s وt، اعثر على أصغر نافذة في s تحتوي على جميع محارف t. استخدم خريطتي تكرارات: need (المحارف المطلوبة) وhave (المحارف الموجودة في النافذة الحالية والتي تحقق المتطلبات). تتبع عدد المحارف المختلفة في t التي جرى تحقيقها باستخدام عداد formed. وسّع النافذة نحو اليمين لتضمين المحارف؛ وعندما تغطي جميع محارف t، قلّصها من اليسار لتصغير النافذة. الزمن O(|s| + |t|).

from collections import Counter

def min_window(s, t):
    if not t or not s: return ''
    need = Counter(t)
    have = {}
    formed = 0
    required = len(need)
    left = 0
    best = float('inf'), 0, 0
    for right, c in enumerate(s):
        have[c] = have.get(c, 0) + 1
        if c in need and have[c] == need[c]:
            formed += 1
        while formed == required:
            if right - left + 1 < best[0]:
                best = right - left + 1, left, right
            have[s[left]] -= 1
            if s[left] in need and have[s[left]] < need[s[left]]:
                formed -= 1
            left += 1
    return s[best[1]:best[2]+1] if best[0] != float('inf') else ''

print(min_window('ADOBECODEBANC', 'ABC'))  # 'BANC'

قالب النافذة المنزلقة

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

def sliding_window_template(s, condition_check, update_state, remove_state):
    """
    Generic sliding window skeleton.
    Adapt condition_check, update_state, remove_state per problem.
    """
    left = 0
    state = {}  # or whatever state you need
    best = 0
    for right in range(len(s)):
        update_state(state, s[right])      # expand window
        while not condition_check(state):  # window invalid
            remove_state(state, s[left])   # shrink window
            left += 1
        best = max(best, right - left + 1)
    return best

تبديل محارف داخل سلسلة

تحقق مما إذا كان أي تبديل لمحارف النمط p موجودًا كسلسلة فرعية في s. يعادل فحص التبديل وجود نافذة لها تكرارات المحارف نفسها الموجودة في p. حافظ على نافذة منزلقة تحتوي بالضبط على len(p) من المحارف، وقارن أعداد التكرارات. تكلف مقارنة كائنات Counter بالكامل O(26) في كل خطوة (وهي قيمة ثابتة للغة الإنجليزية ذات الأحرف الصغيرة)، مما يعطي O(n × 26) = O(n) إجمالًا.

from collections import Counter

def check_inclusion(p, s):
    if len(p) > len(s): return False
    need  = Counter(p)
    window = Counter(s[:len(p)])
    if need == window: return True
    for right in range(len(p), len(s)):
        left = right - len(p)
        window[s[right]] += 1
        window[s[left]]  -= 1
        if window[s[left]] == 0:
            del window[s[left]]
        if window == need:
            return True
    return False

print(check_inclusion('ab', 'eidbaooo'))  # True ('ba')
print(check_inclusion('ab', 'eidboaoo'))  # False

عدّ جميع السلاسل الفرعية ذات التبديلات

اعثر على جميع فهارس البدء لتبديلات p الموجودة في s. هذه هي تقنية النافذة ذات الحجم الثابت نفسها المستخدمة في مسألة التبديل داخل سلسلة، لكن بدلًا من إرجاع True عند العثور على أول تطابق، نجمع جميع المواضع المطابقة. يكون حجم النافذة ثابتًا عند len(p)؛ ونحرّكها عبر s ونقارن أعداد التكرارات عند كل خطوة.

from collections import Counter

def find_anagrams(s, p):
    result = []
    need = Counter(p)
    k = len(p)
    window = Counter(s[:k])
    if window == need:
        result.append(0)
    for right in range(k, len(s)):
        window[s[right]] += 1
        left_char = s[right - k]
        window[left_char] -= 1
        if window[left_char] == 0:
            del window[left_char]
        if window == need:
            result.append(right - k + 1)
    return result

print(find_anagrams('cbaebabacd', 'abc'))  # [0, 6]

أطول سلسلة فرعية تحتوي على محرفين مختلفين على الأكثر

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

def longest_substring_two_distinct(s):
    from collections import defaultdict
    freq = defaultdict(int)
    left = 0
    best = 0
    for right, c in enumerate(s):
        freq[c] += 1
        while len(freq) > 2:
            freq[s[left]] -= 1
            if freq[s[left]] == 0:
                del freq[s[left]]
            left += 1
        best = max(best, right - left + 1)
    return best

print(longest_substring_two_distinct('eceba'))     # 3  ('ece')
print(longest_substring_two_distinct('ccaabbb'))   # 5  ('aabbb')

القيمة العظمى في النافذة المنزلقة

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

from collections import deque

def max_sliding_window(nums, k):
    dq = deque()  # stores indices, decreasing values
    result = []
    for i, n in enumerate(nums):
        # Remove indices outside window
        while dq and dq[0] < i - k + 1:
            dq.popleft()
        # Maintain decreasing order
        while dq and nums[dq[-1]] < n:
            dq.pop()
        dq.append(i)
        if i >= k - 1:  # window is full
            result.append(nums[dq[0]])
    return result

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

متى تستخدم النافذة المنزلقة

استخدم النافذة المنزلقة عندما ترى:

  • سلسلة فرعية أو مصفوفة فرعية لها قيد (الطول الأقصى، أو المجموع = k، أو وجود k من المحارف المختلفة على الأكثر)
  • حجم نافذة ثابتًا مع عملية تجميع (القيمة العظمى، أو المجموع، أو التكرار)
  • أسئلة عن نطاق متصل (وليس مجموعات فرعية عشوائية)
لا تستخدم النافذة المنزلقة من أجل: الاختيارات غير المتصلة، أو المسائل التي تتطلب جميع التبديلات (استخدم البحث بالتراجع)، أو المسائل التي لا يمكن فيها الحفاظ على الحالة تدريجيًا. الاختبار الأساسي: هل يمكنك تحديث الحالة بتكلفة O(1) عند إضافة عنصر واحد أو إزالته؟

# Recognising sliding window problems:

# 1. Fixed window: 'maximum average of subarray of length k'
def max_avg(nums, k):
    s = sum(nums[:k])
    best = s
    for i in range(k, len(nums)):
        s += nums[i] - nums[i-k]
        best = max(best, s)
    return best / k

print(max_avg([1,12,-5,-6,50,3], 4))  # 12.75

# 2. Variable window: 'smallest subarray with sum >= target'
def min_sub_len(target, nums):
    left = s = 0
    best = float('inf')
    for right, n in enumerate(nums):
        s += n
        while s >= target:
            best = min(best, right - left + 1)
            s -= nums[left]; left += 1
    return 0 if best == float('inf') else best
print(min_sub_len(7, [2,3,1,2,4,3]))  # 2

عدّ النوافذ الصالحة: على الأكثر K

تطلب بعض المسائل حساب عدد المصفوفات الفرعية التي تحقق شرطًا معينًا. من الحيل المفيدة: احسب عدد المصفوفات الفرعية التي تحتوي على k من المحارف المختلفة على الأكثر، ثم اطرح منه لتحصل على عدد المصفوفات التي تحتوي على k من المحارف المختلفة بالضبط: exactly(k) = at_most(k) - at_most(k-1). يستغرق كل استدعاء لـ at_most زمن O(n)، مما يعطي O(n) إجمالًا. تحسب الدالة at_most النوافذ التي لا يتجاوز فيها عدد المحارف المختلفة k، وذلك بجمع right - left + 1 (وهو عدد نقاط البداية الصالحة لكل right).

from collections import defaultdict

def subarrays_at_most_k(s, k):
    freq = defaultdict(int)
    left = 0
    count = 0
    for right, c in enumerate(s):
        freq[c] += 1
        while len(freq) > k:
            freq[s[left]] -= 1
            if freq[s[left]] == 0: del freq[s[left]]
            left += 1
        count += right - left + 1  # all valid windows ending at right
    return count

def subarrays_exactly_k(s, k):
    return subarrays_at_most_k(s, k) - subarrays_at_most_k(s, k-1)

print(subarrays_exactly_k('araaci', 2))  # 9

اختبار سريع

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

مراجعة الدرس

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

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

هل درس «النافذة المنزلقة للسلاسل الجزئية» مجاني؟

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

ماذا ستتعلم في «النافذة المنزلقة للسلاسل الجزئية»؟

نفّذ النافذة المنزلقة متغيرة الحجم للعثور على أطول سلسلة جزئية بلا محارف مكررة وأصغر نافذة تحتوي على جميع المحارف المستهدفة تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

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

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

كم من الوقت يستغرق درس «النافذة المنزلقة للسلاسل الجزئية»؟

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

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

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

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

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