Скользящее окно для подстрок
Реализуйте скользящее окно переменного размера, чтобы находить самую длинную подстроку без повторяющихся символов и минимальное окно, содержащее все целевые символы
«Скользящее окно для подстрок» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding 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 различных символов)
- окно фиксированного размера с агрегацией (максимум, сумма, частота)
- задачи о непрерывном диапазоне (а не о произвольных подмножествах)
# 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) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Скользящее окно для подстрок»?
Реализуйте скользящее окно переменного размера, чтобы находить самую длинную подстроку без повторяющихся символов и минимальное окно, содержащее все целевые символы Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Скользящее окно для подстрок»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- API строк Python для собеседований
- Скользящее окно для подстрок
- Анаграммы и карты частот символов
- Кодирование строк, разворот и палиндромы