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) символов и сравнивайте частоты. Сравнение полных объектов-счётчиков на каждом шаге занимает 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. Это тот же метод фиксированного окна, что и для задачи о перестановке в строке, но вместо возврата значения истина при первом совпадении мы собираем все подходящие позиции. Размер окна фиксирован и равен 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 различными символами

Это вариант задачи о скользящем окне: найдите самую длинную подстроку, содержащую не более 2 различных символов. Поддерживайте карту частот символов в текущем окне. Когда карта содержит более 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 различных символов)
  • окно фиксированного размера с агрегацией (максимум, сумма, частота)
  • задачи о непрерывном диапазоне (а не о произвольных подмножествах)
NOT используйте скользящее окно для невзаимосвязанных выборок, задач, требующих перебрать все перестановки (используйте перебор с возвратом), или задач, в которых состояние окна нельзя поддерживать пошагово. Главная проверка: можете ли Вы обновить состояние за 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). Каждый вызов функции подсчёта окон занимает O(n), поэтому общая сложность составляет O(n). Функция подсчёта окон определяет количество окон, в которых число различных символов не превышает k, суммируя right - left + 1 — количество всех допустимых левых границ для каждого правого конца.

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

Быстрая проверка

Проверьте своё понимание концепций «Структуры данных и алгоритмы — подготовка к собеседованию по программированию» из этого урока.

Итоги урока

В этом уроке Вы узнали: скользящее окно устраняет 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 включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. API строк Python для собеседований
  2. Скользящее окно для подстрок
  3. Анаграммы и карты частот символов
  4. Кодирование строк, разворот и палиндромы
← Назад к DSA Interview Prep